enhance(cli-e2e): add sync suite fixtures, timings, and port cleanup

This commit is contained in:
rcmerci
2026-04-11 15:49:45 +08:00
parent 94be3a0bbb
commit 1a5939112a
14 changed files with 1072 additions and 411 deletions

View File

@@ -51,6 +51,60 @@
(is (= [] (:failed-pids result)))
(is (= [] @killed))))
(deftest list-cli-e2e-db-sync-port-pids-filters-port-18080
(let [shell-fn (fn [& _]
{:exit 0
:out (str "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n"
"node 111 me 13u IPv6 0x0 0t0 TCP *:18080 (LISTEN)\n"
"python 222 me 13u IPv4 0x0 0t0 TCP 127.0.0.1:18080 (LISTEN)\n"
"node 333 me 13u IPv6 0x0 0t0 TCP *:18081 (LISTEN)\n")
:err ""})]
(is (= [111 222]
(cleanup/list-cli-e2e-db-sync-port-pids {:shell-fn shell-fn}))))
(testing "returns empty when no listener exists"
(is (= []
(cleanup/list-cli-e2e-db-sync-port-pids {:shell-fn (fn [& _]
{:exit 1
:out ""
:err ""})}))))
(testing "throws when lsof invocation fails"
(is (thrown-with-msg?
clojure.lang.ExceptionInfo
#"Unable to scan db-sync server port listeners"
(cleanup/list-cli-e2e-db-sync-port-pids {:shell-fn (fn [& _]
{:exit 1
:out ""
:err "permission denied"})})))))
(deftest cleanup-db-sync-port-processes-separates-killed-and-failed
(let [killed (atom [])
result (cleanup/cleanup-db-sync-port-processes! {:list-pids-fn (fn [] [44 55])
:kill-pid-fn (fn [pid]
(swap! killed conj pid)
(if (= pid 55)
:failed
:killed))})]
(is (= [44 55] (:found-pids result)))
(is (= [44] (:killed-pids result)))
(is (= [55] (:failed-pids result)))
(is (= [44 55] @killed))))
(deftest cleanup-db-sync-port-processes-dry-run-does-not-kill
(let [killed (atom [])
result (cleanup/cleanup-db-sync-port-processes! {:dry-run true
:list-pids-fn (fn [] [44])
:kill-pid-fn (fn [pid]
(swap! killed conj pid)
:killed)})]
(is (= [44] (:found-pids result)))
(is (true? (:dry-run? result)))
(is (= [44] (:would-kill-pids result)))
(is (= [] (:killed-pids result)))
(is (= [] (:failed-pids result)))
(is (= [] @killed))))
(deftest cleanup-temp-graph-dirs-removes-only-cli-e2e-graphs
(let [tmp-root (fs/create-temp-dir {:prefix "cleanup-e2e-test-"})
matching-graphs (fs/path tmp-root "logseq-cli-e2e-case-a-123" "graphs")

View File

@@ -3,7 +3,8 @@
[clojure.test :refer [deftest is testing]]
[logseq.cli.e2e.cleanup :as cleanup]
[logseq.cli.e2e.main :as main]
[logseq.cli.e2e.manifests :as manifests]))
[logseq.cli.e2e.manifests :as manifests]
[logseq.cli.e2e.sync-fixture :as sync-fixture]))
(def sample-cases
[{:id "global-help"
@@ -168,6 +169,48 @@
[:cases :sync]]
@suite-calls)))))
(deftest run-sync-suite-uses-suite-fixture-once
(let [before-called (atom 0)
after-called (atom 0)
prepared-case-ids (atom [])
run-case-seen (atom [])
sync-inventory {:excluded-command-prefixes ["login" "logout"]
:scopes {:sync {:commands ["sync upload" "sync status"]
:options []}}}
sync-cases [{:id "sync-upload"
:cmds ["node static/logseq-cli.js sync upload"]
:covers {:commands ["sync upload"]}}
{:id "sync-status"
:cmds ["node static/logseq-cli.js sync status"]
:covers {:commands ["sync status"]}}]]
(with-redefs [sync-fixture/before-suite! (fn [_]
(swap! before-called inc)
{:suite :sync})
sync-fixture/prepare-case (fn [case _suite-context]
(swap! prepared-case-ids conj (:id case))
(assoc case :prepared? true))
sync-fixture/after-suite! (fn [_ _]
(swap! after-called inc))]
(let [result (main/run! {:suite :sync
:inventory sync-inventory
:cases sync-cases
:skip-build true
:run-command (fn [_]
{:exit 0
:out ""
:err ""})
:run-case (fn [case _opts]
(swap! run-case-seen conj [(:id case) (:prepared? case)])
{:id (:id case)
:status :ok})})]
(is (= :ok (:status result)))))
(is (= 1 @before-called))
(is (= 1 @after-called))
(is (= ["sync-upload" "sync-status"] @prepared-case-ids))
(is (= [["sync-upload" true]
["sync-status" true]]
@run-case-seen))))
(deftest list-cases-defaults-to-non-sync
(let [selected-suite (atom nil)
output (with-out-str
@@ -212,6 +255,46 @@
(is (string/includes? output "[2/2] ✓ graph-list"))
(is (string/includes? output "Summary: 2 passed, 0 failed"))))
(deftest test-timings-prints-step-details-and-slow-summary
(let [output (with-out-str
(main/test! {:inventory complete-inventory
:cases sample-cases
:include ["smoke"]
:skip-build true
:timings true
:run-command (fn [_]
{:exit 0
:out ""
:err ""})
:run-case (fn [case _opts]
(if (= "global-help" (:id case))
{:id "global-help"
:status :ok
:timings [{:phase :setup
:step-index 1
:step-total 1
:elapsed-ms 12
:status :ok
:cmd "setup-global"}
{:phase :main
:step-index 1
:step-total 1
:elapsed-ms 55
:status :ok
:cmd "main-global"}]}
{:id "graph-list"
:status :ok
:timings [{:phase :main
:step-index 1
:step-total 1
:elapsed-ms 210
:status :ok
:cmd "main-graph-list"}]}))}))]
(is (string/includes? output "==> Step timing enabled (--timings)"))
(is (string/includes? output "step timings:"))
(is (string/includes? output "Slow steps (top 10):"))
(is (string/includes? output "main-graph-list"))))
(deftest test-help-prints-usage-and-skips-execution
(let [ran? (atom false)
result (atom nil)
@@ -232,7 +315,8 @@
(is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test [options]"))
(is (string/includes? output "--skip-build"))
(is (string/includes? output "--include TAG"))
(is (string/includes? output "--case ID"))))
(is (string/includes? output "--case ID"))
(is (string/includes? output "--timings"))))
(deftest test-sync-help-prints-usage-and-skips-execution
(let [ran? (atom false)
@@ -254,7 +338,8 @@
(is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test-sync [options]"))
(is (string/includes? output "--skip-build"))
(is (string/includes? output "--include TAG"))
(is (string/includes? output "--case ID"))))
(is (string/includes? output "--case ID"))
(is (string/includes? output "--timings"))))
(deftest test-single-case-enables-detailed-command-logging
(let [command-opts (atom nil)
@@ -287,12 +372,16 @@
(let [output (with-out-str (main/cleanup! {:help true}))]
(is (string/includes? output "Usage: bb -f cli-e2e/bb.edn cleanup"))
(is (string/includes? output "Terminate cli-e2e db-worker-node processes"))
(is (string/includes? output "Terminate db-sync server listeners on port 18080"))
(is (string/includes? output "--dry-run"))))
(deftest cleanup-prints-summary-and-returns-status
(with-redefs [cleanup/cleanup-db-worker-processes! (fn [_] {:found-pids [101 202]
:killed-pids [101]
:failed-pids [202]})
cleanup/cleanup-db-sync-port-processes! (fn [_] {:found-pids [303]
:killed-pids [303]
:failed-pids []})
cleanup/cleanup-temp-graph-dirs! (fn [_] {:found-dirs ["/tmp/logseq-cli-e2e-a/graphs"
"/tmp/logseq-cli-e2e-b/graphs"]
:removed-dirs ["/tmp/logseq-cli-e2e-a/graphs"]
@@ -302,12 +391,15 @@
(reset! result (main/cleanup! {})))]
(is (= :ok (:status @result)))
(is (= [101] (get-in @result [:processes :killed-pids])))
(is (= [303] (get-in @result [:db-sync-port-processes :killed-pids])))
(is (= ["/tmp/logseq-cli-e2e-a/graphs"] (get-in @result [:temp-graphs :removed-dirs])))
(is (string/includes? output "db-worker-node processes: found 2, killed 1, failed 1"))
(is (string/includes? output "db-sync server processes (port 18080): found 1, killed 1, failed 0"))
(is (string/includes? output "temp graph directories: found 2, removed 1, failed 1")))))
(deftest cleanup-dry-run-prints-summary-and-passes-option
(let [process-opts (atom nil)
db-sync-opts (atom nil)
dir-opts (atom nil)]
(with-redefs [cleanup/cleanup-db-worker-processes! (fn [opts]
(reset! process-opts opts)
@@ -316,6 +408,13 @@
:would-kill-pids [101 202]
:killed-pids []
:failed-pids []})
cleanup/cleanup-db-sync-port-processes! (fn [opts]
(reset! db-sync-opts opts)
{:dry-run? true
:found-pids [303]
:would-kill-pids [303]
:killed-pids []
:failed-pids []})
cleanup/cleanup-temp-graph-dirs! (fn [opts]
(reset! dir-opts opts)
{:dry-run? true
@@ -327,9 +426,12 @@
output (with-out-str
(reset! result (main/cleanup! {:dry-run true})))]
(is (= {:dry-run true} @process-opts))
(is (= {:dry-run true} @db-sync-opts))
(is (= {:dry-run true} @dir-opts))
(is (= :ok (:status @result)))
(is (true? (get-in @result [:processes :dry-run?])))
(is (true? (get-in @result [:db-sync-port-processes :dry-run?])))
(is (true? (get-in @result [:temp-graphs :dry-run?])))
(is (string/includes? output "[dry-run] db-worker-node processes: found 2, would kill 2"))
(is (string/includes? output "[dry-run] db-sync server processes (port 18080): found 1, would kill 1"))
(is (string/includes? output "[dry-run] temp graph directories: found 1, would remove 1"))))))

View File

@@ -0,0 +1,78 @@
(ns logseq.cli.e2e.manifests-test
(:require [clojure.test :refer [deftest is testing]]
[logseq.cli.e2e.manifests :as manifests]))
(deftest load-cases-supports-legacy-vector-format
(with-redefs [manifests/read-edn-file (fn [_]
[{:id "legacy-a"}
{:id "legacy-b"}])]
(is (= ["legacy-a" "legacy-b"]
(mapv :id (manifests/load-cases :non-sync))))))
(deftest load-cases-supports-templates-and-inheritance
(with-redefs [manifests/read-edn-file (fn [_]
{:templates
{:base {:setup ["setup-a"]
:cmds ["cmd-a"]
:cleanup ["cleanup-a"]
:tags [:base]
:vars {:nested {:left 1}
:only-base true}
:covers {:commands ["base-command"]
:options {:global ["--base"]}}
:expect {:stdout-json-paths {[:status] "ok"}}
:graph "base-graph"}
:addon {:setup ["setup-b"]
:cmds ["cmd-b"]
:cleanup ["cleanup-b"]
:tags [:addon]
:vars {:nested {:right 2}}
:covers {:options {:graph ["--addon"]}}
:expect {:stdout-json-paths {[:data :x] 1}}
:graph "addon-graph"}}
:cases
[{:id "templated"
:extends [:base :addon]
:setup ["setup-case"]
:cmds ["cmd-case"]
:cleanup ["cleanup-case"]
:tags [:case]
:vars {:nested {:leaf 3}
:only-case true}
:covers {:options {:graph ["--case"]}}
:expect {:stdout-json-paths {[:data :y] 2}}
:graph "case-graph"}]})]
(let [case (first (manifests/load-cases :sync))]
(testing "append merge keys"
(is (= ["setup-a" "setup-b" "setup-case"] (:setup case)))
(is (= ["cmd-a" "cmd-b" "cmd-case"] (:cmds case)))
(is (= ["cleanup-a" "cleanup-b" "cleanup-case"] (:cleanup case)))
(is (= [:base :addon :case] (:tags case))))
(testing "deep merge keys"
(is (= {:nested {:left 1 :right 2 :leaf 3}
:only-base true
:only-case true}
(:vars case)))
(is (= {:commands ["base-command"]
:options {:global ["--base"]
:graph ["--case"]}}
(:covers case)))
(is (= {[:status] "ok"
[:data :x] 1
[:data :y] 2}
(get-in case [:expect :stdout-json-paths]))))
(testing "scalar child override"
(is (= "case-graph" (:graph case)))))))
(deftest load-cases-detects-circular-template-inheritance
(with-redefs [manifests/read-edn-file (fn [_]
{:templates
{:a {:extends :b
:setup ["a"]}
:b {:extends :a
:setup ["b"]}}
:cases [{:id "cycle" :extends :a}]})]
(is (thrown-with-msg?
clojure.lang.ExceptionInfo
#"Circular template inheritance"
(manifests/load-cases :sync)))))

View File

@@ -57,6 +57,50 @@
{:cmd "cleanup one" :phase :cleanup :step-index 1 :step-total 1 :case-id "graph-info" :throw? false}]
@calls))))
(deftest run-case-collects-step-timings-when-enabled
(let [result (runner/run-case!
{:id "graph-info"
:setup ["setup one"]
:cmds ["main command"]
:cleanup ["cleanup one"]
:expect {:exit 0}}
{:context {}
:timings? true
:run-command (fn [{:keys [cmd]}]
{:cmd cmd
:exit 0
:out ""
:err ""})})]
(is (= 3 (count (:timings result))))
(is (= [:setup :main :cleanup]
(mapv :phase (:timings result))))))
(deftest run-case-attaches-timings-to-error-when-enabled
(let [error (try
(runner/run-case!
{:id "graph-info"
:setup ["setup one"]
:cmds ["main command"]
:cleanup ["cleanup one"]
:expect {:exit 0}}
{:context {}
:timings? true
:run-command (fn [{:keys [cmd]}]
(when (= cmd "main command")
(throw (ex-info "boom" {:cmd cmd})))
{:cmd cmd
:exit 0
:out ""
:err ""})})
nil
(catch clojure.lang.ExceptionInfo error
error))
timings (:timings (ex-data error))]
(is (= "graph-info" (:case-id (ex-data error))))
(is (= [:setup :main :cleanup]
(mapv :phase timings)))
(is (= :failed (:status (second timings))))))
(deftest run-case-validates-json-paths-and-nonzero-exit
(let [result (runner/run-case!
{:id "invalid-shell"

View File

@@ -0,0 +1,52 @@
(ns logseq.cli.e2e.sync-fixture-test
(:require [clojure.string :as string]
[clojure.test :refer [deftest is]]
[logseq.cli.e2e.sync-fixture :as sync-fixture]))
(deftest prepare-case-removes-heavy-steps-and-injects-lightweight-ones
(let [suite-context {:suite-auth-path "/tmp/suite/auth.json"
:suite-config-path "/tmp/suite/config.edn"
:sync-port "18080"
:sync-http-base "http://127.0.0.1:18080"
:sync-ws-url "ws://127.0.0.1:18080/sync/%s"}
input-case {:id "sync-case"
:vars {:existing true}
:setup ["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}}'"
"python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --port 18080"
"{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"]
:cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"
"python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' stop --pid-file '{{tmp-dir}}/db-sync-server.pid'"]}
prepared (sync-fixture/prepare-case input-case suite-context)]
(is (= "sync-case" (:id prepared)))
(is (= 5 (count (:setup prepared))))
(is (= "mkdir -p '{{tmp-dir}}/home/logseq'" (first (:setup prepared))))
(is (string/includes? (second (:setup prepared)) "suite-auth-path"))
(is (not-any? #(string/includes? % "prepare_sync_config.py") (:setup prepared)))
(is (not-any? #(string/includes? % "db_sync_server.py' start") (:setup prepared)))
(is (= ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]
(:cleanup prepared)))
(is (= "/tmp/suite/auth.json" (get-in prepared [:vars :suite-auth-path])))
(is (= "http://127.0.0.1:18080" (get-in prepared [:vars :sync-http-base])))))
(deftest before-and-after-suite-run-expected-commands
(let [calls (atom [])
run-command (fn [opts]
(swap! calls conj opts)
{:exit 0
:out ""
:err ""})
suite-context (sync-fixture/before-suite! {:run-command run-command})]
(is (= 3 (count @calls)))
(is (string/includes? (:cmd (first @calls)) "cp ~/logseq/auth.json"))
(is (string/includes? (:cmd (second @calls)) "prepare_sync_config.py"))
(is (string/includes? (:cmd (nth @calls 2)) "db_sync_server.py"))
(is (string/includes? (:cmd (nth @calls 2)) " start "))
(is (string/includes? (:cmd (nth @calls 2)) "--port 18080"))
(is (string/includes? (:cmd (nth @calls 2)) "--startup-timeout-s 60"))
(sync-fixture/after-suite! suite-context {:run-command run-command})
(is (= 4 (count @calls)))
(is (string/includes? (:cmd (last @calls)) "db_sync_server.py"))
(is (string/includes? (:cmd (last @calls)) " stop "))
(is (false? (:throw? (last @calls))))))