diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9376937ac4..a4d9763efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,6 @@ jobs: # Run the slower tasks on the fastest runner only build_playground: true benchmarks: true - docstring_tests: true - os: buildjet-2vcpu-ubuntu-2204-arm # ARM ocaml_compiler: ocaml-variants.5.2.1+options,ocaml-option-static upload_binaries: true @@ -313,11 +312,6 @@ jobs: - name: Run tests run: node scripts/test.js -all - - name: Docstrings tests - if: matrix.docstring_tests - # Ignore functions that are not available on Node 18 - run: node tests/docstrings_examples/DocTest.res.mjs --ignore-runtime-tests "Array.toReversed, Array.toSorted, Promise.withResolvers, Set.union, Set.isSupersetOf, Set.isSubsetOf, Set.isDisjointFrom, Set.intersection, Set.symmetricDifference, Set.difference" - - name: Check for diffs in tests folder run: git diff --ignore-cr-at-eol --exit-code tests diff --git a/scripts/test.js b/scripts/test.js index 3b5c8ca39e..e33d3add47 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -132,18 +132,54 @@ async function runTests() { console.log(`Skipping docstrings tests on ${process.platform}`); } else { console.log("Running runtime docstrings tests"); + + const generated_mocha_test_res = path.join( + "tests", + "docstrings_examples", + "generated_mocha_test.res", + ); + + // Remove `generated_mocha_test.res` if file exists + if (fs.existsSync(generated_mocha_test_res)) { + console.log(`Removing ${generated_mocha_test_res}`); + fs.unlinkSync(generated_mocha_test_res); + } + + cp.execSync(`${rescript_exe} build`, { + cwd: path.join(__dirname, "..", "tests/docstrings_examples"), + stdio: [0, 1, 2], + }); + + // Generate rescript file with all tests `generated_mocha_test.res` + cp.execSync( + `node ${path.join("tests", "docstrings_examples", "DocTest.res.mjs")}`, + { + cwd: path.join(__dirname, ".."), + stdio: [0, 1, 2], + }, + ); + + // Build again to check if generated_mocha_test.res has syntax or type erros cp.execSync(`${rescript_exe} build`, { cwd: path.join(__dirname, "..", "tests/docstrings_examples"), stdio: [0, 1, 2], }); - // Ignore some tests not supported by node v18 - // cp.execSync( - // `node ${path.join("tests", "docstrings_examples", "DocTest.res.mjs")} --ignore-runtime-tests "Array.toReversed, Array.toSorted, Promise.withResolvers, Set.union, Set.isSupersetOf, Set.isSubsetOf, Set.isDisjointFrom, Set.intersection, Set.symmetricDifference, Set.difference"`, - // { - // cwd: path.join(__dirname, ".."), - // stdio: [0, 1, 2], - // }, - // ); + + // Format generated_mocha_test.res + console.log("Formatting generated_mocha_test.res"); + cp.execSync(`./cli/rescript format ${generated_mocha_test_res}`, { + cwd: path.join(__dirname, ".."), + stdio: [0, 1, 2], + }); + + console.log("Run mocha test"); + cp.execSync( + `npx mocha ${path.join("tests", "docstrings_examples", "generated_mocha_test.res.mjs")}`, + { + cwd: path.join(__dirname, ".."), + stdio: [0, 1, 2], + }, + ); } } } diff --git a/tests/docstrings_examples/.gitignore b/tests/docstrings_examples/.gitignore new file mode 100644 index 0000000000..448ac7b39c --- /dev/null +++ b/tests/docstrings_examples/.gitignore @@ -0,0 +1,2 @@ +generated_mocha_test.res +generated_mocha_test.res.mjs diff --git a/tests/docstrings_examples/DocTest.res b/tests/docstrings_examples/DocTest.res index fa61eb599c..2d3d7bdea6 100644 --- a/tests/docstrings_examples/DocTest.res +++ b/tests/docstrings_examples/DocTest.res @@ -9,24 +9,34 @@ type example = { docstrings: array, } -type error = - | ReScriptError(string) - | RuntimeError({rescript: string, js: string, error: string}) - -let bscBin = Path.join(["cli", "bsc"]) - -let parsed = Util.parseArgs({ - args: Process.argv->Array.sliceToEnd(~start=2), - options: dict{"ignore-runtime-tests": {Util.type_: "string"}}, -}) - -let ignoreRuntimeTests = switch parsed.values->Dict.get("ignore-runtime-tests") { -| Some(v) => - v - ->String.split(",") - ->Array.map(s => s->String.trim) -| None => [] -} +// Only major version +let nodeVersion = + Process.version + ->String.replace("v", "") + ->String.split(".") + ->Array.get(0) + ->Option.getExn(~message="Failed to find major version of Node") + ->Int.fromString + ->Option.getExn(~message="Failed to convert node version to Int") + +let ignoreRuntimeTests = [ + ( + // Ignore some tests not supported by node v18 + 18, + [ + "Array.toReversed", + "Array.toSorted", + "Promise.withResolvers", + "Set.union", + "Set.isSupersetOf", + "Set.isSubsetOf", + "Set.isDisjointFrom", + "Set.intersection", + "Set.symmetricDifference", + "Set.difference", + ], + ), +] let getOutput = buffer => buffer @@ -158,142 +168,85 @@ let extractExamples = async () => { examples } -let compileTest = async (~code) => { - // NOTE: warnings argument (-w) should be before eval (-e) argument - let args = ["-w", "-3-109-44", "-e", code] - let {stderr, stdout} = await SpawnAsync.run(~command=bscBin, ~args) +let main = async () => { + let examples = await extractExamples() - stderr->Array.length > 0 ? Error(stderr->getOutput) : Ok(stdout->getOutput) -} + examples->Array.sort((a, b) => String.compare(a.id, b.id)) -let compileExamples = async examples => { - Console.log(`Compiling ${examples->Array.length->Int.toString} examples from docstrings...`) + let dict = dict{} - let compiled = [] - let compilationErrors = [] + examples->Array.forEach(cur => { + let modulePath = cur.id->String.split(".") - await examples->ArrayUtils.forEachAsyncInBatches(~batchSize, async example => { - // let id = example.id->String.replaceAll(".", "__") - let rescriptCode = example->getCodeBlocks + let id = + modulePath + ->Array.slice(~start=0, ~end=Array.length(modulePath) - 1) + ->Array.join(".") - switch await compileTest(~code=rescriptCode) { - | Ok(jsCode) => compiled->Array.push((example, rescriptCode, jsCode)) - | Error(err) => compilationErrors->Array.push((example, ReScriptError(err))) + let previous = switch dict->Dict.get(id) { + | Some(p) => p + | None => [] } - }) - - (compiled, compilationErrors) -} - -let runTest = async code => { - let {stdout, stderr, code: exitCode} = await SpawnAsync.run( - ~command="node", - ~args=["-e", code, "--input-type", "commonjs"], - ~options={cwd: Process.cwd(), timeout: 2000}, - ) - - // Some expressions, like `console.error("error")` are printed to stderr, - // but exit code is 0 - let std = switch exitCode->Null.toOption { - | Some(exitCode) if exitCode == 0.0 && Array.length(stderr) > 0 => stderr->Ok - | Some(exitCode) if exitCode == 0.0 => stdout->Ok - | None | Some(_) => Error(Array.length(stderr) > 0 ? stderr : stdout) - } - switch std { - | Ok(buf) => Ok(buf->getOutput) - | Error(buf) => Error(buf->getOutput) - } -} - -let runExamples = async compiled => { - Console.log(`Running ${compiled->Array.length->Int.toString} compiled examples...`) - - let tests = compiled->Array.filter((({id}, _, _)) => !(ignoreRuntimeTests->Array.includes(id))) - - let runtimeErrors = [] - await tests->ArrayUtils.forEachAsyncInBatches(~batchSize, async compiled => { - let (example, rescriptCode, jsCode) = compiled - - switch await runTest(jsCode) { - | Ok(_) => () - | Error(error) => - let runtimeError = RuntimeError({rescript: rescriptCode, js: jsCode, error}) - runtimeErrors->Array.push((example, runtimeError)) - } + dict->Dict.set(id, Array.concat([cur], previous)) }) - runtimeErrors -} + let output = [] -let indentOutputCode = code => { - let indent = String.repeat(" ", 2) - - code - ->String.split("\n") - ->Array.map(s => `${indent}${s}`) - ->Array.join("\n") -} + dict->Dict.forEachWithKey((examples, key) => { + let codeExamples = examples->Array.filterMap(example => { + let ignoreExample = Array.find( + ignoreRuntimeTests, + ((version, tests)) => { + nodeVersion === version && Array.includes(tests, example.id) + }, + )->Option.isSome -let printErrors = errors => { - errors->Array.forEach(((example, errors)) => { - let red = s => `\x1B[1;31m${s}\x1B[0m` - let cyan = s => `\x1b[36m${s}\x1b[0m` - let kind = switch example.kind { - | "moduleAlias" => "module alias" - | other => other - } - - let a = switch errors { - | ReScriptError(error) => - let err = - error - ->String.split("\n") - // Drop line of filename - ->Array.filterWithIndex((_, i) => i !== 2) - ->Array.join("\n") - - `${"error"->red}: failed to compile examples from ${kind} ${example.id->cyan} -${err}` - | RuntimeError({rescript, js, error}) => - let indent = String.repeat(" ", 2) - - `${"runtime error"->red}: failed to run examples from ${kind} ${example.id->cyan} - -${indent}${"ReScript"->cyan} - -${rescript->indentOutputCode} - -${indent}${"Compiled Js"->cyan} - -${js->indentOutputCode} - -${indent}${"stacktrace"->red} - -${error->indentOutputCode} -` + switch ignoreExample { + | true => + Console.warn( + `Ignoring ${example.id} tests. Not supported by Node ${nodeVersion->Int.toString}`, + ) + None + | false => + let code = getCodeBlocks(example) + + switch String.length(code) === 0 { + | true => None + | false => + // Let's add the examples inside a Test module because some examples + // have type definitions that are not supported inside a block. + // Also add unit type `()` + Some( + `test("${example.id->String.split(".")->Array.at(-1)->Option.getExn}", () => { + module Test = { + ${code} + } + () +})`, + ) + } + } + }) + + switch Array.length(codeExamples) > 0 { + | true => + let content = `describe("${key}", () => { +${codeExamples->Array.join("\n")} + })` + output->Array.push(content) + | false => () } - - Process.stderrWrite(a) }) -} -let main = async () => { - let examples = await extractExamples() - let (compiled, compilationErrors) = await compileExamples(examples) - let runtimeErrors = await runExamples(compiled) + let dirname = url->URL.fileURLToPath->Path.dirname + let filepath = Path.join([dirname, "generated_mocha_test.res"]) + let fileContent = `open Mocha +@@warning("-32-34-60-37-109-3-44") - let allErrors = Array.concat(runtimeErrors, compilationErrors) +${output->Array.join("\n")}` - if allErrors->Array.length > 0 { - printErrors(allErrors) - 1 - } else { - Console.log("All examples passed successfully") - 0 - } + await Fs.writeFile(filepath, fileContent) } -let exitCode = await main() - -Process.exit(exitCode) +let () = await main() diff --git a/tests/docstrings_examples/DocTest.res.mjs b/tests/docstrings_examples/DocTest.res.mjs index 5a09cd10b3..955335592f 100644 --- a/tests/docstrings_examples/DocTest.res.mjs +++ b/tests/docstrings_examples/DocTest.res.mjs @@ -3,33 +3,41 @@ import * as Fs from "fs"; import * as Os from "os"; import * as Exn from "rescript/lib/es6/Exn.js"; +import * as Int from "rescript/lib/es6/Int.js"; +import * as Url from "url"; +import * as Dict from "rescript/lib/es6/Dict.js"; import * as List from "rescript/lib/es6/List.js"; import * as Path from "path"; import * as $$Array from "rescript/lib/es6/Array.js"; import * as $$Error from "rescript/lib/es6/Error.js"; +import * as Option from "rescript/lib/es6/Option.js"; import * as Belt_List from "rescript/lib/es6/Belt_List.js"; -import * as Nodeutil from "node:util"; import * as ArrayUtils from "./ArrayUtils.res.mjs"; import * as Belt_Array from "rescript/lib/es6/Belt_Array.js"; import * as Pervasives from "rescript/lib/es6/Pervasives.js"; import * as SpawnAsync from "./SpawnAsync.res.mjs"; +import * as Primitive_string from "rescript/lib/es6/Primitive_string.js"; +import * as Promises from "node:fs/promises"; import * as Primitive_exceptions from "rescript/lib/es6/Primitive_exceptions.js"; import * as RescriptTools_Docgen from "rescript/lib/es6/RescriptTools_Docgen.js"; -let bscBin = Path.join("cli", "bsc"); +let nodeVersion = Option.getExn(Int.fromString(Option.getExn(process.version.replace("v", "").split(".")[0], "Failed to find major version of Node"), undefined), "Failed to convert node version to Int"); -let parsed = Nodeutil.parseArgs({ - args: process.argv.slice(2), - options: { - "ignore-runtime-tests": { - type: "string" - } - } -}); - -let v = parsed.values["ignore-runtime-tests"]; - -let ignoreRuntimeTests = v !== undefined ? v.split(",").map(s => s.trim()) : []; +let ignoreRuntimeTests = [[ + 18, + [ + "Array.toReversed", + "Array.toSorted", + "Promise.withResolvers", + "Set.union", + "Set.isSupersetOf", + "Set.isSubsetOf", + "Set.isDisjointFrom", + "Set.intersection", + "Set.symmetricDifference", + "Set.difference" + ] + ]]; function getOutput(buffer) { return buffer.map(e => e.toString()).join(""); @@ -52,7 +60,7 @@ async function extractDocFromFile(file) { RE_EXN_ID: "Assert_failure", _1: [ "DocTest.res", - 48, + 58, 9 ], Error: new Error() @@ -213,174 +221,50 @@ async function extractExamples() { return examples; } -async function compileTest(code) { - let args = [ - "-w", - "-3-109-44", - "-e", - code - ]; - let match = await SpawnAsync.run(bscBin, args, undefined); - let stderr = match.stderr; - if (stderr.length > 0) { - return { - TAG: "Error", - _0: getOutput(stderr) - }; - } else { - return { - TAG: "Ok", - _0: getOutput(match.stdout) - }; - } -} - -async function compileExamples(examples) { - console.log("Compiling " + examples.length.toString() + " examples from docstrings..."); - let compiled = []; - let compilationErrors = []; - await ArrayUtils.forEachAsyncInBatches(examples, batchSize, async example => { - let rescriptCode = getCodeBlocks(example); - let jsCode = await compileTest(rescriptCode); - if (jsCode.TAG === "Ok") { - compiled.push([ - example, - rescriptCode, - jsCode._0 - ]); - return; - } - compilationErrors.push([ - example, - { - TAG: "ReScriptError", - _0: jsCode._0 - } - ]); - }); - return [ - compiled, - compilationErrors - ]; -} - -async function runTest(code) { - let match = await SpawnAsync.run("node", [ - "-e", - code, - "--input-type", - "commonjs" - ], { - cwd: process.cwd(), - timeout: 2000 +async function main() { + let examples = await extractExamples(); + examples.sort((a, b) => Primitive_string.compare(a.id, b.id)); + let dict = {}; + examples.forEach(cur => { + let modulePath = cur.id.split("."); + let id = modulePath.slice(0, modulePath.length - 1 | 0).join("."); + let p = dict[id]; + let previous = p !== undefined ? p : []; + dict[id] = [cur].concat(previous); }); - let exitCode = match.code; - let stderr = match.stderr; - let stdout = match.stdout; - let std; - let exit = 0; - if (exitCode !== null) { - if (exitCode === 0.0 && stderr.length > 0) { - std = { - TAG: "Ok", - _0: stderr - }; - } else if (exitCode === 0.0) { - std = { - TAG: "Ok", - _0: stdout - }; - } else { - exit = 1; - } - } else { - exit = 1; - } - if (exit === 1) { - std = { - TAG: "Error", - _0: stderr.length > 0 ? stderr : stdout - }; - } - if (std.TAG === "Ok") { - return { - TAG: "Ok", - _0: getOutput(std._0) - }; - } else { - return { - TAG: "Error", - _0: getOutput(std._0) - }; - } -} - -async function runExamples(compiled) { - console.log("Running " + compiled.length.toString() + " compiled examples..."); - let tests = compiled.filter(param => !ignoreRuntimeTests.includes(param[0].id)); - let runtimeErrors = []; - await ArrayUtils.forEachAsyncInBatches(tests, batchSize, async compiled => { - let jsCode = compiled[2]; - let error = await runTest(jsCode); - if (error.TAG === "Ok") { + let output = []; + Dict.forEachWithKey(dict, (examples, key) => { + let codeExamples = $$Array.filterMap(examples, example => { + let ignoreExample = Option.isSome(ignoreRuntimeTests.find(param => { + if (nodeVersion === param[0]) { + return param[1].includes(example.id); + } else { + return false; + } + })); + if (ignoreExample) { + console.warn("Ignoring " + example.id + " tests. Not supported by Node " + nodeVersion.toString()); + return; + } + let code = getCodeBlocks(example); + if (code.length === 0) { + return; + } else { + return "test(\"" + Option.getExn(example.id.split(".").at(-1), undefined) + "\", () => {\n module Test = {\n " + code + "\n }\n ()\n})"; + } + }); + if (codeExamples.length <= 0) { return; } - let runtimeError_0 = compiled[1]; - let runtimeError_2 = error._0; - let runtimeError = { - TAG: "RuntimeError", - rescript: runtimeError_0, - js: jsCode, - error: runtimeError_2 - }; - runtimeErrors.push([ - compiled[0], - runtimeError - ]); - }); - return runtimeErrors; -} - -function indentOutputCode(code) { - let indent = " ".repeat(2); - return code.split("\n").map(s => indent + s).join("\n"); -} - -function printErrors(errors) { - errors.forEach(param => { - let errors = param[1]; - let example = param[0]; - let cyan = s => "\x1b[36m" + s + "\x1b[0m"; - let other = example.kind; - let kind = other === "moduleAlias" ? "module alias" : other; - let a; - if (errors.TAG === "ReScriptError") { - let err = errors._0.split("\n").filter((param, i) => i !== 2).join("\n"); - a = "\x1B[1;31merror\x1B[0m: failed to compile examples from " + kind + " " + cyan(example.id) + "\n" + err; - } else { - let indent = " ".repeat(2); - a = "\x1B[1;31mruntime error\x1B[0m: failed to run examples from " + kind + " " + cyan(example.id) + "\n\n" + indent + "\x1b[36mReScript\x1b[0m\n\n" + indentOutputCode(errors.rescript) + "\n\n" + indent + "\x1b[36mCompiled Js\x1b[0m\n\n" + indentOutputCode(errors.js) + "\n\n" + indent + "\x1B[1;31mstacktrace\x1B[0m\n\n" + indentOutputCode(errors.error) + "\n"; - } - process.stderr.write(a); + let content = "describe(\"" + key + "\", () => {\n" + codeExamples.join("\n") + "\n })"; + output.push(content); }); + let dirname = Path.dirname(Url.fileURLToPath(import.meta.url)); + let filepath = Path.join(dirname, "generated_mocha_test.res"); + let fileContent = "open Mocha\n@@warning(\"-32-34-60-37-109-3-44\")\n\n" + output.join("\n"); + return await Promises.writeFile(filepath, fileContent); } -async function main() { - let examples = await extractExamples(); - let match = await compileExamples(examples); - let runtimeErrors = await runExamples(match[0]); - let allErrors = runtimeErrors.concat(match[1]); - if (allErrors.length > 0) { - printErrors(allErrors); - return 1; - } else { - console.log("All examples passed successfully"); - return 0; - } -} - -let exitCode = await main(); - -process.exit(exitCode); +await main(); -/* bscBin Not a pure module */ +/* nodeVersion Not a pure module */ diff --git a/tests/docstrings_examples/Mocha.res b/tests/docstrings_examples/Mocha.res new file mode 100644 index 0000000000..a7fc8c9ed3 --- /dev/null +++ b/tests/docstrings_examples/Mocha.res @@ -0,0 +1,5 @@ +@module("mocha") +external test: (string, unit => unit) => unit = "test" + +@module("mocha") +external describe: (string, unit => unit) => unit = "describe" diff --git a/tests/docstrings_examples/Mocha.res.mjs b/tests/docstrings_examples/Mocha.res.mjs new file mode 100644 index 0000000000..d856702bfe --- /dev/null +++ b/tests/docstrings_examples/Mocha.res.mjs @@ -0,0 +1,2 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE +/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ diff --git a/tests/docstrings_examples/Node.res b/tests/docstrings_examples/Node.res index a7f804b3aa..2182b31703 100644 --- a/tests/docstrings_examples/Node.res +++ b/tests/docstrings_examples/Node.res @@ -1,19 +1,14 @@ module Path = { - @module("path") external join2: (string, string) => string = "join" @module("path") @variadic external join: array => string = "join" + @module("path") external dirname: string => string = "dirname" } module Process = { - @scope("process") external exit: int => unit = "exit" - @scope(("process", "stderr")) - external stderrWrite: string => unit = "write" @scope("process") external cwd: unit => string = "cwd" - @val @scope("process") - external argv: array = "argv" + @scope("process") @val external version: string = "version" } module Fs = { - @module("fs") external existsSync: string => bool = "existsSync" @module("fs") external readdirSync: string => array = "readdirSync" @module("node:fs/promises") external writeFile: (string, string) => promise = "writeFile" } @@ -24,10 +19,6 @@ module Buffer = { } module ChildProcess = { - type spawnSyncReturns = {stdout: Buffer.t} - @module("child_process") - external spawnSync: (string, array) => spawnSyncReturns = "spawnSync" - type readable type spawnReturns = {stderr: readable, stdout: readable} type options = {cwd?: string, env?: Dict.t, timeout?: int} @@ -35,31 +26,18 @@ module ChildProcess = { external spawn: (string, array, ~options: options=?) => spawnReturns = "spawn" @send external on: (readable, string, Buffer.t => unit) => unit = "on" - @send external onFromSpawn: (spawnReturns, string, Js.Null.t => unit) => unit = "on" @send external once: (spawnReturns, string, (Js.Null.t, Js.Null.t) => unit) => unit = "once" - @send - external onceError: (spawnReturns, string, Js.Exn.t => unit) => unit = "once" } module OS = { - @module("os") - external tmpdir: unit => string = "tmpdir" - @module("os") external cpus: unit => array<{.}> = "cpus" } -module Util = { - type arg = {@as("type") type_: string} - type config = { - args: array, - options: Dict.t, - } - type parsed = { - values: Dict.t, - positionals: array, - } - @module("node:util") external parseArgs: config => parsed = "parseArgs" +module URL = { + @module("url") external fileURLToPath: string => string = "fileURLToPath" } + +@val @scope(("import", "meta")) external url: string = "url" diff --git a/tests/docstrings_examples/Node.res.mjs b/tests/docstrings_examples/Node.res.mjs index 46cc52f763..8ae5da3c11 100644 --- a/tests/docstrings_examples/Node.res.mjs +++ b/tests/docstrings_examples/Node.res.mjs @@ -13,7 +13,7 @@ let ChildProcess = {}; let OS = {}; -let Util = {}; +let URL = {}; export { Path, @@ -22,6 +22,6 @@ export { Buffer, ChildProcess, OS, - Util, + URL, } /* No side effect */