Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
(ns logseq.cli.e2e.cleanup
(:require [babashka.fs :as fs]
[clojure.java.shell :as java-shell]
[clojure.string :as string]))
(def ^:private cli-e2e-temp-prefix "logseq-cli-e2e-")
(def ^:private db-sync-default-port 18080)
(defn- parse-ps-line
[line]
(when-let [[_ pid-str command] (re-matches #"\s*(\d+)\s+(.*)" (or line ""))]
{:pid (Long/parseLong pid-str)
:command command}))
(defn- cli-e2e-db-worker-command?
[command]
(and (re-find #"db-worker-node(?:\.js)?\b" command)
(re-find #"logseq-cli-e2e-" command)))
(defn list-cli-e2e-db-worker-pids
([]
(list-cli-e2e-db-worker-pids {}))
([{:keys [shell-fn]
:or {shell-fn java-shell/sh}}]
(let [{:keys [exit out err]} (shell-fn "ps" "-ax" "-o" "pid=" "-o" "command=")]
(when-not (zero? exit)
(throw (ex-info "Unable to scan processes"
{:exit exit
:err err})))
(->> (string/split-lines (or out ""))
(keep parse-ps-line)
(filter (fn [{:keys [command]}]
(cli-e2e-db-worker-command? command)))
(mapv :pid)))))
(defn- pid-alive?
[pid]
(zero? (:exit (java-shell/sh "kill" "-0" (str pid)))))
(defn- kill-pid!
[pid]
(try
(if-not (pid-alive? pid)
:not-found
(do
(java-shell/sh "kill" "-TERM" (str pid))
(Thread/sleep 100)
(if-not (pid-alive? pid)
:killed
(do
(java-shell/sh "kill" "-KILL" (str pid))
(Thread/sleep 50)
(if (pid-alive? pid)
:failed
:killed)))))
(catch Exception _
:failed)))
(defn cleanup-db-worker-processes!
([]
(cleanup-db-worker-processes! {}))
([{:keys [dry-run list-pids-fn kill-pid-fn]}]
(let [list-pids-fn (or list-pids-fn list-cli-e2e-db-worker-pids)
kill-pid-fn (or kill-pid-fn kill-pid!)
found-pids (vec (list-pids-fn))]
(if dry-run
{:dry-run? true
:found-pids found-pids
:would-kill-pids found-pids
:killed-pids []
:failed-pids []}
(let [{:keys [killed-pids failed-pids]}
(reduce (fn [acc pid]
(if (= :failed (kill-pid-fn pid))
(update acc :failed-pids conj pid)
(update acc :killed-pids conj pid)))
{:killed-pids []
:failed-pids []}
found-pids)]
{:dry-run? false
:found-pids found-pids
:would-kill-pids []
:killed-pids killed-pids
:failed-pids failed-pids})))))
(defn- parse-long-safe
[value]
(try
(Long/parseLong value)
(catch Exception _
nil)))
(defn list-cli-e2e-db-sync-port-pids
([]
(list-cli-e2e-db-sync-port-pids {}))
([{:keys [shell-fn port]
:or {shell-fn java-shell/sh
port db-sync-default-port}}]
(let [{:keys [exit out err]} (shell-fn "lsof" "-nP" (str "-iTCP:" port) "-sTCP:LISTEN")
out-lines (->> (string/split-lines (or out ""))
(map string/trim)
(remove string/blank?)
vec)]
(when (and (not (zero? exit))
(or (not (string/blank? err))
(seq out-lines)))
(throw (ex-info "Unable to scan db-sync server port listeners"
{:exit exit
:err err
:port port})))
(->> out-lines
(filter #(re-find (re-pattern (str ":" port "\\b")) %))
(keep (fn [line]
(some-> (string/split line #"\s+")
(nth 1 nil)
parse-long-safe)))
distinct
vec))))
(defn cleanup-db-sync-port-processes!
([]
(cleanup-db-sync-port-processes! {}))
([{:keys [dry-run list-pids-fn kill-pid-fn]
:as _opts}]
(let [list-pids-fn (or list-pids-fn list-cli-e2e-db-sync-port-pids)
kill-pid-fn (or kill-pid-fn kill-pid!)
found-pids (vec (list-pids-fn))]
(if dry-run
{:dry-run? true
:found-pids found-pids
:would-kill-pids found-pids
:killed-pids []
:failed-pids []}
(let [{:keys [killed-pids failed-pids]}
(reduce (fn [acc pid]
(if (= :failed (kill-pid-fn pid))
(update acc :failed-pids conj pid)
(update acc :killed-pids conj pid)))
{:killed-pids []
:failed-pids []}
found-pids)]
{:dry-run? false
:found-pids found-pids
:would-kill-pids []
:killed-pids killed-pids
:failed-pids failed-pids})))))
(defn- list-cli-e2e-temp-roots
[tmp-root]
(->> (fs/list-dir tmp-root)
(filter fs/directory?)
(filter #(string/starts-with? (fs/file-name %) cli-e2e-temp-prefix))
(mapv str)))
(defn cleanup-temp-roots!
([]
(cleanup-temp-roots! {}))
([{:keys [dry-run tmp-root list-dirs-fn delete-dir-fn]
:or {tmp-root (System/getProperty "java.io.tmpdir")
delete-dir-fn fs/delete-tree}}]
(let [list-dirs-fn (or list-dirs-fn
#(list-cli-e2e-temp-roots tmp-root))
found-dirs (vec (list-dirs-fn))]
(if dry-run
{:dry-run? true
:found-dirs found-dirs
:would-remove-dirs found-dirs
:removed-dirs []
:failed-dirs []}
(let [{:keys [removed-dirs failed-dirs]}
(reduce (fn [acc dir]
(try
(delete-dir-fn dir)
(update acc :removed-dirs conj dir)
(catch Exception _
(update acc :failed-dirs conj dir))))
{:removed-dirs []
:failed-dirs []}
found-dirs)]
{:dry-run? false
:found-dirs found-dirs
:would-remove-dirs []
:removed-dirs removed-dirs
:failed-dirs failed-dirs})))))

View File

@@ -0,0 +1,120 @@
(ns logseq.cli.e2e.coverage
(:require [clojure.set :as set]
[clojure.string :as string]))
(defn- normalize-commands
[commands]
(->> commands
(map str)
(remove string/blank?)
distinct
sort
vec))
(defn- normalize-options
[options]
(->> options
(map str)
(remove string/blank?)
distinct
sort
vec))
(defn- excluded-command?
[excluded-prefixes command]
(let [head (first (string/split (str command) #" "))]
(contains? (set excluded-prefixes) head)))
(defn validate-inventory!
[{:keys [excluded-command-prefixes scopes] :as inventory}]
(let [invalid-commands (->> scopes
vals
(mapcat :commands)
(filter #(excluded-command? excluded-command-prefixes %))
normalize-commands)
invalid-scopes (->> scopes
(keep (fn [[scope {:keys [commands]}]]
(when (some #(excluded-command? excluded-command-prefixes %) commands)
scope)))
sort
vec)]
(when (seq invalid-commands)
(throw (ex-info "Excluded commands cannot appear in cli-e2e inventory"
{:invalid-commands invalid-commands
:invalid-scopes invalid-scopes
:inventory inventory})))
inventory))
(defn validate-cases!
[{:keys [excluded-command-prefixes] :as inventory} cases]
(let [invalid-cases (->> cases
(filter (fn [{:keys [covers]}]
(some #(excluded-command? excluded-command-prefixes %)
(get covers :commands []))))
(mapv :id))]
(when (seq invalid-cases)
(throw (ex-info "Excluded commands cannot be covered by cli-e2e cases"
{:invalid-case-ids invalid-cases
:inventory inventory})))
cases))
(defn command->scope
[{:keys [scopes]}]
(into {}
(mapcat (fn [[scope {:keys [commands]}]]
(map (fn [command]
[command scope])
commands)))
scopes))
(defn required-commands
[{:keys [scopes]}]
(->> scopes
vals
(mapcat :commands)
normalize-commands))
(defn covered-commands
[cases]
(->> cases
(mapcat #(get-in % [:covers :commands]))
normalize-commands))
(defn required-options-by-scope
[{:keys [scopes]}]
(into {}
(map (fn [[scope {:keys [options]}]]
[scope (normalize-options options)]))
scopes))
(defn covered-options-by-scope
[cases]
(reduce (fn [acc {:keys [covers]}]
(reduce-kv (fn [acc' scope options]
(update acc' scope (fnil into #{}) options))
acc
(get covers :options {})))
{}
cases))
(defn coverage-report
[inventory cases]
(validate-inventory! inventory)
(validate-cases! inventory cases)
(let [required-commands* (set (required-commands inventory))
covered-commands* (set (covered-commands cases))
covered-options* (covered-options-by-scope cases)
required-options* (required-options-by-scope inventory)]
{:missing-commands (->> (set/difference required-commands* covered-commands*)
normalize-commands)
:missing-options (into {}
(map (fn [[scope options]]
[scope (->> (set/difference (set options)
(get covered-options* scope #{}))
normalize-options)]))
required-options*)}))
(defn complete?
[{:keys [missing-commands missing-options]}]
(and (empty? missing-commands)
(every? empty? (vals missing-options))))

View File

@@ -0,0 +1,468 @@
(ns logseq.cli.e2e.main
(:require [clojure.set :as set]
[logseq.cli.e2e.cleanup :as cleanup]
[logseq.cli.e2e.coverage :as coverage]
[logseq.cli.e2e.manifests :as manifests]
[logseq.cli.e2e.preflight :as preflight]
[logseq.cli.e2e.report :as report]
[logseq.cli.e2e.runner :as runner]
[logseq.cli.e2e.shell :as shell]
[logseq.cli.e2e.sync-fixture :as sync-fixture])
(:import (java.util.concurrent Executors LinkedBlockingQueue TimeUnit)))
(defn select-cases
[cases {:keys [case include]}]
(let [cases (remove :skip cases)]
(cond
case
(filterv #(= case (:id %)) cases)
(seq include)
(let [include-tags (set (map keyword include))]
(filterv (fn [{:keys [tags]}]
(not-empty (set/intersection include-tags (set tags))))
cases))
:else
(vec cases))))
(def default-suite :non-sync)
(def default-jobs 1)
(def default-cli-jobs 4)
(defn- suite-from-opts
[opts]
(or (:suite opts) default-suite))
(defn- elapsed-ms
[started-at]
(long (/ (- (System/nanoTime) started-at) 1000000)))
(defn- format-duration
[started-at]
(format "%.2fs" (/ (double (- (System/nanoTime) started-at)) 1000000000.0)))
(defn- positive-jobs
[jobs]
(let [jobs (or jobs default-jobs)]
(when-not (and (integer? jobs) (pos? jobs))
(throw (ex-info "--jobs must be a positive integer"
{:jobs jobs})))
jobs))
(defn run-selected-cases!
[selected-cases run-case run-command {:keys [on-case-start on-case-success on-case-failure detailed-log? timings? jobs]}]
(let [total (count selected-cases)
_ (positive-jobs jobs)]
(reduce (fn [acc [idx case]]
(let [index (inc idx)
started-at (System/nanoTime)]
(when on-case-start
(on-case-start {:index index
:total total
:case case}))
(try
(let [result (run-case case {:run-command run-command
:detailed-log? detailed-log?
:timings? timings?})
payload {:index index
:total total
:case case
:result result
:elapsed-ms (elapsed-ms started-at)}]
(when on-case-success
(on-case-success payload))
(conj acc result))
(catch Exception error
(when on-case-failure
(on-case-failure {:index index
:total total
:case case
:error error
:elapsed-ms (elapsed-ms started-at)}))
(throw error)))))
[]
(map-indexed vector selected-cases))))
(defn run-selected-cases-in-parallel!
[selected-cases run-case run-command {:keys [on-case-start on-case-success on-case-failure detailed-log? timings? jobs]}]
(let [total (count selected-cases)
jobs (positive-jobs jobs)
executor (Executors/newFixedThreadPool jobs)
completions (LinkedBlockingQueue.)]
(try
(doseq [[idx case] (map-indexed vector selected-cases)]
(let [index (inc idx)]
(when on-case-start
(on-case-start {:index index
:total total
:case case}))
(.submit executor
^Runnable
(fn []
(let [started-at (System/nanoTime)]
(.put completions
(try
(let [result (run-case case {:run-command run-command
:detailed-log? detailed-log?
:timings? timings?})]
{:index index
:total total
:case case
:result result
:elapsed-ms (elapsed-ms started-at)})
(catch Exception error
{:index index
:total total
:case case
:error error
:elapsed-ms (elapsed-ms started-at)}))))))))
(loop [remaining total
results []
failure nil]
(if (zero? remaining)
(do
(when failure
(throw failure))
(->> results
(sort-by :index)
(mapv :result)))
(let [payload (.take completions)]
(if-let [error (:error payload)]
(do
(when on-case-failure
(on-case-failure payload))
(recur (dec remaining) results (or failure error)))
(do
(when on-case-success
(on-case-success payload))
(recur (dec remaining) (conj results payload) failure))))))
(finally
(.shutdown executor)
(.awaitTermination executor 1 TimeUnit/MINUTES)))))
(defn run!
[{:keys [inventory cases skip-build run-command jobs]
:as opts}]
(let [run-command (or run-command shell/run!)
run-case (or (:run-case opts) runner/run-case!)
suite (suite-from-opts opts)
sync-suite? (= suite :sync)
jobs (positive-jobs jobs)
targeted-run? (or (:case opts) (seq (:include opts)))
on-preflight-start (:on-preflight-start opts)
on-preflight-complete (:on-preflight-complete opts)
on-cases-ready (:on-cases-ready opts)]
(when on-preflight-start
(on-preflight-start {:skip-build skip-build}))
(let [preflight-result (preflight/run! {:skip-build skip-build
:run-command run-command})
inventory (or inventory (manifests/load-inventory suite))
cases (or cases (manifests/load-cases suite))]
(when on-preflight-complete
(on-preflight-complete preflight-result))
(let [selected-cases (select-cases cases opts)
coverage-result (when-not targeted-run?
(coverage/coverage-report inventory selected-cases))]
(when (and coverage-result
(not (coverage/complete? coverage-result)))
(throw (ex-info "Missing coverage"
{:coverage coverage-result
:message (report/format-missing-coverage coverage-result)})))
(when on-cases-ready
(on-cases-ready {:total (count selected-cases)
:targeted-run? targeted-run?}))
(let [suite-context (when sync-suite?
(sync-fixture/before-suite! {:run-command run-command}))
sync-context (if suite-context
(assoc suite-context :e2ee-password (:e2ee-password opts))
suite-context)
run-case* (if sync-suite?
(fn [case case-opts]
(run-case (sync-fixture/prepare-case case sync-context)
case-opts))
run-case)]
(try
{:status :ok
:cases selected-cases
:coverage coverage-result
:results ((if (> jobs 1)
run-selected-cases-in-parallel!
run-selected-cases!)
selected-cases
run-case*
run-command
(assoc opts :jobs jobs))}
(finally
(when suite-context
(sync-fixture/after-suite! suite-context {:run-command run-command})))))))))
(defn build!
[opts]
(preflight/run! opts))
(defn list-cases!
[opts]
(doseq [{:keys [id]} (manifests/load-cases (suite-from-opts opts))]
(println id)))
(defn list-sync-cases!
[opts]
(list-cases! (assoc opts :suite :sync)))
(defn- print-cleanup-help!
[]
(println "Usage: bb -f cli-e2e/bb.edn cleanup [options]")
(println)
(println "Options:")
(println " -h, --help Show this help and exit")
(println " --dry-run Scan and report only; do not kill/delete")
(println)
(println "Cleanups performed:")
(println " - Terminate cli-e2e db-worker-node processes")
(println " - Terminate db-sync server listeners on port 18080")
(println " - Remove cli-e2e temp roots")
(flush))
(defn cleanup!
[opts]
(if (:help opts)
(do
(print-cleanup-help!)
{:status :help})
(let [dry-run? (boolean (:dry-run opts))
cleanup-opts (cond-> {}
dry-run? (assoc :dry-run true))
processes (cleanup/cleanup-db-worker-processes! cleanup-opts)
db-sync-port-processes (cleanup/cleanup-db-sync-port-processes! cleanup-opts)
temp-roots (cleanup/cleanup-temp-roots! cleanup-opts)]
(println "==> Running cli-e2e cleanup")
(if dry-run?
(do
(println (format "[dry-run] db-worker-node processes: found %d, would kill %d"
(count (:found-pids processes))
(count (:would-kill-pids processes))))
(println (format "[dry-run] db-sync server processes (port 18080): found %d, would kill %d"
(count (:found-pids db-sync-port-processes))
(count (:would-kill-pids db-sync-port-processes))))
(println (format "[dry-run] temp roots: found %d, would remove %d"
(count (:found-dirs temp-roots))
(count (:would-remove-dirs temp-roots)))))
(do
(println (format "db-worker-node processes: found %d, killed %d, failed %d"
(count (:found-pids processes))
(count (:killed-pids processes))
(count (:failed-pids processes))))
(println (format "db-sync server processes (port 18080): found %d, killed %d, failed %d"
(count (:found-pids db-sync-port-processes))
(count (:killed-pids db-sync-port-processes))
(count (:failed-pids db-sync-port-processes))))
(println (format "temp roots: found %d, removed %d, failed %d"
(count (:found-dirs temp-roots))
(count (:removed-dirs temp-roots))
(count (:failed-dirs temp-roots))))))
(flush)
{:status :ok
:dry-run? dry-run?
:processes processes
:db-sync-port-processes db-sync-port-processes
:temp-roots temp-roots})))
(defn- print-failure-details!
[error]
(let [data (ex-data error)]
(println (str " reason: " (.getMessage error)))
(when-let [cmd (:cmd data)]
(println (str " cmd: " cmd)))
(when-let [stream (:stream data)]
(println (str " stream: " stream)))
(when-let [snippet (:snippet data)]
(println (str " snippet: " snippet)))
(flush)))
(defn- format-step-label
[{:keys [phase step-index step-total]}]
(let [phase-name (name (or phase :command))]
(format "%s %d/%d" phase-name (or step-index 1) (or step-total 1))))
(defn- print-case-timings!
[timings]
(when (seq timings)
(println " step timings:")
(doseq [{:keys [elapsed-ms status cmd] :as timing} timings]
(println (format " - [%-12s] %6dms (%s) $ %s"
(format-step-label timing)
elapsed-ms
(name (or status :ok))
cmd)))
(flush)))
(defn- print-slow-steps!
[all-step-timings]
(when (seq all-step-timings)
(println "Slow steps (top 10):")
(doseq [{:keys [case-id elapsed-ms cmd] :as timing}
(->> all-step-timings
(sort-by :elapsed-ms >)
(take 10))]
(println (format " - %-45s [%-12s] %6dms $ %s"
case-id
(format-step-label timing)
elapsed-ms
cmd)))
(flush)))
(defn- progress-prefix
[{:keys [parallel? index total]} symbol]
(if parallel?
(str symbol " ")
(format "[%d/%d] %s " index total symbol)))
(defn- print-test-help!
[command-name suite]
(let [sync-suite? (= suite :sync)]
(println (str "Usage: bb -f cli-e2e/bb.edn " command-name " [options]"))
(println)
(println "Options:")
(println " -h, --help Show this help and exit")
(println " --skip-build Skip build preflight steps")
(println " -i, --include TAG Run only cases with matching tag (repeatable)")
(println " --case ID Run a single case by id")
(println (format " --jobs N %s (Default: %d)"
(if sync-suite?
"Run up to N sync cases in parallel"
"Run up to N non-sync cases in parallel")
default-cli-jobs))
(when sync-suite?
(println " --e2ee-password VALUE E2EE password for sync commands (Default: 11111)"))
(println " --verbose Enable verbose output")
(println " --timings Print per-step timings and slow-step summary")
(println)
(println "Examples:")
(if sync-suite?
(println (str " bb -f cli-e2e/bb.edn " command-name))
(println (str " bb -f cli-e2e/bb.edn " command-name " --skip-build")))
(println (str " bb -f cli-e2e/bb.edn " command-name
(if sync-suite?
" --jobs 4"
" --skip-build --jobs 4")))
(println (str " bb -f cli-e2e/bb.edn " command-name " -i smoke"))
(if sync-suite?
(println (str " bb -f cli-e2e/bb.edn " command-name " --case sync-upload-download-mvp"))
(println (str " bb -f cli-e2e/bb.edn " command-name " --skip-build --case global-help")))
(when sync-suite?
(println (str " bb -f cli-e2e/bb.edn " command-name " --e2ee-password 'my-secret'")))
(flush)))
(defn- test-suite!
[opts {:keys [suite command-name]
:or {suite default-suite
command-name "test"}}]
(let [suite (or suite default-suite)
opts (assoc opts :suite suite)]
(if (:help opts)
(do
(print-test-help! command-name suite)
{:status :help})
(let [started-at (System/nanoTime)
passed (atom 0)
failed (atom 0)
total-count (atom 0)
timings? (boolean (:timings opts))
all-step-timings (atom [])
detailed-case-log? (some? (:case opts))
parallel? (> (positive-jobs (:jobs opts)) 1)
base-run-command (or (:run-command opts) shell/run!)
run-command (if detailed-case-log?
(fn [{:keys [cmd phase step-index step-total] :as command-opts}]
(let [prefix (case phase
:setup (format " [setup %d/%d]" step-index step-total)
:main (format " [main %d/%d]" (or step-index 1) (or step-total 1))
:cleanup (format " [cleanup %d/%d]" step-index step-total)
" [command]")]
(println (str prefix " $ " cmd))
(flush)
(base-run-command (assoc command-opts :stream-output? true))))
base-run-command)]
(println "==> Running cli-e2e cases")
(when detailed-case-log?
(println (format "==> Detailed case logging enabled (--case %s)" (:case opts))))
(when timings?
(println "==> Step timing enabled (--timings)"))
(flush)
(try
(run! (assoc opts
:run-command run-command
:detailed-log? detailed-case-log?
:timings? timings?
:on-preflight-start (fn [_]
(println "==> Build preflight: running...")
(flush))
:on-preflight-complete (fn [{:keys [status]}]
(println (case status
:skipped "==> Build preflight: skipped (--skip-build)"
"==> Build preflight: completed"))
(flush))
:on-cases-ready (fn [{:keys [total]}]
(reset! total-count total)
(println (format "==> Prepared %d case(s), starting execution" total))
(flush))
:on-case-start (fn [{:keys [index total case]}]
(println (str (progress-prefix {:parallel? parallel?
:index index
:total total}
"▶")
(:id case)))
(flush))
:on-case-success (fn [{:keys [index total result elapsed-ms]}]
(swap! passed inc)
(println (format "%s%s (%dms)"
(progress-prefix {:parallel? parallel?
:index index
:total total}
"✓")
(:id result)
elapsed-ms))
(when timings?
(let [case-timings (vec (:timings result))]
(swap! all-step-timings into
(map #(assoc % :case-id (:id result)) case-timings))
(print-case-timings! case-timings)))
(flush))
:on-case-failure (fn [{:keys [index total case error elapsed-ms]}]
(swap! failed inc)
(println (format "%s%s (%dms)"
(progress-prefix {:parallel? parallel?
:index index
:total total}
"✗")
(:id case)
elapsed-ms))
(print-failure-details! error)
(when timings?
(let [case-timings (vec (:timings (ex-data error)))]
(swap! all-step-timings into
(map #(assoc % :case-id (:id case)) case-timings))
(print-case-timings! case-timings))))))
(println (format "Summary: %d passed, %d failed" @passed @failed))
(println (str "Selected cases: " @total-count))
(println (str "Duration: " (format-duration started-at)))
(when timings?
(print-slow-steps! @all-step-timings))
(catch Exception error
(let [failed-count (max 1 @failed)]
(println (format "Summary: %d passed, %d failed" @passed failed-count))
(println (str "Selected cases: " (max @total-count failed-count)))
(println (str "Duration: " (format-duration started-at)))
(when timings?
(print-slow-steps! @all-step-timings)))
(throw error)))))))
(defn test!
[opts]
(test-suite! opts {:suite :non-sync
:command-name "test"}))
(defn test-sync!
[opts]
(test-suite! opts {:suite :sync
:command-name "test-sync"}))

View File

@@ -0,0 +1,237 @@
(ns logseq.cli.e2e.manifests
(:require [clojure.edn :as edn]
[logseq.cli.e2e.paths :as paths]))
(def suite->manifest-files
{:non-sync {:inventory "non_sync_inventory.edn"
:cases "non_sync_cases.edn"}
:sync {:inventory "sync_inventory.edn"
:cases "sync_cases.edn"}})
(def default-suite :non-sync)
(def ^:private append-merge-keys
#{:setup :cmds :cleanup :tags})
(def ^:private deep-merge-keys
#{:vars :covers :expect})
(defn read-edn-file
[path]
(edn/read-string (slurp path)))
(defn- normalize-suite
[suite]
(let [suite' (cond
(nil? suite) default-suite
(keyword? suite) suite
(string? suite) (keyword suite)
:else suite)]
(when-not (contains? suite->manifest-files suite')
(throw (ex-info "Unknown cli-e2e suite"
{:suite suite
:known-suites (sort (keys suite->manifest-files))})))
suite'))
(defn- manifest-file
[suite kind]
(get-in suite->manifest-files [(normalize-suite suite) kind]))
(defn load-inventory
([]
(load-inventory nil))
([suite]
(read-edn-file (paths/spec-path (manifest-file suite :inventory)))))
(defn- normalize-extends
[extends]
(let [extends' (cond
(nil? extends) []
(keyword? extends) [extends]
(vector? extends) extends
:else
(throw (ex-info "Invalid :extends value in cli-e2e manifest"
{:extends extends
:expected "keyword | vector<keyword> | nil"})))
invalid-entries (remove keyword? extends')]
(when (seq invalid-entries)
(throw (ex-info "Invalid :extends entries in cli-e2e manifest"
{:extends extends
:invalid-entries (vec invalid-entries)
:expected "keyword | vector<keyword> | nil"})))
extends'))
(defn- parse-manifest
[manifest-data]
(if-not (map? manifest-data)
(throw (ex-info "Invalid cli-e2e manifest format"
{:manifest-type (type manifest-data)
:expected "{:templates {...} :cases [...]}"}))
(let [templates (or (:templates manifest-data) {})
cases (:cases manifest-data)]
(when-not (map? templates)
(throw (ex-info "Invalid cli-e2e manifest :templates format"
{:templates templates
:expected "map"})))
(when-not (vector? cases)
(throw (ex-info "Invalid cli-e2e manifest :cases format"
{:cases cases
:expected "vector"})))
(doseq [case cases]
(when-not (map? case)
(throw (ex-info "Invalid cli-e2e case format"
{:case case
:expected "map"}))))
{:templates templates
:cases cases})))
(defn- reachable-template-ids
[templates roots]
(loop [stack (vec roots)
visited #{}]
(if-let [template-id (peek stack)]
(if (contains? visited template-id)
(recur (pop stack) visited)
(let [template (get templates template-id)
parent-ids (if template
(normalize-extends (:extends template))
[])]
(recur (into (pop stack) parent-ids)
(conj visited template-id))))
visited)))
(defn- lint-manifest!
[{:keys [templates cases]}]
(let [template-ids (set (keys templates))
template-refs (mapcat (fn [[template-id template]]
(map (fn [target]
{:type :invalid-extends
:source-type :template
:source template-id
:target target})
(normalize-extends (:extends template))))
templates)
case-refs (mapcat (fn [[index case]]
(map (fn [target]
{:type :invalid-extends
:source-type :case
:source (or (:id case)
(str "case#" (inc index)))
:target target})
(normalize-extends (:extends case))))
(map-indexed vector cases))
invalid-extends-issues (->> (concat template-refs case-refs)
(filter (fn [{:keys [target]}]
(not (contains? template-ids target)))))
duplicate-id-issues (->> cases
(keep :id)
frequencies
(filter (fn [[_ count]] (> count 1)))
(sort-by first)
(map (fn [[case-id count]]
{:type :duplicate-case-id
:id case-id
:count count})))
roots (->> cases
(mapcat #(normalize-extends (:extends %)))
distinct)
used-template-ids (reachable-template-ids templates roots)
unused-template-issues (->> (keys templates)
(remove used-template-ids)
sort
(map (fn [template-id]
{:type :unused-template
:template template-id})))
issues (vec (concat invalid-extends-issues
duplicate-id-issues
unused-template-issues))]
(when (seq issues)
(throw (ex-info "cli-e2e manifest lint failed"
{:issues issues})))))
(defn- as-seq
[value]
(cond
(nil? value) []
(sequential? value) value
:else [value]))
(defn- deep-merge-maps
[left right]
(merge-with (fn [left-val right-val]
(if (and (map? left-val)
(map? right-val))
(deep-merge-maps left-val right-val)
right-val))
(or left {})
(or right {})))
(defn- merge-entry
[parent child]
(let [all-keys (set (concat (keys parent) (keys child)))]
(reduce (fn [acc key]
(let [parent-val (get parent key)
child-val (get child key)]
(assoc acc
key
(cond
(contains? append-merge-keys key)
(if (contains? child key)
(vec (concat (as-seq parent-val)
(as-seq child-val)))
(vec (as-seq parent-val)))
(contains? deep-merge-keys key)
(if (contains? child key)
(deep-merge-maps parent-val child-val)
parent-val)
(contains? child key)
child-val
:else
parent-val))))
{}
all-keys)))
(defn- resolve-template
[templates template-id stack]
(when (some #{template-id} stack)
(throw (ex-info "Circular template inheritance detected in cli-e2e manifest"
{:template template-id
:cycle (conj (vec stack) template-id)})))
(let [template (get templates template-id)]
(when-not template
(throw (ex-info "Unknown template in cli-e2e manifest"
{:template template-id
:known-templates (sort (keys templates))})))
(let [parent-ids (normalize-extends (:extends template))
parent-values (map #(resolve-template templates % (conj stack template-id))
parent-ids)]
(reduce merge-entry
{}
(concat parent-values
[(dissoc template :extends)])))))
(defn- expand-manifest-cases
[{:keys [templates cases]}]
(mapv (fn [case]
(let [parent-ids (normalize-extends (:extends case))
parent-values (map #(resolve-template templates % [])
parent-ids)]
(reduce merge-entry
{}
(concat parent-values
[(dissoc case :extends)]))))
cases))
(defn load-cases
([]
(load-cases nil))
([suite]
(let [manifest-data (-> (manifest-file suite :cases)
paths/spec-path
read-edn-file
parse-manifest)]
(lint-manifest! manifest-data)
(expand-manifest-cases manifest-data))))

View File

@@ -0,0 +1,49 @@
(ns logseq.cli.e2e.paths
(:require [babashka.fs :as fs]))
(defn- ancestors
[path]
(take-while some? (iterate fs/parent path)))
(defn- find-parent-named
[path name]
(some (fn [candidate]
(when (= name (fs/file-name candidate))
candidate))
(ancestors (fs/canonicalize path))))
(def ^:private cli-e2e-root-path
(or (some-> *file*
(find-parent-named "cli-e2e")
str)
(throw (ex-info "Unable to locate cli-e2e root"
{:file *file*}))))
(defn cli-e2e-root
[]
cli-e2e-root-path)
(defn repo-root
[]
(str (fs/parent cli-e2e-root-path)))
(defn repo-path
[& segments]
(str (apply fs/path (repo-root) segments)))
(defn cli-e2e-path
[& segments]
(str (apply fs/path (cli-e2e-root) segments)))
(defn spec-path
[filename]
(cli-e2e-path "spec" filename))
(defn required-artifacts
[]
[(repo-path "cli" "_build" "default" "dist" "logseq-cli.js")
(repo-path "static" "logseq-cli.js")
(repo-path "static" "db-worker-node.js")
(repo-path "dist" "db-worker-node.js")
(repo-path "dist" "db-worker-node-assets.json")
(repo-path "deps" "db-sync" "worker" "dist" "node-adapter.js")])

View File

@@ -0,0 +1,37 @@
(ns logseq.cli.e2e.preflight
(:require [babashka.fs :as fs]
[logseq.cli.e2e.paths :as paths]
[logseq.cli.e2e.shell :as shell]))
(def build-plan
[{:cmd "pnpm cli:release"}
{:cmd "pnpm db-worker-node:compile:bundle"}
{:cmd "pnpm --dir deps/db-sync build:node-adapter"}])
(defn missing-artifacts
([]
(missing-artifacts (paths/required-artifacts) fs/exists?))
([artifacts file-exists?]
(->> artifacts
(remove file-exists?)
vec)))
(defn run!
[{:keys [skip-build run-command file-exists?]
:or {run-command shell/run!
file-exists? fs/exists?}}]
(if skip-build
{:status :skipped
:commands []
:missing-artifacts []}
(let [artifacts (paths/required-artifacts)]
(doseq [{:keys [cmd]} build-plan]
(run-command {:cmd cmd
:dir (paths/repo-root)}))
(let [missing-after-build (missing-artifacts artifacts file-exists?)]
(when (seq missing-after-build)
(throw (ex-info "Build preflight completed but required artifacts are missing"
{:missing-artifacts missing-after-build})))
{:status :ok
:commands build-plan
:missing-artifacts []}))))

View File

@@ -0,0 +1,18 @@
(ns logseq.cli.e2e.report
(:require [clojure.string :as string]))
(defn format-missing-coverage
[{:keys [missing-commands missing-options]}]
(str
"Missing coverage\n"
(when (seq missing-commands)
(str "Commands:\n"
(string/join "\n" (map #(str "- " %) missing-commands))
"\n"))
(string/join
"\n"
(keep (fn [[scope options]]
(when (seq options)
(str (name scope) " options:\n"
(string/join "\n" (map #(str "- " %) options)))))
missing-options))))

View File

@@ -0,0 +1,280 @@
(ns logseq.cli.e2e.runner
(:require [babashka.fs :as fs]
[cheshire.core :as json]
[clojure.edn :as edn]
[clojure.string :as string]
[logseq.cli.e2e.paths :as paths]
[logseq.cli.e2e.shell :as shell]))
(defn shell-escape
[value]
(let [text (str value)]
(if (re-find #"[^\w@%+=:,./-]" text)
(str "'" (string/replace text #"'" "'\"'\"'") "'")
text)))
(def template-pattern #"\{\{([^}]+)\}\}")
(def ^:private e2e-env {"CLI_E2E_TEST" "1"
"NO_COLOR" "1"})
(defn- render-string
[template context]
(string/replace template
template-pattern
(fn [[_ key]]
(let [lookup-key (keyword (string/trim key))]
(when-not (contains? context lookup-key)
(throw (ex-info "Missing case template value"
{:template template
:key lookup-key
:context context})))
(str (get context lookup-key))))))
(defn- render-value
[value context]
(cond
(string? value) (render-string value context)
(map? value) (into {}
(map (fn [[k v]]
[k (render-value v context)]))
value)
(vector? value) (mapv #(render-value % context) value)
(seq? value) (doall (map #(render-value % context) value))
:else value))
(defn render-case
[case context]
(render-value case context))
(defn- ensure-contains!
[id cmd output snippets stream]
(doseq [snippet (or snippets [])]
(let [normalize-pathish-text (fn [text]
(-> (str text)
(string/replace "\\\\" "\\")
(string/replace "\\" "/")
(string/replace "\r" "")))
matches? (or (string/includes? output snippet)
(string/includes? (normalize-pathish-text output)
(normalize-pathish-text snippet)))]
(when-not matches?
(throw (ex-info (str stream " did not contain expected text")
{:id id
:cmd cmd
:snippet snippet
:output output
:stream stream}))))))
(defn- ensure-not-contains!
[id cmd output snippets stream]
(doseq [snippet (or snippets [])]
(when (string/includes? output snippet)
(throw (ex-info (str stream " contained forbidden text")
{:id id
:cmd cmd
:snippet snippet
:output output
:stream stream})))))
(defn- ensure-equals!
[id cmd actual expected label]
(when (and (some? expected)
(not= expected actual))
(throw (ex-info (str label " did not match")
{:id id
:cmd cmd
:expected expected
:actual actual}))))
(defn- ensure-json-paths!
[id cmd output expected-paths]
(when expected-paths
(let [payload (json/parse-string output true)]
(doseq [[path expected] expected-paths]
(let [actual (get-in payload path)]
(when-not (= expected actual)
(throw (ex-info "stdout JSON path did not match"
{:id id
:cmd cmd
:path path
:expected expected
:actual actual
:payload payload}))))))))
(defn- ensure-edn-paths!
[id cmd output expected-paths]
(when expected-paths
(let [payload (edn/read-string output)]
(doseq [[path expected] expected-paths]
(let [actual (get-in payload path)]
(when-not (= expected actual)
(throw (ex-info "stdout EDN path did not match"
{:id id
:cmd cmd
:path path
:expected expected
:actual actual
:payload payload}))))))))
(defn assert-result!
[{:keys [id expect]} {:keys [cmd exit out err] :as result}]
(when-let [expected-exit (:exit expect)]
(when-not (= expected-exit exit)
(throw (ex-info "Unexpected exit code"
{:id id
:cmd cmd
:expected expected-exit
:actual exit
:result result}))))
(ensure-equals! id cmd out (:stdout-equals expect) "stdout")
(ensure-equals! id cmd err (:stderr-equals expect) "stderr")
(ensure-contains! id cmd out (:stdout-contains expect) "stdout")
(ensure-contains! id cmd err (:stderr-contains expect) "stderr")
(ensure-not-contains! id cmd out (:stdout-not-contains expect) "stdout")
(ensure-not-contains! id cmd err (:stderr-not-contains expect) "stderr")
(ensure-json-paths! id cmd out (:stdout-json-paths expect))
(ensure-edn-paths! id cmd out (:stdout-edn-paths expect))
nil)
(defn- default-context
[case context]
(let [tmp-dir (or (:tmp-dir context)
(str (fs/create-temp-dir {:prefix (str "logseq-cli-e2e-" (:id case) "-")})))
root-dir (or (:root-dir context)
tmp-dir)
config-path (or (:config-path context)
(str (fs/path root-dir "cli.edn")))
export-path (or (:export-path context)
(str (fs/path tmp-dir "graph-export.edn")))
graph (or (:graph context)
(:graph case)
(str "cli-e2e-" (:id case)))]
(fs/create-dirs (fs/path root-dir "graphs"))
(spit config-path "{:output-format :json}\n")
{:tmp-dir tmp-dir
:tmp-dir-arg (shell-escape tmp-dir)
:root-dir root-dir
:root-dir-arg (shell-escape root-dir)
:config-path config-path
:config-path-arg (shell-escape config-path)
:export-path export-path
:export-path-arg (shell-escape export-path)
:repo-root (paths/repo-root)
:repo-root-arg (shell-escape (paths/repo-root))
:cli (str "node " (shell-escape (paths/repo-path "static" "logseq-cli.js")))
:graph graph
:graph-arg (shell-escape graph)}))
(defn- case-context
[case {:keys [context]}]
(merge (default-context case context)
context
(:vars case)))
(defn- run-command!
[command context {:keys [run-command stdin allow-failure phase step-index step-total case-id]}]
(run-command {:cmd (render-string command context)
:dir (paths/repo-root)
:env e2e-env
:stdin (some-> stdin (render-string context))
:phase phase
:step-index step-index
:step-total step-total
:case-id case-id
:throw? (not allow-failure)}))
(defn- elapsed-ms
[started-at]
(long (/ (- (System/nanoTime) started-at) 1000000)))
(defn- run-step!
[timings command context {:keys [timings? phase step-index step-total]
:as run-opts}]
(if-not timings?
(run-command! command context run-opts)
(let [started-at (System/nanoTime)]
(try
(let [result (run-command! command context run-opts)]
(swap! timings conj {:phase phase
:step-index step-index
:step-total step-total
:cmd (:cmd result)
:elapsed-ms (elapsed-ms started-at)
:status :ok})
result)
(catch Exception error
(swap! timings conj {:phase phase
:step-index step-index
:step-total step-total
:cmd (or (:cmd (ex-data error))
(render-string command context))
:elapsed-ms (elapsed-ms started-at)
:status :failed})
(throw error))))))
(defn run-case!
[case {:keys [run-command detailed-log? timings?]
:or {run-command shell/run!}
:as opts}]
(let [context (case-context case opts)
rendered (render-case case context)
case-id (:id rendered)
timings? (boolean timings?)
timings (atom [])
cleanup-commands (vec (:cleanup rendered))
setup-commands (vec (:setup rendered))
main-commands (vec (:cmds rendered))
cleanup! (fn []
(doseq [[idx command] (map-indexed vector cleanup-commands)]
(try
(run-step! timings command context {:run-command run-command
:timings? timings?
:allow-failure true
:phase (when (or detailed-log? timings?) :cleanup)
:step-index (inc idx)
:step-total (count cleanup-commands)
:case-id case-id})
(catch Exception _
nil))))]
(try
(doseq [[idx command] (map-indexed vector setup-commands)]
(run-step! timings command context {:run-command run-command
:timings? timings?
:phase (when (or detailed-log? timings?) :setup)
:step-index (inc idx)
:step-total (count setup-commands)
:case-id case-id}))
(let [main-total (count main-commands)
_ (when (zero? main-total)
(throw (ex-info "Missing case commands"
{:id case-id
:case rendered})))
result (reduce (fn [_ [idx command]]
(let [last-step? (= idx (dec main-total))]
(run-step! timings command context {:run-command run-command
:timings? timings?
:stdin (when last-step? (:stdin rendered))
:allow-failure last-step?
:phase (when (or detailed-log? timings?) :main)
:step-index (inc idx)
:step-total main-total
:case-id case-id})))
nil
(map-indexed vector main-commands))]
(assert-result! rendered result)
(cleanup!)
(cond-> {:id case-id
:status :ok
:cmd (:cmd result)
:result result
:context context}
timings? (assoc :timings @timings)))
(catch Exception error
(cleanup!)
(if timings?
(throw (ex-info (.getMessage error)
(assoc (or (ex-data error) {})
:timings @timings
:case-id case-id)
error))
(throw error))))))

View File

@@ -0,0 +1,67 @@
(ns logseq.cli.e2e.shell
(:require [babashka.process :as process])
(:import [java.io ByteArrayOutputStream OutputStream]))
(defn- utf8-string
[^ByteArrayOutputStream payload]
(.toString payload "UTF-8"))
(defn- capture-stream
[target stream-output?]
(let [payload (ByteArrayOutputStream.)]
{:payload payload
:stream (if stream-output?
(proxy [OutputStream] []
(write
([byte]
(.write payload byte)
(.write target byte)
(.flush target))
([bytes offset length]
(.write payload bytes offset length)
(.write target bytes offset length)
(.flush target)))
(flush []
(.flush target))
(close []
(.flush target)))
payload)}))
(defn- default-executor
[cmd {:keys [dir extra-env in stream-output?]}]
(let [{out-payload :payload
out-stream :stream} (capture-stream System/out stream-output?)
{err-payload :payload
err-stream :stream} (capture-stream System/err stream-output?)
result @(process/process {:continue true
:dir dir
:extra-env extra-env
:in in
:out out-stream
:err err-stream}
"bash"
"-c"
cmd)]
{:exit (:exit result 0)
:out (utf8-string out-payload)
:err (utf8-string err-payload)}))
(defn run!
[{:keys [cmd dir env stdin executor throw? stream-output?]
:or {throw? true}}]
(let [runner (or executor default-executor)
result (runner cmd {:dir dir
:extra-env env
:in stdin
:stream-output? stream-output?})
exit (:exit result 0)
payload {:cmd cmd
:dir dir
:exit exit
:out (:out result "")
:err (:err result "")}]
(when (and throw?
(not (zero? exit)))
(throw (ex-info "Shell command failed"
payload)))
payload))

View File

@@ -0,0 +1,140 @@
(ns logseq.cli.e2e.sync-fixture
(:require [babashka.fs :as fs]
[clojure.string :as string]
[logseq.cli.e2e.paths :as paths]
[logseq.cli.e2e.runner :as runner]
[logseq.cli.e2e.shell :as shell]))
(def default-sync-port "18080")
(def default-e2ee-password "11111")
(def default-user-keys-graph "sync-e2e-user-keys-bootstrap")
(def ^:private heavy-setup-patterns
[#"^mkdir -p '\{\{tmp-dir\}\}/home/logseq'$"
#"^cp ~/logseq/auth\.json\b"
#"prepare_sync_config\.py"
#"db_sync_server\.py'? start"])
(def ^:private heavy-cleanup-patterns
[#"db_sync_server\.py'? stop"])
(defn- shell-quote
[value]
(runner/shell-escape value))
(defn- heavy-command?
[command patterns]
(boolean (some #(re-find % command) patterns)))
(defn- case-local-setup-prefix
[]
["mkdir -p '{{tmp-dir}}/home/logseq'"
"cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'"
"python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{tmp-dir}}/home/logseq/auth.json' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'"
"python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{tmp-dir}}/home/logseq/auth.json' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'"])
(def ^:private case-local-resource-markers
["{{cli-home}}"
"{{config-path}}"
"{{config-path-arg}}"
"{{tmp-dir}}/cli-b.edn"
"{{auth-path}}"
"{{home-dir}}"])
(defn- requires-case-local-resources?
[command]
(boolean (some #(string/includes? command %) case-local-resource-markers)))
(defn- suite-user-keys-bootstrap-command
[suite-tmp-dir]
(let [lock-dir (shell-quote (str (fs/path suite-tmp-dir "user-rsa-keys.lock")))
done-file (shell-quote (str (fs/path suite-tmp-dir "user-rsa-keys.ready")))]
(format (str "LOCK_DIR=%s; DONE_FILE=%s; "
"if [ -f \"$DONE_FILE\" ]; then exit 0; fi; "
"while ! mkdir \"$LOCK_DIR\" 2>/dev/null; do [ -f \"$DONE_FILE\" ] && exit 0; sleep 0.1; done; "
"trap 'rmdir \"$LOCK_DIR\" 2>/dev/null || true' EXIT; "
"if [ ! -f \"$DONE_FILE\" ]; then "
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{user-keys-graph-arg}} >/dev/null 2>/dev/null || true; "
"if {{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{user-keys-graph-arg}} --e2ee-password {{e2ee-password-arg}} --upload-keys >/dev/null; then "
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{user-keys-graph-arg}} >/dev/null 2>/dev/null || true; "
"touch \"$DONE_FILE\"; "
"else exit 1; fi; "
"fi")
lock-dir
done-file)))
(defn before-suite!
[{:keys [run-command sync-port]
:or {run-command shell/run!
sync-port default-sync-port}}]
(let [sync-port (str sync-port)
suite-tmp-dir (str (fs/create-temp-dir {:prefix "logseq-cli-e2e-sync-suite-"}))
db-sync-pid-file (str (fs/path suite-tmp-dir "db-sync-server.pid"))
db-sync-log-file (str (fs/path suite-tmp-dir "db-sync-server.log"))
db-sync-root-dir (str (fs/path suite-tmp-dir "db-sync-server-data"))
sync-http-base (str "http://127.0.0.1:" sync-port)
sync-ws-url (str "ws://127.0.0.1:" sync-port "/sync/%s")
auth-path (str (fs/path (System/getProperty "user.home") "logseq" "auth.json"))
start-db-sync-cmd (format "python3 %s start --repo-root %s --pid-file %s --log-file %s --data-dir %s --port %s --startup-timeout-s 60 --auth-path %s"
(shell-quote (paths/repo-path "cli-e2e" "scripts" "db_sync_server.py"))
(shell-quote (paths/repo-root))
(shell-quote db-sync-pid-file)
(shell-quote db-sync-log-file)
(shell-quote db-sync-root-dir)
sync-port
(shell-quote auth-path))]
(run-command {:cmd start-db-sync-cmd
:dir (paths/repo-root)})
{:suite-tmp-dir suite-tmp-dir
:db-sync-pid-file db-sync-pid-file
:db-sync-log-file db-sync-log-file
:db-sync-root-dir db-sync-root-dir
:sync-port sync-port
:sync-http-base sync-http-base
:sync-ws-url sync-ws-url}))
(defn prepare-case
[case {:keys [suite-tmp-dir sync-port sync-http-base sync-ws-url e2ee-password]}]
(let [e2ee-password (or e2ee-password default-e2ee-password)
user-keys-graph (or (get-in case [:vars :user-keys-graph])
default-user-keys-graph)
setup-commands (vec (:setup case))
insertion-point? (fn [command]
(or (heavy-command? command heavy-setup-patterns)
(requires-case-local-resources? command)))
leading-setup (->> setup-commands
(take-while #(not (insertion-point? %)))
vec)
trailing-setup (->> setup-commands
(drop (count leading-setup))
(remove #(heavy-command? % heavy-setup-patterns))
vec)
bootstrap-setup (when suite-tmp-dir
[(suite-user-keys-bootstrap-command suite-tmp-dir)])
cleanup' (->> (:cleanup case)
(remove #(heavy-command? % heavy-cleanup-patterns))
vec)]
(-> case
(update :vars merge {:sync-port sync-port
:sync-http-base sync-http-base
:sync-ws-url sync-ws-url
:e2ee-password e2ee-password
:e2ee-password-arg (shell-quote e2ee-password)
:user-keys-graph-arg (shell-quote user-keys-graph)})
(assoc :setup (vec (concat leading-setup
(case-local-setup-prefix)
bootstrap-setup
trailing-setup)))
(assoc :cleanup cleanup'))))
(defn after-suite!
[{:keys [db-sync-pid-file]}
{:keys [run-command]
:or {run-command shell/run!}}]
(when (and (string? db-sync-pid-file)
(not (string/blank? db-sync-pid-file)))
(run-command {:cmd (format "python3 %s stop --pid-file %s"
(shell-quote (paths/repo-path "cli-e2e" "scripts" "db_sync_server.py"))
(shell-quote db-sync-pid-file))
:dir (paths/repo-root)
:throw? false})))

View File

@@ -0,0 +1,23 @@
(ns logseq.cli.e2e.test-runner
(:require [clojure.test :as test]))
(def test-namespaces
'[logseq.cli.e2e.coverage-test
logseq.cli.e2e.preflight-test
logseq.cli.e2e.paths-test
logseq.cli.e2e.shell-test
logseq.cli.e2e.runner-test
logseq.cli.e2e.cleanup-test
logseq.cli.e2e.manifests-test
logseq.cli.e2e.sync-fixture-test
logseq.cli.e2e.main-test])
(defn run!
[_opts]
(doseq [ns-sym test-namespaces]
(require ns-sym))
(let [{:keys [fail error]} (apply test/run-tests test-namespaces)]
(when (pos? (+ fail error))
(throw (ex-info "cli-e2e unit tests failed"
{:fail fail
:error error})))))