Skip to content

Commit

Permalink
Fasten your Seatbelt
Browse files Browse the repository at this point in the history
  • Loading branch information
PEZ committed Jan 21, 2024
0 parents commit 08ec4f7
Show file tree
Hide file tree
Showing 21 changed files with 783 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .clj-kondo/funcool/promesa/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{:lint-as {promesa.core/-> clojure.core/->
promesa.core/->> clojure.core/->>
promesa.core/as-> clojure.core/as->
promesa.core/let clojure.core/let
promesa.core/plet clojure.core/let
promesa.core/loop clojure.core/loop
promesa.core/recur clojure.core/recur
promesa.core/with-redefs clojure.core/with-redefs
promesa.core/doseq clojure.core/doseq}}
5 changes: 5 additions & 0 deletions .clj-kondo/rewrite-clj/rewrite-clj/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{:lint-as
{rewrite-clj.zip/subedit-> clojure.core/->
rewrite-clj.zip/subedit->> clojure.core/->>
rewrite-clj.zip/edit-> clojure.core/->
rewrite-clj.zip/edit->> clojure.core/->>}}
33 changes: 33 additions & 0 deletions .github/workflows/test-runner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Run Tests

on:
push:
branches:
- '*'
pull_request:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Cache npm deps
uses: actions/cache@v2
with:
path: |
node_modules
~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node

- name: Install dependencies
run: npm i

- name: Run tests
uses: coactions/setup-xvfb@v1
with:
run: npm test
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Nodejs
node_modules/

# Environment variables
.env

# OS generated files
.DS_Store
Thumbs.db

# Clojure cache
.cpcache/

# nREPL
.joyride/.nrepl-port
.nrepl-port

# Calva
.clj-kondo/.cache/
.lsp/.cache/
.lsp/sqlite*.db
.calva/output-window/

# VS Code test-electron
.vscode-test/
16 changes: 16 additions & 0 deletions .joyride/deps.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
;; clojure-lsp needs this config to analyze Joyride code
;; To tell clojure-lsp about this file:
;; 1. Add a source-alias to `.lsp/config.edn`. E.g.:
;; :source-aliases #{:src :test :joyride}
;; (The clojure-lsp defaults are `:src :test`.)
;; 2. Add a `:joyride` alias to the project root deps.edn file.
;; Minimal file content:
;; {:aliases {:joyride {:extra-deps {joyride/workspace {:local/root ".joyride"}}}}}
;;
;; To also tell clojure-lsp about your Joyride user scripts, see instructions
;; in your User Joyride config directory `deps.edn` file.
;; (`~/.config/joyride/deps.edn` on Mac/Linux, somewhere similar on Windows?)
{:deps {org.clojure/clojurescript {:mvn/version "1.11.54"}
funcool/promesa {:mvn/version "9.0.471"}
rewrite-clj/rewrite-clj {:mvn/version "1.1.46"}}
:paths ["src" "scripts" "test"]}
29 changes: 29 additions & 0 deletions .joyride/scripts/workspace_activate.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(ns workspace-activate
(:require [joyride.core :as joyride]
[seatbelt.runner] ;; The test runner needs to know when the workspace is activated
))

;; This script only initializes the Backseat Driver app,
;; It must be done before any keyboard shortcuts for the app works.
;; See backseat-driver.app for a sample shortcut binding

;; The app is intended to be a global (User) script.
;; Try it out as a workspace script first, and if you want
;; to use it in your projects,s see README.md for how to install
;; it as a User script.

(println "Backseat Driver: Hello World, from workspace_activate.cljs script")

