Files
js-maker/testing/jsmaker-executors.rkt
T

165 lines
6.3 KiB
Racket

#lang racket/base
(require racket/file
racket/list
racket/match
racket/port
racket/string)
(provide js-engine?
js-engine-name
js-engine-path
js-engine-kind
js-engine-version
js-run-result?
js-run-result-engine
js-run-result-status
js-run-result-stdout
js-run-result-stderr
js-run-result-success?
known-js-engine-names
find-js-engine
run-js-file)
(struct js-engine (name path kind version-args) #:transparent)
(struct js-run-result (engine status stdout stderr) #:transparent)
(define (path-string/non-empty? v)
(and (string? v) (not (string=? v ""))))
(define (executable-file? p)
(and p (file-exists? p)))
(define (getenv/path name)
(define v (getenv name))
(and (path-string/non-empty? v) (string->path v)))
(define engine-specs
;; name aliases kind version-args extra-paths
'((node ("node" "nodejs") plain ("--version")
("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node"))
(deno ("deno") plain ("--version") ())
(bun ("bun") plain ("--version") ())
(qjs ("qjs" "quickjs") plain ("-v") ())
(d8 ("d8") plain ("--version") ())
(jsc ("jsc") plain ("--version") ())
(js ("js") plain ("--version") ())
(chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version")
("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome"))))
(define (known-js-engine-names)
(map car engine-specs))
(define (lookup-engine-spec name)
(define wanted (string-downcase (format "~a" name)))
(for/first ([spec (in-list engine-specs)]
#:when (or (string=? wanted (symbol->string (car spec)))
(member wanted (cadr spec))))
spec))
(define (candidate-paths spec)
(match spec
[(list name aliases kind version-args extra-paths)
(append
(if (eq? name 'node)
(filter values (list (getenv/path "JSMAKER_NODE")))
'())
(filter values (list (getenv/path "JSMAKER_ENGINE_PATH")))
(filter values (map find-executable-path aliases))
(map string->path extra-paths))]))
(define (make-engine-from-spec spec)
(match spec
[(list name aliases kind version-args extra-paths)
(for/first ([p (in-list (candidate-paths spec))]
#:when (executable-file? p))
(js-engine name p kind version-args))]))
(define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")])
(define req (string-downcase requested))
(cond
[(or (string=? req "") (string=? req "auto"))
(define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK"))
(or (for*/first ([spec (in-list engine-specs)]
#:unless (and (not browser-fallback?) (eq? (car spec) 'chromium))
[engine (in-value (make-engine-from-spec spec))]
#:when engine)
engine)
#f)]
[else
(define spec (lookup-engine-spec req))
(and spec (make-engine-from-spec spec))]))
(define (capture-timeout-seconds)
(define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS"))
(cond
[(and (path-string/non-empty? v) (string->number v)) => values]
[else 15]))
(define (capture path args)
(with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))])
(define-values (proc stdout stdin stderr)
(apply subprocess #f #f #f path args))
(define waiter (thread (lambda () (subprocess-wait proc))))
(define finished? (sync/timeout (capture-timeout-seconds) waiter))
(unless finished?
(subprocess-kill proc #t)
(thread-wait waiter))
(values (if finished? (subprocess-status proc) 124)
(port->string stdout)
(string-append (port->string stderr)
(if finished? "" "\nJavaScript engine timed out and was killed.\n")))))
(define (js-engine-version engine)
(define-values (status stdout stderr)
(capture (js-engine-path engine) (js-engine-version-args engine)))
(define s (string-trim (if (string=? stdout "") stderr stdout)))
(and (zero? status) (not (string=? s "")) s))
(define (plain-run-args engine js-path)
(case (js-engine-name engine)
[(deno) (list "run" "--quiet" js-path)]
[else (list js-path)]))
(define (copy-js-as-browser-html js-path)
(define html-path (build-path (find-system-path 'temp-dir)
(format "jsmaker-browser-~a.html" (current-inexact-milliseconds))))
(define js (file->string js-path))
(call-with-output-file html-path
#:exists 'replace
(lambda (out)
(displayln "<!doctype html><meta charset=\"utf-8\"><pre id=\"out\"></pre>" out)
(displayln "<script>" out)
(displayln "const __out = document.getElementById('out');" out)
(displayln "const print = (...xs) => { __out.append(xs.join(' ') + '\\n'); };" out)
(displayln "console.log = (...xs) => print(...xs);" out)
(displayln "console.error = (...xs) => print(...xs);" out)
(displayln "try {" out)
(display js out)
(displayln "\ndocument.documentElement.setAttribute('data-jsmaker-status', 'ok');" out)
(displayln "} catch (e) {" out)
(displayln " print('ERROR\\t' + (e && e.stack ? e.stack : String(e)));" out)
(displayln " document.documentElement.setAttribute('data-jsmaker-status', 'fail');" out)
(displayln "}" out)
(displayln "</script>" out)))
html-path)
(define (chromium-run engine js-path)
(define html-path (copy-js-as-browser-html js-path))
(define url (string-append "file://" (path->string html-path)))
(define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
(define-values (status stdout stderr) (capture (js-engine-path engine) args))
(define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
(js-run-result engine (if browser-fail? 1 status) stdout stderr))
(define (run-js-file engine js-path)
(case (js-engine-kind engine)
[(chromium) (chromium-run engine js-path)]
[else
(define-values (status stdout stderr)
(capture (js-engine-path engine) (plain-run-args engine js-path)))
(js-run-result engine status stdout stderr)]))
(define (js-run-result-success? result)
(zero? (js-run-result-status result)))