Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 1 addition & 6 deletions elm/src/Test/Runner/Node.elm
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now turn this into a JSON string in JS instead – and not at all in single-threaded mode.



port elmTestPort__receive : (Decode.Value -> msg) -> Sub msg
Expand Down Expand Up @@ -140,7 +140,6 @@ update msg ({ testReporter } as model) =
, ( "exitCode", Encode.int exitCode )
, ( "message", summary )
]
|> Encode.encode 0
|> elmTestPort__send
in
( model, cmd )
Expand All @@ -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 )
Expand Down Expand Up @@ -224,7 +222,6 @@ sendResults isFinished testReporter results =
|> Encode.object
)
]
|> Encode.encode 0
|> elmTestPort__send


Expand All @@ -245,7 +242,6 @@ sendBegin model =
[]
in
Encode.object (baseFields ++ extraFields)
|> Encode.encode 0
|> elmTestPort__send


Expand Down Expand Up @@ -336,7 +332,6 @@ failInit message report _ =
, ( "exitCode", Encode.int 1 )
, ( "message", Encode.string message )
]
|> Encode.encode 0
|> elmTestPort__send
in
( model, cmd )
Expand Down
14 changes: 10 additions & 4 deletions lib/Generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
Expand Down Expand Up @@ -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
Expand All @@ -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', '')
Comment on lines +75 to +76

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the previous approach of re-assigning console.warn. This is simpler, and avoids messing with the global console.warn in the main thread.

);
}

Expand Down
241 changes: 129 additions & 112 deletions lib/Supervisor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}

Expand Down
Loading