(defn- -main []
(println "Test Runner Example Workspace: -main called")
(if js/process.env.JOYRIDE_HEADLESS
(println "HEADLESS TEST RUN: Not Initializing Interactive things.")
(do
(println "Initializing interactive things...")
; Beware of the Gilardi scenario: https://technomancy.us/143
(require '[my.thing])
((resolve 'my.thing/init!))))
(seatbelt.runner/ready-to-run-tests! "Workspace activated."))

(when (= (joyride/invoked-script) joyride/*file*)
(-main))
13 changes: 13 additions & 0 deletions .joyride/src/my/thing.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(ns my.thing
(:require ["vscode" :as vscode]))

(defn- button-label [n]
(case n
0 "Awesome!"
1 "OK"
2 "Roger that"
"NYIP"))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn init! []
(vscode/window.showInformationMessage "My Thing is activated now." (button-label 0)))
7 changes: 7 additions & 0 deletions .joyride/src/my/util.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(ns my.util)

(defn the-question []
:the-universe-and-everything)

(defn the-answer []
42)
65 changes: 65 additions & 0 deletions .joyride/src/seatbelt/launcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const cp = require("child_process");
const path = require("path");
const process = require("process");
const {
downloadAndUnzipVSCode,
resolveCliArgsFromVSCodeExecutablePath,
runTests,
} = require("@vscode/test-electron");

async function main(testWorkspace, isWatchMode) {
console.log("launch-test-runner.js: testWorkspace", testWorkspace);
try {
const extensionTestsPath = path.resolve(__dirname, isWatchMode ? 'watchTests' : 'runTests');
const vscodeExecutablePath = await downloadAndUnzipVSCode('insiders');
const [cliPath, ...args] =
resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);

const launchArgs = [
testWorkspace,
...args,
// '--verbose',
'--disable-workspace-trust',
'--install-extension',
'betterthantomorrow.joyride',
'--force',
// Install other extensions that your Joyride scripts under test depend on.
// E.g. Clojure project scripts often can have use for the Calva extension API.
// '--install-extension',
// 'betterthantomorrow.calva',
// '--force',
];

console.log("launch-test-runner.js: launchArgs", launchArgs);
cp.spawnSync(cliPath, launchArgs, {
encoding: "utf-8",
stdio: "inherit",
});

const runOptions = {
vscodeExecutablePath,
extensionTestsPath,
launchArgs: [testWorkspace],
};
await runTests(runOptions)
.then((_result) => {
console.info("launch-test-runner.js: 👍 Tests finished successfully");
})
.catch((err) => {
console.error("launch-test-runner.js: 👎 Tests finished with failures or errors:", err);
process.exit(1);
});
} catch (err) {
console.error("launch-test-runner.js: Failed to run tests:", err);
process.exit(1);
}
}

const workspace = path.resolve(__dirname, "../../..");
console.info(`launch-test-runner.js: Using Workspace: ${workspace}`);

const args = process.argv.slice(2);
const isWatchMode = args.length > 0 && args[0] === "--watch";

main(workspace, isWatchMode);

13 changes: 13 additions & 0 deletions .joyride/src/seatbelt/runTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const vscode = require('vscode');

exports.run = async () => {
console.log("runTests.js: run started");
// Execute and await any VS Code commands that should be run before the tests start.
// (E.g. activating an extension, opening a file, etc.)
// await vscode.commands.executeCommand('some.command');
return vscode.commands.executeCommand(
'joyride.runCode',
`(require '[seatbelt.runner :as runner])
(runner/run-tests!+ "Waiting for workspace to activate...")`
);
};
156 changes: 156 additions & 0 deletions .joyride/src/seatbelt/runner.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
(ns seatbelt.runner
(:require ["vscode" :as vscode]
["path" :as path]
[clojure.string :as string]
[cljs.test]
[promesa.core :as p]))

(def ^:private default-db {:runner+ nil
:ready-to-run? false
:pass 0
:fail 0
:error 0})

(def ^:private !state (atom default-db))

(defn ^:private init-counters! []
(swap! !state merge (select-keys default-db [:pass :fail :error])))

(defn- write [& xs]
(js/process.stdout.write (string/join " " xs)))

(defmethod cljs.test/report [:cljs.test/default :begin-test-var] [m]
(write "===" (str (-> m :var meta :name) ": ")))

(defmethod cljs.test/report [:cljs.test/default :end-test-var] [_m]
(write " ===\n"))

(def original-pass (get-method cljs.test/report [:cljs.test/default :pass]))

(defmethod cljs.test/report [:cljs.test/default :pass] [m]
(binding [*print-fn* write] (original-pass m))
(write "")
(swap! !state update :pass inc))

(def original-fail (get-method cljs.test/report [:cljs.test/default :fail]))

(defmethod cljs.test/report [:cljs.test/default :fail] [m]
(binding [*print-fn* write] (original-fail m))
(write "")
(swap! !state update :fail inc))

(def original-error (get-method cljs.test/report [:cljs.test/default :error]))

(defmethod cljs.test/report [:cljs.test/default :error] [m]
(binding [*print-fn* write] (original-error m))
(write "🚫")
(swap! !state update :error inc))

(def original-end-run-tests (get-method cljs.test/report [:cljs.test/default :end-run-tests]))

(defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
(binding [*print-fn* write]
(original-end-run-tests m)
(let [{:keys [runner+ pass fail error] :as state} @!state
passed-minimum-threshold 2
fail-reason (cond
(< 0 (+ fail error)) "seatbelt: 👎 FAILURE: Some tests failed or errored"
(< pass passed-minimum-threshold) (str "seatbelt: 👎 FAILURE: Less than " passed-minimum-threshold " assertions passed. (Passing: " pass ")")
:else nil)]
(println "seatbelt: tests run, results:"
(select-keys state [:pass :fail :error]) "\n")
(when runner+ ; When not using the runner, there's no promise to resolve or reject
(if fail-reason
(p/reject! runner+ fail-reason)
(p/resolve! runner+ true))))))

(defn- run-tests-impl!+ [test-nss]
(println "seatbelt: Starting tests...")
(init-counters!)
(try
(doseq [test-ns test-nss]
(require test-ns :reload))
(apply cljs.test/run-tests test-nss)
(catch :default e
(p/reject! (:runner+ @!state) e))))

(defn ready-to-run-tests!
"The test runner will wait for this to be called before running any tests.
`ready-message` will be logged when this is called"
[ready-message]
(println "seatbelt:" ready-message)
(swap! !state assoc :ready-to-run? true))

(defn run-ns-tests!+
"Runs the `test-nss` test
NB: Will wait for `ready-to-run-tests!` to be called before doing so.
`waiting-message` will be logged if the test runner is waiting."
[test-nss waiting-message]
(let [runner+ (p/deferred)]
(swap! !state assoc :runner+ runner+)
(if (:ready-to-run? @!state)
(run-tests-impl!+ test-nss)
(do
(println "seatbelt: " waiting-message)
(add-watch !state :runner (fn [k r _o n]
(when (:ready-to-run? n)
(remove-watch r k)
(run-tests-impl!+ test-nss))))))
runner+))

(defn- uri->ns-symbol [uri]
(-> uri
(vscode/workspace.asRelativePath)
(string/split path/sep)
(->> (drop 2)
(string/join "."))
(string/replace "_" "-")
(string/replace #"\.clj[cs]$" "")
symbol))

(defn- glob->ns-symbols [glob]
(p/let [uris (vscode/workspace.findFiles glob)]
(map uri->ns-symbol uris)))

(defn run-tests!+
"Runs the tests in any `_test.cljs` files in `.joyride/src/test/`
NB: Will wait for `ready-to-run-tests!` to be called before doing so.
`waiting-message` will be logged if the test runner is waiting."
[waiting-message]
(p/let [nss (glob->ns-symbols ".joyride/src/test/**/*_test.clj[sc]")]
(println "seatbelt: Running tests in these" (count nss) "namespaces" (pr-str nss))
(run-ns-tests!+ nss waiting-message)))

(defn watcher-test-run!+
([uri reason]
(watcher-test-run!+ uri reason nil))
([uri reason waiting-message]
(println reason (vscode/workspace.asRelativePath uri))
(println "Running tests...")
(when-not (= "." uri)
(require (uri->ns-symbol uri) :reload-all))
(-> (run-tests!+ waiting-message)
(p/then (fn [_]
(js/setImmediate
#(println "🟢 YAY! 🟢"))))
(p/catch (fn [e]
; No sound? Check your settings for terminal.integrated.enableBell
(js/setImmediate #(do (println "\u0007")
(println "🔴 NAY! 🔴" e)))))
(p/finally (fn []
(js/setImmediate
#(println "Waiting for changes...")))))))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn watch!+ [waiting-message]
(let [glob-pattern "**/.joyride/**/*.cljs"
watcher (vscode/workspace.createFileSystemWatcher glob-pattern)]
(.onDidChange watcher (fn [uri]
(watcher-test-run!+ uri "File changed:")))
(.onDidCreate watcher (fn [uri]
(watcher-test-run!+ uri "File created:")))
(.onDidDelete watcher (fn [uri]
(watcher-test-run!+ uri "File deleted:")))
(watcher-test-run!+ "." "Watcher started" waiting-message))
; We leave the vscode electron test runner waiting for this promise
(p/deferred))
13 changes: 13 additions & 0 deletions .joyride/src/seatbelt/watchTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const vscode = require('vscode');

exports.run = async () => {
console.log("runTests.js: run started");
// Execute and await any VS Code commands that should be run before the tests start.
// (E.g. activating an extension, opening a file, etc.)
// await vscode.commands.executeCommand('some.command');
return vscode.commands.executeCommand(
'joyride.runCode',
`(require '[seatbelt.runner :as runner])
(runner/watch!+ "Waiting for workspace to activate...")`
);
};
Loading

0 comments on commit 08ec4f7

Please sign in to comment.