Skip to content

Commit d5ce578

Browse files
committed
add node runtime tests
1 parent 319ff9b commit d5ce578

File tree

2 files changed

+369
-76
lines changed

2 files changed

+369
-76
lines changed

tests/docstrings_examples/DocTest.res

Lines changed: 174 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module Node = {
1414
@scope(("process", "stderr"))
1515
external stderrWrite: string => unit = "write"
1616
@scope("process") external cwd: unit => string = "cwd"
17+
@val @scope("process")
18+
external argv: array<string> = "argv"
1719
}
1820

1921
module Fs = {
@@ -42,9 +44,10 @@ module Node = {
4244
external spawnSync: (string, array<string>) => spawnSyncReturns = "spawnSync"
4345

4446
type readable
45-
type spawnReturns = {stderr: readable}
47+
type spawnReturns = {stderr: readable, stdout: readable}
48+
type options = {cwd?: string, env?: Dict.t<string>, timeout?: int}
4649
@module("child_process")
47-
external spawn: (string, array<string>) => spawnReturns = "spawn"
50+
external spawn: (string, array<string>, ~options: options=?) => spawnReturns = "spawn"
4851

4952
@send external on: (readable, string, Buffer.t => unit) => unit = "on"
5053
@send
@@ -56,6 +59,19 @@ module Node = {
5659
@module("os")
5760
external tmpdir: unit => string = "tmpdir"
5861
}
62+
63+
module Util = {
64+
type arg = {@as("type") type_: string}
65+
type config = {
66+
args: array<string>,
67+
options: Dict.t<arg>,
68+
}
69+
type parsed = {
70+
values: Dict.t<string>,
71+
positionals: array<string>,
72+
}
73+
@module("node:util") external parseArgs: config => parsed = "parseArgs"
74+
}
5975
}
6076

6177
open Node
@@ -64,6 +80,45 @@ module Docgen = RescriptTools.Docgen
6480

6581
let bscBin = Path.join(["cli", "bsc"])
6682

83+
let options = Dict.fromArray([("ignore-runtime-tests", {Util.type_: "string"})])
84+
85+
let {Util.values: values} = Util.parseArgs({
86+
args: Process.argv->Array.sliceToEnd(~start=2),
87+
options,
88+
})
89+
90+
let ignoreRuntimeTests = switch values->Dict.get("ignore-runtime-tests") {
91+
| Some(v) =>
92+
v
93+
->String.split(",")
94+
->Array.map(s => s->String.trim)
95+
| None => []
96+
}
97+
98+
module SpawnAsync = {
99+
type t = {
100+
stdout: array<Buffer.t>,
101+
stderr: array<Buffer.t>,
102+
code: Null.t<float>,
103+
}
104+
let run = async (~command, ~args, ~options=?) => {
105+
await Promise.make((resolve, _reject) => {
106+
let spawn = ChildProcess.spawn(command, args, ~options?)
107+
let stdout = []
108+
let stderr = []
109+
spawn.stdout->ChildProcess.on("data", data => {
110+
Array.push(stdout, data)
111+
})
112+
spawn.stderr->ChildProcess.on("data", data => {
113+
Array.push(stderr, data)
114+
})
115+
spawn->ChildProcess.once("close", (code, _signal) => {
116+
resolve({stdout, stderr, code})
117+
})
118+
})
119+
}
120+
}
121+
67122
type example = {
68123
id: string,
69124
kind: string,
@@ -73,35 +128,75 @@ type example = {
73128

74129
let createFileInTempDir = id => Path.join2(OS.tmpdir(), id)
75130

76-
let testCode = async (~id, ~code) => {
131+
let compileTest = async (~id, ~code) => {
77132
let id = id->String.includes("/") ? String.replace(id, "/", "slash_op") : id
78133
let tempFileName = createFileInTempDir(id)
79134

80135
let () = await Fs.writeFile(tempFileName ++ ".res", code)
81136

82137
let args = [tempFileName ++ ".res", "-w", "-3-109"]
83138

84-
let promise = await Promise.make((resolve, _reject) => {
85-
let spawn = ChildProcess.spawn(bscBin, args)
86-
let stderr = []
87-
spawn.stderr->ChildProcess.on("data", data => {
88-
Array.push(stderr, data)
89-
})
90-
spawn->ChildProcess.once("close", (_code, _signal) => {
91-
resolve(stderr)
92-
})
93-
})
139+
let {stderr, stdout} = await SpawnAsync.run(~command=bscBin, ~args)
94140

95-
switch Array.length(promise) > 0 {
141+
switch Array.length(stderr) > 0 {
96142
| true =>
97-
promise
143+
stderr
98144
->Array.map(e => e->Buffer.toString)
99145
->Array.join("")
100146
->Error
101-
| false => Ok()
147+
| false =>
148+
stdout
149+
->Array.map(e => e->Buffer.toString)
150+
->Array.join("")
151+
->Ok
102152
}
103153
}
104154

155+
let runtimeTests = async code => {
156+
let {stdout, stderr, code: exitCode} = await SpawnAsync.run(
157+
~command="node",
158+
~args=["-e", code],
159+
~options={
160+
cwd: Process.cwd(),
161+
timeout: 2000,
162+
},
163+
)
164+
165+
// Some expressions, like, `console.error("error")` is printed to stderr but
166+
// exit code is 0
167+
let std = switch exitCode->Null.toOption {
168+
| Some(exitCode) if exitCode == 0.0 && Array.length(stderr) > 0 => stderr->Ok
169+
| Some(exitCode) if exitCode == 0.0 => stdout->Ok
170+
| None | Some(_) => Error(Array.length(stderr) > 0 ? stderr : stdout)
171+
}
172+
173+
switch std {
174+
| Ok(buf) =>
175+
buf
176+
->Array.map(e => e->Buffer.toString)
177+
->Array.join("")
178+
->Ok
179+
| Error(buf) =>
180+
buf
181+
->Array.map(e => e->Buffer.toString)
182+
->Array.join("")
183+
->Error
184+
}
185+
}
186+
187+
let indentOutputCode = code => {
188+
let indent = String.repeat(" ", 2)
189+
190+
code
191+
->String.split("\n")
192+
->Array.map(s => `${indent}${s}`)
193+
->Array.join("\n")
194+
}
195+
196+
type error =
197+
| ReScript({error: string})
198+
| Runtime({rescript: string, js: string, error: string})
199+
105200
let extractDocFromFile = file => {
106201
let toolsBin = Path.join([Process.cwd(), "cli", "rescript-tools"])
107202
let spawn = ChildProcess.spawnSync(toolsBin, ["doc", file])
@@ -209,54 +304,90 @@ let main = async () => {
209304
await codes
210305
->Array.mapWithIndex(async (code, int) => {
211306
let id = `${id}_${Int.toString(int)}`
212-
await testCode(~id, ~code)
307+
(code, await compileTest(~id, ~code))
213308
})
214309
->Promise.all
215310
(example, results)
216311
})
217312
->Promise.all
218313

219-
let errors = results->Belt.Array.keepMap(((example, results)) => {
220-
let errors = results->Belt.Array.keepMap(result =>
314+
let examples = results->Array.map(((example, results)) => {
315+
let (compiled, errors) = results->Array.reduce(([], []), (acc, (resCode, result)) => {
316+
let (oks, errors) = acc
221317
switch result {
222-
| Ok() => None
223-
| Error(msg) => Some(msg)
318+
| Ok(jsCode) => ([...oks, (resCode, jsCode)], errors)
319+
| Error(output) => (oks, [...errors, ReScript({error: output})])
224320
}
225-
)
226-
227-
if Array.length(errors) > 0 {
228-
Some((example, errors))
229-
} else {
230-
None
231-
}
321+
})
322+
(example, (compiled, errors))
232323
})
233324

325+
let exampleErrors =
326+
await examples
327+
->Array.filter((({id}, _)) => !Array.includes(ignoreRuntimeTests, id))
328+
->Array.map(async ((example, (compiled, errors))) => {
329+
let nodeTests =
330+
await compiled
331+
->Array.map(async ((res, js)) => (res, js, await runtimeTests(js)))
332+
->Promise.all
333+
334+
let runtimeErrors = nodeTests->Belt.Array.keepMap(((res, js, output)) =>
335+
switch output {
336+
| Ok(_) => None
337+
| Error(error) => Some(Runtime({rescript: res, js, error}))
338+
}
339+
)
340+
341+
(example, Array.concat(runtimeErrors, errors))
342+
})
343+
->Promise.all
344+
234345
// Print Errors
235-
let () = errors->Array.forEach(((test, errors)) => {
346+
let () = exampleErrors->Array.forEach(((example, errors)) => {
236347
let red = s => `\x1B[1;31m${s}\x1B[0m`
237348
let cyan = s => `\x1b[36m${s}\x1b[0m`
238-
let kind = switch test.kind {
349+
let kind = switch example.kind {
239350
| "moduleAlias" => "module alias"
240351
| other => other
241352
}
242353

243-
let errorMessage =
244-
errors
245-
->Array.map(e => {
246-
// Drop line from path file
247-
e
248-
->String.split("\n")
249-
->Array.filterWithIndex((_, i) => i !== 2)
250-
->Array.join("\n")
251-
})
252-
->Array.join("\n")
354+
let errorMessage = errors->Array.map(err =>
355+
switch err {
356+
| ReScript({error}) =>
357+
let err =
358+
error
359+
->String.split("\n")
360+
->Array.filterWithIndex((_, i) => i !== 2)
361+
->Array.join("\n")
362+
363+
`${"error"->red}: failed to compile examples from ${kind} ${example.id->cyan}
364+
${err}`
365+
| Runtime({rescript, js, error}) =>
366+
let indent = String.repeat(" ", 2)
367+
368+
`${"runtime error"->red}: failed to run examples from ${kind} ${example.id->cyan}
369+
370+
${indent}${"ReScript"->cyan}
253371
254-
let message = `${"error"->red}: failed to compile examples from ${kind} ${test.id->cyan}\n${errorMessage}`
372+
${rescript->indentOutputCode}
255373
256-
Process.stderrWrite(message)
374+
${indent}${"Compiled Js"->cyan}
375+
376+
${js->indentOutputCode}
377+
378+
${indent}${"stacktrace"->red}
379+
380+
${error->indentOutputCode}
381+
`
382+
}
383+
)
384+
385+
errorMessage->Array.forEach(e => Process.stderrWrite(e))
257386
})
258387

259-
errors->Array.length == 0 ? 0 : 1
388+
let someError = exampleErrors->Array.some(((_, err)) => Array.length(err) > 0)
389+
390+
someError ? 1 : 0
260391
}
261392

262393
let exitCode = await main()

0 commit comments

Comments
 (0)