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:
149
cli-e2e/test/logseq/cli/e2e/cleanup_test.clj
Normal file
149
cli-e2e/test/logseq/cli/e2e/cleanup_test.clj
Normal file
@@ -0,0 +1,149 @@
|
||||
(ns logseq.cli.e2e.cleanup-test
|
||||
(:require [babashka.fs :as fs]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.cleanup :as cleanup]))
|
||||
|
||||
(deftest list-cli-e2e-db-worker-pids-filters-processes
|
||||
(let [shell-fn (fn [& _]
|
||||
{:exit 0
|
||||
:out (str " 101 node /repo/dist/db-worker-node.js --repo graph-a --root-dir /tmp/logseq-cli-e2e-graph-a-123/graphs --owner-source cli\n"
|
||||
" 202 node /repo/dist/db-worker-node.js --repo production --root-dir /tmp/production-graphs --owner-source cli\n"
|
||||
" 303 node /repo/static/logseq-cli.js graph list\n"
|
||||
" 404 /usr/bin/python3 background.py\n"
|
||||
" 505 node /repo/static/db-worker-node.js --repo graph-b --root-dir /private/tmp/logseq-cli-e2e-graph-b-999/graphs --owner-source cli\n")
|
||||
:err ""})]
|
||||
(is (= [101 505]
|
||||
(cleanup/list-cli-e2e-db-worker-pids {:shell-fn shell-fn}))))
|
||||
|
||||
(testing "throws when ps invocation fails"
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Unable to scan processes"
|
||||
(cleanup/list-cli-e2e-db-worker-pids {:shell-fn (fn [& _]
|
||||
{:exit 1
|
||||
:out ""
|
||||
:err "permission denied"})})))))
|
||||
|
||||
(deftest cleanup-db-worker-processes-separates-killed-and-failed
|
||||
(let [killed (atom [])
|
||||
result (cleanup/cleanup-db-worker-processes! {:list-pids-fn (fn [] [11 22 33])
|
||||
:kill-pid-fn (fn [pid]
|
||||
(swap! killed conj pid)
|
||||
(if (= pid 22)
|
||||
:failed
|
||||
:killed))})]
|
||||
(is (= [11 22 33] (:found-pids result)))
|
||||
(is (= [11 33] (:killed-pids result)))
|
||||
(is (= [22] (:failed-pids result)))
|
||||
(is (= [11 22 33] @killed))))
|
||||
|
||||
(deftest cleanup-db-worker-processes-dry-run-does-not-kill
|
||||
(let [killed (atom [])
|
||||
result (cleanup/cleanup-db-worker-processes! {:dry-run true
|
||||
:list-pids-fn (fn [] [11 22])
|
||||
:kill-pid-fn (fn [pid]
|
||||
(swap! killed conj pid)
|
||||
:killed)})]
|
||||
(is (= [11 22] (:found-pids result)))
|
||||
(is (true? (:dry-run? result)))
|
||||
(is (= [11 22] (:would-kill-pids result)))
|
||||
(is (= [] (:killed-pids result)))
|
||||
(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-roots-removes-only-cli-e2e-temp-roots
|
||||
(let [tmp-root (fs/create-temp-dir {:prefix "cleanup-e2e-test-"})
|
||||
matching-root (fs/path tmp-root "logseq-cli-e2e-case-a-123")
|
||||
another-matching-root (fs/path tmp-root "logseq-cli-e2e-case-b-456")
|
||||
non-matching-root (fs/path tmp-root "other-case")]
|
||||
(try
|
||||
(fs/create-dirs (fs/path matching-root "graphs"))
|
||||
(fs/create-dirs (fs/path matching-root "home" "logseq"))
|
||||
(fs/create-dirs (fs/path another-matching-root "graphs"))
|
||||
(fs/create-dirs (fs/path non-matching-root "graphs"))
|
||||
|
||||
(let [result (cleanup/cleanup-temp-roots! {:tmp-root (str tmp-root)})
|
||||
expected-dirs (sort [(str matching-root)
|
||||
(str another-matching-root)])]
|
||||
(is (= expected-dirs
|
||||
(sort (:found-dirs result))))
|
||||
(is (= expected-dirs
|
||||
(sort (:removed-dirs result))))
|
||||
(is (empty? (:failed-dirs result)))
|
||||
(is (false? (fs/exists? matching-root)))
|
||||
(is (false? (fs/exists? another-matching-root)))
|
||||
(is (true? (fs/exists? non-matching-root))))
|
||||
(finally
|
||||
(fs/delete-tree tmp-root)))))
|
||||
|
||||
(deftest cleanup-temp-roots-dry-run-does-not-delete
|
||||
(let [deleted (atom [])
|
||||
result (cleanup/cleanup-temp-roots! {:dry-run true
|
||||
:list-dirs-fn (fn [] ["/tmp/logseq-cli-e2e-a"
|
||||
"/tmp/logseq-cli-e2e-b"])
|
||||
:delete-dir-fn (fn [dir]
|
||||
(swap! deleted conj dir))})]
|
||||
(is (= ["/tmp/logseq-cli-e2e-a"
|
||||
"/tmp/logseq-cli-e2e-b"]
|
||||
(:found-dirs result)))
|
||||
(is (true? (:dry-run? result)))
|
||||
(is (= ["/tmp/logseq-cli-e2e-a"
|
||||
"/tmp/logseq-cli-e2e-b"]
|
||||
(:would-remove-dirs result)))
|
||||
(is (= [] (:removed-dirs result)))
|
||||
(is (= [] (:failed-dirs result)))
|
||||
(is (= [] @deleted))))
|
||||
97
cli-e2e/test/logseq/cli/e2e/compare_graph_queries_test.py
Normal file
97
cli-e2e/test/logseq/cli/e2e/compare_graph_queries_test.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[4] / "scripts" / "compare_graph_queries.py"
|
||||
spec = importlib.util.spec_from_file_location("compare_graph_queries", MODULE_PATH)
|
||||
compare_graph_queries = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(compare_graph_queries)
|
||||
|
||||
|
||||
def test_parse_args_supports_repeated_queries(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
compare_graph_queries.sys,
|
||||
"argv",
|
||||
[
|
||||
"compare_graph_queries.py",
|
||||
"--cli",
|
||||
"/tmp/logseq-cli.js",
|
||||
"--graph",
|
||||
"demo",
|
||||
"--config-a",
|
||||
"/tmp/a.edn",
|
||||
"--root-dir-a",
|
||||
"/tmp/a",
|
||||
"--config-b",
|
||||
"/tmp/b.edn",
|
||||
"--root-dir-b",
|
||||
"/tmp/b",
|
||||
"--query",
|
||||
"[:find ?x]",
|
||||
"--query",
|
||||
"[:find ?y]",
|
||||
],
|
||||
)
|
||||
|
||||
args = compare_graph_queries.parse_args()
|
||||
|
||||
assert args.root_dir_a == "/tmp/a"
|
||||
assert args.root_dir_b == "/tmp/b"
|
||||
assert args.query == ["[:find ?x]", "[:find ?y]"]
|
||||
|
||||
|
||||
def test_main_batches_multiple_queries_in_one_process(tmp_path: Path) -> None:
|
||||
left_queries = []
|
||||
right_queries = []
|
||||
printed = []
|
||||
cli_path = tmp_path / "logseq-cli.js"
|
||||
cli_path.write_text("// mock cli\n")
|
||||
|
||||
def fake_run_query(cli_path, config_path, root_dir, graph, query):
|
||||
record = {
|
||||
"cli_path": str(cli_path),
|
||||
"config_path": str(config_path),
|
||||
"root_dir": str(root_dir),
|
||||
"graph": graph,
|
||||
"query": query,
|
||||
}
|
||||
if str(config_path).endswith("a.edn"):
|
||||
left_queries.append(record)
|
||||
else:
|
||||
right_queries.append(record)
|
||||
return {
|
||||
"payload": {"status": "ok"},
|
||||
"result": [{"title": query}],
|
||||
}
|
||||
|
||||
compare_graph_queries.run_query = fake_run_query
|
||||
compare_graph_queries.parse_args = lambda: SimpleNamespace(
|
||||
cli=str(cli_path),
|
||||
graph="demo",
|
||||
query=["[:find ?x]", "[:find ?y]"],
|
||||
config_a="/tmp/a.edn",
|
||||
root_dir_a="/tmp/a",
|
||||
config_b="/tmp/b.edn",
|
||||
root_dir_b="/tmp/b",
|
||||
require_result=False,
|
||||
)
|
||||
compare_graph_queries.print = lambda value, **kwargs: printed.append(json.loads(value))
|
||||
|
||||
compare_graph_queries.main()
|
||||
|
||||
assert [item["query"] for item in left_queries] == ["[:find ?x]", "[:find ?y]"]
|
||||
assert [item["query"] for item in right_queries] == ["[:find ?x]", "[:find ?y]"]
|
||||
assert printed == [
|
||||
{
|
||||
"status": "ok",
|
||||
"results": {
|
||||
"[:find ?x]": [{"title": "[:find ?x]"}],
|
||||
"[:find ?y]": [{"title": "[:find ?y]"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
58
cli-e2e/test/logseq/cli/e2e/coverage_test.clj
Normal file
58
cli-e2e/test/logseq/cli/e2e/coverage_test.clj
Normal file
@@ -0,0 +1,58 @@
|
||||
(ns logseq.cli.e2e.coverage-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.coverage :as coverage]
|
||||
[logseq.cli.e2e.manifests :as manifests]))
|
||||
|
||||
(def sample-inventory
|
||||
{:excluded-command-prefixes ["sync" "login" "logout"]
|
||||
:scopes {:global {:options ["--help" "--version"]}
|
||||
:graph {:commands ["graph list" "graph create"]
|
||||
:options ["--file" "--type"]}}})
|
||||
|
||||
(deftest reports-missing-commands-and-options
|
||||
(let [cases [{:id "global-help"
|
||||
:covers {:options {:global ["--help"]}}}
|
||||
{:id "graph-list"
|
||||
:covers {:commands ["graph list"]
|
||||
:options {:graph ["--file"]}}}]
|
||||
report (coverage/coverage-report sample-inventory cases)]
|
||||
(is (= ["graph create"] (:missing-commands report)))
|
||||
(is (= ["--version"] (get-in report [:missing-options :global])))
|
||||
(is (= ["--type"] (get-in report [:missing-options :graph])))))
|
||||
|
||||
(deftest rejects-excluded-commands-in-inventory
|
||||
(let [inventory (assoc-in sample-inventory [:scopes :bad :commands] ["login"])]
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Excluded commands"
|
||||
(coverage/validate-inventory! inventory)))))
|
||||
|
||||
(deftest rejects-excluded-commands-in-case-covers
|
||||
(let [cases [{:id "bad-login"
|
||||
:covers {:commands ["logout"]}}]]
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Excluded commands"
|
||||
(coverage/validate-cases! sample-inventory cases)))))
|
||||
|
||||
(deftest complete-coverage-is-recognized
|
||||
(let [cases [{:id "global"
|
||||
:covers {:options {:global ["--help" "--version"]}}}
|
||||
{:id "graph-create"
|
||||
:covers {:commands ["graph create"]
|
||||
:options {:graph ["--type"]}}}
|
||||
{:id "graph-list"
|
||||
:covers {:commands ["graph list"]
|
||||
:options {:graph ["--file"]}}}]
|
||||
report (coverage/coverage-report sample-inventory cases)]
|
||||
(is (coverage/complete? report))))
|
||||
|
||||
(deftest sync-suite-manifests-cover-mvp-commands
|
||||
(let [inventory (manifests/load-inventory :sync)
|
||||
cases (manifests/load-cases :sync)
|
||||
covered-commands (set (mapcat #(get-in % [:covers :commands]) cases))
|
||||
report (coverage/coverage-report inventory cases)]
|
||||
(is (contains? covered-commands "sync upload"))
|
||||
(is (contains? covered-commands "sync download"))
|
||||
(is (contains? covered-commands "sync status"))
|
||||
(is (coverage/complete? report))))
|
||||
798
cli-e2e/test/logseq/cli/e2e/main_test.clj
Normal file
798
cli-e2e/test/logseq/cli/e2e/main_test.clj
Normal file
@@ -0,0 +1,798 @@
|
||||
(ns logseq.cli.e2e.main-test
|
||||
(:require [babashka.cli :as cli]
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is]]
|
||||
[logseq.cli.e2e.cleanup :as cleanup]
|
||||
[logseq.cli.e2e.main :as main]
|
||||
[logseq.cli.e2e.manifests :as manifests]
|
||||
[logseq.cli.e2e.sync-fixture :as sync-fixture]))
|
||||
|
||||
(def sample-cases
|
||||
[{:id "global-help"
|
||||
:cmds ["node static/logseq-cli.js --help"]
|
||||
:covers {:options {:global ["--help"]}}
|
||||
:tags [:global :smoke]}
|
||||
{:id "graph-create"
|
||||
:cmds ["node static/logseq-cli.js graph create --graph demo"]
|
||||
:covers {:commands ["graph create"]
|
||||
:options {:graph ["--type"]}}
|
||||
:tags [:graph]}
|
||||
{:id "graph-list"
|
||||
:cmds ["node static/logseq-cli.js graph list"]
|
||||
:covers {:commands ["graph list"]
|
||||
:options {:graph ["--file"]}}
|
||||
:tags [:graph :smoke]}])
|
||||
|
||||
(def complete-inventory
|
||||
{:excluded-command-prefixes ["sync" "login" "logout"]
|
||||
:scopes {:global {:options ["--help"]}
|
||||
:graph {:commands ["graph create" "graph list"]
|
||||
:options ["--type" "--file"]}}})
|
||||
|
||||
(def cli-parse-config
|
||||
{:alias {:i :include
|
||||
:h :help}
|
||||
:spec {:jobs {:default 4}}
|
||||
:coerce {:include []
|
||||
:help :boolean
|
||||
:dry-run :boolean
|
||||
:skip-build :boolean
|
||||
:verbose :boolean
|
||||
:timings :boolean
|
||||
:jobs :long}})
|
||||
|
||||
(deftest cli-opts-parses-jobs-as-integer
|
||||
(is (= 3
|
||||
(:jobs (cli/parse-opts ["--jobs" "3"] cli-parse-config)))))
|
||||
|
||||
(deftest cli-opts-defaults-jobs-to-four
|
||||
(is (= 4
|
||||
(:jobs (cli/parse-opts [] cli-parse-config)))))
|
||||
|
||||
(deftest cli-opts-rejects-non-integer-jobs
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Coerce failure"
|
||||
(cli/parse-opts ["--jobs" "nope"] cli-parse-config))))
|
||||
|
||||
(deftest run-rejects-jobs-less-than-one
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"--jobs must be a positive integer"
|
||||
(main/run! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:skip-build true
|
||||
:jobs 0
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})}))))
|
||||
|
||||
(deftest run-non-sync-uses-parallel-runner-when-jobs-greater-than-one
|
||||
(let [parallel-call (atom nil)
|
||||
serial-called? (atom false)]
|
||||
(with-redefs [main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts]
|
||||
(reset! parallel-call {:case-ids (mapv :id selected-cases)
|
||||
:run-case run-case
|
||||
:run-command run-command
|
||||
:jobs (:jobs opts)})
|
||||
[{:id "global-help" :status :ok}
|
||||
{:id "graph-list" :status :ok}])
|
||||
main/run-selected-cases! (fn [& _]
|
||||
(reset! serial-called? true)
|
||||
(throw (ex-info "serial runner should not be used" {})))]
|
||||
(let [result (main/run! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:jobs 2
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["global-help" "graph-list"] (:case-ids @parallel-call)))
|
||||
(is (= 2 (:jobs @parallel-call)))
|
||||
(is (false? @serial-called?))))))
|
||||
|
||||
(deftest run-sync-suite-uses-parallel-runner-when-jobs-greater-than-one
|
||||
(let [parallel-call (atom nil)
|
||||
serial-called? (atom false)
|
||||
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 [main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts]
|
||||
(reset! parallel-call {:case-ids (mapv :id selected-cases)
|
||||
:run-case run-case
|
||||
:run-command run-command
|
||||
:jobs (:jobs opts)})
|
||||
(mapv (fn [case]
|
||||
{:id (:id case)
|
||||
:status :ok})
|
||||
selected-cases))
|
||||
main/run-selected-cases! (fn [& _]
|
||||
(reset! serial-called? true)
|
||||
(throw (ex-info "serial runner should not be used" {})))]
|
||||
(let [result (main/run! {:suite :sync
|
||||
:inventory sync-inventory
|
||||
:cases sync-cases
|
||||
:skip-build true
|
||||
:jobs 4
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["sync-upload" "sync-status"] (:case-ids @parallel-call)))
|
||||
(is (= 4 (:jobs @parallel-call)))
|
||||
(is (false? @serial-called?))))))
|
||||
|
||||
(deftest run-jobs-one-keeps-serial-runner
|
||||
(let [serial-call (atom nil)
|
||||
parallel-called? (atom false)]
|
||||
(with-redefs [main/run-selected-cases! (fn [selected-cases run-case run-command opts]
|
||||
(reset! serial-call {:case-ids (mapv :id selected-cases)
|
||||
:run-case run-case
|
||||
:run-command run-command
|
||||
:jobs (:jobs opts)})
|
||||
(mapv (fn [case]
|
||||
{:id (:id case)
|
||||
:status :ok})
|
||||
selected-cases))
|
||||
main/run-selected-cases-in-parallel! (fn [& _]
|
||||
(reset! parallel-called? true)
|
||||
(throw (ex-info "parallel runner should not be used" {})))]
|
||||
(let [result (main/run! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:jobs 1
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["global-help" "graph-list"] (:case-ids @serial-call)))
|
||||
(is (= 1 (:jobs @serial-call)))
|
||||
(is (false? @parallel-called?))))))
|
||||
|
||||
(deftest parallel-runner-collects-completions-before-rethrowing-failure
|
||||
(let [started (atom [])
|
||||
finished (atom [])
|
||||
started-latch (java.util.concurrent.CountDownLatch. 2)
|
||||
release-success (promise)
|
||||
cases [{:id "global-help"}
|
||||
{:id "graph-list"}]
|
||||
error (try
|
||||
(main/run-selected-cases-in-parallel!
|
||||
cases
|
||||
(fn [case _opts]
|
||||
(swap! started conj (:id case))
|
||||
(.countDown started-latch)
|
||||
(.await started-latch)
|
||||
(if (= "graph-list" (:id case))
|
||||
(do
|
||||
(swap! finished conj [:failed (:id case)])
|
||||
(deliver release-success true)
|
||||
(throw (ex-info "boom" {:id (:id case)})))
|
||||
(do
|
||||
@release-success
|
||||
(swap! finished conj [:ok (:id case)])
|
||||
{:id (:id case)
|
||||
:status :ok})))
|
||||
(fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
{:jobs 2})
|
||||
nil
|
||||
(catch Exception ex
|
||||
ex))]
|
||||
(is (instance? Exception error))
|
||||
(is (= #{"global-help" "graph-list"}
|
||||
(set @started)))
|
||||
(is (= #{[:ok "global-help"]
|
||||
[:failed "graph-list"]}
|
||||
(set @finished)))))
|
||||
|
||||
(deftest parallel-runner-elapsed-ms-starts-when-case-begins-running
|
||||
(let [events (atom [])]
|
||||
(main/run-selected-cases-in-parallel!
|
||||
[{:id "slow-1"}
|
||||
{:id "slow-2"}
|
||||
{:id "fast-after-queue"}]
|
||||
(fn [test-case _opts]
|
||||
(clojure.core/case (:id test-case)
|
||||
"slow-1" (Thread/sleep 180)
|
||||
"slow-2" (Thread/sleep 180)
|
||||
"fast-after-queue" (Thread/sleep 10))
|
||||
{:id (:id test-case)
|
||||
:status :ok})
|
||||
(fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
{:jobs 2
|
||||
:on-case-success (fn [payload]
|
||||
(swap! events conj [(:id (:result payload)) (:elapsed-ms payload)]))})
|
||||
(let [elapsed-map (into {} @events)]
|
||||
(is (< (get elapsed-map "fast-after-queue" 1000) 120)
|
||||
"queued case should measure only its own execution time, not time spent waiting in the pool")
|
||||
(is (>= (get elapsed-map "slow-1" 0) 150))
|
||||
(is (>= (get elapsed-map "slow-2" 0) 150)))))
|
||||
|
||||
(deftest select-cases-supports-case-id
|
||||
(is (= ["graph-create"]
|
||||
(mapv :id (main/select-cases sample-cases {:case "graph-create"})))))
|
||||
|
||||
(deftest select-cases-supports-include-tags
|
||||
(is (= ["global-help" "graph-list"]
|
||||
(mapv :id (main/select-cases sample-cases {:include ["smoke"]})))))
|
||||
|
||||
(deftest select-cases-omits-skipped-cases
|
||||
(let [cases (conj sample-cases
|
||||
{:id "stale-fallback"
|
||||
:cmds ["node static/logseq-cli.js debug pull --id 1"]
|
||||
:skip "Requires a removed current graph fallback"
|
||||
:covers {:commands ["debug pull"]}
|
||||
:tags [:debug :smoke]})]
|
||||
(is (= ["global-help" "graph-create" "graph-list"]
|
||||
(mapv :id (main/select-cases cases {}))))
|
||||
(is (= ["global-help" "graph-list"]
|
||||
(mapv :id (main/select-cases cases {:include ["smoke"]}))))
|
||||
(is (= []
|
||||
(mapv :id (main/select-cases cases {:case "stale-fallback"}))))))
|
||||
|
||||
(deftest run-fails-on-missing-coverage-before-case-execution
|
||||
(let [ran? (atom false)]
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Missing coverage"
|
||||
(main/run! {:inventory complete-inventory
|
||||
:cases [{:id "global-help"
|
||||
:cmds ["node static/logseq-cli.js --help"]
|
||||
:covers {:options {:global ["--help"]}}}]
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
(reset! ran? true)
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})})))
|
||||
(is (false? @ran?))))
|
||||
|
||||
(deftest run-succeeds-when-coverage-is-complete
|
||||
(let [result (main/run! {:inventory complete-inventory
|
||||
:cases [{:id "global-help"
|
||||
:cmds ["node static/logseq-cli.js --help"]
|
||||
:covers {:options {:global ["--help"]}}}
|
||||
{:id "graph-create"
|
||||
:cmds ["node static/logseq-cli.js graph create --type markdown"]
|
||||
:covers {:commands ["graph create"]
|
||||
:options {:graph ["--type"]}}}
|
||||
{:id "graph-list"
|
||||
:cmds ["node static/logseq-cli.js graph list --file demo.edn"]
|
||||
:covers {:commands ["graph list"]
|
||||
:options {:graph ["--file"]}}}]
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= 3 (count (:cases result))))))
|
||||
|
||||
(deftest targeted-run-skips-full-coverage-check
|
||||
(let [executed (atom [])]
|
||||
(let [result (main/run! {:inventory complete-inventory
|
||||
:cases [{:id "global-help"
|
||||
:cmds ["node static/logseq-cli.js --help"]
|
||||
:covers {:options {:global ["--help"]}}}
|
||||
{:id "graph-create"
|
||||
:cmds ["node static/logseq-cli.js graph create --graph demo"]
|
||||
:covers {:commands ["graph create"]
|
||||
:options {:graph ["--type"]}}}]
|
||||
:case "global-help"
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
(swap! executed conj (:id case))
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["global-help"] @executed)))))
|
||||
|
||||
(deftest run-invokes-case-progress-hooks
|
||||
(let [events (atom [])]
|
||||
(main/run! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})
|
||||
:on-case-start (fn [{:keys [index total case]}]
|
||||
(swap! events conj [:start index total (:id case)]))
|
||||
:on-case-success (fn [{:keys [index total result]}]
|
||||
(swap! events conj [:ok index total (:id result)]))})
|
||||
(is (= [[:start 1 2 "global-help"]
|
||||
[:ok 1 2 "global-help"]
|
||||
[:start 2 2 "graph-list"]
|
||||
[:ok 2 2 "graph-list"]]
|
||||
@events))))
|
||||
|
||||
(deftest run-loads-non-sync-suite-by-default
|
||||
(let [suite-calls (atom [])]
|
||||
(with-redefs [manifests/load-inventory (fn [suite]
|
||||
(swap! suite-calls conj [:inventory suite])
|
||||
complete-inventory)
|
||||
manifests/load-cases (fn [suite]
|
||||
(swap! suite-calls conj [:cases suite])
|
||||
sample-cases)]
|
||||
(let [result (main/run! {:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result))))
|
||||
(is (= [[:inventory :non-sync]
|
||||
[:cases :non-sync]]
|
||||
@suite-calls)))))
|
||||
|
||||
(deftest run-loads-sync-suite-when-explicit
|
||||
(let [suite-calls (atom [])
|
||||
sync-inventory {:excluded-command-prefixes ["login" "logout"]
|
||||
:scopes {:sync {:commands ["sync upload"]
|
||||
:options []}}}
|
||||
sync-cases [{:id "sync-upload"
|
||||
:cmds ["node static/logseq-cli.js sync upload"]
|
||||
:covers {:commands ["sync upload"]}}]]
|
||||
(with-redefs [manifests/load-inventory (fn [suite]
|
||||
(swap! suite-calls conj [:inventory suite])
|
||||
sync-inventory)
|
||||
manifests/load-cases (fn [suite]
|
||||
(swap! suite-calls conj [:cases suite])
|
||||
sync-cases)]
|
||||
(let [result (main/run! {:suite :sync
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result))))
|
||||
(is (= [[:inventory :sync]
|
||||
[:cases :sync]]
|
||||
@suite-calls)))))
|
||||
|
||||
(deftest run-sync-suite-uses-suite-fixture-once-with-parallel-runner
|
||||
(let [before-called (atom 0)
|
||||
after-called (atom 0)
|
||||
prepared-case-ids (atom [])
|
||||
run-case-seen (atom [])
|
||||
parallel-call (atom nil)
|
||||
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))
|
||||
main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts]
|
||||
(reset! parallel-call {:case-ids (mapv :id selected-cases)
|
||||
:jobs (:jobs opts)})
|
||||
(mapv (fn [case]
|
||||
(run-case case {:run-command run-command}))
|
||||
selected-cases))]
|
||||
(let [result (main/run! {:suite :sync
|
||||
:inventory sync-inventory
|
||||
:cases sync-cases
|
||||
:skip-build true
|
||||
:jobs 2
|
||||
: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"] (:case-ids @parallel-call)))
|
||||
(is (= 2 (:jobs @parallel-call)))
|
||||
(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
|
||||
(with-redefs [manifests/load-cases (fn [suite]
|
||||
(reset! selected-suite suite)
|
||||
[{:id "non-sync-case"}])]
|
||||
(main/list-cases! {})))]
|
||||
(is (= :non-sync @selected-suite))
|
||||
(is (string/includes? output "non-sync-case"))))
|
||||
|
||||
(deftest list-sync-cases-uses-sync-suite
|
||||
(let [selected-suite (atom nil)
|
||||
output (with-out-str
|
||||
(with-redefs [manifests/load-cases (fn [suite]
|
||||
(reset! selected-suite suite)
|
||||
[{:id "sync-case"}])]
|
||||
(main/list-sync-cases! {})))]
|
||||
(is (= :sync @selected-suite))
|
||||
(is (string/includes? output "sync-case"))))
|
||||
|
||||
(deftest test-prints-progress-and-summary
|
||||
(let [output (with-out-str
|
||||
(main/test! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok
|
||||
:cmd (last (:cmds case))})}))]
|
||||
(is (string/includes? output "==> Running cli-e2e cases"))
|
||||
(is (string/includes? output "==> Build preflight: running..."))
|
||||
(is (string/includes? output "==> Build preflight: skipped (--skip-build)"))
|
||||
(is (string/includes? output "==> Prepared 2 case(s), starting execution"))
|
||||
(is (string/includes? output "[1/2] ▶ global-help"))
|
||||
(is (string/includes? output "[1/2] ✓ global-help"))
|
||||
(is (string/includes? output "[2/2] ▶ graph-list"))
|
||||
(is (string/includes? output "[2/2] ✓ graph-list"))
|
||||
(is (string/includes? output "Summary: 2 passed, 0 failed"))))
|
||||
|
||||
(deftest test-parallel-output-omits-meaningless-index-prefixes
|
||||
(let [output (with-out-str
|
||||
(main/test! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:jobs 2
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
(Thread/sleep (if (= "global-help" (:id case)) 25 5))
|
||||
{:id (:id case)
|
||||
:status :ok})}))]
|
||||
(is (string/includes? output "▶ global-help"))
|
||||
(is (string/includes? output "✓ global-help"))
|
||||
(is (string/includes? output "✓ graph-list"))
|
||||
(is (not (string/includes? output "[1/2]")))
|
||||
(is (not (string/includes? output "[2/2]")))))
|
||||
|
||||
(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-without-timings-keeps-output-concise
|
||||
(let [output (with-out-str
|
||||
(main/test! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok
|
||||
:timings [{:phase :main
|
||||
:step-index 1
|
||||
:step-total 1
|
||||
:elapsed-ms 88
|
||||
:status :ok
|
||||
:cmd "hidden-step"}]})}))]
|
||||
(is (not (string/includes? output "==> Step timing enabled (--timings)")))
|
||||
(is (not (string/includes? output "step timings:")))
|
||||
(is (not (string/includes? output "Slow steps (top 10):")))))
|
||||
|
||||
(deftest test-sync-timings-prints-step-details-and-slow-summary
|
||||
(let [sync-inventory {:excluded-command-prefixes ["login" "logout"]
|
||||
:scopes {:sync {:commands ["sync status"]
|
||||
:options []}}}
|
||||
sync-cases [{:id "sync-status-case"
|
||||
:cmds ["node static/logseq-cli.js sync status"]
|
||||
:covers {:commands ["sync status"]}}]
|
||||
output (with-out-str
|
||||
(main/test-sync! {:inventory sync-inventory
|
||||
:cases sync-cases
|
||||
:skip-build true
|
||||
:timings true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok
|
||||
:timings [{:phase :setup
|
||||
:step-index 1
|
||||
:step-total 1
|
||||
:elapsed-ms 10
|
||||
:status :ok
|
||||
:cmd "sync-setup"}
|
||||
{:phase :main
|
||||
:step-index 1
|
||||
:step-total 1
|
||||
:elapsed-ms 150
|
||||
:status :ok
|
||||
:cmd "sync-main"}
|
||||
{:phase :cleanup
|
||||
:step-index 1
|
||||
:step-total 1
|
||||
:elapsed-ms 20
|
||||
:status :ok
|
||||
:cmd "sync-cleanup"}]})}))]
|
||||
(is (string/includes? output "==> Running cli-e2e cases"))
|
||||
(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 "sync-main"))))
|
||||
|
||||
(deftest test-help-prints-usage-and-skips-execution
|
||||
(let [ran? (atom false)
|
||||
result (atom nil)
|
||||
output (with-out-str
|
||||
(reset! result
|
||||
(main/test! {:help true
|
||||
:run-command (fn [_]
|
||||
(reset! ran? true)
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [_ _]
|
||||
(reset! ran? true)
|
||||
{:id "unexpected"
|
||||
:status :ok})}))) ]
|
||||
(is (= :help (:status @result)))
|
||||
(is (false? @ran?))
|
||||
(is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test [options]"))
|
||||
(is (string/includes? output "--skip-build"))
|
||||
(is (not (string/includes? output "--force-build")))
|
||||
(is (string/includes? output "--include TAG"))
|
||||
(is (string/includes? output "--case ID"))
|
||||
(is (string/includes? output "--jobs N"))
|
||||
(is (string/includes? output "Run up to N non-sync cases in parallel"))
|
||||
(is (string/includes? output "--skip-build --jobs 4"))
|
||||
(is (string/includes? output "Default: 4"))
|
||||
(is (string/includes? output "--timings"))
|
||||
(is (not (string/includes? output "--e2ee-password")))))
|
||||
|
||||
(deftest test-sync-help-prints-usage-and-skips-execution
|
||||
(let [ran? (atom false)
|
||||
result (atom nil)
|
||||
output (with-out-str
|
||||
(reset! result
|
||||
(main/test-sync! {:help true
|
||||
:run-command (fn [_]
|
||||
(reset! ran? true)
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [_ _]
|
||||
(reset! ran? true)
|
||||
{:id "unexpected"
|
||||
:status :ok})})))]
|
||||
(is (= :help (:status @result)))
|
||||
(is (false? @ran?))
|
||||
(is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test-sync [options]"))
|
||||
(is (string/includes? output "--skip-build"))
|
||||
(is (not (string/includes? output "--force-build")))
|
||||
(is (string/includes? output "--include TAG"))
|
||||
(is (string/includes? output "--case ID"))
|
||||
(is (string/includes? output "--jobs N"))
|
||||
(is (string/includes? output "Run up to N sync cases in parallel"))
|
||||
(is (string/includes? output "--jobs 4"))
|
||||
(is (string/includes? output "bb -f cli-e2e/bb.edn test-sync"))
|
||||
(is (string/includes? output "--case sync-upload-download-mvp"))
|
||||
(is (not (string/includes? output "--skip-build --case sync-upload-download-mvp")))
|
||||
(is (string/includes? output "Default: 4"))
|
||||
(is (string/includes? output "--timings"))
|
||||
(is (string/includes? output "--e2ee-password VALUE"))
|
||||
(is (string/includes? output "Default: 11111"))))
|
||||
|
||||
(deftest run-does-not-pass-force-build-to-preflight
|
||||
(let [preflight-call (atom nil)]
|
||||
(with-redefs [logseq.cli.e2e.preflight/run! (fn [opts]
|
||||
(reset! preflight-call opts)
|
||||
{:status :ok
|
||||
:commands []
|
||||
:missing-artifacts []})]
|
||||
(let [result (main/run! {:inventory complete-inventory
|
||||
:cases sample-cases
|
||||
:include ["smoke"]
|
||||
:skip-build false
|
||||
:force-build true
|
||||
:run-command (fn [_]
|
||||
{:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:run-case (fn [case _opts]
|
||||
{:id (:id case)
|
||||
:status :ok})})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (not (contains? @preflight-call :force-build)))))))
|
||||
|
||||
(deftest test-single-case-enables-detailed-command-logging
|
||||
(let [command-opts (atom nil)
|
||||
output (with-out-str
|
||||
(main/test! {:inventory complete-inventory
|
||||
:cases [{:id "global-help"
|
||||
:cmds ["node static/logseq-cli.js --help"]
|
||||
:covers {:options {:global ["--help"]}}}]
|
||||
:case "global-help"
|
||||
:skip-build true
|
||||
:run-command (fn [{:keys [cmd] :as opts}]
|
||||
(reset! command-opts opts)
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out "ok"
|
||||
:err ""})
|
||||
:run-case (fn [case {:keys [run-command]}]
|
||||
(let [result (run-command {:cmd (first (:cmds case))
|
||||
:phase :main
|
||||
:step-index 1
|
||||
:step-total 1})]
|
||||
{:id (:id case)
|
||||
:status :ok
|
||||
:cmd (:cmd result)}) )}))]
|
||||
(is (string/includes? output "==> Detailed case logging enabled (--case global-help)"))
|
||||
(is (string/includes? output " [main 1/1] $ node static/logseq-cli.js --help"))
|
||||
(is (true? (:stream-output? @command-opts)))))
|
||||
|
||||
(deftest cleanup-help-prints-usage
|
||||
(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 "Remove cli-e2e temp roots"))
|
||||
(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-roots! (fn [_] {:found-dirs ["/tmp/logseq-cli-e2e-a"
|
||||
"/tmp/logseq-cli-e2e-b"]
|
||||
:removed-dirs ["/tmp/logseq-cli-e2e-a"]
|
||||
:failed-dirs ["/tmp/logseq-cli-e2e-b"]})]
|
||||
(let [result (atom nil)
|
||||
output (with-out-str
|
||||
(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"] (get-in @result [:temp-roots :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 roots: 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)
|
||||
{:dry-run? true
|
||||
:found-pids [101 202]
|
||||
: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-roots! (fn [opts]
|
||||
(reset! dir-opts opts)
|
||||
{:dry-run? true
|
||||
:found-dirs ["/tmp/logseq-cli-e2e-a"]
|
||||
:would-remove-dirs ["/tmp/logseq-cli-e2e-a"]
|
||||
:removed-dirs []
|
||||
:failed-dirs []})]
|
||||
(let [result (atom nil)
|
||||
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-roots :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 roots: found 1, would remove 1"))))))
|
||||
225
cli-e2e/test/logseq/cli/e2e/manifests_test.clj
Normal file
225
cli-e2e/test/logseq/cli/e2e/manifests_test.clj
Normal file
@@ -0,0 +1,225 @@
|
||||
(ns logseq.cli.e2e.manifests-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.manifests :as manifests]))
|
||||
|
||||
(deftest load-cases-requires-new-map-format
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
[{:id "legacy-a"}
|
||||
{:id "legacy-b"}])]
|
||||
(let [error (try
|
||||
(manifests/load-cases :non-sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))]
|
||||
(is (some? error))
|
||||
(is (re-find #"Invalid cli-e2e manifest format" (.getMessage error)))
|
||||
(is (= "{:templates {...} :cases [...]}"
|
||||
(:expected (ex-data error)))))))
|
||||
|
||||
(deftest load-cases-merges-templates-and-cases-by-rules
|
||||
(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"
|
||||
[:data :base] 1}}
|
||||
: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 :addon] 2}}
|
||||
:graph "addon-graph"}
|
||||
:shared {:setup ["setup-shared"]
|
||||
:cmds ["cmd-shared"]
|
||||
:cleanup ["cleanup-shared"]
|
||||
:tags [:shared]
|
||||
:vars {:nested {:shared 9}}
|
||||
:graph "shared-graph"}}
|
||||
:cases
|
||||
[{:id "single-parent"
|
||||
:extends :base
|
||||
:cmds ["cmd-case"]
|
||||
:expect {:stdout-json-paths {[:data :case] 3}}
|
||||
:graph "single-parent-graph"}
|
||||
{:id "multi-parent"
|
||||
:extends [:base :addon]
|
||||
:setup ["setup-case"]
|
||||
:cmds ["cmd-case"]
|
||||
:cleanup ["cleanup-case"]
|
||||
:tags [:case]
|
||||
:vars {:nested {:leaf 3}
|
||||
:only-case true}
|
||||
:covers {:commands ["case-command"]
|
||||
:options {:graph ["--case"]}}
|
||||
:expect {:stdout-json-paths {[:data :case] 3}}
|
||||
:graph "case-graph"}
|
||||
{:id "negative-case"
|
||||
:extends :shared
|
||||
:expect {:exit 1
|
||||
:stdout-json-paths {[:error :code] "boom"}}
|
||||
:graph "negative-graph"}]})]
|
||||
(let [[single-parent multi-parent negative-case] (manifests/load-cases :sync)]
|
||||
(testing "supports :extends keyword"
|
||||
(is (= "single-parent" (:id single-parent)))
|
||||
(is (= ["cmd-a" "cmd-case"] (:cmds single-parent)))
|
||||
(is (= "single-parent-graph" (:graph single-parent))))
|
||||
(testing "append merge keys"
|
||||
(is (= ["setup-a" "setup-b" "setup-case"] (:setup multi-parent)))
|
||||
(is (= ["cmd-a" "cmd-b" "cmd-case"] (:cmds multi-parent)))
|
||||
(is (= ["cleanup-a" "cleanup-b" "cleanup-case"] (:cleanup multi-parent)))
|
||||
(is (= [:base :addon :case] (:tags multi-parent))))
|
||||
(testing "deep merge keys"
|
||||
(is (= {:nested {:left 1 :right 2 :leaf 3}
|
||||
:only-base true
|
||||
:only-case true}
|
||||
(:vars multi-parent)))
|
||||
(is (= {:commands ["case-command"]
|
||||
:options {:global ["--base"]
|
||||
:graph ["--case"]}}
|
||||
(:covers multi-parent)))
|
||||
(is (= {[:status] "ok"
|
||||
[:data :base] 1
|
||||
[:data :addon] 2
|
||||
[:data :case] 3}
|
||||
(get-in multi-parent [:expect :stdout-json-paths]))))
|
||||
(testing "child expect overrides inherited maps when parent omits them"
|
||||
(is (= {:exit 1
|
||||
:stdout-json-paths {[:error :code] "boom"}}
|
||||
(:expect negative-case))))
|
||||
(testing "base templates can omit happy-path defaults"
|
||||
(is (= ["setup-shared"] (:setup negative-case)))
|
||||
(is (= ["cmd-shared"] (:cmds negative-case)))
|
||||
(is (= ["cleanup-shared"] (:cleanup negative-case))))
|
||||
(testing "scalar keys are overridden by child"
|
||||
(is (= "case-graph" (:graph multi-parent)))))))
|
||||
|
||||
(deftest load-cases-detects-circular-template-inheritance-with-cycle-path
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
{:templates
|
||||
{:a {:extends :b
|
||||
:setup ["a"]}
|
||||
:b {:extends :a
|
||||
:setup ["b"]}}
|
||||
:cases [{:id "cycle" :extends :a}]})]
|
||||
(let [error (try
|
||||
(manifests/load-cases :sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))]
|
||||
(is (some? error))
|
||||
(is (re-find #"Circular template inheritance" (.getMessage error)))
|
||||
(is (= [:a :b :a] (:cycle (ex-data error)))))))
|
||||
|
||||
(deftest load-cases-validates-extends-entries
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
{:templates {:base {:cmds ["base"]}}
|
||||
:cases [{:id "invalid-extends"
|
||||
:extends [:base "bad"]
|
||||
:cmds ["case"]}]})]
|
||||
(let [error (try
|
||||
(manifests/load-cases :non-sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))]
|
||||
(is (some? error))
|
||||
(is (re-find #"Invalid :extends entries" (.getMessage error)))
|
||||
(is (= ["bad"] (:invalid-entries (ex-data error)))))))
|
||||
|
||||
(deftest load-cases-lint-detects-invalid-extends-references
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
{:templates {:base {:cmds ["base"]}
|
||||
:unused {:extends :missing-template
|
||||
:cmds ["unused"]}}
|
||||
:cases [{:id "valid" :extends :base :cmds ["case"]}
|
||||
{:id "invalid" :extends :also-missing :cmds ["case"]}]})]
|
||||
(let [error (try
|
||||
(manifests/load-cases :sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))
|
||||
issues (:issues (ex-data error))]
|
||||
(is (some? error))
|
||||
(is (re-find #"manifest lint failed" (.getMessage error)))
|
||||
(is (= #{:also-missing :missing-template}
|
||||
(set (map :target (filter #(= :invalid-extends (:type %)) issues))))))))
|
||||
|
||||
(deftest load-cases-lint-detects-duplicate-case-ids
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
{:templates {:base {:cmds ["base"]}}
|
||||
:cases [{:id "dup" :extends :base :cmds ["case-a"]}
|
||||
{:id "dup" :extends :base :cmds ["case-b"]}]})]
|
||||
(let [error (try
|
||||
(manifests/load-cases :non-sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))
|
||||
duplicate-issues (filter #(= :duplicate-case-id (:type %))
|
||||
(:issues (ex-data error)))]
|
||||
(is (some? error))
|
||||
(is (= [{:type :duplicate-case-id :id "dup" :count 2}]
|
||||
(vec duplicate-issues))))))
|
||||
|
||||
(deftest load-cases-lint-detects-unused-templates
|
||||
(with-redefs [manifests/read-edn-file (fn [_]
|
||||
{:templates {:base {:cmds ["base"]}
|
||||
:unused {:cmds ["unused"]}}
|
||||
:cases [{:id "only" :extends :base :cmds ["case"]}]})]
|
||||
(let [error (try
|
||||
(manifests/load-cases :sync)
|
||||
nil
|
||||
(catch clojure.lang.ExceptionInfo error
|
||||
error))
|
||||
unused-issues (filter #(= :unused-template (:type %))
|
||||
(:issues (ex-data error)))]
|
||||
(is (some? error))
|
||||
(is (= [{:type :unused-template :template :unused}]
|
||||
(vec unused-issues))))))
|
||||
|
||||
(deftest sync-multi-batch-operations-uses-state-driven-waits-instead-of-fixed-sleeps
|
||||
(let [cases (manifests/load-cases :sync)
|
||||
multi-batch (some #(when (= "sync-multi-batch-operations" (:id %)) %) cases)
|
||||
commands (:cmds multi-batch)
|
||||
upload-commands (filter #(re-find #"sync upload --graph" %) commands)
|
||||
wait-commands (filter #(re-find #"wait_sync_status\.py" %) commands)]
|
||||
(is (some? multi-batch))
|
||||
(is (not-any? #(= "sleep 1" %) commands))
|
||||
(is (= 1 (count upload-commands)))
|
||||
(is (= 5 (count wait-commands)))
|
||||
(is (= 3 (count (filter #(re-find #"--root-dir '\{\{tmp-dir\}\}/graphs-b'.+--timeout-s 30 --interval-s 1" %) wait-commands))))))
|
||||
|
||||
(deftest sync-manifest-includes-duplicate-upload-negative-case
|
||||
(let [cases (manifests/load-cases :sync)
|
||||
duplicate-upload (some #(when (= "sync-upload-rejects-duplicate-remote-graph" (:id %)) %) cases)
|
||||
json-paths (get-in duplicate-upload [:expect :stdout-json-paths])
|
||||
setup-commands (:setup duplicate-upload)
|
||||
main-commands (:cmds duplicate-upload)]
|
||||
(is (some? duplicate-upload))
|
||||
(is (= 1 (get-in duplicate-upload [:expect :exit])))
|
||||
(is (= "graph-already-exists"
|
||||
(get-in duplicate-upload [:expect :stdout-json-paths [:error :code]])))
|
||||
(is (= ["delete it before uploading again"]
|
||||
(get-in duplicate-upload [:expect :stdout-contains])))
|
||||
(is (= 1 (count (filter #(re-find #"sync upload --graph" %) setup-commands))))
|
||||
(is (= 1 (count (filter #(re-find #"sync upload --graph" %) main-commands))))
|
||||
(is (false? (contains? json-paths [:data :pending-local])))
|
||||
(is (false? (contains? json-paths [:data :pending-asset])))
|
||||
(is (false? (contains? json-paths [:data :pending-server])))
|
||||
(is (false? (contains? json-paths [:data :last-error])))))
|
||||
|
||||
(deftest sync-status-steady-state-does-not-repeat-identical-b-side-steady-state-waits
|
||||
(let [cases (manifests/load-cases :sync)
|
||||
steady-state (some #(when (= "sync-status-steady-state" (:id %)) %) cases)
|
||||
steady-waits (filter #(re-find #"wait_sync_status\.py.+--root-dir '\{\{tmp-dir\}\}/graphs-b'.+--timeout-s 30 --interval-s 1" %) (:cmds steady-state))]
|
||||
(is (some? steady-state))
|
||||
(is (= 1 (count steady-waits)))
|
||||
(is (= 1 (count (filter #(re-find #"sync status --graph" %) (:cmds steady-state)))))))
|
||||
18
cli-e2e/test/logseq/cli/e2e/paths_test.clj
Normal file
18
cli-e2e/test/logseq/cli/e2e/paths_test.clj
Normal file
@@ -0,0 +1,18 @@
|
||||
(ns logseq.cli.e2e.paths-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[logseq.cli.e2e.paths :as paths]))
|
||||
|
||||
(deftest repo-root-does-not-depend-on-runtime-file-binding
|
||||
(let [expected (paths/repo-root)]
|
||||
(binding [*file* nil]
|
||||
(is (= expected
|
||||
(paths/repo-root))))))
|
||||
|
||||
(deftest required-artifacts-include-cli-runtime-contract
|
||||
(is (= [(paths/repo-path "cli" "_build" "default" "dist" "logseq-cli.js")
|
||||
(paths/repo-path "static" "logseq-cli.js")
|
||||
(paths/repo-path "static" "db-worker-node.js")
|
||||
(paths/repo-path "dist" "db-worker-node.js")
|
||||
(paths/repo-path "dist" "db-worker-node-assets.json")
|
||||
(paths/repo-path "deps" "db-sync" "worker" "dist" "node-adapter.js")]
|
||||
(paths/required-artifacts))))
|
||||
98
cli-e2e/test/logseq/cli/e2e/preflight_test.clj
Normal file
98
cli-e2e/test/logseq/cli/e2e/preflight_test.clj
Normal file
@@ -0,0 +1,98 @@
|
||||
(ns logseq.cli.e2e.preflight-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.preflight :as preflight]))
|
||||
|
||||
(def required-artifacts
|
||||
["/repo/cli/_build/default/dist/logseq-cli.js"
|
||||
"/repo/static/logseq-cli.js"
|
||||
"/repo/static/db-worker-node.js"
|
||||
"/repo/dist/db-worker-node.js"
|
||||
"/repo/dist/db-worker-node-assets.json"
|
||||
"/repo/deps/db-sync/worker/dist/node-adapter.js"])
|
||||
|
||||
(defn- with-required-artifacts
|
||||
[f]
|
||||
(with-redefs [logseq.cli.e2e.paths/repo-root (constantly "/repo")
|
||||
logseq.cli.e2e.paths/required-artifacts (fn [] required-artifacts)]
|
||||
(f)))
|
||||
|
||||
(deftest build-plan-matches-required-commands
|
||||
(is (= ["pnpm cli:release"
|
||||
"pnpm db-worker-node:compile:bundle"
|
||||
"pnpm --dir deps/db-sync build:node-adapter"]
|
||||
(mapv :cmd preflight/build-plan)))
|
||||
(is (not-any? #(re-find #"clojure -M:cljs compile logseq-cli" (:cmd %))
|
||||
preflight/build-plan)))
|
||||
|
||||
(deftest missing-artifacts-returns-unreadable-paths
|
||||
(let [artifacts ["/repo/static/logseq-cli.js"
|
||||
"/repo/static/db-worker-node.js"
|
||||
"/repo/dist/db-worker-node.js"]
|
||||
present? #{"/repo/static/logseq-cli.js"}]
|
||||
(is (= ["/repo/static/db-worker-node.js"
|
||||
"/repo/dist/db-worker-node.js"]
|
||||
(preflight/missing-artifacts artifacts present?)))))
|
||||
|
||||
(deftest skip-build-avoids-running-shell-commands
|
||||
(let [called? (atom false)
|
||||
result (preflight/run! {:skip-build true
|
||||
:run-command (fn [_]
|
||||
(reset! called? true))
|
||||
:file-exists? (constantly false)})]
|
||||
(is (= :skipped (:status result)))
|
||||
(is (false? @called?))))
|
||||
|
||||
(deftest build-runs-commands-even-when-artifacts-are-ready
|
||||
(let [calls (atom [])]
|
||||
(with-required-artifacts
|
||||
#(let [result (preflight/run! {:run-command (fn [{:keys [cmd]}]
|
||||
(swap! calls conj cmd)
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:file-exists? (set required-artifacts)})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["pnpm cli:release"
|
||||
"pnpm db-worker-node:compile:bundle"
|
||||
"pnpm --dir deps/db-sync build:node-adapter"]
|
||||
@calls))))))
|
||||
|
||||
(deftest build-runs-commands-when-artifacts-are-partially-present
|
||||
(let [calls (atom [])
|
||||
existing (atom (disj (set required-artifacts)
|
||||
"/repo/dist/db-worker-node-assets.json"))]
|
||||
(with-required-artifacts
|
||||
#(let [result (preflight/run! {:run-command (fn [{:keys [cmd]}]
|
||||
(swap! calls conj cmd)
|
||||
(reset! existing (set required-artifacts))
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:file-exists? (fn [path]
|
||||
(contains? @existing path))})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["pnpm cli:release"
|
||||
"pnpm db-worker-node:compile:bundle"
|
||||
"pnpm --dir deps/db-sync build:node-adapter"]
|
||||
@calls))))))
|
||||
|
||||
(deftest build-runs-commands-when-artifacts-are-absent
|
||||
(let [calls (atom [])
|
||||
existing (atom #{})]
|
||||
(with-required-artifacts
|
||||
#(let [result (preflight/run! {:run-command (fn [{:keys [cmd]}]
|
||||
(swap! calls conj cmd)
|
||||
(reset! existing (set required-artifacts))
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out ""
|
||||
:err ""})
|
||||
:file-exists? (fn [path]
|
||||
(contains? @existing path))})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= ["pnpm cli:release"
|
||||
"pnpm db-worker-node:compile:bundle"
|
||||
"pnpm --dir deps/db-sync build:node-adapter"]
|
||||
@calls))))))
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[4] / "scripts" / "random_bidirectional_block_ops.py"
|
||||
spec = importlib.util.spec_from_file_location("random_bidirectional_block_ops", MODULE_PATH)
|
||||
random_bidirectional_block_ops = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
sys.modules[spec.name] = random_bidirectional_block_ops
|
||||
spec.loader.exec_module(random_bidirectional_block_ops)
|
||||
|
||||
|
||||
def test_default_profile_is_faster_than_high_stress(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
random_bidirectional_block_ops.sys,
|
||||
"argv",
|
||||
[
|
||||
"random_bidirectional_block_ops.py",
|
||||
"--cli",
|
||||
"/tmp/logseq-cli.js",
|
||||
"--graph",
|
||||
"demo",
|
||||
"--config-a",
|
||||
"/tmp/a.edn",
|
||||
"--root-dir-a",
|
||||
"/tmp/a",
|
||||
"--config-b",
|
||||
"/tmp/b.edn",
|
||||
"--root-dir-b",
|
||||
"/tmp/b",
|
||||
"--page",
|
||||
"Home",
|
||||
],
|
||||
)
|
||||
default_args = random_bidirectional_block_ops.parse_args()
|
||||
|
||||
monkeypatch.setattr(
|
||||
random_bidirectional_block_ops.sys,
|
||||
"argv",
|
||||
[
|
||||
"random_bidirectional_block_ops.py",
|
||||
"--cli",
|
||||
"/tmp/logseq-cli.js",
|
||||
"--graph",
|
||||
"demo",
|
||||
"--config-a",
|
||||
"/tmp/a.edn",
|
||||
"--root-dir-a",
|
||||
"/tmp/a",
|
||||
"--config-b",
|
||||
"/tmp/b.edn",
|
||||
"--root-dir-b",
|
||||
"/tmp/b",
|
||||
"--page",
|
||||
"Home",
|
||||
"--profile",
|
||||
"high-stress",
|
||||
],
|
||||
)
|
||||
high_stress_args = random_bidirectional_block_ops.parse_args()
|
||||
|
||||
assert default_args.root_dir_a == "/tmp/a"
|
||||
assert default_args.root_dir_b == "/tmp/b"
|
||||
assert default_args.profile == "default"
|
||||
assert high_stress_args.profile == "high-stress"
|
||||
assert default_args.rounds_per_client < high_stress_args.rounds_per_client
|
||||
|
||||
|
||||
def test_explicit_rounds_override_profile_default(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
random_bidirectional_block_ops.sys,
|
||||
"argv",
|
||||
[
|
||||
"random_bidirectional_block_ops.py",
|
||||
"--cli",
|
||||
"/tmp/logseq-cli.js",
|
||||
"--graph",
|
||||
"demo",
|
||||
"--config-a",
|
||||
"/tmp/a.edn",
|
||||
"--root-dir-a",
|
||||
"/tmp/a",
|
||||
"--config-b",
|
||||
"/tmp/b.edn",
|
||||
"--root-dir-b",
|
||||
"/tmp/b",
|
||||
"--page",
|
||||
"Home",
|
||||
"--profile",
|
||||
"high-stress",
|
||||
"--rounds-per-client",
|
||||
"12",
|
||||
],
|
||||
)
|
||||
|
||||
args = random_bidirectional_block_ops.parse_args()
|
||||
|
||||
assert args.profile == "high-stress"
|
||||
assert args.rounds_per_client == 12
|
||||
152
cli-e2e/test/logseq/cli/e2e/runner_test.clj
Normal file
152
cli-e2e/test/logseq/cli/e2e/runner_test.clj
Normal file
@@ -0,0 +1,152 @@
|
||||
(ns logseq.cli.e2e.runner-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[logseq.cli.e2e.runner :as runner]))
|
||||
|
||||
(deftest render-case-expands-template-values-recursively
|
||||
(let [rendered (runner/render-case
|
||||
{:id "graph-create"
|
||||
:setup ["{{cli}} --graph {{graph-arg}}"]
|
||||
:cmds ["{{cli}} graph info --graph {{graph-arg}}"]
|
||||
:expect {:stdout-json-paths {[:data :graph] "{{graph}}"
|
||||
[:status] "ok"}}}
|
||||
{:cli "node /tmp/logseq-cli.js"
|
||||
:graph "demo"
|
||||
:graph-arg "'demo'"})]
|
||||
(is (= ["node /tmp/logseq-cli.js --graph 'demo'"] (:setup rendered)))
|
||||
(is (= ["node /tmp/logseq-cli.js graph info --graph 'demo'"] (:cmds rendered)))
|
||||
(is (= "demo" (get-in rendered [:expect :stdout-json-paths [:data :graph]])))))
|
||||
|
||||
(deftest run-case-executes-setup-before-main-command
|
||||
(let [calls (atom [])
|
||||
result (runner/run-case!
|
||||
{:id "graph-info"
|
||||
:setup ["setup one" "setup two"]
|
||||
:cmds ["main command one" "main command two"]
|
||||
:expect {:exit 0}}
|
||||
{:context {}
|
||||
:run-command (fn [{:keys [cmd]}]
|
||||
(swap! calls conj cmd)
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out ""
|
||||
:err ""})})]
|
||||
(is (= ["setup one" "setup two" "main command one" "main command two"] @calls))
|
||||
(is (= "graph-info" (:id result)))
|
||||
(is (= "main command two" (get-in result [:result :cmd])))))
|
||||
|
||||
(deftest run-case-includes-command-phase-metadata-when-detailed
|
||||
(let [calls (atom [])]
|
||||
(runner/run-case!
|
||||
{:id "graph-info"
|
||||
:setup ["setup one" "setup two"]
|
||||
:cmds ["main command one" "main command two"]
|
||||
:cleanup ["cleanup one"]
|
||||
:expect {:exit 0}}
|
||||
{:context {}
|
||||
:detailed-log? true
|
||||
:run-command (fn [{:keys [cmd phase step-index step-total case-id throw?] :as opts}]
|
||||
(swap! calls conj (select-keys opts [:cmd :phase :step-index :step-total :case-id :throw?]))
|
||||
{:cmd cmd
|
||||
:exit 0
|
||||
:out ""
|
||||
:err ""})})
|
||||
(is (= [{:cmd "setup one" :phase :setup :step-index 1 :step-total 2 :case-id "graph-info" :throw? true}
|
||||
{:cmd "setup two" :phase :setup :step-index 2 :step-total 2 :case-id "graph-info" :throw? true}
|
||||
{:cmd "main command one" :phase :main :step-index 1 :step-total 2 :case-id "graph-info" :throw? true}
|
||||
{:cmd "main command two" :phase :main :step-index 2 :step-total 2 :case-id "graph-info" :throw? false}
|
||||
{: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"
|
||||
:cmds ["node static/logseq-cli.js completion fish"]
|
||||
:expect {:exit 1
|
||||
:stdout-json-paths {[:status] "error"
|
||||
[:error :code] "invalid-options"}}}
|
||||
{:context {}
|
||||
:run-command (fn [{:keys [cmd]}]
|
||||
{:cmd cmd
|
||||
:exit 1
|
||||
:out "{\"status\":\"error\",\"error\":{\"code\":\"invalid-options\"}}"
|
||||
:err ""})})]
|
||||
(is (= 1 (get-in result [:result :exit])))))
|
||||
|
||||
(deftest assert-result-validates-edn-paths
|
||||
(is (nil? (runner/assert-result!
|
||||
{:id "graph-list-edn"
|
||||
:expect {:exit 0
|
||||
:stdout-edn-paths {[:status] :ok
|
||||
[:data :graphs] ["demo"]}}}
|
||||
{:cmd "node cli.js graph list"
|
||||
:exit 0
|
||||
:out "{:status :ok, :data {:graphs [\"demo\"]}}"
|
||||
:err ""}))))
|
||||
|
||||
(deftest assert-result-validates-stdout-not-contains
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"stdout contained forbidden text"
|
||||
(runner/assert-result!
|
||||
{:id "remove-block"
|
||||
:expect {:exit 0
|
||||
:stdout-not-contains ["Alpha block"]}}
|
||||
{:cmd "node cli.js show --page Home"
|
||||
:exit 0
|
||||
:out "Home\n- Alpha block"
|
||||
:err ""}))))
|
||||
|
||||
(deftest assert-result-normalizes-json-escaped-windows-paths-for-contains
|
||||
(is (nil? (runner/assert-result!
|
||||
{:id "doctor-dev-script-json"
|
||||
:expect {:exit 0
|
||||
:stdout-contains ["static/db-worker-node.js"
|
||||
"C:\\Users\\demo\\tmp\\graph-export.edn"]}}
|
||||
{:cmd "node cli.js doctor --dev-script"
|
||||
:exit 0
|
||||
:out "{\"status\":\"ok\",\"data\":{\"path\":\"C:\\\\Users\\\\demo\\\\tmp\\\\graph-export.edn\",\"message\":\"Found readable file: C:\\\\Users\\\\demo\\\\project\\\\static\\\\db-worker-node.js\"}}"
|
||||
:err ""}))))
|
||||
54
cli-e2e/test/logseq/cli/e2e/shell_test.clj
Normal file
54
cli-e2e/test/logseq/cli/e2e/shell_test.clj
Normal file
@@ -0,0 +1,54 @@
|
||||
(ns logseq.cli.e2e.shell-test
|
||||
(:require [babashka.process :as process]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.shell :as shell]))
|
||||
|
||||
(deftest preserves-the-exact-command-string
|
||||
(let [captured (atom nil)
|
||||
command "node static/logseq-cli.js --graph 'Graph With Space' --help"
|
||||
result (shell/run! {:cmd command
|
||||
:dir "/repo"
|
||||
:executor (fn [cmd opts]
|
||||
(reset! captured {:cmd cmd
|
||||
:opts opts})
|
||||
{:exit 0
|
||||
:out "ok"
|
||||
:err ""})})]
|
||||
(is (= command (:cmd result)))
|
||||
(is (= command (:cmd @captured)))
|
||||
(is (= "/repo" (get-in @captured [:opts :dir])))))
|
||||
|
||||
(deftest shell-failures-include-command-context
|
||||
(let [command "node static/logseq-cli.js completion fish"]
|
||||
(try
|
||||
(shell/run! {:cmd command
|
||||
:executor (fn [_ _]
|
||||
{:exit 2
|
||||
:out ""
|
||||
:err "unsupported shell"})})
|
||||
(is false "Expected shell/run! to throw")
|
||||
(catch clojure.lang.ExceptionInfo ex
|
||||
(is (= command (:cmd (ex-data ex))))
|
||||
(is (= 2 (:exit (ex-data ex))))
|
||||
(is (= "unsupported shell" (:err (ex-data ex))))))))
|
||||
|
||||
(deftest allow-failure-returns-result-without-throwing
|
||||
(let [command "node static/logseq-cli.js graph info --graph missing"
|
||||
result (shell/run! {:cmd command
|
||||
:throw? false
|
||||
:executor (fn [_ _]
|
||||
{:exit 1
|
||||
:out "failed"
|
||||
:err "missing"})})]
|
||||
(is (= command (:cmd result)))
|
||||
(is (= 1 (:exit result)))
|
||||
(is (= "failed" (:out result)))
|
||||
(is (= "missing" (:err result)))))
|
||||
|
||||
(deftest default-executor-uses-resolvable-shell-command
|
||||
(let [captured (atom nil)]
|
||||
(with-redefs [process/process (fn [_opts & args]
|
||||
(reset! captured args)
|
||||
(future {:exit 0}))]
|
||||
(shell/run! {:cmd "echo ok"}))
|
||||
(is (= ["bash" "-c" "echo ok"] @captured))))
|
||||
184
cli-e2e/test/logseq/cli/e2e/sync_fixture_test.clj
Normal file
184
cli-e2e/test/logseq/cli/e2e/sync_fixture_test.clj
Normal file
@@ -0,0 +1,184 @@
|
||||
(ns logseq.cli.e2e.sync-fixture-test
|
||||
(:require [babashka.fs :as fs]
|
||||
[clojure.java.shell :as shell]
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[logseq.cli.e2e.sync-fixture :as sync-fixture]))
|
||||
|
||||
(defn- free-port []
|
||||
(with-open [socket (java.net.ServerSocket. 0)]
|
||||
(.getLocalPort socket)))
|
||||
|
||||
(deftest prepare-sync-config-writes-oauth-token-endpoint
|
||||
(let [tmp-dir (fs/create-temp-dir {:prefix "logseq-cli-sync-config-test-"})
|
||||
auth-path (fs/path tmp-dir "auth.json")
|
||||
config-path (fs/path tmp-dir "cli.edn")
|
||||
script (fs/path "cli-e2e" "scripts" "prepare_sync_config.py")
|
||||
result (do
|
||||
(spit (str auth-path) "{\"refresh-token\":\"refresh-token\"}\n")
|
||||
(shell/sh "python3" (str script)
|
||||
"--output" (str config-path)
|
||||
"--auth-path" (str auth-path)
|
||||
"--http-base" "http://127.0.0.1:18080"
|
||||
"--ws-url" "ws://127.0.0.1:18080/sync/%s"))]
|
||||
(try
|
||||
(is (= 0 (:exit result)) (:err result))
|
||||
(let [config (slurp (str config-path))]
|
||||
(is (string/includes? config ":http-base \"http://127.0.0.1:18080\""))
|
||||
(is (string/includes? config ":ws-url \"ws://127.0.0.1:18080/sync/%s\""))
|
||||
(is (string/includes? config ":oauth-token-endpoint "))
|
||||
(is (string/includes? config "https://logseq-prod.auth.us-east-1.amazoncognito.com/oauth2/token")))
|
||||
(finally
|
||||
(fs/delete-tree tmp-dir)))))
|
||||
|
||||
(deftest prepare-case-injects-case-local-sync-resources
|
||||
(let [suite-context {: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}}/graphs-b'"
|
||||
"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}}'"
|
||||
"python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --port 18080"
|
||||
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"]
|
||||
:cleanup ["{{cli}} --root-dir {{root-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 (= ["mkdir -p '{{tmp-dir}}/graphs-b'"
|
||||
"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}}'"
|
||||
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"]
|
||||
(:setup prepared)))
|
||||
(is (= ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]
|
||||
(:cleanup prepared)))
|
||||
(is (= "http://127.0.0.1:18080" (get-in prepared [:vars :sync-http-base])))
|
||||
(is (= "ws://127.0.0.1:18080/sync/%s" (get-in prepared [:vars :sync-ws-url])))
|
||||
(is (= "18080" (get-in prepared [:vars :sync-port])))
|
||||
(is (= "11111" (get-in prepared [:vars :e2ee-password])))
|
||||
(is (= "11111" (get-in prepared [:vars :e2ee-password-arg])))
|
||||
(is (not (contains? (:vars prepared) :suite-auth-path)))
|
||||
(is (not (contains? (:vars prepared) :suite-config-path)))
|
||||
(is (not-any? #(string/includes? % "suite-auth-path") (:setup prepared)))
|
||||
(is (not-any? #(string/includes? % "suite-config-path") (:setup prepared)))
|
||||
(is (not-any? #(string/includes? % "db_sync_server.py' start") (:setup prepared)))
|
||||
(is (not-any? #(string/includes? % "db_sync_server.py' stop") (:cleanup prepared)))))
|
||||
|
||||
(deftest prepare-case-places-case-local-auth-before-cli-sync-commands
|
||||
(let [suite-context {:suite-tmp-dir "/tmp/sync-suite"
|
||||
:sync-port "18080"
|
||||
:sync-http-base "http://127.0.0.1:18080"
|
||||
:sync-ws-url "ws://127.0.0.1:18080/sync/%s"}
|
||||
prepared (sync-fixture/prepare-case
|
||||
{:id "sync-case"
|
||||
:setup ["mkdir -p '{{tmp-dir}}/graphs-b'"
|
||||
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"
|
||||
"{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password {{e2ee-password-arg}}"]
|
||||
:cleanup []}
|
||||
suite-context)
|
||||
setup (:setup prepared)
|
||||
bootstrap-cmd (nth setup 5)]
|
||||
(is (= "mkdir -p '{{tmp-dir}}/graphs-b'" (first setup)))
|
||||
(is (= "mkdir -p '{{tmp-dir}}/home/logseq'" (second setup)))
|
||||
(is (= "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" (nth setup 2)))
|
||||
(is (= "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}}'"
|
||||
(nth setup 3)))
|
||||
(is (= "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}}'"
|
||||
(nth setup 4)))
|
||||
(is (string/includes? bootstrap-cmd "sync ensure-keys"))
|
||||
(is (string/includes? bootstrap-cmd "--upload-keys"))
|
||||
(is (string/includes? bootstrap-cmd "/tmp/sync-suite/user-rsa-keys.lock"))
|
||||
(is (string/includes? bootstrap-cmd "/tmp/sync-suite/user-rsa-keys.ready"))
|
||||
(is (= "{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"
|
||||
(nth setup 6)))
|
||||
(is (= "{{cli-home}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password {{e2ee-password-arg}}"
|
||||
(nth setup 7)))
|
||||
(is (not-any? #(and (string/includes? % "sync ensure-keys")
|
||||
(not= % bootstrap-cmd)
|
||||
(string/includes? % "--upload-keys"))
|
||||
setup))))
|
||||
|
||||
(deftest prepare-case-accepts-custom-e2ee-password
|
||||
(let [suite-context {:sync-port "18080"
|
||||
:sync-http-base "http://127.0.0.1:18080"
|
||||
:sync-ws-url "ws://127.0.0.1:18080/sync/%s"
|
||||
:e2ee-password "pass word"}
|
||||
prepared (sync-fixture/prepare-case {:id "sync-case"
|
||||
:setup []
|
||||
:cleanup []}
|
||||
suite-context)]
|
||||
(is (= "pass word" (get-in prepared [:vars :e2ee-password])))
|
||||
(is (= "'pass word'" (get-in prepared [:vars :e2ee-password-arg])))))
|
||||
|
||||
(deftest before-and-after-suite-only-manage-shared-sync-server-lifecycle
|
||||
(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})]
|
||||
(testing "before-suite only starts the shared server"
|
||||
(is (= 1 (count @calls)))
|
||||
(is (string/includes? (:cmd (first @calls)) "db_sync_server.py"))
|
||||
(is (string/includes? (:cmd (first @calls)) " start "))
|
||||
(is (string/includes? (:cmd (first @calls)) "--data-dir"))
|
||||
(is (not (string/includes? (:cmd (first @calls)) "--root-dir")))
|
||||
(is (string/includes? (:cmd (first @calls)) "--port 18080"))
|
||||
(is (string/includes? (:cmd (first @calls)) "--startup-timeout-s 60"))
|
||||
(is (not (string/includes? (:cmd (first @calls)) "prepare_sync_config.py")))
|
||||
(is (not (contains? suite-context :suite-auth-path)))
|
||||
(is (not (contains? suite-context :suite-config-path))))
|
||||
(sync-fixture/after-suite! suite-context {:run-command run-command})
|
||||
(testing "after-suite only stops the shared server once"
|
||||
(is (= 2 (count @calls)))
|
||||
(is (string/includes? (:cmd (last @calls)) "db_sync_server.py"))
|
||||
(is (string/includes? (:cmd (last @calls)) " stop "))
|
||||
(is (false? (:throw? (last @calls)))))))
|
||||
|
||||
(deftest db-sync-server-start-adds-db-sync-node-modules-to-node-path
|
||||
(let [tmp-dir (fs/create-temp-dir {:prefix "logseq-cli-db-sync-server-test-"})
|
||||
repo-root (fs/path tmp-dir "repo")
|
||||
entry (fs/path repo-root "deps" "db-sync" "worker" "dist" "node-adapter.js")
|
||||
node-modules (fs/path repo-root "deps" "db-sync" "node_modules")
|
||||
pid-file (fs/path tmp-dir "server.pid")
|
||||
log-file (fs/path tmp-dir "server.log")
|
||||
data-dir (fs/path tmp-dir "server-data")
|
||||
node-path-file (fs/path tmp-dir "node-path.txt")
|
||||
script (fs/path "cli-e2e" "scripts" "db_sync_server.py")
|
||||
port (free-port)]
|
||||
(try
|
||||
(fs/create-dirs (fs/parent entry))
|
||||
(fs/create-dirs node-modules)
|
||||
(spit (str entry)
|
||||
(str "const fs = require('node:fs');\n"
|
||||
"const http = require('node:http');\n"
|
||||
"fs.writeFileSync(" (pr-str (str node-path-file)) ", process.env.NODE_PATH || '', 'utf8');\n"
|
||||
"http.createServer((req, res) => {\n"
|
||||
" if (req.url === '/health') { res.writeHead(200); res.end('ok'); return; }\n"
|
||||
" res.writeHead(404); res.end('not found');\n"
|
||||
"}).listen(process.env.DB_SYNC_PORT, '127.0.0.1');\n"))
|
||||
(let [start (shell/sh "python3" (str script)
|
||||
"start"
|
||||
"--repo-root" (str repo-root)
|
||||
"--pid-file" (str pid-file)
|
||||
"--log-file" (str log-file)
|
||||
"--data-dir" (str data-dir)
|
||||
"--port" (str port)
|
||||
"--startup-timeout-s" "5"
|
||||
"--auth-path" "")]
|
||||
(try
|
||||
(is (= 0 (:exit start)) (:err start))
|
||||
(is (string/includes? (slurp (str node-path-file)) (str node-modules)))
|
||||
(finally
|
||||
(shell/sh "python3" (str script)
|
||||
"stop"
|
||||
"--pid-file" (str pid-file)
|
||||
"--shutdown-timeout-s" "2"))))
|
||||
(finally
|
||||
(fs/delete-tree tmp-dir)))))
|
||||
55
cli-e2e/test/logseq/cli/e2e/wait_sync_status_test.py
Normal file
55
cli-e2e/test/logseq/cli/e2e/wait_sync_status_test.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[4] / "scripts" / "wait_sync_status.py"
|
||||
spec = importlib.util.spec_from_file_location("wait_sync_status", MODULE_PATH)
|
||||
wait_sync_status = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(wait_sync_status)
|
||||
|
||||
|
||||
def test_resolved_status_command_is_built_once_and_reused() -> None:
|
||||
args = SimpleNamespace(
|
||||
cli="~/repo/static/logseq-cli.js",
|
||||
root_dir="~/tmp/root",
|
||||
config="~/tmp/cli.edn",
|
||||
graph="demo",
|
||||
)
|
||||
|
||||
command = wait_sync_status.status_command(args)
|
||||
|
||||
assert command[0] == "node"
|
||||
assert command[2] == "--root-dir"
|
||||
assert command[-2:] == ["--graph", "demo"]
|
||||
assert Path(command[1]).is_absolute()
|
||||
assert Path(command[3]).is_absolute()
|
||||
assert Path(command[5]).is_absolute()
|
||||
assert wait_sync_status.status_command(args) is command
|
||||
|
||||
|
||||
def test_run_status_uses_precomputed_command() -> None:
|
||||
command = ["node", "/abs/cli.js", "--graph", "demo"]
|
||||
args = SimpleNamespace(status_command=command)
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, capture_output, text):
|
||||
calls.append(cmd)
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout='{"status":"ok","data":{"pending-local":0,"pending-asset":0,"pending-server":0}}',
|
||||
stderr="",
|
||||
)
|
||||
|
||||
original_run = wait_sync_status.subprocess.run
|
||||
wait_sync_status.subprocess.run = fake_run
|
||||
try:
|
||||
payload = wait_sync_status.run_status(args)
|
||||
finally:
|
||||
wait_sync_status.subprocess.run = original_run
|
||||
|
||||
assert payload["status"] == "ok"
|
||||
assert calls == [command]
|
||||
Reference in New Issue
Block a user