babashka/script/add_libtest.clj
2022-04-23 11:35:49 +02:00

206 lines
8.6 KiB
Clojure
Executable file

#!/usr/bin/env bb
;; Adds a library to bb-tested-libs.edn and libraries.csv and optionally run its
;; tests. There are two modes to this script - automatic (default) and manual.
;; The script defaults to automatically copying tests as this normally works.
;; There are several options to specify where the library is including
;; --git-url, --dir and --test-directories. See --help for more. In manual mode,
;; tests are manually added outside of the script and the script is run to add
;; the library to library lists.
(ns add-libtest
(:require [babashka.deps :as deps]
[babashka.fs :as fs]
[babashka.tasks :refer [shell]]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.cli :as cli]
[org.httpkit.client :as http]))
(deps/add-deps '{:deps {org.clojure/tools.gitlibs {:mvn/version "2.4.172"}
borkdude/rewrite-edn {:mvn/version "0.1.0"}
org.clojure/data.csv {:mvn/version "1.0.0"}}})
(require '[clojure.tools.gitlibs :as gl])
(require '[borkdude.rewrite-edn :as r])
(require '[clojure.data.csv :as csv])
;; CLI Utils
;; =========
(defn- error
"Print error message(s) and exit"
[& msgs]
(apply println "Error:" msgs)
(System/exit 1))
(defn- print-summary
"Print help summary given args and opts strings"
[args-string options-summary]
(println (format "Usage: %s [OPTIONS]%s\nOptions:\n%s"
(.getName (io/file *file*))
args-string
options-summary)))
(defn- run-command
"Processes a command's functionality given a cli options definition, arguments
and primary command fn. This handles option parsing, handles any errors with
parsing and then passes parsed input to command fn"
[command-fn args cli-opts & parse-opts-options]
(let [{:keys [errors] :as parsed-input}
(apply cli/parse-opts args cli-opts parse-opts-options)]
(if (seq errors)
(do
(error (str/join "\n" (into ["Options failed to parse:"] errors)))
(System/exit 1))
(command-fn parsed-input))))
;; Add libtest
;; ===========
(defn- add-lib-to-deps
[lib-name lib-coordinate]
(let [nodes (-> "deps.edn" slurp r/parse-string)]
(spit "deps.edn"
(str (r/assoc-in nodes
[:aliases :lib-tests :extra-deps lib-name]
lib-coordinate)))))
(defn- default-test-dir
[lib-root-dir]
(some #(when (fs/exists? (fs/file lib-root-dir %))
(str (fs/file lib-root-dir %)))
;; Most common test dir
["test"
;; official clojure repos like https://github.com/clojure/tools.gitlibs
"src/test/clojure"]))
(defn- copy-tests
[git-url lib-name {:keys [directory branch test-directories]}]
(let [lib-dir (if branch
(gl/procure git-url lib-name branch)
(or (gl/procure git-url lib-name "master")
(gl/procure git-url lib-name "main")
(throw (ex-info "Unable to clone git-url" {}))))
_ (println "Git clone is at" lib-dir)
lib-root-dir (if directory (fs/file lib-dir directory) lib-dir)
test-dirs (if test-directories
(map #(when (fs/exists? (fs/file lib-root-dir %))
(str (fs/file lib-root-dir %)))
test-directories)
(some-> (default-test-dir lib-root-dir) vector))]
(when (empty? test-dirs)
(error "No test directories found"))
(doseq [test-dir test-dirs]
(shell "cp -R" (str test-dir fs/file-separator) "test-resources/lib_tests/"))
{:lib-dir lib-dir
:test-dirs test-dirs}))
(defn- default-test-namespaces
[test-dir]
(let [relative-test-files (map #(str (fs/relativize test-dir %))
(fs/glob test-dir "**/*.{clj,cljc}"))]
(when (empty? relative-test-files)
(error (str "No test files found in " test-dir)))
(map #(-> %
(str/replace fs/file-separator ".")
(str/replace "_" "-")
(str/replace-first #"\.clj(c?)$" "")
symbol)
relative-test-files)))
(defn- add-lib-to-tested-libs
[lib-name git-url {:keys [lib-dir test-dirs]} options]
(let [namespaces (or (get-in options [:manually-added :test-namespaces])
(mapcat default-test-namespaces test-dirs))
default-lib (merge
{:git-url git-url
:test-namespaces namespaces}
;; Options needed to update libs
(select-keys options [:branch :directory :test-directories]))
lib (if (:manually-added options)
(-> default-lib
(merge (:manually-added options))
(assoc :manually-added true))
(assoc default-lib
:git-sha (fs/file-name lib-dir)))
nodes (-> "test-resources/lib_tests/bb-tested-libs.edn" slurp r/parse-string)]
(spit "test-resources/lib_tests/bb-tested-libs.edn"
(str (r/assoc-in nodes
[(symbol lib-name)]
lib)))
namespaces))
(defn- fetch-artifact
"Using the clojars api to get a library's git url doesn't always work. A
possibly more reliable data source could be the scm urls in this POM feed -
https://github.com/clojars/clojars-web/wiki/Data#useful-extracts-from-the-poms"
[artifact]
(let [url (str "https://clojars.org/api/artifacts/" artifact)
_ (println "GET" url "...")
resp @(http/get url {:headers {"Accept" "application/edn"}})]
(if (= 200 (:status resp))
(-> resp :body slurp edn/read-string)
(error (str "Response failed and returned " (pr-str resp))))))
(defn- get-lib-map
[deps-string options]
;; if deps-string is artifact name
(if (re-matches #"\S+/\S+" deps-string)
(let [artifact-edn (fetch-artifact deps-string)]
{:lib-name (symbol deps-string)
:lib-coordinate {:mvn/version (:latest_version artifact-edn)}
:git-url (or (:git-url options) (:homepage artifact-edn))})
(let [deps-map (edn/read-string deps-string)
_ (when (or (not (map? deps-map)) (not= 1 (count deps-map)))
(error "Deps map must have one key"))
lib-coordinate (-> deps-map vals first)]
{:lib-name (ffirst deps-map)
:lib-coordinate lib-coordinate
:git-url (or (:git/url lib-coordinate) (:git-url options))})))
(defn- write-lib-to-csv
"Updates libraries.csv with latest bb-tested-libs.edn"
[]
(let [libs (-> "test-resources/lib_tests/bb-tested-libs.edn" slurp edn/read-string)
rows (sort-by first
(map (fn [[name {:keys [git-url]}]]
[name git-url]) libs))]
(with-open [w (io/writer "doc/libraries.csv")]
(csv/write-csv w (into [["maven-name" "git-url"]] rows)))))
(defn- add-libtest*
[args options]
(let [[artifact-or-deps-string] args
{:keys [lib-name lib-coordinate git-url]}
(get-lib-map artifact-or-deps-string options)
_ (when (nil? git-url)
(error "git-url is required. Please specify with --git-url"))
_ (when-not (:manually-added options) (add-lib-to-deps lib-name lib-coordinate))
dirs (when-not (:manually-added options) (copy-tests git-url lib-name options))
namespaces (add-lib-to-tested-libs lib-name git-url dirs options)]
(println "Added lib" lib-name "which tests the following namespaces:" namespaces)
(write-lib-to-csv)
(when (:test options)
(apply shell "script/lib_tests/run_all_libtests" namespaces))))
(defn add-libtest
[{:keys [arguments options summary]}]
(if (or (< (count arguments) 1) (:help options))
(print-summary "ARTIFACT-OR-DEPS-MAP " summary)
(add-libtest* arguments options)))
(def cli-options
[["-h" "--help"]
["-t" "--test" "Run tests"]
;; https://github.com/weavejester/environ/tree/master/environ used this option
["-d" "--directory DIRECTORY" "Directory where library is located"]
;; https://github.com/reifyhealth/specmonstah used this option
["-b" "--branch BRANCH" "Default branch for git url"]
["-g" "--git-url GITURL" "Git url for artifact. Defaults to homepage on clojars"]
["-m" "--manually-added LIB-MAP" "Only add library to edn file with LIB-MAP merged into library entry"
:parse-fn edn/read-string :validate-fn map?]
;; https://github.com/jeaye/orchestra used this option
["-T" "--test-directories TEST-DIRECTORY" "Directories where library tests are located"
:multi true :update-fn conj]])
(when (= *file* (System/getProperty "babashka.file"))
(run-command add-libtest *command-line-args* cli-options))