diff --git a/README.md b/README.md index 0c0f1007..ec510fcb 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ Your computer might say that it has 12 logical CPU cores. Then dividing up the t To see the number of logical CPU cores on your machine, run `node -p "os.cpus().length"` (it’s also shown in `elm-test --help`). +If you pass `--workers 1`, elm-test won’t even start a new thread for running the tests in – it’ll do everything in the main thread (single-threaded mode). + ### --report Specify which format to use for reporting test results. Valid options are: diff --git a/elm/src/Test/Runner/Node.elm b/elm/src/Test/Runner/Node.elm index 75235773..679d7e35 100644 --- a/elm/src/Test/Runner/Node.elm +++ b/elm/src/Test/Runner/Node.elm @@ -81,7 +81,7 @@ type Msg {-| The port names are prefixed to reduce the likelihood of the project having a port with the same name, which is a compile error. -} -port elmTestPort__send : String -> Cmd msg +port elmTestPort__send : Decode.Value -> Cmd msg port elmTestPort__receive : (Decode.Value -> msg) -> Sub msg @@ -140,7 +140,6 @@ update msg ({ testReporter } as model) = , ( "exitCode", Encode.int exitCode ) , ( "message", summary ) ] - |> Encode.encode 0 |> elmTestPort__send in ( model, cmd ) @@ -152,7 +151,6 @@ update msg ({ testReporter } as model) = [ ( "type", Encode.string "ERROR" ) , ( "message", Encode.string (Decode.errorToString err) ) ] - |> Encode.encode 0 |> elmTestPort__send in ( model, cmd ) @@ -224,7 +222,6 @@ sendResults isFinished testReporter results = |> Encode.object ) ] - |> Encode.encode 0 |> elmTestPort__send @@ -245,7 +242,6 @@ sendBegin model = [] in Encode.object (baseFields ++ extraFields) - |> Encode.encode 0 |> elmTestPort__send @@ -336,7 +332,6 @@ failInit message report _ = , ( "exitCode", Encode.int 1 ) , ( "message", Encode.string message ) ] - |> Encode.encode 0 |> elmTestPort__send in ( model, cmd ) diff --git a/lib/Generate.js b/lib/Generate.js index f38e5b0e..c2e799f2 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -24,7 +24,7 @@ function prepareCompiledJsFile(pipeFilename, dest) { const finalContent = ` ${before} var Elm = (function(module) { -${addKernelTestChecking(content)} +${patch(content)} return this.Elm; })({}); var pipeFilename = ${JSON.stringify(pipeFilename)}; @@ -53,13 +53,17 @@ const checkDefinition = /^(var\s+\$author\$project\$Test\$Runner\$Node\$check)\s*=\s*\$author\$project\$Test\$Runner\$Node\$checkHelperReplaceMe___;?$/m; /** - * Create a symbol, tag all `Test` constructors with it and make the `check` - * function look for it. + * Patch the JavaScript output from Elm: + * + * - Create a symbol, tag all `Test` constructors with it and make the `check` + * function look for it. + * - Silence `console.warn('Compiled in DEV mode. ...')`. The call is near the top of the file, + * and the first usage of `console.warn`. * * @param { string } content * @returns { string } */ -function addKernelTestChecking(content) { +function patch(content) { return ( 'var __elmTestSymbol = Symbol("elmTestSymbol");\n' + content @@ -68,6 +72,8 @@ function addKernelTestChecking(content) { checkDefinition, '$1 = value => value && value.__elmTestSymbol === __elmTestSymbol ? $elm$core$Maybe$Just(value) : $elm$core$Maybe$Nothing;' ) + // Simply remove the first occurrence of `console.warn`. This leaves the message string in parentheses behind, but that’s fine. + .replace('console.warn', '') ); } diff --git a/lib/Supervisor.js b/lib/Supervisor.js index 94baa200..fa8a53a9 100644 --- a/lib/Supervisor.js +++ b/lib/Supervisor.js @@ -172,141 +172,158 @@ function run(elmTestVersion, pipeFilename, report, processes, dest, watch) { }); stream.on('line', function (data) { - var response = JSON.parse(data); - - switch (response.type) { - case 'FINISHED': - handleResults(response); - - // This worker found no tests remaining to run; it's finished! - finishedWorkers++; - - // If all the workers have finished, print the summary. - if (finishedWorkers === workers.length) { - socket.write( - JSON.stringify({ - type: 'SUMMARY', - duration: Date.now() - startingTime, - failures: failures, - todos: todos, - }) - ); - } - break; - case 'SUMMARY': - flushResults(); - - if (response.exitCode === 1) { - // The tests could not even run. At the time of this writing, the - // only case is “No exposed values of type Test found”. That - // _could_ have been caught at compile time, but the current - // architecture needs to actually run the JS to figure out which - // exposed values are of type Test. That’s why this type of - // response is handled differently than others. - console.error(response.message); - } else { - printResult(response.message); - - if (report === 'junit') { - var xml = response.message; - var values = Array.from(results.values()); - xml.testsuite.testcase = xml.testsuite.testcase.concat(values); - console.log(XMLBuilder.toString(xml)); - } - } + handleResponse(JSON.parse(data), (message) => { + socket.write(JSON.stringify(message)); + }); + }); + } - // Close all the workers. - workers.forEach(function (worker) { - worker.kill(); + /** + * @param { any } response This `any` became explicit instead of implicit when extracting this function. + * @param { (message: any) => void } send + * @returns { void } + */ + function handleResponse(response, send) { + switch (response.type) { + case 'FINISHED': + handleResults(response); + + // This worker found no tests remaining to run; it's finished! + finishedWorkers++; + + // If all the workers have finished (or we run single-threaded), print the summary. + if (finishedWorkers === workers.length || processes === 1) { + send({ + type: 'SUMMARY', + duration: Date.now() - startingTime, + failures: failures, + todos: todos, }); - resolve(response.exitCode); - break; - case 'BEGIN': - testsToRun = response.testCount; - - if (!Report.isMachineReadable(report)) { - var headline = 'elm-test ' + elmTestVersion; - var bar = '-'.repeat(headline.length); + } + break; + case 'SUMMARY': + flushResults(); + + if (response.exitCode === 1) { + // The tests could not even run. At the time of this writing, the + // only case is “No exposed values of type Test found”. That + // _could_ have been caught at compile time, but the current + // architecture needs to actually run the JS to figure out which + // exposed values are of type Test. That’s why this type of + // response is handled differently than others. + console.error(response.message); + } else { + printResult(response.message); - console.log('\n' + headline + '\n' + bar + '\n'); + if (report === 'junit') { + var xml = response.message; + var values = Array.from(results.values()); + xml.testsuite.testcase = xml.testsuite.testcase.concat(values); + console.log(XMLBuilder.toString(xml)); } + } - printResult(response.message); + // Close all the workers. + workers.forEach(function (worker) { + worker.kill(); + }); + resolve(response.exitCode); + break; + case 'BEGIN': + testsToRun = response.testCount; - // Now we're ready to print results! - nextResultToPrint = 0; + if (!Report.isMachineReadable(report)) { + var headline = 'elm-test ' + elmTestVersion; + var bar = '-'.repeat(headline.length); - flushResults(); + console.log('\n' + headline + '\n' + bar + '\n'); + } - break; - case 'RESULTS': - handleResults(response); + printResult(response.message); - break; - case 'ERROR': - throw new Error(response.message); - default: - throw new Error( - 'Unrecognized message from worker:' + response.type - ); - } - }); + // Now we're ready to print results! + nextResultToPrint = 0; + + flushResults(); + + break; + case 'RESULTS': + handleResults(response); + + break; + case 'ERROR': + throw new Error(response.message); + default: + throw new Error('Unrecognized message from worker:' + response.type); + } } - var pendingException = false; + // If just one process, run single-threaded. + if (processes === 1) { + var { run } = require(dest); + var send = run( + 0, + /** @type { (response: any) => void } */ + (response) => { + handleResponse(response, send); + } + ); + } else { + var pendingException = false; - // Using a named pipe to communicate is actually faster than - // using `process.send` or `worker_threads`! See: - // https://github.com/rtfeldman/node-test-runner/pull/674 - var server = net.createServer(initWorker); + // Using a named pipe to communicate is actually faster than + // using `process.send` or `worker_threads`! See: + // https://github.com/rtfeldman/node-test-runner/pull/674 + var server = net.createServer(initWorker); - server.on('error', function (err) { - console.error(err.stack); - server.close(); - }); + server.on('error', function (err) { + console.error(err.stack); + server.close(); + }); - server.on('listening', function () { - workers = Array.from({ length: processes }, (_, index) => { - var worker = child_process.fork(dest, [index.toString()]); + server.on('listening', function () { + workers = Array.from({ length: processes }, (_, index) => { + var worker = child_process.fork(dest, [index.toString()]); - worker.on('close', function (code) { - // code can be null. - var hasNonZeroExitCode = typeof code === 'number' && code !== 0; + worker.on('close', function (code) { + // code can be null. + var hasNonZeroExitCode = typeof code === 'number' && code !== 0; - if (watch && !Report.isMachineReadable(report)) { - if (hasNonZeroExitCode) { - // Queue up complaining about an exception. - // Don't print it immediately, or else it might print N times - // where N is the number of cores. - pendingException = true; - } - closedWorkers++; - // If all the workers have closed, we're done! Continue watching. - if (closedWorkers === workers.length) { - if (pendingException) { - // If we had an exception pending, print it and clear pending flag. - reportRuntimeException(); - pendingException = false; + if (watch && !Report.isMachineReadable(report)) { + if (hasNonZeroExitCode) { + // Queue up complaining about an exception. + // Don't print it immediately, or else it might print N times + // where N is the number of cores. + pendingException = true; } + closedWorkers++; + // If all the workers have closed, we're done! Continue watching. + if (closedWorkers === workers.length) { + if (pendingException) { + // If we had an exception pending, print it and clear pending flag. + reportRuntimeException(); + pendingException = false; + } + resolve(1); + } + } else if (hasNonZeroExitCode) { + reportRuntimeException(); resolve(1); } - } else if (hasNonZeroExitCode) { - reportRuntimeException(); - resolve(1); - } - }); + }); - return worker; + return worker; + }); }); - }); - if (fs.existsSync(pipeFilename) && process.platform !== 'win32') { - // Never remove named pipes on Windows. The OS will clean them up when - // nothing has a handle to them anymore. - fs.unlinkSync(pipeFilename); - } + if (fs.existsSync(pipeFilename) && process.platform !== 'win32') { + // Never remove named pipes on Windows. The OS will clean them up when + // nothing has a handle to them anymore. + fs.unlinkSync(pipeFilename); + } - server.listen(pipeFilename); + server.listen(pipeFilename); + } }); } diff --git a/templates/after.js b/templates/after.js index 3ea350fc..0689d426 100644 --- a/templates/after.js +++ b/templates/after.js @@ -1,29 +1,43 @@ -var net = require('net'), - client = net.createConnection(pipeFilename); +function run(index, receive) { + var app = Elm.Test.Generated.Main.init({ flags: index }); + app.ports.elmTestPort__send.subscribe(receive); + return app.ports.elmTestPort__receive.send; +} -client.on('error', function (error) { - console.error(error); - client.end(); - process.exit(1); -}); +function main() { + var net = require('net'), + client = net.createConnection(pipeFilename); -client.setEncoding('utf8'); -client.setNoDelay(true); + client.on('error', function (error) { + console.error(error); + client.end(); + process.exit(1); + }); -// Run the Elm app. -var app = Elm.Test.Generated.Main.init({ flags: Number(process.argv[2]) }); + client.setEncoding('utf8'); + client.setNoDelay(true); -client.on('data', function (msg) { - app.ports.elmTestPort__receive.send(JSON.parse(msg)); -}); + var send = run(Number(process.argv[2]), function (msg) { + // We split incoming messages on the socket on newlines. The gist is that node + // is rather unpredictable in whether or not a single `write` will result in a + // single `on('data')` callback. Sometimes it does, sometimes multiple writes + // result in a single callback and - worst of all - sometimes a single read + // results in multiple callbacks, each receiving a piece of the data. The + // horror. + client.write(JSON.stringify(msg) + '\n'); + }); -// Use ports for inter-process communication. -app.ports.elmTestPort__send.subscribe(function (msg) { - // We split incoming messages on the socket on newlines. The gist is that node - // is rather unpredictable in whether or not a single `write` will result in a - // single `on('data')` callback. Sometimes it does, sometimes multiple writes - // result in a single callback and - worst of all - sometimes a single read - // results in multiple callbacks, each receiving a piece of the data. The - // horror. - client.write(msg + '\n'); -}); + client.on('data', function (msg) { + send(JSON.parse(msg)); + }); +} + +// For running single-threaded, export the `run` function. +module.exports = { + run, +}; + +// When running multi-threaded, connect the pipe and run. +if (require.main === module) { + main(); +} diff --git a/templates/before.js b/templates/before.js index b341c119..ac0914ff 100644 --- a/templates/before.js +++ b/templates/before.js @@ -36,16 +36,6 @@ if (typeof XMLHttpRequest === 'undefined') { send: function () {}, }; }; - - var oldConsoleWarn = console.warn; - console.warn = function () { - if ( - arguments.length === 1 && - arguments[0].indexOf('Compiled in DEV mode') === 0 - ) - return; - return oldConsoleWarn.apply(console, arguments); - }; } if (typeof FormData === 'undefined') { diff --git a/tests/flags.js b/tests/flags.js index 4880fb23..cab52ddd 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -476,6 +476,16 @@ describe('flags', () => { assert.ok(Number.isInteger(runResult.status)); assert.notStrictEqual(runResult.status, 0); }); + + it('should work in single-threaded mode', () => { + const runResult = execElmTest([ + '--workers', + '1', + path.join('tests', 'Passing', 'One.elm'), + ]); + console.log(runResult); + assert.strictEqual(runResult.status, 0); + }); }); describe('--compiler', () => {