rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1// Package nodetest runs Node from a Go test, for the JavaScript that
2// has a Go twin or a pure-logic core: a crypto envelope both languages
3// must open, a date parser, a golden corpus. Node here is a dev
4// toolchain, never a build dependency — application JS stays
5// browser-only, and a browser test stays the engine of record for
6// anything that needs a DOM.
7//
8// It exists because every package that did this carried its own copy
9// of the same ten lines, and the copies had drifted in the ways that
10// matter: some skipped with a message naming what went unverified and
11// some did not, and none could be told that a missing node is a
12// failure rather than a skip. A skip is the right answer on a laptop
13// without Node and the wrong one on CI, where a runner image that lost
14// node would retire every JavaScript check in the tree without anybody
15// deciding to. RequireEnv is that switch, read in one place.
16package nodetest
17
18import (
19 "bytes"
20 "context"
21 "os"
22 "os/exec"
23 "strings"
24 "testing"
25 "time"
26)
27
28// RequireEnv names the environment variable that turns a missing node
29// from a skip into a failure. Set it (to anything) wherever the suite
30// must not quietly shrink — CI.
31const RequireEnv = "FIESTA_TEST_REQUIRE_NODE"
32
33// DefaultTimeout bounds a run whose Cmd sets none. It is a backstop
34// against a script that never exits (an unresolved promise, a stdin
35// read nobody feeds), not a performance assertion: a hang fails the
36// test with its output instead of holding the whole package until go
37// test's own ten-minute alarm, which names no culprit.
38const DefaultTimeout = 2 * time.Minute
39
40// Node returns the path of the node binary. With none on PATH it skips
41// the test, naming the test so the skip says what went unverified — or
42// fails it when RequireEnv is set.
43func Node(t testing.TB) string {
44 t.Helper()
45 node, err := exec.LookPath("node")
46 if err != nil {
47 if os.Getenv(RequireEnv) != "" {
48 t.Fatalf("%s is set, so a Node test may not skip, but node is not on PATH: %v", RequireEnv, err)
49 }
50 t.Skipf("node is not on PATH, so %s's JavaScript is unverified here: install Node and rerun it", t.Name())
51 }
52 return node
53}
54
55// Cmd is one node invocation.
56type Cmd struct {
57 // Args are node's arguments: its flags, the script, and the
58 // script's own arguments — {"--test", "js/golden.test.mjs"}, or
59 // {"--import", hook, "harness.mjs", "--mode"}.
60 Args []string
61 // Dir is the working directory. Empty means the test's own, which
62 // go test makes the package directory.
63 Dir string
64 // Stdin is fed to the script; nil is an empty stdin, never the
65 // test binary's own.
66 Stdin []byte
67 // Env is added to the test process's environment, so PATH and
68 // HOME survive and a caller only names what it changes (TZ=UTC).
69 Env []string
70 // Timeout overrides DefaultTimeout.
71 Timeout time.Duration
72}
73
74// Run runs node (skipping as Node does when there is none) and returns
75// its stdout. A non-zero exit or a timeout fails the test with BOTH
76// streams: node --test reports its failures on stdout and a crashed
77// script on stderr, so either alone hides half of what went wrong.
78func Run(t testing.TB, c Cmd) []byte {
79 t.Helper()
80 node := Node(t)
81 timeout := c.Timeout
82 if timeout <= 0 {
83 timeout = DefaultTimeout
84 }
85 ctx, cancel := context.WithTimeout(context.Background(), timeout)
86 defer cancel()
87
88 cmd := exec.CommandContext(ctx, node, c.Args...)
89 // The timeout kills node, but a child it spawned can still hold
90 // the output pipes open and Wait would block on them; WaitDelay
91 // bounds that too, so the backstop is a backstop.
92 cmd.WaitDelay = 5 * time.Second
93 cmd.Dir = c.Dir
94 cmd.Stdin = bytes.NewReader(c.Stdin)
95 if len(c.Env) > 0 {
96 cmd.Env = append(os.Environ(), c.Env...)
97 }
98 var stdout, stderr bytes.Buffer
99 cmd.Stdout, cmd.Stderr = &stdout, &stderr
100 if err := cmd.Run(); err != nil {
101 what := "node " + strings.Join(c.Args, " ")
102 if ctx.Err() == context.DeadlineExceeded {
103 t.Fatalf("%s: timed out after %s\nstdout:\n%s\nstderr:\n%s", what, timeout, stdout.String(), stderr.String())
104 }
105 t.Fatalf("%s: %v\nstdout:\n%s\nstderr:\n%s", what, err, stdout.String(), stderr.String())
106 }
107 return stdout.Bytes()
108}
109