From 28c4f95eb9c73841cfeacb763ab414fff4475a80 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 18 Nov 2020 19:42:15 +0100 Subject: [PATCH 01/67] First pass at better watcher --- flow-typed/Result.js | 18 --- lib/Generate.js | 9 +- lib/Install.js | 13 +- lib/Report.js | 9 +- lib/Solve.js | 3 +- lib/elm-test.js | 286 ++++++++++++++++++++++--------------------- 6 files changed, 160 insertions(+), 178 deletions(-) delete mode 100644 flow-typed/Result.js diff --git a/flow-typed/Result.js b/flow-typed/Result.js deleted file mode 100644 index fc975735..00000000 --- a/flow-typed/Result.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow - -// We can’t use /*:: type Result = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -// -// Because of the type arguments we can’t use the regular trick of making a type -// annotation instead and then use `typeof Result`. The workaround is to define -// `Result` globally – this file is not included during runtime. -// -// This also lets us use `Result` in several files without having to define it -// multiple times or figure out some way to import types. -// -// If you wonder why we use a weird mix of “real” syntax and comment syntax here -// – it’s because of Prettier again. If you “uncomment” the `` -// part, Prettier adds `/*::` and `*/` back. -type Result/*:: */ = - | { tag: 'Ok', value: Value } - | { tag: 'Error', error: Error }; diff --git a/lib/Generate.js b/lib/Generate.js index edf35601..a1b75b71 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -63,7 +63,6 @@ function generateElmJson( projectRootDir /*: string */, generatedCodeDir /*: string */, hasBeenGivenCustomGlobs /*: boolean */, - elmJsonPath /*: string */, projectElmJson /*: any */ ) /*: [string, Array] */ { const testRootDir = Compile.getTestRootDir(projectRootDir); @@ -87,21 +86,19 @@ function generateElmJson( if (needToCareAboutTestsDir) { if (!fs.existsSync(testRootDir)) { - console.error( + throw new Error( 'Error: ' + testRootDir + ' does not exist. Please create a tests/ directory in your project root!' ); - process.exit(1); } if (!fs.lstatSync(testRootDir).isDirectory()) { - console.error( + throw new Error( 'Error: ' + testRootDir + ' exists, but it is not a directory. Please create a tests/ directory in your project root!' ); - process.exit(1); } } @@ -114,7 +111,7 @@ function generateElmJson( 'elm-version': '0.19.1', dependencies: Solve.getDependenciesCached( generatedCodeDir, - elmJsonPath, + path.join(projectRootDir, 'elm.json'), projectElmJson ), 'test-dependencies': { diff --git a/lib/Install.js b/lib/Install.js index b2d75e04..6fb949b5 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -6,11 +6,15 @@ const path = require('path'); const rimraf = require('rimraf'); const Compiler = require('./Compile'); -function install(pathToElmBinary /*: string */, packageName /*: string */) { +function install( + projectRootDir /*: string */, + pathToElmBinary /*: string */, + packageName /*: string */ +) { var oldSourceDirectories; const dirPath = path.join( - Compiler.getGeneratedCodeDir(process.cwd()), + Compiler.getGeneratedCodeDir(projectRootDir), 'install' ); @@ -32,8 +36,9 @@ function install(pathToElmBinary /*: string */, packageName /*: string */) { process.exit(1); } - var elmJson = JSON.parse(fs.readFileSync('elm.json', 'utf8')); + var elmJsonPath = path.join(projectRootDir, 'elm.json'); var tmpElmJsonPath = path.join(dirPath, 'elm.json'); + var elmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); var isPackage; switch (elmJson['type']) { @@ -120,7 +125,7 @@ function install(pathToElmBinary /*: string */, packageName /*: string */) { newElmJson['source-directories'] = oldSourceDirectories; fs.writeFileSync( - 'elm.json', + elmJsonPath, JSON.stringify(newElmJson, null, 4) + '\n', 'utf8' ); diff --git a/lib/Report.js b/lib/Report.js index 819e1159..e8fbc33c 100644 --- a/lib/Report.js +++ b/lib/Report.js @@ -4,17 +4,14 @@ // https://github.com/prettier/prettier/issues/2597 const Report /*: 'console' | 'json' | 'junit' */ = 'console'; -function parse(string /*: string */) /*: Result */ { +function parse(string /*: string */) /*: typeof Report */ { switch (string) { case 'console': case 'json': case 'junit': - return { tag: 'Ok', value: string }; + return string; default: - return { - tag: 'Error', - error: `unknown reporter: ${string}`, - }; + throw new Error(`unknown reporter: ${string}`); } } diff --git a/lib/Solve.js b/lib/Solve.js index 78ada5f4..8e1e9085 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -60,8 +60,7 @@ function getDependencies(elmJsonPath) { ); if (result.status != 0) { - console.error(`Failed to run \`elm-json solve\`:\n${result.stderr}`); - process.exit(1); + throw new Error(`Failed to run \`elm-json solve\`:\n${result.stderr}`); } return result.stdout; diff --git a/lib/elm-test.js b/lib/elm-test.js index bc908524..54d8f325 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -18,26 +18,6 @@ const Supervisor = require('./Supervisor.js'); void Report; -function getPathToElmBinary( - compiler /*: string | void */ -) /*: Result */ { - const name = compiler === undefined ? 'elm' : compiler; - try { - return { tag: 'Ok', value: path.resolve(which.sync(name)) }; - } catch (_error) { - return compiler === undefined - ? { - tag: 'Error', - error: `Cannot find elm executable, make sure it is installed. -(If elm is not on your path or is called something different the --compiler flag might help.)`, - } - : { - tag: 'Error', - error: `The elm executable passed to --compiler must exist and be exectuble. Got: ${compiler}`, - }; - } -} - function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { const results = fileGlobs.length > 0 @@ -107,66 +87,65 @@ function clearConsole() { ); } +function diffArrays/*:: */( + from /*: Array */, + to /*: Array */ +) /*: { added: Array, removed: Array } */ { + return { + added: to.filter((item) => !from.includes(item)), + removed: from.filter((item) => !to.includes(item)), + }; +} + function makeAndTestHelper( - testFileGlobs /*: Array */, - compiler /*: string | void */ -) /*: Result */ { + projectRootDir /*: string */, + testFileGlobs /*: Array */ +) /*: { + testFilePaths: Array, + generatedCodeDir: string, + hasBeenGivenCustomGlobs: boolean, + projectElmJson: any, +} */ { // Resolve arguments that look like globs for shells that don’t support globs. const testFilePaths = resolveGlobs(testFileGlobs); - const projectRootDir = process.cwd(); const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); const hasBeenGivenCustomGlobs = testFileGlobs.length > 0; const elmJsonPath = path.resolve(path.join(projectRootDir, 'elm.json')); - const pathToElmBinary = getPathToElmBinary(compiler); - if (pathToElmBinary.tag === 'Error') { - return pathToElmBinary; - } - try { const projectElmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); return { - tag: 'Ok', - value: { - pathToElmBinary: pathToElmBinary.value, - testFilePaths, - projectRootDir, - generatedCodeDir, - hasBeenGivenCustomGlobs, - elmJsonPath, - projectElmJson, - isPackageProject: projectElmJson.type === 'package', - }, + testFilePaths, + generatedCodeDir, + hasBeenGivenCustomGlobs, + projectElmJson, }; } catch (error) { - return { - tag: 'Error', - error: `Error reading elm.json: ${error.message}`, - }; + throw new Error(`Error reading elm.json: ${error.message}`); } } -function make( - report, - { - pathToElmBinary, +async function make( + projectRootDir /*: string */, + pathToElmBinary /*: string */, + testFileGlobs /*: Array */, + report /*: typeof Report.Report */ +) { + const { testFilePaths, - projectRootDir, generatedCodeDir, hasBeenGivenCustomGlobs, - elmJsonPath, projectElmJson, - } -) { + } = makeAndTestHelper(projectRootDir, testFileGlobs); + Generate.generateElmJson( projectRootDir, generatedCodeDir, hasBeenGivenCustomGlobs, - elmJsonPath, projectElmJson ); - return Compile.compileSources( + await Compile.compileSources( testFilePaths, generatedCodeDir, pathToElmBinary, @@ -175,35 +154,53 @@ function make( } function test( - testFileGlobs, - processes, - { - pathToElmBinary, - testFilePaths, - projectRootDir, - generatedCodeDir, - hasBeenGivenCustomGlobs, - elmJsonPath, - projectElmJson, - isPackageProject, - }, + projectRootDir /*: string */, + pathToElmBinary /*: string */, + testFileGlobs /*: Array */, + processes /*: number */, { watch, report, seed, fuzz } ) { - const [generatedSrc, sourceDirs] = Generate.generateElmJson( - projectRootDir, - generatedCodeDir, - hasBeenGivenCustomGlobs, - elmJsonPath, - projectElmJson - ); + let watcher; + let watchedGlobs /*: Array */ = []; + let currentRun; async function run() { try { + const { + testFilePaths, + generatedCodeDir, + hasBeenGivenCustomGlobs, + projectElmJson, + } = makeAndTestHelper(projectRootDir, testFileGlobs); + + if (testFilePaths.length === 0) { + throw new Error(noFilesFoundError(testFileGlobs)); + } + + if (watcher !== undefined) { + const nextGlobsToWatch = getGlobsToWatch(projectElmJson); + const diff = diffArrays(watchedGlobs, nextGlobsToWatch); + watchedGlobs = nextGlobsToWatch; + watcher.add(diff.added); + watcher.unwatch(diff.removed); + } + + const [generatedSrc, sourceDirs] = Generate.generateElmJson( + projectRootDir, + generatedCodeDir, + hasBeenGivenCustomGlobs, + projectElmJson + ); + + const isPackageProject = projectElmJson.type === 'package'; + const testModules = await Runner.findTests( testFilePaths, sourceDirs, isPackageProject ); + + // TODO: Try to get rid of this? process.chdir(generatedCodeDir); const mainFile = Generate.generateMainModule( @@ -216,6 +213,7 @@ function test( generatedSrc, processes ); + await runTests( generatedCodeDir, mainFile, @@ -233,14 +231,13 @@ function test( console.log(chalk.blue('Watching for changes...')); } - let currentRun = run(); - if (watch) { clearConsole(); infoLog(report, 'Running in watch mode'); - const globsToWatch = getGlobsToWatch(projectElmJson); - const watcher = chokidar.watch(globsToWatch, { + // More globs to watch are added later. + const initialGlobsToWatch = [path.join(projectRootDir, 'elm.json')]; + watcher = chokidar.watch(initialGlobsToWatch, { awaitWriteFinish: { stabilityThreshold: 500, }, @@ -249,6 +246,8 @@ function test( cwd: projectRootDir, }); + currentRun = run(); + const eventNameMap = { add: 'added', addDir: 'added', @@ -258,6 +257,7 @@ function test( }; watcher.on('all', (event, filePath) => { + // TODO: Handle different events slightly differently. const eventName = eventNameMap[event] || event; clearConsole(); infoLog(report, '\n' + filePath + ' ' + eventName + '. Rebuilding!'); @@ -265,6 +265,8 @@ function test( // TODO if a previous run is in progress, wait until it's done. currentRun = currentRun.then(run); }); + } else { + run(); } } @@ -354,16 +356,26 @@ const parsePositiveInteger = (flag /*: string */) => ( const parseReport = (flag /*: string */) => ( string /*: string */ ) /*: typeof Report.Report */ => { - const result = Report.parse(string); - switch (result.tag) { - case 'Ok': - return result.value; - case 'Error': - console.error(`error: option '${flag}' ${result.error}`); - throw process.exit(1); + try { + return Report.parse(string); + } catch (error) { + console.error(`error: option '${flag}' ${error.message}`); + throw process.exit(1); } }; +function findClosest( + name /*: string */, + dir /*: string */ +) /*: string | void */ { + const entry = path.join(dir, name); + return fs.existsSync(entry) + ? entry + : dir === path.parse(dir).root + ? undefined + : findClosest(name, path.dirname(dir)); +} + const examples = ` elm-test Run tests in the tests/ folder @@ -375,16 +387,30 @@ elm-test "src/**/*Tests.elm" function main() { process.title = 'elm-test'; - const processes = Math.max(1, os.cpus().length); - - const assertElmJsonInCwd = (subcommand /*: string */) => { - if (!fs.existsSync('elm.json')) { + const getClosestElmJsonPath = (subcommand /*: string */) /*: string */ => { + const elmJsonPath = findClosest('elm.json', process.cwd()); + if (elmJsonPath === undefined) { const command = subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; console.error( `\`${command}\` must be run in the same directory as an existing elm.json file! To make one: elm init` ); - process.exit(1); + throw process.exit(1); + } + return elmJsonPath; + }; + + const getPathToElmBinary = (compiler /*: string | void */) /*: string */ => { + const name = compiler === undefined ? 'elm' : compiler; + try { + return path.resolve(which.sync(name)); + } catch (_error) { + throw new Error( + compiler === undefined + ? `Cannot find elm executable, make sure it is installed. +(If elm is not on your path or is called something different the --compiler flag might help.)` + : `The elm executable passed to --compiler must exist and be exectuble. Got: ${compiler}` + ); } }; @@ -436,25 +462,21 @@ function main() { ); throw process.exit(1); } - assertElmJsonInCwd('init'); const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - switch (pathToElmBinary.tag) { - case 'Ok': - Install.install(pathToElmBinary.value, 'elm-explorations/test'); - fs.mkdirSync('tests', { recursive: true }); - fs.copyFileSync( - path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), - path.join('tests', 'Example.elm') - ); - console.log( - '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' - ); - throw process.exit(0); - case 'Error': - console.error(pathToElmBinary.error); - throw process.exit(1); - } + const elmJsonPath = getClosestElmJsonPath('init'); + const projectRootDir = path.dirname(elmJsonPath); + const testsDir = path.join(projectRootDir, 'tests'); + Install.install(projectRootDir, pathToElmBinary, 'elm-explorations/test'); + fs.mkdirSync(testsDir, { recursive: true }); + fs.copyFileSync( + path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), + path.join(testsDir, 'Example.elm') + ); + console.log( + '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' + ); + process.exit(0); }); program @@ -474,37 +496,26 @@ function main() { ); throw process.exit(1); } - assertElmJsonInCwd('install'); const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - switch (pathToElmBinary.tag) { - case 'Ok': - Install.install(pathToElmBinary.value, packageName); - throw process.exit(0); - case 'Error': - console.error(pathToElmBinary.error); - throw process.exit(1); - } + const elmJsonPath = getClosestElmJsonPath('install'); + const projectRootDir = path.dirname(elmJsonPath); + Install.install(projectRootDir, pathToElmBinary, packageName); + process.exit(0); }); program .command('make [globs...]') .description('Check files matching the globs for compilation errors') .action((testFileGlobs) => { - assertElmJsonInCwd('make'); const options = program.opts(); - const result = makeAndTestHelper(testFileGlobs, options.compiler); - switch (result.tag) { - case 'Ok': - make(options.report, result.value).then( - () => process.exit(0), - () => process.exit(1) - ); - break; - case 'Error': - console.error(result.error); - throw process.exit(1); - } + const pathToElmBinary = getPathToElmBinary(options.compiler); + const elmJsonPath = getClosestElmJsonPath('make'); + const projectRootDir = path.dirname(elmJsonPath); + make(projectRootDir, pathToElmBinary, testFileGlobs, options.report).then( + () => process.exit(0), + () => process.exit(1) + ); }); program @@ -512,21 +523,12 @@ function main() { // as `elm-test tests/` (due defaulting to `tests/` if no arguments given). .command('tests [globs...]', { hidden: true, isDefault: true }) .action((testFileGlobs) => { - assertElmJsonInCwd('tests'); const options = program.opts(); - const result = makeAndTestHelper(testFileGlobs, options.compiler); - switch (result.tag) { - case 'Ok': - if (result.value.testFilePaths.length === 0) { - console.error(noFilesFoundError(testFileGlobs)); - throw process.exit(1); - } - test(testFileGlobs, processes, result.value, options); - break; - case 'Error': - console.error(result.error); - throw process.exit(1); - } + const pathToElmBinary = getPathToElmBinary(options.compiler); + const elmJsonPath = getClosestElmJsonPath('install'); + const projectRootDir = path.dirname(elmJsonPath); + const processes = Math.max(1, os.cpus().length); + test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); }); program.parse(process.argv); From 2355d3d5760758399103b05f166513b54e70a385 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 18 Nov 2020 20:05:48 +0100 Subject: [PATCH 02/67] Fix chdir --- lib/Compile.js | 8 +++++--- lib/elm-test.js | 17 +++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/Compile.js b/lib/Compile.js index 8169d6cd..8ff5f123 100644 --- a/lib/Compile.js +++ b/lib/Compile.js @@ -7,6 +7,7 @@ const ElmCompiler = require('./ElmCompiler'); const Report = require('./Report'); function compile( + cwd /*: string */, testFile /*: string */, dest /*: string */, pathToElmBinary /*: string */, @@ -15,7 +16,7 @@ function compile( return new Promise((resolve, reject) => { const compileProcess = ElmCompiler.compile([testFile], { output: dest, - spawn: spawnCompiler({ ignoreStdout: true }), + spawn: spawnCompiler({ ignoreStdout: true, cwd }), pathToElm: pathToElmBinary, processOpts: processOptsForReporter(report), }); @@ -57,7 +58,7 @@ function compileSources( const compileProcess = ElmCompiler.compile(testFilePaths, { output: '/dev/null', cwd: projectRootDir, - spawn: spawnCompiler({ ignoreStdout: false }), + spawn: spawnCompiler({ ignoreStdout: false, cwd: projectRootDir }), pathToElm: pathToElmBinary, report: compilerReport, processOpts: processOptsForReporter(report), @@ -73,13 +74,14 @@ function compileSources( }); } -function spawnCompiler({ ignoreStdout }) { +function spawnCompiler({ ignoreStdout, cwd }) { return ( pathToElm /*: string */, processArgs /*: Array */, processOpts /*: Object */ ) => { const finalOpts = Object.assign({ env: process.env }, processOpts, { + cwd, stdio: [ process.stdin, ignoreStdout ? 'ignore' : process.stdout, diff --git a/lib/elm-test.js b/lib/elm-test.js index 54d8f325..f95ba025 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -200,12 +200,9 @@ function test( isPackageProject ); - // TODO: Try to get rid of this? - process.chdir(generatedCodeDir); - const mainFile = Generate.generateMainModule( - parseInt(fuzz), - parseInt(seed), + fuzz, + seed, report, testFileGlobs, testFilePaths, @@ -297,8 +294,16 @@ async function runTests( ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` : `/tmp/elm_test-${process.pid}.sock`; - await Compile.compile(testFile, dest, pathToElmBinary, report); + await Compile.compile( + generatedCodeDir, + testFile, + dest, + pathToElmBinary, + report + ); + Generate.prepareCompiledJsFile(pipeFilename, dest); + await Supervisor.run( packageInfo.version, pipeFilename, From 11f99c4b61d70b82ae6e40a866dee3e89cd3da8b Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 18 Nov 2020 21:04:27 +0100 Subject: [PATCH 03/67] Get rid of arbitrary split --- lib/elm-test.js | 83 ++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index f95ba025..a5b83b02 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -163,6 +163,7 @@ function test( let watcher; let watchedGlobs /*: Array */ = []; let currentRun; + let runsExecuted = 0; async function run() { try { @@ -211,13 +212,42 @@ function test( processes ); - await runTests( + const dest = path.resolve( + path.join(generatedCodeDir, 'elmTestOutput.js') + ); + + // Incorporate the process PID into the socket name, so elm-test processes can + // be run parallel without accidentally sharing each others' sockets. + // + // See https://github.com/rtfeldman/node-test-runner/pull/231 + // Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - + // see https://github.com/rtfeldman/node-test-runner/issues/275 - because the + // alternative approach of deleting the file before creating a new one doesn't + // work on Windows. We have to let Windows clean up the named pipe. This is + // essentially a band-aid fix. The alternative is to rewrite a ton of stuff. + runsExecuted++; + const pipeFilename = + process.platform === 'win32' + ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` + : `/tmp/elm_test-${process.pid}.sock`; + + await Compile.compile( generatedCodeDir, mainFile, + dest, pathToElmBinary, + report + ); + + Generate.prepareCompiledJsFile(pipeFilename, dest); + + await Supervisor.run( + packageInfo.version, + pipeFilename, report, - watch, - processes + processes, + dest, + watch ); } catch (err) { console.error(err.message); @@ -267,53 +297,6 @@ function test( } } -let runsExecuted = 0; - -async function runTests( - generatedCodeDir /*: string */, - testFile /*: string */, - pathToElmBinary /*: string */, - report /*: typeof Report.Report */, - watch /*: boolean */, - processes /*: number */ -) { - const dest = path.resolve(path.join(generatedCodeDir, 'elmTestOutput.js')); - - // Incorporate the process PID into the socket name, so elm-test processes can - // be run parallel without accidentally sharing each others' sockets. - // - // See https://github.com/rtfeldman/node-test-runner/pull/231 - // Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - - // see https://github.com/rtfeldman/node-test-runner/issues/275 - because the - // alternative approach of deleting the file before creating a new one doesn't - // work on Windows. We have to let Windows clean up the named pipe. This is - // essentially a band-aid fix. The alternative is to rewrite a ton of stuff. - runsExecuted++; - const pipeFilename = - process.platform === 'win32' - ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` - : `/tmp/elm_test-${process.pid}.sock`; - - await Compile.compile( - generatedCodeDir, - testFile, - dest, - pathToElmBinary, - report - ); - - Generate.prepareCompiledJsFile(pipeFilename, dest); - - await Supervisor.run( - packageInfo.version, - pipeFilename, - report, - processes, - dest, - watch - ); -} - function noFilesFoundError(testFileGlobs) { return testFileGlobs.length === 0 ? ` From 762e3f7cefa368082c295e5dfb011e671c809592 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 18 Nov 2020 21:21:43 +0100 Subject: [PATCH 04/67] Remove unnecessary path.resolve --- lib/Compile.js | 5 ----- lib/Generate.js | 8 +++----- lib/elm-test.js | 6 ++---- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/Compile.js b/lib/Compile.js index 8ff5f123..fd1fc963 100644 --- a/lib/Compile.js +++ b/lib/Compile.js @@ -42,10 +42,6 @@ function getGeneratedCodeDir(projectRootDir /*: string */) /*: string */ { ); } -function getTestRootDir(projectRootDir /*: string */) /*: string */ { - return path.resolve(path.join(projectRootDir, 'tests')); -} - function compileSources( testFilePaths /*: Array */, projectRootDir /*: string */, @@ -104,6 +100,5 @@ function processOptsForReporter(report /*: typeof Report.Report */) { module.exports = { compile, compileSources, - getTestRootDir, getGeneratedCodeDir, }; diff --git a/lib/Generate.js b/lib/Generate.js index a1b75b71..0d7637b7 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -65,7 +65,7 @@ function generateElmJson( hasBeenGivenCustomGlobs /*: boolean */, projectElmJson /*: any */ ) /*: [string, Array] */ { - const testRootDir = Compile.getTestRootDir(projectRootDir); + const testRootDir = path.join(projectRootDir, 'tests'); const generatedSrc = path.join(generatedCodeDir, 'src'); var isPackageProject = projectElmJson.type === 'package'; @@ -128,9 +128,7 @@ function generateElmJson( projectSourceDirs = projectElmJson['source-directories']; } var sourceDirs /*: Array */ = projectSourceDirs - .map(function (src) { - return path.resolve(path.join(projectRootDir, src)); - }) + .map((src) => path.join(projectRootDir, src)) .concat(shouldAddTestsDirAsSource ? [testRootDir] : []); testElmJson['source-directories'] = [ @@ -139,7 +137,7 @@ function generateElmJson( // NOTE: we must include node-test-runner's Elm source as a source-directory // instead of adding it as a dependency so that it can include port modules - path.resolve(path.join(__dirname, '..', 'elm', 'src')), + path.join(__dirname, '..', 'elm', 'src'), ] .concat(sourceDirs) .filter( diff --git a/lib/elm-test.js b/lib/elm-test.js index a5b83b02..51d4d76b 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -110,7 +110,7 @@ function makeAndTestHelper( const testFilePaths = resolveGlobs(testFileGlobs); const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); const hasBeenGivenCustomGlobs = testFileGlobs.length > 0; - const elmJsonPath = path.resolve(path.join(projectRootDir, 'elm.json')); + const elmJsonPath = path.join(projectRootDir, 'elm.json'); try { const projectElmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); @@ -212,9 +212,7 @@ function test( processes ); - const dest = path.resolve( - path.join(generatedCodeDir, 'elmTestOutput.js') - ); + const dest = path.join(generatedCodeDir, 'elmTestOutput.js'); // Incorporate the process PID into the socket name, so elm-test processes can // be run parallel without accidentally sharing each others' sockets. From 5764ced8520e0e711af2067012bb0894e974e1c7 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 18 Nov 2020 22:26:46 +0100 Subject: [PATCH 05/67] Replace dead code --- lib/Generate.js | 35 +++-------------------------------- lib/elm-test.js | 40 +++++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/lib/Generate.js b/lib/Generate.js index 0d7637b7..132f9add 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -62,7 +62,6 @@ function addKernelTestChecking(content) { function generateElmJson( projectRootDir /*: string */, generatedCodeDir /*: string */, - hasBeenGivenCustomGlobs /*: boolean */, projectElmJson /*: any */ ) /*: [string, Array] */ { const testRootDir = path.join(projectRootDir, 'tests'); @@ -70,37 +69,9 @@ function generateElmJson( var isPackageProject = projectElmJson.type === 'package'; - // if we were given file globs, we don't need to check the tests/ directory exists - // this is only for elm applications, which people may need to introduce slowly into their apps - // for packages, we stick with the existing behaviour and assume tests are in tests/ so do the check always - const needToCareAboutTestsDir = - hasBeenGivenCustomGlobs === false || isPackageProject === true; - - // we add the tests dir as a source if: - // - we decided above we need to care about it - // - if it exists on disk: this supports the case where we have all our tests in tests/ but - // want to pass a glob to only run one of the test files - const shouldAddTestsDirAsSource = - needToCareAboutTestsDir || - fs.existsSync(path.join(projectRootDir, 'tests')); - - if (needToCareAboutTestsDir) { - if (!fs.existsSync(testRootDir)) { - throw new Error( - 'Error: ' + - testRootDir + - ' does not exist. Please create a tests/ directory in your project root!' - ); - } - - if (!fs.lstatSync(testRootDir).isDirectory()) { - throw new Error( - 'Error: ' + - testRootDir + - ' exists, but it is not a directory. Please create a tests/ directory in your project root!' - ); - } - } + const shouldAddTestsDirAsSource = fs.existsSync( + path.join(projectRootDir, 'tests') + ); fs.mkdirSync(generatedCodeDir, { recursive: true }); fs.mkdirSync(generatedSrc, { recursive: true }); diff --git a/lib/elm-test.js b/lib/elm-test.js index 51d4d76b..21302ace 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -103,13 +103,11 @@ function makeAndTestHelper( ) /*: { testFilePaths: Array, generatedCodeDir: string, - hasBeenGivenCustomGlobs: boolean, projectElmJson: any, } */ { // Resolve arguments that look like globs for shells that don’t support globs. const testFilePaths = resolveGlobs(testFileGlobs); const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); - const hasBeenGivenCustomGlobs = testFileGlobs.length > 0; const elmJsonPath = path.join(projectRootDir, 'elm.json'); try { @@ -117,7 +115,6 @@ function makeAndTestHelper( return { testFilePaths, generatedCodeDir, - hasBeenGivenCustomGlobs, projectElmJson, }; } catch (error) { @@ -131,20 +128,13 @@ async function make( testFileGlobs /*: Array */, report /*: typeof Report.Report */ ) { - const { - testFilePaths, - generatedCodeDir, - hasBeenGivenCustomGlobs, - projectElmJson, - } = makeAndTestHelper(projectRootDir, testFileGlobs); - - Generate.generateElmJson( + const { testFilePaths, generatedCodeDir, projectElmJson } = makeAndTestHelper( projectRootDir, - generatedCodeDir, - hasBeenGivenCustomGlobs, - projectElmJson + testFileGlobs ); + Generate.generateElmJson(projectRootDir, generatedCodeDir, projectElmJson); + await Compile.compileSources( testFilePaths, generatedCodeDir, @@ -170,12 +160,11 @@ function test( const { testFilePaths, generatedCodeDir, - hasBeenGivenCustomGlobs, projectElmJson, } = makeAndTestHelper(projectRootDir, testFileGlobs); if (testFilePaths.length === 0) { - throw new Error(noFilesFoundError(testFileGlobs)); + throw new Error(noFilesFoundError(projectRootDir, testFileGlobs)); } if (watcher !== undefined) { @@ -189,7 +178,6 @@ function test( const [generatedSrc, sourceDirs] = Generate.generateElmJson( projectRootDir, generatedCodeDir, - hasBeenGivenCustomGlobs, projectElmJson ); @@ -295,10 +283,10 @@ function test( } } -function noFilesFoundError(testFileGlobs) { +function noFilesFoundError(projectRootDir, testFileGlobs) { return testFileGlobs.length === 0 ? ` -No .elm files found in the tests/ directory. +${noFilesFoundInTestsDir(projectRootDir)} To generate some initial tests to get things going: elm-test init @@ -314,6 +302,20 @@ Are the above patterns correct? Maybe try running elm-test with no arguments? `.trim(); } +function noFilesFoundInTestsDir(projectRootDir) { + const testsDir = path.join(projectRootDir, 'tests'); + try { + const stats = fs.statSync(testsDir); + return stats.isDirectory() + ? 'No .elm files found in the tests/ directory.' + : `Expected a directory but found something else at: ${testsDir}\nCheck it out! Could you remove it?`; + } catch (error) { + return error.code === 'ENOENT' + ? 'The tests/ directory does not exist.' + : `Failed to read the tests/ directory: ${error.message}`; + } +} + // TODO(https://github.com/rtfeldman/node-test-runner/pull/465): replace this // function with commander's custom error messages once // https://github.com/tj/commander.js/pull/1392 lands and is released. From 24cd9cf7a827a3962bb0e70d487809af25ceec27 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 08:44:13 +0100 Subject: [PATCH 06/67] Update .flowconfig --- .flowconfig | 15 ++++++--------- lib/Install.js | 2 +- lib/Supervisor.js | 1 - 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.flowconfig b/.flowconfig index d04a6a61..68b196d2 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,11 +1,8 @@ -[ignore] - -.*elm.json - -[include] - -[libs] - [options] +include_warnings=true +exact_by_default=true -module.ignore_non_literal_requires=true +[lints] +all=error +untyped-import=off +unclear-type=off diff --git a/lib/Install.js b/lib/Install.js index 6fb949b5..b05ed622 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -39,7 +39,7 @@ function install( var elmJsonPath = path.join(projectRootDir, 'elm.json'); var tmpElmJsonPath = path.join(dirPath, 'elm.json'); var elmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); - var isPackage; + var isPackage = false; switch (elmJson['type']) { case 'package': diff --git a/lib/Supervisor.js b/lib/Supervisor.js index b1833276..4200a295 100644 --- a/lib/Supervisor.js +++ b/lib/Supervisor.js @@ -6,7 +6,6 @@ const fs = require('fs'); const net = require('net'); const split = require('split'); const XmlBuilder = require('xmlbuilder'); -// $FlowFixMe: Flow marks this line as an error (and only in this file!) but then it knows the types of `Report` anyway. This is the “error”: Cannot build a typed interface for this module. You should annotate the exports of this module with types. Missing type annotation at declaration of variable `Report`: [signature-verification-failure] const Report = require('./Report'); function run( From 125fd8cf05ec9195cd57e0a742cd6eebd6a6265d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 13:07:14 +0100 Subject: [PATCH 07/67] Initial pass at ElmJson.js --- lib/ElmJson.js | 189 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/Generate.js | 18 ++--- lib/Install.js | 75 ++++++++----------- lib/Solve.js | 16 ++-- lib/elm-test.js | 50 +++---------- 5 files changed, 248 insertions(+), 100 deletions(-) create mode 100644 lib/ElmJson.js diff --git a/lib/ElmJson.js b/lib/ElmJson.js new file mode 100644 index 00000000..c5b8d58d --- /dev/null +++ b/lib/ElmJson.js @@ -0,0 +1,189 @@ +// @flow + +const fs = require('fs'); +const path = require('path'); + +// Poor man’s type alias. We can’t use /*:: type Dependencies = ... */ because of: +// https://github.com/prettier/prettier/issues/2597 +const Dependencies /*: { [string]: string } */ = {}; + +const DirectAndIndirectDependencies /*: { + direct: typeof Dependencies, + indirect: typeof Dependencies, +} */ = { direct: {}, indirect: {} }; + +const ElmJson /*: + | { + type: 'application', + 'source-directories': Array, + dependencies: typeof DirectAndIndirectDependencies, + 'test-dependencies': typeof DirectAndIndirectDependencies, + [string]: mixed, + } + | { + type: 'package', + dependencies: typeof Dependencies, + 'test-dependencies': typeof Dependencies, + [string]: mixed, + } */ = { + type: 'package', + dependencies: Dependencies, + 'test-dependencies': Dependencies, +}; + +// TODO: Does this change the order of the keys? +function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { + const elmJsonPath = path.join(dir, 'elm.json'); + + try { + fs.writeFileSync(elmJsonPath, JSON.stringify(elmJson, null, 4) + '\n'); + } catch (error) { + throw new Error( + `${elmJsonPath}\nFailed to write elm.json:\n${error.message}` + ); + } +} + +function read(dir /*: string */) /*: typeof ElmJson */ { + const elmJsonPath = path.join(dir, 'elm.json'); + + try { + return readHelper(elmJsonPath); + } catch (error) { + throw new Error( + `${elmJsonPath}\nFailed to read elm.json:\n${error.message}` + ); + } +} + +function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { + const json = parseObject( + JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')), + 'the file' + ); + + switch (json.type) { + case 'application': + return { + ...json, + type: 'application', + 'source-directories': parseSourceDirectories( + json['source-directories'] + ), + dependencies: parseDirectAndIndirectDependencies( + json.dependencies, + 'dependencies' + ), + 'test-dependencies': parseDirectAndIndirectDependencies( + json['test-dependencies'], + 'test-dependencies' + ), + }; + + case 'package': + return { + ...json, + type: 'package', + dependencies: parseDependencies(json.dependencies, 'dependencies'), + 'test-dependencies': parseDependencies( + json['test-dependencies'], + 'test-dependencies' + ), + }; + + default: + throw new Error( + `Expected "type" to be "application" or "package", but got: ${stringify( + json.type + )}` + ); + } +} + +function parseSourceDirectories(json /*: mixed */) /*: Array */ { + if (!Array.isArray(json)) { + throw new Error( + `Expected "source-directories" to be an array, but got: ${stringify( + json + )}` + ); + } + + const result = []; + + for (const [index, item] of json.entries()) { + if (typeof item !== 'string') { + throw new Error( + `Expected "source-directories" -> ${index} to be a string, but got: ${stringify( + item + )}` + ); + } + result.push(item); + } + + if (result.length === 0) { + throw new Error( + 'Expected "source-directories" to contain at least one item, but it is empty.' + ); + } + + return result; +} + +function parseDirectAndIndirectDependencies( + json /*: mixed */, + what /*: string */ +) /*: typeof DirectAndIndirectDependencies */ { + const jsonObject = parseObject(json, what); + return { + direct: parseDependencies(jsonObject.direct, `${what} -> "direct"`), + indirect: parseDependencies(jsonObject.indirect, `${what} -> "indirect"`), + }; +} + +function parseDependencies( + json /*: mixed */, + what /*: string */ +) /*: typeof Dependencies */ { + const jsonObject = parseObject(json, what); + const result = {}; + + for (const [key, value] of Object.entries(jsonObject)) { + if (typeof value !== 'string') { + throw new Error( + `Expected ${what} -> ${stringify( + key + )} to be a string, but got: ${stringify(value)}` + ); + } + result[key] = value; + } + + return result; +} + +function parseObject( + json /*: mixed */, + what /*: string */ +) /*: { +[string]: mixed } */ { + if (json == null || typeof json !== 'object' || Array.isArray(json)) { + throw new Error( + `Expected ${what} to be an object, but got: ${stringify(json)}` + ); + } + return json; +} + +function stringify(json /*: mixed */) /*: string */ { + const maybeString = JSON.stringify(json); + return maybeString === undefined ? 'undefined' : maybeString; +} + +module.exports = { + DirectAndIndirectDependencies, + ElmJson, + parseDirectAndIndirectDependencies, + read, + write, +}; diff --git a/lib/Generate.js b/lib/Generate.js index 132f9add..c1fbe883 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -4,10 +4,11 @@ const { supportsColor } = require('chalk'); const fs = require('fs'); const Murmur = require('murmur-hash-js'); const path = require('path'); -const Compile = require('./Compile.js'); +const ElmJson = require('./ElmJson.js'); const Solve = require('./Solve.js'); const Report = require('./Report.js'); +void ElmJson; void Report; const before = fs.readFileSync( @@ -62,13 +63,11 @@ function addKernelTestChecking(content) { function generateElmJson( projectRootDir /*: string */, generatedCodeDir /*: string */, - projectElmJson /*: any */ + projectElmJson /*: typeof ElmJson.ElmJson */ ) /*: [string, Array] */ { const testRootDir = path.join(projectRootDir, 'tests'); const generatedSrc = path.join(generatedCodeDir, 'src'); - var isPackageProject = projectElmJson.type === 'package'; - const shouldAddTestsDirAsSource = fs.existsSync( path.join(projectRootDir, 'tests') ); @@ -92,12 +91,11 @@ function generateElmJson( }; // Make all the source-directories absolute, and introduce a new one. - var projectSourceDirs; - if (isPackageProject) { - projectSourceDirs = ['./src']; - } else { - projectSourceDirs = projectElmJson['source-directories']; - } + const projectSourceDirs = + projectElmJson.type === 'package' + ? ['./src'] + : projectElmJson['source-directories']; + var sourceDirs /*: Array */ = projectSourceDirs .map((src) => path.join(projectRootDir, src)) .concat(shouldAddTestsDirAsSource ? [testRootDir] : []); diff --git a/lib/Install.js b/lib/Install.js index b05ed622..fcd6ec67 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -4,17 +4,16 @@ const child_process = require('child_process'); const fs = require('fs'); const path = require('path'); const rimraf = require('rimraf'); -const Compiler = require('./Compile'); +const Compile = require('./Compile'); +const ElmJson = require('./ElmJson'); function install( projectRootDir /*: string */, pathToElmBinary /*: string */, packageName /*: string */ ) { - var oldSourceDirectories; - - const dirPath = path.join( - Compiler.getGeneratedCodeDir(projectRootDir), + const generatedCodeDir = path.join( + Compile.getGeneratedCodeDir(projectRootDir), 'install' ); @@ -22,12 +21,12 @@ function install( // Recreate the directory to remove any artifacts from the last time // someone ran `elm-test install`. We do not delete this directory after // the installation finishes in case the user needs to debug the test run. - if (fs.existsSync(dirPath)) { + if (fs.existsSync(generatedCodeDir)) { // We can replace this with `fs.rmdirSync(dir, { recursive: true })` // once Node.js 10 is EOL 2021-04-30 and support for Node.js 10 is dropped. - rimraf.sync(dirPath); + rimraf.sync(generatedCodeDir); } - fs.mkdirSync(dirPath, { recursive: true }); + fs.mkdirSync(generatedCodeDir, { recursive: true }); } catch (err) { console.error( 'Unable to create temporary directory for elm-test install.', @@ -36,56 +35,43 @@ function install( process.exit(1); } - var elmJsonPath = path.join(projectRootDir, 'elm.json'); - var tmpElmJsonPath = path.join(dirPath, 'elm.json'); - var elmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); - var isPackage = false; - - switch (elmJson['type']) { - case 'package': - isPackage = true; - break; - - case 'application': - isPackage = false; - break; - - default: - console.error('Unrecognized elm.json type:', elmJson['type']); - process.exit(1); - } + const elmJson = ElmJson.read(projectRootDir); // This mirrors the behavior of `elm install` passing a package that is // already installed. Say it's already installed, then exit 0. if ( - (isPackage && elmJson['test-dependencies'].hasOwnProperty(packageName)) || - (!isPackage && - elmJson['test-dependencies']['direct'].hasOwnProperty(packageName)) + elmJson.type === 'package' + ? elmJson['test-dependencies'].hasOwnProperty(packageName) + : elmJson['test-dependencies'].direct.hasOwnProperty(packageName) ) { console.log('It is already installed!'); return; } - oldSourceDirectories = elmJson['source-directories']; + const tmpElmJson = + elmJson.type === 'package' + ? elmJson + : { + ...elmJson, + // Without this, `elm install` will complain about missing source dirs + // in the temp dir. This way we don't have to create them! + 'source-directories': ['.'], + }; - // Without this, `elm install` will complain about missing source dirs - // in the temp dir. This way we don't have to create them! - elmJson['source-directories'] = ['.']; - - fs.writeFileSync(tmpElmJsonPath, JSON.stringify(elmJson), 'utf8'); + ElmJson.write(generatedCodeDir, tmpElmJson); try { child_process.execFileSync(pathToElmBinary, ['install', packageName], { stdio: 'inherit', - cwd: dirPath, + cwd: generatedCodeDir, }); } catch (error) { process.exit(error.status || 1); } - var newElmJson = JSON.parse(fs.readFileSync(tmpElmJsonPath, 'utf8')); + const newElmJson = ElmJson.read(generatedCodeDir); - if (isPackage) { + if (newElmJson.type === 'package') { Object.keys(newElmJson['dependencies']).forEach(function (key) { if (!elmJson['dependencies'].hasOwnProperty(key)) { // If we didn't have this dep before, move it to test-dependencies. @@ -119,16 +105,13 @@ function install( moveToTestDeps('direct'); moveToTestDeps('indirect'); + if (elmJson.type === 'application') { + // Restore the old source-directories value. + newElmJson['source-directories'] = elmJson['source-directories']; + } } - // Restore the old source-directories value. - newElmJson['source-directories'] = oldSourceDirectories; - - fs.writeFileSync( - elmJsonPath, - JSON.stringify(newElmJson, null, 4) + '\n', - 'utf8' - ); + ElmJson.write(projectRootDir, newElmJson); } module.exports = { diff --git a/lib/Solve.js b/lib/Solve.js index 8e1e9085..f893995a 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -4,6 +4,7 @@ const spawn = require('cross-spawn'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); +const ElmJson = require('./ElmJson'); function sha1(string) { return crypto.createHash('sha1').update(string).digest('hex'); @@ -12,12 +13,12 @@ function sha1(string) { function getDependenciesCached( generatedCodeDir /*: string */, elmJsonPath /*: string */, - projectElmJson /*: any */ -) /*: { direct: { [string]: string }, indirect: { [string]: string } } */ { + elmJson /*: typeof ElmJson.ElmJson */ +) /*: typeof ElmJson.DirectAndIndirectDependencies */ { const hash = sha1( JSON.stringify({ - dependencies: projectElmJson.dependencies, - 'test-dependencies': projectElmJson['test-dependencies'], + dependencies: elmJson.dependencies, + 'test-dependencies': elmJson['test-dependencies'], }) ); @@ -37,10 +38,13 @@ function getDependenciesCached( fs.writeFileSync(cacheFile, dependencies); - return JSON.parse(dependencies); + return ElmJson.parseDirectAndIndirectDependencies( + JSON.parse(dependencies), + 'elm-json solve output' + ); } -function getDependencies(elmJsonPath) { +function getDependencies(elmJsonPath /*: string */) /*: string */ { var result = spawn.sync( 'elm-json', [ diff --git a/lib/elm-test.js b/lib/elm-test.js index 21302ace..f50844e5 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -10,6 +10,7 @@ const path = require('path'); const which = require('which'); const packageInfo = require('../package.json'); const Compile = require('./Compile.js'); +const ElmJson = require('./ElmJson.js'); const Generate = require('./Generate.js'); const Install = require('./Install.js'); const Report = require('./Report.js'); @@ -18,6 +19,7 @@ const Supervisor = require('./Supervisor.js'); void Report; +// Resolve arguments that look like globs for shells that don’t support globs. function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { const results = fileGlobs.length > 0 @@ -97,43 +99,17 @@ function diffArrays/*:: */( }; } -function makeAndTestHelper( - projectRootDir /*: string */, - testFileGlobs /*: Array */ -) /*: { - testFilePaths: Array, - generatedCodeDir: string, - projectElmJson: any, -} */ { - // Resolve arguments that look like globs for shells that don’t support globs. - const testFilePaths = resolveGlobs(testFileGlobs); - const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); - const elmJsonPath = path.join(projectRootDir, 'elm.json'); - - try { - const projectElmJson = JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')); - return { - testFilePaths, - generatedCodeDir, - projectElmJson, - }; - } catch (error) { - throw new Error(`Error reading elm.json: ${error.message}`); - } -} - async function make( projectRootDir /*: string */, pathToElmBinary /*: string */, testFileGlobs /*: Array */, report /*: typeof Report.Report */ ) { - const { testFilePaths, generatedCodeDir, projectElmJson } = makeAndTestHelper( - projectRootDir, - testFileGlobs - ); + const testFilePaths = resolveGlobs(testFileGlobs); + const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); + const elmJson = ElmJson.read(projectRootDir); - Generate.generateElmJson(projectRootDir, generatedCodeDir, projectElmJson); + Generate.generateElmJson(projectRootDir, generatedCodeDir, elmJson); await Compile.compileSources( testFilePaths, @@ -157,18 +133,16 @@ function test( async function run() { try { - const { - testFilePaths, - generatedCodeDir, - projectElmJson, - } = makeAndTestHelper(projectRootDir, testFileGlobs); + const testFilePaths = resolveGlobs(testFileGlobs); + const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); + const elmJson = ElmJson.read(projectRootDir); if (testFilePaths.length === 0) { throw new Error(noFilesFoundError(projectRootDir, testFileGlobs)); } if (watcher !== undefined) { - const nextGlobsToWatch = getGlobsToWatch(projectElmJson); + const nextGlobsToWatch = getGlobsToWatch(elmJson); const diff = diffArrays(watchedGlobs, nextGlobsToWatch); watchedGlobs = nextGlobsToWatch; watcher.add(diff.added); @@ -178,10 +152,10 @@ function test( const [generatedSrc, sourceDirs] = Generate.generateElmJson( projectRootDir, generatedCodeDir, - projectElmJson + elmJson ); - const isPackageProject = projectElmJson.type === 'package'; + const isPackageProject = elmJson.type === 'package'; const testModules = await Runner.findTests( testFilePaths, From a9ada09c2229f92932b8e98e8891c49da1d47fde Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 13:09:45 +0100 Subject: [PATCH 08/67] Fix error message --- lib/elm-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index f50844e5..aa591259 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -355,7 +355,7 @@ function main() { const command = subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; console.error( - `\`${command}\` must be run in the same directory as an existing elm.json file! To make one: elm init` + `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` ); throw process.exit(1); } From a17f07a6be2c737c259cbd39c392562256576083 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 13:11:57 +0100 Subject: [PATCH 09/67] Get rid of the last `any` annotations --- lib/Parser.js | 11 +++++++---- lib/elm-test.js | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/Parser.js b/lib/Parser.js index f2a63490..79bf2af1 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -312,16 +312,19 @@ void OnParserTokenResult; function expected( expectedDescription /*: string */, - actual /*: any */ + actual /*: mixed */ ) /*: typeof ParseError */ { return { tag: 'ParseError', - message: `Expected ${expectedDescription} but got: ${JSON.stringify( - actual - )}`, + message: `Expected ${expectedDescription} but got: ${stringify(actual)}`, }; } +function stringify(json /*: mixed */) /*: string */ { + const maybeString = JSON.stringify(json); + return maybeString === undefined ? 'undefined' : maybeString; +} + function backslashError(actual) { return expected( `one of \`${Array.from(backslashableChars).join(' ')}\``, diff --git a/lib/elm-test.js b/lib/elm-test.js index aa591259..68ce2327 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -66,7 +66,9 @@ function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { ); } -function getGlobsToWatch(elmJson /*: any */) /*: Array */ { +function getGlobsToWatch( + elmJson /*: typeof ElmJson.ElmJson */ +) /*: Array */ { const sourceDirectories = elmJson.type === 'package' ? ['src'] : elmJson['source-directories']; return [...sourceDirectories, 'tests'].map((sourceDirectory) => From 1c4d753e5ea158f7c0523366fa153ac7d4c63cd1 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 19:14:40 +0100 Subject: [PATCH 10/67] ElmJson.getPath --- lib/ElmJson.js | 9 +++++++-- lib/Generate.js | 5 ++--- lib/elm-test.js | 12 ++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/ElmJson.js b/lib/ElmJson.js index c5b8d58d..91450ef0 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -31,9 +31,13 @@ const ElmJson /*: 'test-dependencies': Dependencies, }; +function getPath(dir /*: string */) /*: string */ { + return path.join(dir, 'elm.json'); +} + // TODO: Does this change the order of the keys? function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { - const elmJsonPath = path.join(dir, 'elm.json'); + const elmJsonPath = getPath(dir); try { fs.writeFileSync(elmJsonPath, JSON.stringify(elmJson, null, 4) + '\n'); @@ -45,7 +49,7 @@ function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { } function read(dir /*: string */) /*: typeof ElmJson */ { - const elmJsonPath = path.join(dir, 'elm.json'); + const elmJsonPath = getPath(dir); try { return readHelper(elmJsonPath); @@ -183,6 +187,7 @@ function stringify(json /*: mixed */) /*: string */ { module.exports = { DirectAndIndirectDependencies, ElmJson, + getPath, parseDirectAndIndirectDependencies, read, write, diff --git a/lib/Generate.js b/lib/Generate.js index c1fbe883..eacb5e0b 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -8,7 +8,6 @@ const ElmJson = require('./ElmJson.js'); const Solve = require('./Solve.js'); const Report = require('./Report.js'); -void ElmJson; void Report; const before = fs.readFileSync( @@ -81,7 +80,7 @@ function generateElmJson( 'elm-version': '0.19.1', dependencies: Solve.getDependenciesCached( generatedCodeDir, - path.join(projectRootDir, 'elm.json'), + ElmJson.getPath(projectRootDir), projectElmJson ), 'test-dependencies': { @@ -126,7 +125,7 @@ function generateElmJson( // Generate the new elm.json, if necessary. const generatedContents = JSON.stringify(testElmJson, null, 4); - const generatedPath = path.join(generatedCodeDir, 'elm.json'); + const generatedPath = ElmJson.getPath(generatedCodeDir); // Don't write a fresh elm.json if it's going to be the same. If we do, // it will update the timestamp on the file, which will cause `elm make` diff --git a/lib/elm-test.js b/lib/elm-test.js index 68ce2327..f2a05b9b 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -225,7 +225,7 @@ function test( infoLog(report, 'Running in watch mode'); // More globs to watch are added later. - const initialGlobsToWatch = [path.join(projectRootDir, 'elm.json')]; + const initialGlobsToWatch = [ElmJson.getPath(projectRootDir)]; watcher = chokidar.watch(initialGlobsToWatch, { awaitWriteFinish: { stabilityThreshold: 500, @@ -329,15 +329,15 @@ const parseReport = (flag /*: string */) => ( }; function findClosest( - name /*: string */, - dir /*: string */ + dir /*: string */, + dirToPath /*: (dir: string) => string */ ) /*: string | void */ { - const entry = path.join(dir, name); + const entry = dirToPath(dir); return fs.existsSync(entry) ? entry : dir === path.parse(dir).root ? undefined - : findClosest(name, path.dirname(dir)); + : findClosest(path.dirname(dir), dirToPath); } const examples = ` @@ -352,7 +352,7 @@ function main() { process.title = 'elm-test'; const getClosestElmJsonPath = (subcommand /*: string */) /*: string */ => { - const elmJsonPath = findClosest('elm.json', process.cwd()); + const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); if (elmJsonPath === undefined) { const command = subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; From a2e61c390d756a98af7a0741912941df463db2d8 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 19:19:36 +0100 Subject: [PATCH 11/67] Simplify --- lib/elm-test.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index f2a05b9b..666b5718 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -351,7 +351,7 @@ elm-test "src/**/*Tests.elm" function main() { process.title = 'elm-test'; - const getClosestElmJsonPath = (subcommand /*: string */) /*: string */ => { + const getProjectRootDir = (subcommand /*: string */) /*: string */ => { const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); if (elmJsonPath === undefined) { const command = @@ -361,7 +361,7 @@ function main() { ); throw process.exit(1); } - return elmJsonPath; + return path.dirname(elmJsonPath); }; const getPathToElmBinary = (compiler /*: string | void */) /*: string */ => { @@ -428,8 +428,7 @@ function main() { } const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const elmJsonPath = getClosestElmJsonPath('init'); - const projectRootDir = path.dirname(elmJsonPath); + const projectRootDir = getProjectRootDir('init'); const testsDir = path.join(projectRootDir, 'tests'); Install.install(projectRootDir, pathToElmBinary, 'elm-explorations/test'); fs.mkdirSync(testsDir, { recursive: true }); @@ -462,8 +461,7 @@ function main() { } const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const elmJsonPath = getClosestElmJsonPath('install'); - const projectRootDir = path.dirname(elmJsonPath); + const projectRootDir = getProjectRootDir('install'); Install.install(projectRootDir, pathToElmBinary, packageName); process.exit(0); }); @@ -474,8 +472,7 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const elmJsonPath = getClosestElmJsonPath('make'); - const projectRootDir = path.dirname(elmJsonPath); + const projectRootDir = getProjectRootDir('make'); make(projectRootDir, pathToElmBinary, testFileGlobs, options.report).then( () => process.exit(0), () => process.exit(1) @@ -489,8 +486,7 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const elmJsonPath = getClosestElmJsonPath('install'); - const projectRootDir = path.dirname(elmJsonPath); + const projectRootDir = getProjectRootDir('install'); const processes = Math.max(1, os.cpus().length); test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); }); From 9c55e25461b924279e60cc344faf88d31e00df7d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 19:24:26 +0100 Subject: [PATCH 12/67] Remove resolved TODO comment --- lib/ElmJson.js | 1 - lib/Install.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ElmJson.js b/lib/ElmJson.js index 91450ef0..d6f36ac2 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -35,7 +35,6 @@ function getPath(dir /*: string */) /*: string */ { return path.join(dir, 'elm.json'); } -// TODO: Does this change the order of the keys? function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { const elmJsonPath = getPath(dir); diff --git a/lib/Install.js b/lib/Install.js index fcd6ec67..e61350f5 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -105,6 +105,7 @@ function install( moveToTestDeps('direct'); moveToTestDeps('indirect'); + if (elmJson.type === 'application') { // Restore the old source-directories value. newElmJson['source-directories'] = elmJson['source-directories']; From b64079d2d0178dba0a1b5156a69619b2689ca081 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 20:35:22 +0100 Subject: [PATCH 13/67] Refactor into a Project type --- lib/Compile.js | 14 ------- lib/Generate.js | 60 ++++++++++------------------ lib/Install.js | 15 ++++--- lib/Project.js | 63 ++++++++++++++++++++++++++++++ lib/Solve.js | 18 +++++---- lib/elm-test.js | 102 +++++++++++++++++++++++++----------------------- 6 files changed, 156 insertions(+), 116 deletions(-) create mode 100644 lib/Project.js diff --git a/lib/Compile.js b/lib/Compile.js index fd1fc963..6a7c8064 100644 --- a/lib/Compile.js +++ b/lib/Compile.js @@ -1,8 +1,6 @@ //@flow const spawn = require('cross-spawn'); -const path = require('path'); -const packageInfo = require('../package.json'); const ElmCompiler = require('./ElmCompiler'); const Report = require('./Report'); @@ -31,17 +29,6 @@ function compile( }); } -function getGeneratedCodeDir(projectRootDir /*: string */) /*: string */ { - return path.join( - projectRootDir, - 'elm-stuff', - 'generated-code', - 'elm-community', - 'elm-test', - packageInfo.version - ); -} - function compileSources( testFilePaths /*: Array */, projectRootDir /*: string */, @@ -100,5 +87,4 @@ function processOptsForReporter(report /*: typeof Report.Report */) { module.exports = { compile, compileSources, - getGeneratedCodeDir, }; diff --git a/lib/Generate.js b/lib/Generate.js index eacb5e0b..8855a6a7 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -5,9 +5,11 @@ const fs = require('fs'); const Murmur = require('murmur-hash-js'); const path = require('path'); const ElmJson = require('./ElmJson.js'); -const Solve = require('./Solve.js'); +const Project = require('./Project.js'); const Report = require('./Report.js'); +const Solve = require('./Solve.js'); +void Project; void Report; const before = fs.readFileSync( @@ -59,46 +61,26 @@ function addKernelTestChecking(content) { ); } -function generateElmJson( - projectRootDir /*: string */, - generatedCodeDir /*: string */, - projectElmJson /*: typeof ElmJson.ElmJson */ -) /*: [string, Array] */ { - const testRootDir = path.join(projectRootDir, 'tests'); - const generatedSrc = path.join(generatedCodeDir, 'src'); +function getGeneratedSrcDir(generatedCodeDir /*: string */) /*: string */ { + return path.join(generatedCodeDir, 'src'); +} - const shouldAddTestsDirAsSource = fs.existsSync( - path.join(projectRootDir, 'tests') - ); +function generateElmJson(project /*: typeof Project.Project */) /*: void */ { + const generatedSrc = getGeneratedSrcDir(project.generatedCodeDir); - fs.mkdirSync(generatedCodeDir, { recursive: true }); fs.mkdirSync(generatedSrc, { recursive: true }); let testElmJson = { type: 'application', 'source-directories': [], // these are added below 'elm-version': '0.19.1', - dependencies: Solve.getDependenciesCached( - generatedCodeDir, - ElmJson.getPath(projectRootDir), - projectElmJson - ), + dependencies: Solve.getDependenciesCached(project), 'test-dependencies': { direct: {}, indirect: {}, }, }; - // Make all the source-directories absolute, and introduce a new one. - const projectSourceDirs = - projectElmJson.type === 'package' - ? ['./src'] - : projectElmJson['source-directories']; - - var sourceDirs /*: Array */ = projectSourceDirs - .map((src) => path.join(projectRootDir, src)) - .concat(shouldAddTestsDirAsSource ? [testRootDir] : []); - testElmJson['source-directories'] = [ // Include elm-stuff/generated-sources - since we'll be generating sources in there. generatedSrc, @@ -107,25 +89,23 @@ function generateElmJson( // instead of adding it as a dependency so that it can include port modules path.join(__dirname, '..', 'elm', 'src'), ] - .concat(sourceDirs) + .concat(project.testsSourceDirs) .filter( // When running node-test-runner's own test suite, the node-test-runner/src folder // will get added twice: once because it's the source-directory of the packge being tested, // and once because elm-test will always add it. // To prevent elm from being confused, we need to remove the duplicate when this happens. - function (value, index, self) { - return self.indexOf(value) === index; - } + (value, index, self) => self.indexOf(value) === index ) - .map(function (absolutePath) { + .map((absolutePath) => // Relative paths have the nice benefit that if the user moves their // directory, this doesn't break. - return path.relative(generatedCodeDir, absolutePath); - }); + path.relative(project.generatedCodeDir, absolutePath) + ); // Generate the new elm.json, if necessary. const generatedContents = JSON.stringify(testElmJson, null, 4); - const generatedPath = ElmJson.getPath(generatedCodeDir); + const generatedPath = ElmJson.getPath(project.generatedCodeDir); // Don't write a fresh elm.json if it's going to be the same. If we do, // it will update the timestamp on the file, which will cause `elm make` @@ -139,8 +119,6 @@ function generateElmJson( // we have so far, and run elm to see what it thinks is missing. fs.writeFileSync(generatedPath, generatedContents); } - - return [generatedSrc, sourceDirs]; } function generateMainModule( @@ -153,7 +131,7 @@ function generateMainModule( moduleName: string, possiblyTests: Array, }> */, - generatedSrc /*: string */, + generatedCodeDir /*: string */, processes /*: number */ ) /*: string */ { const testFileBody = makeTestFileBody( @@ -168,7 +146,11 @@ function generateMainModule( // why would they?) so we don't have to recompile it. const salt = Murmur.murmur3(testFileBody); const moduleName = 'Main' + salt; - const mainPath = path.join(generatedSrc, 'Test', 'Generated'); + const mainPath = path.join( + getGeneratedSrcDir(generatedCodeDir), + 'Test', + 'Generated' + ); const mainFile = path.join(mainPath, moduleName + '.elm'); // We'll be putting the generated Main in something like this: diff --git a/lib/Install.js b/lib/Install.js index e61350f5..6cd1f93f 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -4,18 +4,17 @@ const child_process = require('child_process'); const fs = require('fs'); const path = require('path'); const rimraf = require('rimraf'); -const Compile = require('./Compile'); const ElmJson = require('./ElmJson'); +const Project = require('./Project'); + +void Project; function install( - projectRootDir /*: string */, + project /*: typeof Project.Project */, pathToElmBinary /*: string */, packageName /*: string */ ) { - const generatedCodeDir = path.join( - Compile.getGeneratedCodeDir(projectRootDir), - 'install' - ); + const generatedCodeDir = path.join(project.generatedCodeDir, 'install'); try { // Recreate the directory to remove any artifacts from the last time @@ -35,7 +34,7 @@ function install( process.exit(1); } - const elmJson = ElmJson.read(projectRootDir); + const { elmJson } = project; // This mirrors the behavior of `elm install` passing a package that is // already installed. Say it's already installed, then exit 0. @@ -112,7 +111,7 @@ function install( } } - ElmJson.write(projectRootDir, newElmJson); + ElmJson.write(project.rootDir, newElmJson); } module.exports = { diff --git a/lib/Project.js b/lib/Project.js new file mode 100644 index 00000000..d25842e4 --- /dev/null +++ b/lib/Project.js @@ -0,0 +1,63 @@ +// @flow + +const fs = require('fs'); +const path = require('path'); +const ElmJson = require('./ElmJson'); + +// Poor man’s type alias. We can’t use /*:: type Project = ... */ because of: +// https://github.com/prettier/prettier/issues/2597 +const Project /*: { + rootDir: string, + testsDir: string, + generatedCodeDir: string, + testsSourceDirs: Array, + elmJson: typeof ElmJson.ElmJson, +} */ = { + rootDir: '', + testsDir: '', + generatedCodeDir: '', + testsSourceDirs: [], + elmJson: ElmJson.ElmJson, +}; + +function init( + rootDir /*: string */, + version /*: string */ +) /*: typeof Project */ { + const testsDir = path.join(rootDir, 'tests'); + + // The tests/ directory is not required. You can also co-locate tests with + // their source files. + const shouldAddTestsDirAsSource = fs.existsSync(testsDir); + + const elmJson = ElmJson.read(rootDir); + + const projectSourceDirs = + elmJson.type === 'package' ? ['src'] : elmJson['source-directories']; + + const testsSourceDirs /*: Array */ = projectSourceDirs + .map((src) => path.resolve(rootDir, src)) + .concat(shouldAddTestsDirAsSource ? [testsDir] : []); + + const generatedCodeDir = path.join( + rootDir, + 'elm-stuff', + 'generated-code', + 'elm-community', + 'elm-test', + version + ); + + return { + rootDir, + testsDir, + generatedCodeDir, + testsSourceDirs, + elmJson, + }; +} + +module.exports = { + Project, + init, +}; diff --git a/lib/Solve.js b/lib/Solve.js index f893995a..7654ba37 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -5,24 +5,28 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ElmJson = require('./ElmJson'); +const Project = require('./Project'); + +void Project; function sha1(string) { return crypto.createHash('sha1').update(string).digest('hex'); } function getDependenciesCached( - generatedCodeDir /*: string */, - elmJsonPath /*: string */, - elmJson /*: typeof ElmJson.ElmJson */ + project /*: typeof Project.Project */ ) /*: typeof ElmJson.DirectAndIndirectDependencies */ { const hash = sha1( JSON.stringify({ - dependencies: elmJson.dependencies, - 'test-dependencies': elmJson['test-dependencies'], + dependencies: project.elmJson.dependencies, + 'test-dependencies': project.elmJson['test-dependencies'], }) ); - const cacheFile = path.join(generatedCodeDir, `dependencies.${hash}.json`); + const cacheFile = path.join( + project.generatedCodeDir, + `dependencies.${hash}.json` + ); try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); @@ -34,7 +38,7 @@ function getDependenciesCached( } } - const dependencies = getDependencies(elmJsonPath); + const dependencies = getDependencies(ElmJson.getPath(project.rootDir)); fs.writeFileSync(cacheFile, dependencies); diff --git a/lib/elm-test.js b/lib/elm-test.js index 666b5718..3025adb0 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -13,6 +13,7 @@ const Compile = require('./Compile.js'); const ElmJson = require('./ElmJson.js'); const Generate = require('./Generate.js'); const Install = require('./Install.js'); +const Project = require('./Project.js'); const Report = require('./Report.js'); const Runner = require('./Runner.js'); const Supervisor = require('./Supervisor.js'); @@ -102,31 +103,36 @@ function diffArrays/*:: */( } async function make( - projectRootDir /*: string */, + project /*: typeof Project.Project */, pathToElmBinary /*: string */, testFileGlobs /*: Array */, report /*: typeof Report.Report */ ) { - const testFilePaths = resolveGlobs(testFileGlobs); - const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); - const elmJson = ElmJson.read(projectRootDir); - - Generate.generateElmJson(projectRootDir, generatedCodeDir, elmJson); - + Generate.generateElmJson(project); await Compile.compileSources( - testFilePaths, - generatedCodeDir, + resolveGlobs(testFileGlobs), + project.generatedCodeDir, pathToElmBinary, report ); } function test( - projectRootDir /*: string */, + project /*: typeof Project.Project */, pathToElmBinary /*: string */, testFileGlobs /*: Array */, processes /*: number */, - { watch, report, seed, fuzz } + { + watch, + report, + seed, + fuzz, + } /*: { + watch: boolean, + report: typeof Report.Report, + seed: number, + fuzz: number, + } */ ) { let watcher; let watchedGlobs /*: Array */ = []; @@ -135,12 +141,13 @@ function test( async function run() { try { + // Files maybe be changed, added or removed so always read from disk to + // stay fresh. const testFilePaths = resolveGlobs(testFileGlobs); - const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir); - const elmJson = ElmJson.read(projectRootDir); + const elmJson = ElmJson.read(project.rootDir); if (testFilePaths.length === 0) { - throw new Error(noFilesFoundError(projectRootDir, testFileGlobs)); + throw new Error(noFilesFoundError(project.rootDir, testFileGlobs)); } if (watcher !== undefined) { @@ -151,18 +158,12 @@ function test( watcher.unwatch(diff.removed); } - const [generatedSrc, sourceDirs] = Generate.generateElmJson( - projectRootDir, - generatedCodeDir, - elmJson - ); - - const isPackageProject = elmJson.type === 'package'; + Generate.generateElmJson(project); const testModules = await Runner.findTests( testFilePaths, - sourceDirs, - isPackageProject + project.testsSourceDirs, + elmJson.type === 'package' ); const mainFile = Generate.generateMainModule( @@ -172,11 +173,11 @@ function test( testFileGlobs, testFilePaths, testModules, - generatedSrc, + project.generatedCodeDir, processes ); - const dest = path.join(generatedCodeDir, 'elmTestOutput.js'); + const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); // Incorporate the process PID into the socket name, so elm-test processes can // be run parallel without accidentally sharing each others' sockets. @@ -194,7 +195,7 @@ function test( : `/tmp/elm_test-${process.pid}.sock`; await Compile.compile( - generatedCodeDir, + project.generatedCodeDir, mainFile, dest, pathToElmBinary, @@ -225,14 +226,14 @@ function test( infoLog(report, 'Running in watch mode'); // More globs to watch are added later. - const initialGlobsToWatch = [ElmJson.getPath(projectRootDir)]; + const initialGlobsToWatch = [ElmJson.getPath(project.rootDir)]; watcher = chokidar.watch(initialGlobsToWatch, { awaitWriteFinish: { stabilityThreshold: 500, }, ignoreInitial: true, ignored: /(\/|^)elm-stuff(\/|$)/, - cwd: projectRootDir, + cwd: project.rootDir, }); currentRun = run(); @@ -351,17 +352,23 @@ elm-test "src/**/*Tests.elm" function main() { process.title = 'elm-test'; - const getProjectRootDir = (subcommand /*: string */) /*: string */ => { - const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); - if (elmJsonPath === undefined) { - const command = - subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; - console.error( - `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` - ); + const getProject = ( + subcommand /*: string */ + ) /*: typeof Project.Project */ => { + try { + const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); + if (elmJsonPath === undefined) { + const command = + subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; + throw new Error( + `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` + ); + } + return Project.init(path.dirname(elmJsonPath), packageInfo.version); + } catch (error) { + console.error(error.message); throw process.exit(1); } - return path.dirname(elmJsonPath); }; const getPathToElmBinary = (compiler /*: string | void */) /*: string */ => { @@ -428,13 +435,12 @@ function main() { } const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const projectRootDir = getProjectRootDir('init'); - const testsDir = path.join(projectRootDir, 'tests'); - Install.install(projectRootDir, pathToElmBinary, 'elm-explorations/test'); - fs.mkdirSync(testsDir, { recursive: true }); + const project = getProject('init'); + Install.install(project, pathToElmBinary, 'elm-explorations/test'); + fs.mkdirSync(project.testsDir, { recursive: true }); fs.copyFileSync( path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), - path.join(testsDir, 'Example.elm') + path.join(project.testsDir, 'Example.elm') ); console.log( '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' @@ -461,8 +467,8 @@ function main() { } const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const projectRootDir = getProjectRootDir('install'); - Install.install(projectRootDir, pathToElmBinary, packageName); + const project = getProject('install'); + Install.install(project, pathToElmBinary, packageName); process.exit(0); }); @@ -472,8 +478,8 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const projectRootDir = getProjectRootDir('make'); - make(projectRootDir, pathToElmBinary, testFileGlobs, options.report).then( + const project = getProject('make'); + make(project, pathToElmBinary, testFileGlobs, options.report).then( () => process.exit(0), () => process.exit(1) ); @@ -486,9 +492,9 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const projectRootDir = getProjectRootDir('install'); + const project = getProject('install'); const processes = Math.max(1, os.cpus().length); - test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); + test(project, pathToElmBinary, testFileGlobs, processes, options); }); program.parse(process.argv); From 101b96c169de3d03c7ac4945eb73f8246fc76177 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 20:49:28 +0100 Subject: [PATCH 14/67] Make globs watcher globs independent of cwd --- lib/elm-test.js | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 3025adb0..d1977c99 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -68,11 +68,10 @@ function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { } function getGlobsToWatch( - elmJson /*: typeof ElmJson.ElmJson */ + project /*: typeof Project.Project */ ) /*: Array */ { - const sourceDirectories = - elmJson.type === 'package' ? ['src'] : elmJson['source-directories']; - return [...sourceDirectories, 'tests'].map((sourceDirectory) => + return project.testsSourceDirs.map((sourceDirectory) => + // TODO: Test this on Windows. path.posix.join(sourceDirectory, '**', '*.elm') ); } @@ -118,7 +117,7 @@ async function make( } function test( - project /*: typeof Project.Project */, + projectRootDir /*: string */, pathToElmBinary /*: string */, testFileGlobs /*: Array */, processes /*: number */, @@ -143,15 +142,15 @@ function test( try { // Files maybe be changed, added or removed so always read from disk to // stay fresh. + const project = Project.init(projectRootDir, packageInfo.version); const testFilePaths = resolveGlobs(testFileGlobs); - const elmJson = ElmJson.read(project.rootDir); if (testFilePaths.length === 0) { throw new Error(noFilesFoundError(project.rootDir, testFileGlobs)); } if (watcher !== undefined) { - const nextGlobsToWatch = getGlobsToWatch(elmJson); + const nextGlobsToWatch = getGlobsToWatch(project); const diff = diffArrays(watchedGlobs, nextGlobsToWatch); watchedGlobs = nextGlobsToWatch; watcher.add(diff.added); @@ -163,7 +162,7 @@ function test( const testModules = await Runner.findTests( testFilePaths, project.testsSourceDirs, - elmJson.type === 'package' + project.elmJson.type === 'package' ); const mainFile = Generate.generateMainModule( @@ -226,14 +225,14 @@ function test( infoLog(report, 'Running in watch mode'); // More globs to watch are added later. - const initialGlobsToWatch = [ElmJson.getPath(project.rootDir)]; + const initialGlobsToWatch = [ElmJson.getPath(projectRootDir)]; watcher = chokidar.watch(initialGlobsToWatch, { awaitWriteFinish: { stabilityThreshold: 500, }, ignoreInitial: true, ignored: /(\/|^)elm-stuff(\/|$)/, - cwd: project.rootDir, + cwd: projectRootDir, }); currentRun = run(); @@ -352,19 +351,24 @@ elm-test "src/**/*Tests.elm" function main() { process.title = 'elm-test'; + const getProjectRootDir = (subcommand /*: string */) /*: string */ => { + const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); + if (elmJsonPath === undefined) { + const command = + subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; + console.error( + `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` + ); + throw process.exit(1); + } + return path.dirname(elmJsonPath); + }; + const getProject = ( subcommand /*: string */ ) /*: typeof Project.Project */ => { try { - const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); - if (elmJsonPath === undefined) { - const command = - subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; - throw new Error( - `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` - ); - } - return Project.init(path.dirname(elmJsonPath), packageInfo.version); + return Project.init(getProjectRootDir(subcommand), packageInfo.version); } catch (error) { console.error(error.message); throw process.exit(1); @@ -492,9 +496,9 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const project = getProject('install'); + const projectRootDir = getProjectRootDir('install'); const processes = Math.max(1, os.cpus().length); - test(project, pathToElmBinary, testFileGlobs, processes, options); + test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); }); program.parse(process.argv); From ae0e6bfbf7c89724e262c33b7c7d567e1ec835b5 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 20:50:53 +0100 Subject: [PATCH 15/67] Inline make --- lib/elm-test.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index d1977c99..474cd468 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -101,21 +101,6 @@ function diffArrays/*:: */( }; } -async function make( - project /*: typeof Project.Project */, - pathToElmBinary /*: string */, - testFileGlobs /*: Array */, - report /*: typeof Report.Report */ -) { - Generate.generateElmJson(project); - await Compile.compileSources( - resolveGlobs(testFileGlobs), - project.generatedCodeDir, - pathToElmBinary, - report - ); -} - function test( projectRootDir /*: string */, pathToElmBinary /*: string */, @@ -483,7 +468,13 @@ function main() { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); const project = getProject('make'); - make(project, pathToElmBinary, testFileGlobs, options.report).then( + Generate.generateElmJson(project); + Compile.compileSources( + resolveGlobs(testFileGlobs), + project.generatedCodeDir, + pathToElmBinary, + options.report + ).then( () => process.exit(0), () => process.exit(1) ); From a278b6ee563f203bd93abfdb8821ea5388bc6146 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:00:24 +0100 Subject: [PATCH 16/67] Concentrate console.log into elm-test.js and Supervisor.js --- lib/Install.js | 17 +++++++---------- lib/elm-test.js | 33 ++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/lib/Install.js b/lib/Install.js index 6cd1f93f..5669b27d 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -13,7 +13,7 @@ function install( project /*: typeof Project.Project */, pathToElmBinary /*: string */, packageName /*: string */ -) { +) /*: 'SuccessfullyInstalled' | 'AlreadyInstalled' */ { const generatedCodeDir = path.join(project.generatedCodeDir, 'install'); try { @@ -26,25 +26,20 @@ function install( rimraf.sync(generatedCodeDir); } fs.mkdirSync(generatedCodeDir, { recursive: true }); - } catch (err) { - console.error( - 'Unable to create temporary directory for elm-test install.', - err + } catch (error) { + throw new Error( + `Unable to create temporary directory for elm-test install: ${error.message}` ); - process.exit(1); } const { elmJson } = project; - // This mirrors the behavior of `elm install` passing a package that is - // already installed. Say it's already installed, then exit 0. if ( elmJson.type === 'package' ? elmJson['test-dependencies'].hasOwnProperty(packageName) : elmJson['test-dependencies'].direct.hasOwnProperty(packageName) ) { - console.log('It is already installed!'); - return; + return 'AlreadyInstalled'; } const tmpElmJson = @@ -112,6 +107,8 @@ function install( } ElmJson.write(project.rootDir, newElmJson); + + return 'SuccessfullyInstalled'; } module.exports = { diff --git a/lib/elm-test.js b/lib/elm-test.js index 474cd468..09c252a7 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -202,7 +202,7 @@ function test( process.exit(1); } } - console.log(chalk.blue('Watching for changes...')); + infoLog(report, chalk.blue('Watching for changes...')); } if (watch) { @@ -425,12 +425,17 @@ function main() { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); const project = getProject('init'); - Install.install(project, pathToElmBinary, 'elm-explorations/test'); - fs.mkdirSync(project.testsDir, { recursive: true }); - fs.copyFileSync( - path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), - path.join(project.testsDir, 'Example.elm') - ); + try { + Install.install(project, pathToElmBinary, 'elm-explorations/test'); + fs.mkdirSync(project.testsDir, { recursive: true }); + fs.copyFileSync( + path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), + path.join(project.testsDir, 'Example.elm') + ); + } catch (error) { + console.error(error.message); + throw process.exit(1); + } console.log( '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' ); @@ -457,8 +462,18 @@ function main() { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); const project = getProject('install'); - Install.install(project, pathToElmBinary, packageName); - process.exit(0); + try { + const result = Install.install(project, pathToElmBinary, packageName); + // This mirrors the behavior of `elm install` passing a package that is + // already installed. Say it's already installed, then exit 0. + if (result === 'AlreadyInstalled') { + console.log('It is already installed!'); + } + process.exit(0); + } catch (error) { + console.error(error.message); + process.exit(1); + } }); program From ed3e1e3cafc4d2a03a87ebf7b9db4d13d6d1e118 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:01:51 +0100 Subject: [PATCH 17/67] =?UTF-8?q?Don=E2=80=99t=20clear=20console=20for=20j?= =?UTF-8?q?son=20and=20junit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/elm-test.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 09c252a7..bc779a3c 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -85,10 +85,12 @@ function infoLog( } } -function clearConsole() { - process.stdout.write( - process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' - ); +function clearConsole(report /*: typeof Report.Report */) { + if (report === 'console') { + process.stdout.write( + process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' + ); + } } function diffArrays/*:: */( @@ -206,7 +208,7 @@ function test( } if (watch) { - clearConsole(); + clearConsole(report); infoLog(report, 'Running in watch mode'); // More globs to watch are added later. @@ -233,7 +235,7 @@ function test( watcher.on('all', (event, filePath) => { // TODO: Handle different events slightly differently. const eventName = eventNameMap[event] || event; - clearConsole(); + clearConsole(report); infoLog(report, '\n' + filePath + ' ' + eventName + '. Rebuilding!'); // TODO if a previous run is in progress, wait until it's done. From 3699ef7774c85222f31080d2f76ad1a097bfe4fe Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:06:40 +0100 Subject: [PATCH 18/67] Move some functions to top level --- lib/elm-test.js | 74 ++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index bc779a3c..73645e65 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -327,6 +327,42 @@ function findClosest( : findClosest(path.dirname(dir), dirToPath); } +function getProjectRootDir(subcommand /*: string */) { + const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); + if (elmJsonPath === undefined) { + const command = + subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; + console.error( + `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` + ); + throw process.exit(1); + } + return path.dirname(elmJsonPath); +} + +function getProject(subcommand /*: string */) { + try { + return Project.init(getProjectRootDir(subcommand), packageInfo.version); + } catch (error) { + console.error(error.message); + throw process.exit(1); + } +} + +function getPathToElmBinary(compiler /*: string | void */) { + const name = compiler === undefined ? 'elm' : compiler; + try { + return path.resolve(which.sync(name)); + } catch (_error) { + throw new Error( + compiler === undefined + ? `Cannot find elm executable, make sure it is installed. +(If elm is not on your path or is called something different the --compiler flag might help.)` + : `The elm executable passed to --compiler must exist and be exectuble. Got: ${compiler}` + ); + } +} + const examples = ` elm-test Run tests in the tests/ folder @@ -338,44 +374,6 @@ elm-test "src/**/*Tests.elm" function main() { process.title = 'elm-test'; - const getProjectRootDir = (subcommand /*: string */) /*: string */ => { - const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); - if (elmJsonPath === undefined) { - const command = - subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; - console.error( - `\`${command}\` requires an elm.json up the directory tree, but none could be found! To make one: elm init` - ); - throw process.exit(1); - } - return path.dirname(elmJsonPath); - }; - - const getProject = ( - subcommand /*: string */ - ) /*: typeof Project.Project */ => { - try { - return Project.init(getProjectRootDir(subcommand), packageInfo.version); - } catch (error) { - console.error(error.message); - throw process.exit(1); - } - }; - - const getPathToElmBinary = (compiler /*: string | void */) /*: string */ => { - const name = compiler === undefined ? 'elm' : compiler; - try { - return path.resolve(which.sync(name)); - } catch (_error) { - throw new Error( - compiler === undefined - ? `Cannot find elm executable, make sure it is installed. -(If elm is not on your path or is called something different the --compiler flag might help.)` - : `The elm executable passed to --compiler must exist and be exectuble. Got: ${compiler}` - ); - } - }; - program .storeOptionsAsProperties(false) .name('elm-test') From 06b5c687980578b5e8de71501cc2f17b28925a17 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:11:07 +0100 Subject: [PATCH 19/67] Move pipe filename to its own function --- lib/elm-test.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 73645e65..5367be6c 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -76,6 +76,21 @@ function getGlobsToWatch( ); } +// Incorporate the process PID into the socket name, so elm-test processes can +// be run parallel without accidentally sharing each others' sockets. +// +// See https://github.com/rtfeldman/node-test-runner/pull/231 +// Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - +// see https://github.com/rtfeldman/node-test-runner/issues/275 - because the +// alternative approach of deleting the file before creating a new one doesn't +// work on Windows. We have to let Windows clean up the named pipe. This is +// essentially a band-aid fix. The alternative is to rewrite a ton of stuff. +function getPipeFilename(runsExecuted /*: number */) /*: string */ { + return process.platform === 'win32' + ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` + : `/tmp/elm_test-${process.pid}.sock`; +} + function infoLog( report /*: typeof Report.Report */, msg /*: string */ @@ -165,20 +180,8 @@ function test( const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); - // Incorporate the process PID into the socket name, so elm-test processes can - // be run parallel without accidentally sharing each others' sockets. - // - // See https://github.com/rtfeldman/node-test-runner/pull/231 - // Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - - // see https://github.com/rtfeldman/node-test-runner/issues/275 - because the - // alternative approach of deleting the file before creating a new one doesn't - // work on Windows. We have to let Windows clean up the named pipe. This is - // essentially a band-aid fix. The alternative is to rewrite a ton of stuff. runsExecuted++; - const pipeFilename = - process.platform === 'win32' - ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` - : `/tmp/elm_test-${process.pid}.sock`; + const pipeFilename = getPipeFilename(runsExecuted); await Compile.compile( project.generatedCodeDir, From 9044e5fc5366b053f1387d769aae11f7a334f335 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:17:12 +0100 Subject: [PATCH 20/67] Fix typo --- lib/elm-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 5367be6c..7cb59459 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -142,7 +142,7 @@ function test( async function run() { try { - // Files maybe be changed, added or removed so always read from disk to + // Files may be changed, added or removed so always read from disk to // stay fresh. const project = Project.init(projectRootDir, packageInfo.version); const testFilePaths = resolveGlobs(testFileGlobs); From ce7ca745bd7402388efa249f802f42da47a2f0c5 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 21:35:47 +0100 Subject: [PATCH 21/67] Fix tests --- tests/fixtures/elm.json | 4 +++- tests/flags.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/elm.json b/tests/fixtures/elm.json index 13bd15a4..5d3b83bd 100644 --- a/tests/fixtures/elm.json +++ b/tests/fixtures/elm.json @@ -1,6 +1,8 @@ { "type": "application", - "source-directories": [], + "source-directories": [ + "src" + ], "elm-version": "0.19.1", "dependencies": { "direct": { diff --git a/tests/flags.js b/tests/flags.js index af5b90ad..b551f46f 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -11,7 +11,8 @@ const rimraf = require('rimraf'); const stripAnsi = require('strip-ansi'); const { fixturesDir, spawnOpts, dummyBinPath } = require('./util'); -const elmTestPath = path.join(__dirname, '..', 'bin', 'elm-test'); +const rootDir = path.join(__dirname, '..'); +const elmTestPath = path.join(rootDir, 'bin', 'elm-test'); const scratchDir = path.join(fixturesDir, 'scratch'); const scratchElmJsonPath = path.join(scratchDir, 'elm.json'); @@ -140,7 +141,7 @@ describe('flags', () => { }).timeout(60000); it('should fail if the current directory does not contain an elm.json', () => { - const runResult = execElmTest(['install', 'elm/regex'], scratchDir); + const runResult = execElmTest(['install', 'elm/regex'], rootDir); assert.ok(Number.isInteger(runResult.status)); assert.notStrictEqual(runResult.status, 0); }).timeout(60000); @@ -164,7 +165,6 @@ describe('flags', () => { it('should exit with success if package already installed', () => { const runResult = execElmTest(['install', 'elm-explorations/test']); - console.log(runResult); assert.strictEqual(runResult.status, 0); }).timeout(60000); }); From f96da98a87ca97c16d9cdf8bf013066e11a5d3df Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 22:03:38 +0100 Subject: [PATCH 22/67] Fix globbing --- lib/elm-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 7cb59459..9005dc61 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -22,11 +22,7 @@ void Report; // Resolve arguments that look like globs for shells that don’t support globs. function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { - const results = - fileGlobs.length > 0 - ? flatMap(fileGlobs, globify) - : globify('test?(s)/**/*.elm'); - return flatMap(results, resolveFilePath); + return flatMap(flatMap(fileGlobs, globify), resolveFilePath); } function flatMap/*:: */( @@ -37,7 +33,9 @@ function flatMap/*:: */( } function globify(globString /*: string */) /*: Array */ { - return glob.sync(globString, { + // Without `path.resolve`, `../tests` gives 0 results even if `../tests` + // exists (at least on MacOS). + return glob.sync(path.resolve(globString), { nocase: true, ignore: '**/elm-stuff/**', nodir: false, @@ -145,7 +143,9 @@ function test( // Files may be changed, added or removed so always read from disk to // stay fresh. const project = Project.init(projectRootDir, packageInfo.version); - const testFilePaths = resolveGlobs(testFileGlobs); + const testFilePaths = resolveGlobs( + testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs + ); if (testFilePaths.length === 0) { throw new Error(noFilesFoundError(project.rootDir, testFileGlobs)); From fbd0d5b3b34f70af5e96f42c177d6f3b3fa35bec Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 22:10:58 +0100 Subject: [PATCH 23/67] Fix `elm-test tests src` --- lib/elm-test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 9005dc61..6a7e79e0 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -499,9 +499,10 @@ function main() { }); program - // Hack: This command is named `tests` so that `elm-test tests` means the same - // as `elm-test tests/` (due defaulting to `tests/` if no arguments given). - .command('tests [globs...]', { hidden: true, isDefault: true }) + // Hack: This command has a name that isn’t likely to exist as a directory. + // If the command where called for example “tests” then `elm-test tests src` + // would only run tests in `src/`, not `tests/`. + .command('__elmTestCommand__ [globs...]', { hidden: true, isDefault: true }) .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); From 22392ac434806823931d3aac6e32288f5784f754 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 22:42:14 +0100 Subject: [PATCH 24/67] Optimize watching --- lib/elm-test.js | 58 ++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 6a7e79e0..399a33a2 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -137,15 +137,29 @@ function test( let watchedGlobs /*: Array */ = []; let currentRun; let runsExecuted = 0; + let testFilePaths = []; - async function run() { + async function run(event /*: 'init' | 'added' | 'changed' | 'removed' */) { try { - // Files may be changed, added or removed so always read from disk to - // stay fresh. + // Files may be changed, added or removed so always re-create project info + // from disk to stay fresh. const project = Project.init(projectRootDir, packageInfo.version); - const testFilePaths = resolveGlobs( - testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs - ); + + // Resolving globs is usually pretty fast. When running `elm-test` without + // arguments it takes around 20-40 ms. Running `elm-test` with 100 files + // as arguments it takes more than 100 ms (we still need to look for globs + // in all arguments). Still pretty fast, but it’s super easy to avoid + // recalculating them: Only when files are added or removed we need to + // re-calculate what files match the globs. In other words, if only a file + // has changed there’s nothing to do. + // Actually, all operations down to `Generate.generateElmJson(project)` + // could potentially be avoided depending on what changed. But all of them + // are super fast (often less than 1 ms) so it’s not worth bothering. + if (event !== 'changed') { + testFilePaths = resolveGlobs( + testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs + ); + } if (testFilePaths.length === 0) { throw new Error(noFilesFoundError(project.rootDir, testFileGlobs)); @@ -225,27 +239,27 @@ function test( cwd: projectRootDir, }); - currentRun = run(); + currentRun = run('init'); - const eventNameMap = { - add: 'added', - addDir: 'added', - change: 'changed', - unlink: 'removed', - unlinkDir: 'removed', - }; - - watcher.on('all', (event, filePath) => { - // TODO: Handle different events slightly differently. - const eventName = eventNameMap[event] || event; + const rerun = (event) => (filePath) => { clearConsole(report); - infoLog(report, '\n' + filePath + ' ' + eventName + '. Rebuilding!'); + infoLog(report, `${filePath} ${event}. Rebuilding!`); // TODO if a previous run is in progress, wait until it's done. - currentRun = currentRun.then(run); - }); + currentRun = currentRun.then(() => run(event)); + }; + + // There are events for adding and removing directories as well, but that + // never affects tests (adding/removing a directory with files in it + // produces events for both the directory and all the files). Besides, our + // watched globs only match elm.json and .elm files so the directory events + // don’t even trigger. + watcher.on('add', rerun('added')); + watcher.on('change', rerun('changed')); + watcher.on('unlink', rerun('removed')); + watcher.on('error', (error) => console.error('Watcher error:', error)); } else { - run(); + run('init'); } } From aa22a6b105921b14e344a2a0aeb79c22d3aa6820 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 23:21:35 +0100 Subject: [PATCH 25/67] Queue runs --- lib/elm-test.js | 75 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 399a33a2..803d766d 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -116,6 +116,25 @@ function diffArrays/*:: */( }; } +const Queue /*: Array<{ + event: 'added' | 'changed' | 'removed', + filePath: string, +}> */ = []; +void Queue; + +function watcherEventMessage(queue /*: typeof Queue */) /*: string */ { + const suffix = '. Rebuilding!'; + + const filePaths = Array.from(new Set(queue.map(({ filePath }) => filePath))); + if (filePaths.length === 1) { + const { event, filePath } = queue[0]; + return `${filePath} ${event}${suffix}`; + } + + const events = Array.from(new Set(queue.map(({ event }) => event))).sort(); + return `${filePaths.length} files ${events.join('/')}${suffix}`; +} + function test( projectRootDir /*: string */, pathToElmBinary /*: string */, @@ -133,13 +152,16 @@ function test( fuzz: number, } */ ) { - let watcher; + let watcher = undefined; let watchedGlobs /*: Array */ = []; - let currentRun; - let runsExecuted = 0; - let testFilePaths = []; - - async function run(event /*: 'init' | 'added' | 'changed' | 'removed' */) { + let testFilePaths /*: Array */ = []; + let runsExecuted /*: number */ = 0; + let currentRun /*: { + promise: Promise, + queue: typeof Queue, + } | void */ = undefined; + + async function run(events /*: typeof Queue */) { try { // Files may be changed, added or removed so always re-create project info // from disk to stay fresh. @@ -150,12 +172,14 @@ function test( // as arguments it takes more than 100 ms (we still need to look for globs // in all arguments). Still pretty fast, but it’s super easy to avoid // recalculating them: Only when files are added or removed we need to - // re-calculate what files match the globs. In other words, if only a file - // has changed there’s nothing to do. + // re-calculate what files match the globs. In other words, if only files + // have changed there’s nothing to do. // Actually, all operations down to `Generate.generateElmJson(project)` // could potentially be avoided depending on what changed. But all of them // are super fast (often less than 1 ms) so it’s not worth bothering. - if (event !== 'changed') { + const onlyChanged = + events.length > 0 && events.every(({ event }) => event === 'changed'); + if (!onlyChanged) { testFilePaths = resolveGlobs( testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs ); @@ -239,14 +263,31 @@ function test( cwd: projectRootDir, }); - currentRun = run('init'); + const onRunFinish = () => { + if (currentRun === undefined) { + return; + } + if (currentRun.queue.length > 0) { + clearConsole(report); + infoLog(report, watcherEventMessage(currentRun.queue)); + currentRun = { + promise: run(currentRun.queue).then(onRunFinish), + queue: [], + }; + } else { + currentRun = undefined; + } + }; const rerun = (event) => (filePath) => { - clearConsole(report); - infoLog(report, `${filePath} ${event}. Rebuilding!`); - - // TODO if a previous run is in progress, wait until it's done. - currentRun = currentRun.then(() => run(event)); + const queue = [{ event, filePath }]; + if (currentRun === undefined) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + currentRun = { promise: run(queue).then(onRunFinish), queue: [] }; + } else { + currentRun.queue.push(...queue); + } }; // There are events for adding and removing directories as well, but that @@ -258,8 +299,10 @@ function test( watcher.on('change', rerun('changed')); watcher.on('unlink', rerun('removed')); watcher.on('error', (error) => console.error('Watcher error:', error)); + + currentRun = { promise: run([]).then(onRunFinish), queue: [] }; } else { - run('init'); + run([]); } } From 4467759d0d86539e9ff3631a62611fcc610fe742 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 19 Nov 2020 23:51:11 +0100 Subject: [PATCH 26/67] Batch events that happen roughly at the same time --- lib/elm-test.js | 61 ++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 803d766d..1a9634de 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -116,6 +116,12 @@ function diffArrays/*:: */( }; } +function delay(ms /*: number */) /*: Promise */ { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + const Queue /*: Array<{ event: 'added' | 'changed' | 'removed', filePath: string, @@ -156,13 +162,30 @@ function test( let watchedGlobs /*: Array */ = []; let testFilePaths /*: Array */ = []; let runsExecuted /*: number */ = 0; - let currentRun /*: { - promise: Promise, - queue: typeof Queue, - } | void */ = undefined; + let currentRun /*: Promise | void */ = undefined; + let queue /*: typeof Queue */ = []; - async function run(events /*: typeof Queue */) { + async function run() { try { + // Don’t delay the first run (that’s the only time the queue is empty). + // Otherwise, wait for a little bit to batch events that happened roughly + // at the same time. The chokidar docs also mention that by default, the + // add event will fire when a file first appears on disk, before the + // entire file has been written. They have a `awaitWriteFinish` option for + // that, with a `stabilityThreshold` that is an amount of time in + // milliseconds for a file size to remain constant before emitting its + // event. Elm files aren’t that huge so it should be fine to just wait a + // fixed amount of time here instead. + if (queue.length > 0) { + await delay(200); + } + + // Re-print the message in case the queue has become longer while waiting. + if (queue.length > 0) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + } + // Files may be changed, added or removed so always re-create project info // from disk to stay fresh. const project = Project.init(projectRootDir, packageInfo.version); @@ -178,7 +201,8 @@ function test( // could potentially be avoided depending on what changed. But all of them // are super fast (often less than 1 ms) so it’s not worth bothering. const onlyChanged = - events.length > 0 && events.every(({ event }) => event === 'changed'); + queue.length > 0 && queue.every(({ event }) => event === 'changed'); + queue = []; if (!onlyChanged) { testFilePaths = resolveGlobs( testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs @@ -255,38 +279,27 @@ function test( // More globs to watch are added later. const initialGlobsToWatch = [ElmJson.getPath(projectRootDir)]; watcher = chokidar.watch(initialGlobsToWatch, { - awaitWriteFinish: { - stabilityThreshold: 500, - }, ignoreInitial: true, ignored: /(\/|^)elm-stuff(\/|$)/, cwd: projectRootDir, }); const onRunFinish = () => { - if (currentRun === undefined) { - return; - } - if (currentRun.queue.length > 0) { + if (queue.length > 0) { clearConsole(report); - infoLog(report, watcherEventMessage(currentRun.queue)); - currentRun = { - promise: run(currentRun.queue).then(onRunFinish), - queue: [], - }; + infoLog(report, watcherEventMessage(queue)); + currentRun = run().then(onRunFinish); } else { currentRun = undefined; } }; const rerun = (event) => (filePath) => { - const queue = [{ event, filePath }]; + queue.push({ event, filePath }); if (currentRun === undefined) { clearConsole(report); infoLog(report, watcherEventMessage(queue)); - currentRun = { promise: run(queue).then(onRunFinish), queue: [] }; - } else { - currentRun.queue.push(...queue); + currentRun = run().then(onRunFinish); } }; @@ -300,9 +313,9 @@ function test( watcher.on('unlink', rerun('removed')); watcher.on('error', (error) => console.error('Watcher error:', error)); - currentRun = { promise: run([]).then(onRunFinish), queue: [] }; + currentRun = run().then(onRunFinish); } else { - run([]); + run(); } } From e79bea88bcaabed525e134c4b2cff6523834ab5a Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 00:14:12 +0100 Subject: [PATCH 27/67] Always watch tests/ --- lib/Project.js | 7 ++++++- lib/elm-test.js | 34 +++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/Project.js b/lib/Project.js index d25842e4..188d2f79 100644 --- a/lib/Project.js +++ b/lib/Project.js @@ -20,11 +20,15 @@ const Project /*: { elmJson: ElmJson.ElmJson, }; +function getTestsDir(rootDir /*: string */) /*: string */ { + return path.join(rootDir, 'tests'); +} + function init( rootDir /*: string */, version /*: string */ ) /*: typeof Project */ { - const testsDir = path.join(rootDir, 'tests'); + const testsDir = getTestsDir(rootDir); // The tests/ directory is not required. You can also co-locate tests with // their source files. @@ -59,5 +63,6 @@ function init( module.exports = { Project, + getTestsDir, init, }; diff --git a/lib/elm-test.js b/lib/elm-test.js index 1a9634de..3ddc8bf2 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -276,14 +276,6 @@ function test( clearConsole(report); infoLog(report, 'Running in watch mode'); - // More globs to watch are added later. - const initialGlobsToWatch = [ElmJson.getPath(projectRootDir)]; - watcher = chokidar.watch(initialGlobsToWatch, { - ignoreInitial: true, - ignored: /(\/|^)elm-stuff(\/|$)/, - cwd: projectRootDir, - }); - const onRunFinish = () => { if (queue.length > 0) { clearConsole(report); @@ -303,14 +295,30 @@ function test( } }; - // There are events for adding and removing directories as well, but that - // never affects tests (adding/removing a directory with files in it - // produces events for both the directory and all the files). Besides, our - // watched globs only match elm.json and .elm files so the directory events - // don’t even trigger. + // The globs to watch change over time and are added and removed as needed + // in `run`. We should always watch `elm.json` and `tests/`, though (see the + // 'addDir' event below). + const initialGlobsToWatch = [ + ElmJson.getPath(projectRootDir), + Project.getTestsDir(projectRootDir), + ]; + watcher = chokidar.watch(initialGlobsToWatch, { + ignoreInitial: true, + ignored: /(\/|^)elm-stuff(\/|$)/, + cwd: projectRootDir, + }); + watcher.on('add', rerun('added')); watcher.on('change', rerun('changed')); watcher.on('unlink', rerun('removed')); + + // The only time this event fires is when the `tests/` directory is added + // (all other glob patterns only match files, not directories). + // That’s useful if starting the watcher before `tests/` exists. + // There’s no need to listen for 'unlinkDir' – that makes no difference. + watcher.on('addDir', rerun('added')); + + // It’s unclear when this event occurrs. watcher.on('error', (error) => console.error('Watcher error:', error)); currentRun = run().then(onRunFinish); From 7a2b5f66c169ead12edfaae74e30d84a7c711c3e Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 00:32:12 +0100 Subject: [PATCH 28/67] Make init and install commands code less noisy --- lib/elm-test.js | 122 ++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 3ddc8bf2..53e71d57 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -444,6 +444,33 @@ function getPathToElmBinary(compiler /*: string | void */) { } } +// Unfortunately commander is very permissive about extra arguments. Therefore, +// we manually check for excessive arguments. +// See: https://github.com/tj/commander.js/issues/1268 +function handleTooManyArgs(action) { + return (...args) => { + if (args.length < 2) { + action(...args); + } else { + // The arguments to Commander actions are: + // expectedCliArg1, expectedCliArg2, expectedCliArgN, Cmd, restCliArgs + const rest = args[args.length - 1]; + if (rest.length > 0) { + const expected = args.length - 2; + const s = expected === 1 ? '' : 's'; + console.error( + `Expected ${expected} argument${s}, but got ${ + expected + rest.length + }.` + ); + process.exit(1); + } else { + action(...args); + } + } + }; +} + const examples = ` elm-test Run tests in the tests/ folder @@ -494,68 +521,53 @@ function main() { program .command('init') .description('Create example tests') - .action((cmd) => { - if (cmd.args.length > 0) { - console.error( - `error: init takes no arguments, but got ${ - cmd.args.length - }: ${cmd.args.join(' ')}` - ); - throw process.exit(1); - } - const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); - const project = getProject('init'); - try { - Install.install(project, pathToElmBinary, 'elm-explorations/test'); - fs.mkdirSync(project.testsDir, { recursive: true }); - fs.copyFileSync( - path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), - path.join(project.testsDir, 'Example.elm') + .action( + handleTooManyArgs(() => { + const options = program.opts(); + const pathToElmBinary = getPathToElmBinary(options.compiler); + const project = getProject('init'); + try { + Install.install(project, pathToElmBinary, 'elm-explorations/test'); + fs.mkdirSync(project.testsDir, { recursive: true }); + fs.copyFileSync( + path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'), + path.join(project.testsDir, 'Example.elm') + ); + } catch (error) { + console.error(error.message); + throw process.exit(1); + } + console.log( + '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' ); - } catch (error) { - console.error(error.message); - throw process.exit(1); - } - console.log( - '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest' - ); - process.exit(0); - }); + process.exit(0); + }) + ); program .command('install ') .description( 'Like `elm install package`, except it installs to "test-dependencies" in your elm.json' ) - .action((packageName, cmd) => { - if (cmd.args.length > 1) { - // Unfortunately commander is very permissive about extra arguments. Therefore, - // we manually check for excessive arguments. - // See: https://github.com/tj/commander.js/issues/1268 - console.error( - `error: install takes one single argument, but got ${ - cmd.args.length - }: ${cmd.args.join(' ')}` - ); - throw process.exit(1); - } - const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); - const project = getProject('install'); - try { - const result = Install.install(project, pathToElmBinary, packageName); - // This mirrors the behavior of `elm install` passing a package that is - // already installed. Say it's already installed, then exit 0. - if (result === 'AlreadyInstalled') { - console.log('It is already installed!'); + .action( + handleTooManyArgs((packageName) => { + const options = program.opts(); + const pathToElmBinary = getPathToElmBinary(options.compiler); + const project = getProject('install'); + try { + const result = Install.install(project, pathToElmBinary, packageName); + // This mirrors the behavior of `elm install` passing a package that is + // already installed. Say it's already installed, then exit 0. + if (result === 'AlreadyInstalled') { + console.log('It is already installed!'); + } + process.exit(0); + } catch (error) { + console.error(error.message); + process.exit(1); } - process.exit(0); - } catch (error) { - console.error(error.message); - process.exit(1); - } - }); + }) + ); program .command('make [globs...]') @@ -584,7 +596,7 @@ function main() { .action((testFileGlobs) => { const options = program.opts(); const pathToElmBinary = getPathToElmBinary(options.compiler); - const projectRootDir = getProjectRootDir('install'); + const projectRootDir = getProjectRootDir('tests'); const processes = Math.max(1, os.cpus().length); test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); }); From f78c221ee86c41642e812a7f96dccec89b6d2827 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 00:47:52 +0100 Subject: [PATCH 29/67] Remove last `var` in Solve.js --- lib/Solve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Solve.js b/lib/Solve.js index 7654ba37..f0d72d3d 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -49,7 +49,7 @@ function getDependenciesCached( } function getDependencies(elmJsonPath /*: string */) /*: string */ { - var result = spawn.sync( + const result = spawn.sync( 'elm-json', [ 'solve', From 656112df190f87ed7a6200581a74b016739674d5 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 08:16:58 +0100 Subject: [PATCH 30/67] Use object shorthand --- lib/Install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Install.js b/lib/Install.js index 5669b27d..d199646a 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -112,5 +112,5 @@ function install( } module.exports = { - install: install, + install, }; From 3df45e96626483597e19cc0df645adda9e9a5cc7 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 08:17:17 +0100 Subject: [PATCH 31/67] Rename Runner.js to FindTests.js --- lib/{Runner.js => FindTests.js} | 2 +- lib/elm-test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/{Runner.js => FindTests.js} (99%) diff --git a/lib/Runner.js b/lib/FindTests.js similarity index 99% rename from lib/Runner.js rename to lib/FindTests.js index 3a2c8d25..cfd805ba 100644 --- a/lib/Runner.js +++ b/lib/FindTests.js @@ -119,5 +119,5 @@ Make sure that all parts start with an uppercase letter and don’t contain any } module.exports = { - findTests: findTests, + findTests, }; diff --git a/lib/elm-test.js b/lib/elm-test.js index 53e71d57..b3aef12c 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -11,11 +11,11 @@ const which = require('which'); const packageInfo = require('../package.json'); const Compile = require('./Compile.js'); const ElmJson = require('./ElmJson.js'); +const FindTests = require('./FindTests.js'); const Generate = require('./Generate.js'); const Install = require('./Install.js'); const Project = require('./Project.js'); const Report = require('./Report.js'); -const Runner = require('./Runner.js'); const Supervisor = require('./Supervisor.js'); void Report; @@ -223,7 +223,7 @@ function test( Generate.generateElmJson(project); - const testModules = await Runner.findTests( + const testModules = await FindTests.findTests( testFilePaths, project.testsSourceDirs, project.elmJson.type === 'package' From 0212108cb55717e554193514e82eb75b07f5d533 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 08:23:25 +0100 Subject: [PATCH 32/67] Move more code related to finding tests to FindTests.js --- lib/FindTests.js | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/elm-test.js | 98 +++--------------------------------------------- 2 files changed, 104 insertions(+), 92 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index cfd805ba..86f68495 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -1,8 +1,67 @@ // @flow const gracefulFs = require('graceful-fs'); +const fs = require('fs'); +const glob = require('glob'); const path = require('path'); const Parser = require('./Parser'); +const Project = require('./Project.js'); + +void Project; + +function flatMap/*:: */( + array /*: Array */, + f /*: (T) => Array */ +) /*: Array */ { + return array.reduce((result, item) => result.concat(f(item)), []); +} + +// Resolve arguments that look like globs for shells that don’t support globs. +function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { + return flatMap(flatMap(fileGlobs, globify), resolveFilePath); +} + +function globify(globString /*: string */) /*: Array */ { + // Without `path.resolve`, `../tests` gives 0 results even if `../tests` + // exists (at least on MacOS). + return glob.sync(path.resolve(globString), { + nocase: true, + ignore: '**/elm-stuff/**', + nodir: false, + absolute: true, + }); +} + +// Recursively search directories for *.elm files, excluding elm-stuff/ +function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { + const candidates = !fs.existsSync(elmFilePathOrDir) + ? [] + : fs.lstatSync(elmFilePathOrDir).isDirectory() + ? flatMap( + glob.sync('/**/*.elm', { + root: elmFilePathOrDir, + nocase: true, + ignore: '/**/elm-stuff/**', + nodir: true, + }), + resolveFilePath + ) + : [path.resolve(elmFilePathOrDir)]; + + // Exclude everything having anything to do with elm-stuff + return candidates.filter( + (candidate) => !candidate.split(path.sep).includes('elm-stuff') + ); +} + +function getGlobsToWatch( + project /*: typeof Project.Project */ +) /*: Array */ { + return project.testsSourceDirs.map((sourceDirectory) => + // TODO: Test this on Windows. + path.posix.join(sourceDirectory, '**', '*.elm') + ); +} function findTests( testFilePaths /*: Array */, @@ -118,6 +177,45 @@ Make sure that all parts start with an uppercase letter and don’t contain any `.trim(); } +function noFilesFoundError( + projectRootDir /*: string */, + testFileGlobs /*: Array */ +) /*: string */ { + return testFileGlobs.length === 0 + ? ` +${noFilesFoundInTestsDir(projectRootDir)} + +To generate some initial tests to get things going: elm-test init + +Alternatively, if your project has tests in a different directory, +try calling elm-test with a glob such as: elm-test "src/**/*Tests.elm" + `.trim() + : ` +No files found matching: + +${testFileGlobs.join('\n')} + +Are the above patterns correct? Maybe try running elm-test with no arguments? + `.trim(); +} + +function noFilesFoundInTestsDir(projectRootDir) { + const testsDir = path.join(projectRootDir, 'tests'); + try { + const stats = fs.statSync(testsDir); + return stats.isDirectory() + ? 'No .elm files found in the tests/ directory.' + : `Expected a directory but found something else at: ${testsDir}\nCheck it out! Could you remove it?`; + } catch (error) { + return error.code === 'ENOENT' + ? 'The tests/ directory does not exist.' + : `Failed to read the tests/ directory: ${error.message}`; + } +} + module.exports = { findTests, + getGlobsToWatch, + noFilesFoundError, + resolveGlobs, }; diff --git a/lib/elm-test.js b/lib/elm-test.js index b3aef12c..c58a0bae 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -4,7 +4,6 @@ const chalk = require('chalk'); const chokidar = require('chokidar'); const { program } = require('commander'); const fs = require('fs'); -const glob = require('glob'); const os = require('os'); const path = require('path'); const which = require('which'); @@ -20,60 +19,6 @@ const Supervisor = require('./Supervisor.js'); void Report; -// Resolve arguments that look like globs for shells that don’t support globs. -function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { - return flatMap(flatMap(fileGlobs, globify), resolveFilePath); -} - -function flatMap/*:: */( - array /*: Array */, - f /*: (T) => Array */ -) /*: Array */ { - return array.reduce((result, item) => result.concat(f(item)), []); -} - -function globify(globString /*: string */) /*: Array */ { - // Without `path.resolve`, `../tests` gives 0 results even if `../tests` - // exists (at least on MacOS). - return glob.sync(path.resolve(globString), { - nocase: true, - ignore: '**/elm-stuff/**', - nodir: false, - absolute: true, - }); -} - -// Recursively search directories for *.elm files, excluding elm-stuff/ -function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { - const candidates = !fs.existsSync(elmFilePathOrDir) - ? [] - : fs.lstatSync(elmFilePathOrDir).isDirectory() - ? flatMap( - glob.sync('/**/*.elm', { - root: elmFilePathOrDir, - nocase: true, - ignore: '/**/elm-stuff/**', - nodir: true, - }), - resolveFilePath - ) - : [path.resolve(elmFilePathOrDir)]; - - // Exclude everything having anything to do with elm-stuff - return candidates.filter( - (candidate) => !candidate.split(path.sep).includes('elm-stuff') - ); -} - -function getGlobsToWatch( - project /*: typeof Project.Project */ -) /*: Array */ { - return project.testsSourceDirs.map((sourceDirectory) => - // TODO: Test this on Windows. - path.posix.join(sourceDirectory, '**', '*.elm') - ); -} - // Incorporate the process PID into the socket name, so elm-test processes can // be run parallel without accidentally sharing each others' sockets. // @@ -204,17 +149,19 @@ function test( queue.length > 0 && queue.every(({ event }) => event === 'changed'); queue = []; if (!onlyChanged) { - testFilePaths = resolveGlobs( + testFilePaths = FindTests.resolveGlobs( testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs ); } if (testFilePaths.length === 0) { - throw new Error(noFilesFoundError(project.rootDir, testFileGlobs)); + throw new Error( + FindTests.noFilesFoundError(project.rootDir, testFileGlobs) + ); } if (watcher !== undefined) { - const nextGlobsToWatch = getGlobsToWatch(project); + const nextGlobsToWatch = FindTests.getGlobsToWatch(project); const diff = diffArrays(watchedGlobs, nextGlobsToWatch); watchedGlobs = nextGlobsToWatch; watcher.add(diff.added); @@ -327,39 +274,6 @@ function test( } } -function noFilesFoundError(projectRootDir, testFileGlobs) { - return testFileGlobs.length === 0 - ? ` -${noFilesFoundInTestsDir(projectRootDir)} - -To generate some initial tests to get things going: elm-test init - -Alternatively, if your project has tests in a different directory, -try calling elm-test with a glob such as: elm-test "src/**/*Tests.elm" - `.trim() - : ` -No files found matching: - -${testFileGlobs.join('\n')} - -Are the above patterns correct? Maybe try running elm-test with no arguments? - `.trim(); -} - -function noFilesFoundInTestsDir(projectRootDir) { - const testsDir = path.join(projectRootDir, 'tests'); - try { - const stats = fs.statSync(testsDir); - return stats.isDirectory() - ? 'No .elm files found in the tests/ directory.' - : `Expected a directory but found something else at: ${testsDir}\nCheck it out! Could you remove it?`; - } catch (error) { - return error.code === 'ENOENT' - ? 'The tests/ directory does not exist.' - : `Failed to read the tests/ directory: ${error.message}`; - } -} - // TODO(https://github.com/rtfeldman/node-test-runner/pull/465): replace this // function with commander's custom error messages once // https://github.com/tj/commander.js/pull/1392 lands and is released. @@ -578,7 +492,7 @@ function main() { const project = getProject('make'); Generate.generateElmJson(project); Compile.compileSources( - resolveGlobs(testFileGlobs), + FindTests.resolveGlobs(testFileGlobs), project.generatedCodeDir, pathToElmBinary, options.report From a0fe808c13559cf575ce9034d0cf19d5f88a5326 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 08:27:44 +0100 Subject: [PATCH 33/67] Move code for running the tests to RunTests.js --- lib/RunTests.js | 274 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/elm-test.js | 267 ++-------------------------------------------- 2 files changed, 282 insertions(+), 259 deletions(-) create mode 100644 lib/RunTests.js diff --git a/lib/RunTests.js b/lib/RunTests.js new file mode 100644 index 00000000..6321576a --- /dev/null +++ b/lib/RunTests.js @@ -0,0 +1,274 @@ +// @flow + +const chalk = require('chalk'); +const chokidar = require('chokidar'); +const path = require('path'); +const packageInfo = require('../package.json'); +const Compile = require('./Compile.js'); +const ElmJson = require('./ElmJson.js'); +const FindTests = require('./FindTests.js'); +const Generate = require('./Generate.js'); +const Project = require('./Project.js'); +const Report = require('./Report.js'); +const Supervisor = require('./Supervisor.js'); + +void Report; + +// Incorporate the process PID into the socket name, so elm-test processes can +// be run parallel without accidentally sharing each others' sockets. +// +// See https://github.com/rtfeldman/node-test-runner/pull/231 +// Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - +// see https://github.com/rtfeldman/node-test-runner/issues/275 - because the +// alternative approach of deleting the file before creating a new one doesn't +// work on Windows. We have to let Windows clean up the named pipe. This is +// essentially a band-aid fix. The alternative is to rewrite a ton of stuff. +function getPipeFilename(runsExecuted /*: number */) /*: string */ { + return process.platform === 'win32' + ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` + : `/tmp/elm_test-${process.pid}.sock`; +} + +function infoLog( + report /*: typeof Report.Report */, + msg /*: string */ +) /*: void */ { + if (report === 'console') { + console.log(msg); + } +} + +function clearConsole(report /*: typeof Report.Report */) { + if (report === 'console') { + process.stdout.write( + process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' + ); + } +} + +function diffArrays/*:: */( + from /*: Array */, + to /*: Array */ +) /*: { added: Array, removed: Array } */ { + return { + added: to.filter((item) => !from.includes(item)), + removed: from.filter((item) => !to.includes(item)), + }; +} + +function delay(ms /*: number */) /*: Promise */ { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +const Queue /*: Array<{ + event: 'added' | 'changed' | 'removed', + filePath: string, +}> */ = []; +void Queue; + +function watcherEventMessage(queue /*: typeof Queue */) /*: string */ { + const suffix = '. Rebuilding!'; + + const filePaths = Array.from(new Set(queue.map(({ filePath }) => filePath))); + if (filePaths.length === 1) { + const { event, filePath } = queue[0]; + return `${filePath} ${event}${suffix}`; + } + + const events = Array.from(new Set(queue.map(({ event }) => event))).sort(); + return `${filePaths.length} files ${events.join('/')}${suffix}`; +} + +function runTests( + projectRootDir /*: string */, + pathToElmBinary /*: string */, + testFileGlobs /*: Array */, + processes /*: number */, + { + watch, + report, + seed, + fuzz, + } /*: { + watch: boolean, + report: typeof Report.Report, + seed: number, + fuzz: number, + } */ +) { + let watcher = undefined; + let watchedGlobs /*: Array */ = []; + let testFilePaths /*: Array */ = []; + let runsExecuted /*: number */ = 0; + let currentRun /*: Promise | void */ = undefined; + let queue /*: typeof Queue */ = []; + + async function run() { + try { + // Don’t delay the first run (that’s the only time the queue is empty). + // Otherwise, wait for a little bit to batch events that happened roughly + // at the same time. The chokidar docs also mention that by default, the + // add event will fire when a file first appears on disk, before the + // entire file has been written. They have a `awaitWriteFinish` option for + // that, with a `stabilityThreshold` that is an amount of time in + // milliseconds for a file size to remain constant before emitting its + // event. Elm files aren’t that huge so it should be fine to just wait a + // fixed amount of time here instead. + if (queue.length > 0) { + await delay(200); + } + + // Re-print the message in case the queue has become longer while waiting. + if (queue.length > 0) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + } + + // Files may be changed, added or removed so always re-create project info + // from disk to stay fresh. + const project = Project.init(projectRootDir, packageInfo.version); + + // Resolving globs is usually pretty fast. When running `elm-test` without + // arguments it takes around 20-40 ms. Running `elm-test` with 100 files + // as arguments it takes more than 100 ms (we still need to look for globs + // in all arguments). Still pretty fast, but it’s super easy to avoid + // recalculating them: Only when files are added or removed we need to + // re-calculate what files match the globs. In other words, if only files + // have changed there’s nothing to do. + // Actually, all operations down to `Generate.generateElmJson(project)` + // could potentially be avoided depending on what changed. But all of them + // are super fast (often less than 1 ms) so it’s not worth bothering. + const onlyChanged = + queue.length > 0 && queue.every(({ event }) => event === 'changed'); + queue = []; + if (!onlyChanged) { + testFilePaths = FindTests.resolveGlobs( + testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs + ); + } + + if (testFilePaths.length === 0) { + throw new Error( + FindTests.noFilesFoundError(project.rootDir, testFileGlobs) + ); + } + + if (watcher !== undefined) { + const nextGlobsToWatch = FindTests.getGlobsToWatch(project); + const diff = diffArrays(watchedGlobs, nextGlobsToWatch); + watchedGlobs = nextGlobsToWatch; + watcher.add(diff.added); + watcher.unwatch(diff.removed); + } + + Generate.generateElmJson(project); + + const testModules = await FindTests.findTests( + testFilePaths, + project.testsSourceDirs, + project.elmJson.type === 'package' + ); + + const mainFile = Generate.generateMainModule( + fuzz, + seed, + report, + testFileGlobs, + testFilePaths, + testModules, + project.generatedCodeDir, + processes + ); + + const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); + + runsExecuted++; + const pipeFilename = getPipeFilename(runsExecuted); + + await Compile.compile( + project.generatedCodeDir, + mainFile, + dest, + pathToElmBinary, + report + ); + + Generate.prepareCompiledJsFile(pipeFilename, dest); + + await Supervisor.run( + packageInfo.version, + pipeFilename, + report, + processes, + dest, + watch + ); + } catch (err) { + console.error(err.message); + if (!watch) { + process.exit(1); + } + } + infoLog(report, chalk.blue('Watching for changes...')); + } + + if (watch) { + clearConsole(report); + infoLog(report, 'Running in watch mode'); + + const onRunFinish = () => { + if (queue.length > 0) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + currentRun = run().then(onRunFinish); + } else { + currentRun = undefined; + } + }; + + const rerun = (event) => (filePath) => { + queue.push({ event, filePath }); + if (currentRun === undefined) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + currentRun = run().then(onRunFinish); + } + }; + + // The globs to watch change over time and are added and removed as needed + // in `run`. We should always watch `elm.json` and `tests/`, though (see the + // 'addDir' event below). + const initialGlobsToWatch = [ + ElmJson.getPath(projectRootDir), + Project.getTestsDir(projectRootDir), + ]; + watcher = chokidar.watch(initialGlobsToWatch, { + ignoreInitial: true, + ignored: /(\/|^)elm-stuff(\/|$)/, + cwd: projectRootDir, + }); + + watcher.on('add', rerun('added')); + watcher.on('change', rerun('changed')); + watcher.on('unlink', rerun('removed')); + + // The only time this event fires is when the `tests/` directory is added + // (all other glob patterns only match files, not directories). + // That’s useful if starting the watcher before `tests/` exists. + // There’s no need to listen for 'unlinkDir' – that makes no difference. + watcher.on('addDir', rerun('added')); + + // It’s unclear when this event occurrs. + watcher.on('error', (error) => console.error('Watcher error:', error)); + + currentRun = run().then(onRunFinish); + } else { + run(); + } +} + +module.exports = { + runTests, +}; diff --git a/lib/elm-test.js b/lib/elm-test.js index c58a0bae..52510b58 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -1,7 +1,5 @@ // @flow -const chalk = require('chalk'); -const chokidar = require('chokidar'); const { program } = require('commander'); const fs = require('fs'); const os = require('os'); @@ -15,265 +13,10 @@ const Generate = require('./Generate.js'); const Install = require('./Install.js'); const Project = require('./Project.js'); const Report = require('./Report.js'); -const Supervisor = require('./Supervisor.js'); +const RunTests = require('./RunTests.js'); void Report; -// Incorporate the process PID into the socket name, so elm-test processes can -// be run parallel without accidentally sharing each others' sockets. -// -// See https://github.com/rtfeldman/node-test-runner/pull/231 -// Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - -// see https://github.com/rtfeldman/node-test-runner/issues/275 - because the -// alternative approach of deleting the file before creating a new one doesn't -// work on Windows. We have to let Windows clean up the named pipe. This is -// essentially a band-aid fix. The alternative is to rewrite a ton of stuff. -function getPipeFilename(runsExecuted /*: number */) /*: string */ { - return process.platform === 'win32' - ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` - : `/tmp/elm_test-${process.pid}.sock`; -} - -function infoLog( - report /*: typeof Report.Report */, - msg /*: string */ -) /*: void */ { - if (report === 'console') { - console.log(msg); - } -} - -function clearConsole(report /*: typeof Report.Report */) { - if (report === 'console') { - process.stdout.write( - process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' - ); - } -} - -function diffArrays/*:: */( - from /*: Array */, - to /*: Array */ -) /*: { added: Array, removed: Array } */ { - return { - added: to.filter((item) => !from.includes(item)), - removed: from.filter((item) => !to.includes(item)), - }; -} - -function delay(ms /*: number */) /*: Promise */ { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -const Queue /*: Array<{ - event: 'added' | 'changed' | 'removed', - filePath: string, -}> */ = []; -void Queue; - -function watcherEventMessage(queue /*: typeof Queue */) /*: string */ { - const suffix = '. Rebuilding!'; - - const filePaths = Array.from(new Set(queue.map(({ filePath }) => filePath))); - if (filePaths.length === 1) { - const { event, filePath } = queue[0]; - return `${filePath} ${event}${suffix}`; - } - - const events = Array.from(new Set(queue.map(({ event }) => event))).sort(); - return `${filePaths.length} files ${events.join('/')}${suffix}`; -} - -function test( - projectRootDir /*: string */, - pathToElmBinary /*: string */, - testFileGlobs /*: Array */, - processes /*: number */, - { - watch, - report, - seed, - fuzz, - } /*: { - watch: boolean, - report: typeof Report.Report, - seed: number, - fuzz: number, - } */ -) { - let watcher = undefined; - let watchedGlobs /*: Array */ = []; - let testFilePaths /*: Array */ = []; - let runsExecuted /*: number */ = 0; - let currentRun /*: Promise | void */ = undefined; - let queue /*: typeof Queue */ = []; - - async function run() { - try { - // Don’t delay the first run (that’s the only time the queue is empty). - // Otherwise, wait for a little bit to batch events that happened roughly - // at the same time. The chokidar docs also mention that by default, the - // add event will fire when a file first appears on disk, before the - // entire file has been written. They have a `awaitWriteFinish` option for - // that, with a `stabilityThreshold` that is an amount of time in - // milliseconds for a file size to remain constant before emitting its - // event. Elm files aren’t that huge so it should be fine to just wait a - // fixed amount of time here instead. - if (queue.length > 0) { - await delay(200); - } - - // Re-print the message in case the queue has become longer while waiting. - if (queue.length > 0) { - clearConsole(report); - infoLog(report, watcherEventMessage(queue)); - } - - // Files may be changed, added or removed so always re-create project info - // from disk to stay fresh. - const project = Project.init(projectRootDir, packageInfo.version); - - // Resolving globs is usually pretty fast. When running `elm-test` without - // arguments it takes around 20-40 ms. Running `elm-test` with 100 files - // as arguments it takes more than 100 ms (we still need to look for globs - // in all arguments). Still pretty fast, but it’s super easy to avoid - // recalculating them: Only when files are added or removed we need to - // re-calculate what files match the globs. In other words, if only files - // have changed there’s nothing to do. - // Actually, all operations down to `Generate.generateElmJson(project)` - // could potentially be avoided depending on what changed. But all of them - // are super fast (often less than 1 ms) so it’s not worth bothering. - const onlyChanged = - queue.length > 0 && queue.every(({ event }) => event === 'changed'); - queue = []; - if (!onlyChanged) { - testFilePaths = FindTests.resolveGlobs( - testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs - ); - } - - if (testFilePaths.length === 0) { - throw new Error( - FindTests.noFilesFoundError(project.rootDir, testFileGlobs) - ); - } - - if (watcher !== undefined) { - const nextGlobsToWatch = FindTests.getGlobsToWatch(project); - const diff = diffArrays(watchedGlobs, nextGlobsToWatch); - watchedGlobs = nextGlobsToWatch; - watcher.add(diff.added); - watcher.unwatch(diff.removed); - } - - Generate.generateElmJson(project); - - const testModules = await FindTests.findTests( - testFilePaths, - project.testsSourceDirs, - project.elmJson.type === 'package' - ); - - const mainFile = Generate.generateMainModule( - fuzz, - seed, - report, - testFileGlobs, - testFilePaths, - testModules, - project.generatedCodeDir, - processes - ); - - const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); - - runsExecuted++; - const pipeFilename = getPipeFilename(runsExecuted); - - await Compile.compile( - project.generatedCodeDir, - mainFile, - dest, - pathToElmBinary, - report - ); - - Generate.prepareCompiledJsFile(pipeFilename, dest); - - await Supervisor.run( - packageInfo.version, - pipeFilename, - report, - processes, - dest, - watch - ); - } catch (err) { - console.error(err.message); - if (!watch) { - process.exit(1); - } - } - infoLog(report, chalk.blue('Watching for changes...')); - } - - if (watch) { - clearConsole(report); - infoLog(report, 'Running in watch mode'); - - const onRunFinish = () => { - if (queue.length > 0) { - clearConsole(report); - infoLog(report, watcherEventMessage(queue)); - currentRun = run().then(onRunFinish); - } else { - currentRun = undefined; - } - }; - - const rerun = (event) => (filePath) => { - queue.push({ event, filePath }); - if (currentRun === undefined) { - clearConsole(report); - infoLog(report, watcherEventMessage(queue)); - currentRun = run().then(onRunFinish); - } - }; - - // The globs to watch change over time and are added and removed as needed - // in `run`. We should always watch `elm.json` and `tests/`, though (see the - // 'addDir' event below). - const initialGlobsToWatch = [ - ElmJson.getPath(projectRootDir), - Project.getTestsDir(projectRootDir), - ]; - watcher = chokidar.watch(initialGlobsToWatch, { - ignoreInitial: true, - ignored: /(\/|^)elm-stuff(\/|$)/, - cwd: projectRootDir, - }); - - watcher.on('add', rerun('added')); - watcher.on('change', rerun('changed')); - watcher.on('unlink', rerun('removed')); - - // The only time this event fires is when the `tests/` directory is added - // (all other glob patterns only match files, not directories). - // That’s useful if starting the watcher before `tests/` exists. - // There’s no need to listen for 'unlinkDir' – that makes no difference. - watcher.on('addDir', rerun('added')); - - // It’s unclear when this event occurrs. - watcher.on('error', (error) => console.error('Watcher error:', error)); - - currentRun = run().then(onRunFinish); - } else { - run(); - } -} - // TODO(https://github.com/rtfeldman/node-test-runner/pull/465): replace this // function with commander's custom error messages once // https://github.com/tj/commander.js/pull/1392 lands and is released. @@ -512,7 +255,13 @@ function main() { const pathToElmBinary = getPathToElmBinary(options.compiler); const projectRootDir = getProjectRootDir('tests'); const processes = Math.max(1, os.cpus().length); - test(projectRootDir, pathToElmBinary, testFileGlobs, processes, options); + RunTests.runTests( + projectRootDir, + pathToElmBinary, + testFileGlobs, + processes, + options + ); }); program.parse(process.argv); From 2a6c832b24ad3f63461089adcaba98b21cb13eca Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 08:30:39 +0100 Subject: [PATCH 34/67] Remove .js prefix from require --- lib/FindTests.js | 2 +- lib/Generate.js | 8 ++++---- lib/RunTests.js | 14 +++++++------- lib/elm-test.js | 16 ++++++++-------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index 86f68495..7915282c 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -5,7 +5,7 @@ const fs = require('fs'); const glob = require('glob'); const path = require('path'); const Parser = require('./Parser'); -const Project = require('./Project.js'); +const Project = require('./Project'); void Project; diff --git a/lib/Generate.js b/lib/Generate.js index 8855a6a7..ae0bee77 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -4,10 +4,10 @@ const { supportsColor } = require('chalk'); const fs = require('fs'); const Murmur = require('murmur-hash-js'); const path = require('path'); -const ElmJson = require('./ElmJson.js'); -const Project = require('./Project.js'); -const Report = require('./Report.js'); -const Solve = require('./Solve.js'); +const ElmJson = require('./ElmJson'); +const Project = require('./Project'); +const Report = require('./Report'); +const Solve = require('./Solve'); void Project; void Report; diff --git a/lib/RunTests.js b/lib/RunTests.js index 6321576a..0f89c47b 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -4,13 +4,13 @@ const chalk = require('chalk'); const chokidar = require('chokidar'); const path = require('path'); const packageInfo = require('../package.json'); -const Compile = require('./Compile.js'); -const ElmJson = require('./ElmJson.js'); -const FindTests = require('./FindTests.js'); -const Generate = require('./Generate.js'); -const Project = require('./Project.js'); -const Report = require('./Report.js'); -const Supervisor = require('./Supervisor.js'); +const Compile = require('./Compile'); +const ElmJson = require('./ElmJson'); +const FindTests = require('./FindTests'); +const Generate = require('./Generate'); +const Project = require('./Project'); +const Report = require('./Report'); +const Supervisor = require('./Supervisor'); void Report; diff --git a/lib/elm-test.js b/lib/elm-test.js index 52510b58..ed555f78 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -6,14 +6,14 @@ const os = require('os'); const path = require('path'); const which = require('which'); const packageInfo = require('../package.json'); -const Compile = require('./Compile.js'); -const ElmJson = require('./ElmJson.js'); -const FindTests = require('./FindTests.js'); -const Generate = require('./Generate.js'); -const Install = require('./Install.js'); -const Project = require('./Project.js'); -const Report = require('./Report.js'); -const RunTests = require('./RunTests.js'); +const Compile = require('./Compile'); +const ElmJson = require('./ElmJson'); +const FindTests = require('./FindTests'); +const Generate = require('./Generate'); +const Install = require('./Install'); +const Project = require('./Project'); +const Report = require('./Report'); +const RunTests = require('./RunTests'); void Report; From 25cab7f5496fc6fba38c8bddfdbad5e78362c5fa Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 21:12:59 +0100 Subject: [PATCH 35/67] Use cross-spawn when installing packages --- lib/Install.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/Install.js b/lib/Install.js index d199646a..342dffcd 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -1,6 +1,6 @@ // @flow -const child_process = require('child_process'); +const spawn = require('cross-spawn'); const fs = require('fs'); const path = require('path'); const rimraf = require('rimraf'); @@ -54,13 +54,13 @@ function install( ElmJson.write(generatedCodeDir, tmpElmJson); - try { - child_process.execFileSync(pathToElmBinary, ['install', packageName], { - stdio: 'inherit', - cwd: generatedCodeDir, - }); - } catch (error) { - process.exit(error.status || 1); + const result = spawn.sync(pathToElmBinary, ['install', packageName], { + stdio: 'inherit', + cwd: generatedCodeDir, + }); + + if (result.status !== 0) { + process.exit(result.status); } const newElmJson = ElmJson.read(generatedCodeDir); From 42205029d33776676f32eaf584556a1e9edd47cd Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 21:42:55 +0100 Subject: [PATCH 36/67] Concentrate process.exit into elm-test.js --- lib/RunTests.js | 17 +++++++++-------- lib/Supervisor.js | 12 ++++-------- lib/elm-test.js | 6 +++++- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/RunTests.js b/lib/RunTests.js index 0f89c47b..24a54118 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -97,7 +97,7 @@ function runTests( seed: number, fuzz: number, } */ -) { +) /*: Promise */ { let watcher = undefined; let watchedGlobs /*: Array */ = []; let testFilePaths /*: Array */ = []; @@ -105,7 +105,7 @@ function runTests( let currentRun /*: Promise | void */ = undefined; let queue /*: typeof Queue */ = []; - async function run() { + async function run() /*: Promise */ { try { // Don’t delay the first run (that’s the only time the queue is empty). // Otherwise, wait for a little bit to batch events that happened roughly @@ -197,7 +197,7 @@ function runTests( Generate.prepareCompiledJsFile(pipeFilename, dest); - await Supervisor.run( + return await Supervisor.run( packageInfo.version, pipeFilename, report, @@ -207,11 +207,8 @@ function runTests( ); } catch (err) { console.error(err.message); - if (!watch) { - process.exit(1); - } + return 1; } - infoLog(report, chalk.blue('Watching for changes...')); } if (watch) { @@ -224,6 +221,7 @@ function runTests( infoLog(report, watcherEventMessage(queue)); currentRun = run().then(onRunFinish); } else { + infoLog(report, chalk.blue('Watching for changes...')); currentRun = undefined; } }; @@ -264,8 +262,11 @@ function runTests( watcher.on('error', (error) => console.error('Watcher error:', error)); currentRun = run().then(onRunFinish); + + // A promise that never resolves. We’ll watch until killed. + return new Promise(() => {}); } else { - run(); + return run(); } } diff --git a/lib/Supervisor.js b/lib/Supervisor.js index 4200a295..bb1f2a18 100644 --- a/lib/Supervisor.js +++ b/lib/Supervisor.js @@ -15,7 +15,7 @@ function run( processes /*: number */, dest /*: string */, watch /*: boolean */ -) /*: Promise */ { +) /*: Promise */ { return new Promise(function (resolve) { var nextResultToPrint = null; var finishedWorkers = 0; @@ -176,11 +176,7 @@ function run( workers.forEach(function (worker) { worker.kill(); }); - if (watch) { - resolve(); - } else { - process.exit(response.exitCode); - } + resolve(response.exitCode); break; case 'BEGIN': testsToRun = response.testCount; @@ -249,11 +245,11 @@ function run( reportRuntimeException(); pendingException = false; } - resolve(); + resolve(1); } } else if (hasNonZeroExitCode) { reportRuntimeException(); - process.exit(1); + resolve(1); } }); diff --git a/lib/elm-test.js b/lib/elm-test.js index ed555f78..01f119b5 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -241,6 +241,7 @@ function main() { options.report ).then( () => process.exit(0), + // `elm-test make` has never logged errors it seems. () => process.exit(1) ); }); @@ -261,7 +262,10 @@ function main() { testFileGlobs, processes, options - ); + ).then(process.exit, (error) => { + console.error(error.message); + process.exit(1); + }); }); program.parse(process.argv); From fe97919efd02b5d3f3685d94a3a8145b62d7cdae Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 09:22:36 +0100 Subject: [PATCH 37/67] Add ElmJson tests --- lib/ElmJson.js | 8 +- tests/ElmJson.js | 103 ++++++++++++++++++ .../elm.json | 16 +++ .../expected.txt | 3 + .../dependency-not-string/elm.json | 23 ++++ .../dependency-not-string/expected.txt | 3 + .../empty-source-directories/elm.json | 22 ++++ .../empty-source-directories/expected.txt | 3 + .../is-folder/elm.json/.gitkeep | 0 .../invalid-elm-json/is-folder/expected.txt | 3 + .../invalid-elm-json/is-null/elm.json | 1 + .../invalid-elm-json/is-null/expected.txt | 3 + .../json-syntax-error/elm.json | 23 ++++ .../json-syntax-error/expected.txt | 3 + .../invalid-elm-json/null-type/elm.json | 23 ++++ .../invalid-elm-json/null-type/expected.txt | 3 + .../elm.json | 20 ++++ .../expected.txt | 3 + .../source-directories-not-array/elm.json | 23 ++++ .../source-directories-not-array/expected.txt | 3 + .../source-directory-not-string/elm.json | 21 ++++ .../source-directory-not-string/expected.txt | 3 + .../invalid-elm-json/unknown-type/elm.json | 23 ++++ .../unknown-type/expected.txt | 3 + tests/fixtures/write-elm-json/.gitignore | 1 + tests/fixtures/write-elm-json/elm.input.json | 25 +++++ 26 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 tests/ElmJson.js create mode 100644 tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json create mode 100644 tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/dependency-not-string/elm.json create mode 100644 tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/empty-source-directories/elm.json create mode 100644 tests/fixtures/invalid-elm-json/empty-source-directories/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/is-folder/elm.json/.gitkeep create mode 100644 tests/fixtures/invalid-elm-json/is-folder/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/is-null/elm.json create mode 100644 tests/fixtures/invalid-elm-json/is-null/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/json-syntax-error/elm.json create mode 100644 tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/null-type/elm.json create mode 100644 tests/fixtures/invalid-elm-json/null-type/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json create mode 100644 tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/source-directories-not-array/elm.json create mode 100644 tests/fixtures/invalid-elm-json/source-directories-not-array/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json create mode 100644 tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt create mode 100644 tests/fixtures/invalid-elm-json/unknown-type/elm.json create mode 100644 tests/fixtures/invalid-elm-json/unknown-type/expected.txt create mode 100644 tests/fixtures/write-elm-json/.gitignore create mode 100644 tests/fixtures/write-elm-json/elm.input.json diff --git a/lib/ElmJson.js b/lib/ElmJson.js index d6f36ac2..c9784220 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -117,7 +117,7 @@ function parseSourceDirectories(json /*: mixed */) /*: Array */ { for (const [index, item] of json.entries()) { if (typeof item !== 'string') { throw new Error( - `Expected "source-directories" -> ${index} to be a string, but got: ${stringify( + `Expected "source-directories"->${index} to be a string, but got: ${stringify( item )}` ); @@ -140,8 +140,8 @@ function parseDirectAndIndirectDependencies( ) /*: typeof DirectAndIndirectDependencies */ { const jsonObject = parseObject(json, what); return { - direct: parseDependencies(jsonObject.direct, `${what} -> "direct"`), - indirect: parseDependencies(jsonObject.indirect, `${what} -> "indirect"`), + direct: parseDependencies(jsonObject.direct, `${what}->"direct"`), + indirect: parseDependencies(jsonObject.indirect, `${what}->"indirect"`), }; } @@ -155,7 +155,7 @@ function parseDependencies( for (const [key, value] of Object.entries(jsonObject)) { if (typeof value !== 'string') { throw new Error( - `Expected ${what} -> ${stringify( + `Expected ${what}->${stringify( key )} to be a string, but got: ${stringify(value)}` ); diff --git a/tests/ElmJson.js b/tests/ElmJson.js new file mode 100644 index 00000000..3d9494a3 --- /dev/null +++ b/tests/ElmJson.js @@ -0,0 +1,103 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const ElmJson = require('../lib/ElmJson'); +const { fixturesDir } = require('./util'); + +const invalidElmJsonContainerDir = path.join(fixturesDir, 'invalid-elm-json'); + +const invalidElmJsonDirs = [ + 'application-with-package-style-test-dependencies', + 'dependency-not-string', + 'empty-source-directories', + 'is-folder', + 'is-null', + 'json-syntax-error', + 'null-type', + 'package-with-application-style-dependencies', + 'source-directories-not-array', + 'source-directory-not-string', + 'unknown-type', +]; + +describe('handling invalid elm.json', () => { + it('Should run every directory in invalid-elm-json/', () => { + const filesFound = fs.readdirSync(invalidElmJsonContainerDir).sort(); + assert.deepStrictEqual(filesFound, invalidElmJsonDirs); + }); + + for (const dir of invalidElmJsonDirs) { + it(`Should handle error for: ${dir}`, () => { + const fullPath = path.join(invalidElmJsonContainerDir, dir); + const expected = fs + .readFileSync(path.join(fullPath, 'expected.txt'), 'utf8') + .trim() + .replace('/full/path/to/elm.json', path.join(fullPath, 'elm.json')); + assert.throws(() => ElmJson.read(fullPath), { + message: expected, + }); + }); + } +}); + +// Note: +// - The fields should be in the same order as the input file. +// - The changed fields should be updated. +// - The non-standard fields should be preserved. +// - The file should use 4 spaces of indentation. +const expectedWrittenElmJson = `{ + "nonStandardFieldStart": 1, + "type": "application", + "source-directories": [ + "other/directory" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "nonStandardFieldMiddle": [ + 1, + 2, + 3 + ], + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "nonStandardFieldEnd": { + "a": 1, + "b": 2 + } +} +`; + +describe('Writing an elm.json', () => { + it('Should have a correct output', () => { + const dir = path.join(fixturesDir, 'write-elm-json'); + fs.copyFileSync( + path.join(dir, 'elm.input.json'), + path.join(dir, 'elm.json') + ); + const elmJson = ElmJson.read(dir); + const newElmJson = { + ...elmJson, + 'elm-version': '0.19.0', + 'source-directories': ['other/directory'], + }; + ElmJson.write(dir, newElmJson); + const actual = fs.readFileSync(path.join(dir, 'elm.json'), 'utf8'); + assert.strictEqual(actual, expectedWrittenElmJson); + assert(actual.endsWith('\n'), 'elm.json should end with a newline'); + }); +}); diff --git a/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json new file mode 100644 index 00000000..7776be19 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json @@ -0,0 +1,16 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "elm-explorations/test": "1.2.0 <= v < 2.0.0" + } +} diff --git a/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt new file mode 100644 index 00000000..8664d236 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt @@ -0,0 +1,3 @@ +/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json +Failed to read elm.json: +Expected test-dependencies->"direct" to be an object, but got: undefined diff --git a/tests/fixtures/invalid-elm-json/dependency-not-string/elm.json b/tests/fixtures/invalid-elm-json/dependency-not-string/elm.json new file mode 100644 index 00000000..8347124c --- /dev/null +++ b/tests/fixtures/invalid-elm-json/dependency-not-string/elm.json @@ -0,0 +1,23 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": 1.2 + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt b/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt new file mode 100644 index 00000000..986b9077 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt @@ -0,0 +1,3 @@ +/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/dependency-not-string/elm.json +Failed to read elm.json: +Expected test-dependencies->"direct"->"elm-explorations/test" to be a string, but got: 1.2 diff --git a/tests/fixtures/invalid-elm-json/empty-source-directories/elm.json b/tests/fixtures/invalid-elm-json/empty-source-directories/elm.json new file mode 100644 index 00000000..8b5be192 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/empty-source-directories/elm.json @@ -0,0 +1,22 @@ +{ + "type": "application", + "source-directories": [ + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/empty-source-directories/expected.txt b/tests/fixtures/invalid-elm-json/empty-source-directories/expected.txt new file mode 100644 index 00000000..5a707680 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/empty-source-directories/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Expected "source-directories" to contain at least one item, but it is empty. diff --git a/tests/fixtures/invalid-elm-json/is-folder/elm.json/.gitkeep b/tests/fixtures/invalid-elm-json/is-folder/elm.json/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/invalid-elm-json/is-folder/expected.txt b/tests/fixtures/invalid-elm-json/is-folder/expected.txt new file mode 100644 index 00000000..29fec2ea --- /dev/null +++ b/tests/fixtures/invalid-elm-json/is-folder/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +EISDIR: illegal operation on a directory, read diff --git a/tests/fixtures/invalid-elm-json/is-null/elm.json b/tests/fixtures/invalid-elm-json/is-null/elm.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/is-null/elm.json @@ -0,0 +1 @@ +null diff --git a/tests/fixtures/invalid-elm-json/is-null/expected.txt b/tests/fixtures/invalid-elm-json/is-null/expected.txt new file mode 100644 index 00000000..dc387968 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/is-null/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Expected the file to be an object, but got: null diff --git a/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json b/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json new file mode 100644 index 00000000..277cb795 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json @@ -0,0 +1,23 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2", + } + } +} diff --git a/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt b/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt new file mode 100644 index 00000000..ee983e94 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Unexpected token } in JSON at position 462 diff --git a/tests/fixtures/invalid-elm-json/null-type/elm.json b/tests/fixtures/invalid-elm-json/null-type/elm.json new file mode 100644 index 00000000..e14f0998 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/null-type/elm.json @@ -0,0 +1,23 @@ +{ + "type": null, + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/null-type/expected.txt b/tests/fixtures/invalid-elm-json/null-type/expected.txt new file mode 100644 index 00000000..b3a19247 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/null-type/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Expected "type" to be "application" or "package", but got: null diff --git a/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json new file mode 100644 index 00000000..e09be081 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json @@ -0,0 +1,20 @@ +{ + "type": "package", + "name": "elm-explorations/node-test-runner-example-package", + "summary": "Example package with tests", + "license": "BSD-3-Clause", + "version": "1.0.0", + "exposed-modules": [ + "Something" + ], + "elm-version": "0.19.0 <= v < 0.20.0", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "elm-explorations/test": "1.2.0 <= v < 2.0.0" + } +} diff --git a/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt new file mode 100644 index 00000000..383493c0 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt @@ -0,0 +1,3 @@ +/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json +Failed to read elm.json: +Expected dependencies->"direct" to be a string, but got: {"elm/core":"1.0.0"} diff --git a/tests/fixtures/invalid-elm-json/source-directories-not-array/elm.json b/tests/fixtures/invalid-elm-json/source-directories-not-array/elm.json new file mode 100644 index 00000000..7c275e3b --- /dev/null +++ b/tests/fixtures/invalid-elm-json/source-directories-not-array/elm.json @@ -0,0 +1,23 @@ +{ + "type": "application", + "source-directories": { + "src": true + }, + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/source-directories-not-array/expected.txt b/tests/fixtures/invalid-elm-json/source-directories-not-array/expected.txt new file mode 100644 index 00000000..a375e83e --- /dev/null +++ b/tests/fixtures/invalid-elm-json/source-directories-not-array/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Expected "source-directories" to be an array, but got: {"src":true} diff --git a/tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json b/tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json new file mode 100644 index 00000000..40dd8f93 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json @@ -0,0 +1,21 @@ +{ + "type": "application", + "source-directories": ["src", 1, "app"], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt b/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt new file mode 100644 index 00000000..9a95b1ef --- /dev/null +++ b/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt @@ -0,0 +1,3 @@ +/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json +Failed to read elm.json: +Expected "source-directories"->1 to be a string, but got: 1 diff --git a/tests/fixtures/invalid-elm-json/unknown-type/elm.json b/tests/fixtures/invalid-elm-json/unknown-type/elm.json new file mode 100644 index 00000000..6797a949 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/unknown-type/elm.json @@ -0,0 +1,23 @@ +{ + "type": "Application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + } +} diff --git a/tests/fixtures/invalid-elm-json/unknown-type/expected.txt b/tests/fixtures/invalid-elm-json/unknown-type/expected.txt new file mode 100644 index 00000000..0f500585 --- /dev/null +++ b/tests/fixtures/invalid-elm-json/unknown-type/expected.txt @@ -0,0 +1,3 @@ +/full/path/to/elm.json +Failed to read elm.json: +Expected "type" to be "application" or "package", but got: "Application" diff --git a/tests/fixtures/write-elm-json/.gitignore b/tests/fixtures/write-elm-json/.gitignore new file mode 100644 index 00000000..e4e99f51 --- /dev/null +++ b/tests/fixtures/write-elm-json/.gitignore @@ -0,0 +1 @@ +elm.json diff --git a/tests/fixtures/write-elm-json/elm.input.json b/tests/fixtures/write-elm-json/elm.input.json new file mode 100644 index 00000000..b31a08e5 --- /dev/null +++ b/tests/fixtures/write-elm-json/elm.input.json @@ -0,0 +1,25 @@ +{ + "nonStandardFieldStart": 1, + "type": "application", + "source-directories": [ "src" ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { "elm/core": "1.0.0" }, + "indirect": {} + }, + "nonStandardFieldMiddle": [1, 2, 3], + "test-dependencies": { + "direct": { + "elm/regex": "1.0.0", + "elm-explorations/test": "1.2.0" + }, + "indirect": { + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "nonStandardFieldEnd": { + "a": 1, + "b": 2 + } +} \ No newline at end of file From 72b8f0e3ade70a7c54c8cedd57480acdeccf1529 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 22:45:01 +0100 Subject: [PATCH 38/67] Test that elm.json is watched --- tests/flags.js | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/tests/flags.js b/tests/flags.js index b551f46f..d8ffea99 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -48,6 +48,11 @@ function ensureEmptyDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } +function touch(filePath) { + const now = new Date(); + fs.utimesSync(filePath, now, now); +} + function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } @@ -140,7 +145,7 @@ describe('flags', () => { assert.notStrictEqual(runResult.status, 0); }).timeout(60000); - it('should fail if the current directory does not contain an elm.json', () => { + it('should fail if no elm.json can be found', () => { const runResult = execElmTest(['install', 'elm/regex'], rootDir); assert.ok(Number.isInteger(runResult.status)); assert.notStrictEqual(runResult.status, 0); @@ -409,14 +414,14 @@ describe('flags', () => { assert.notStrictEqual(runResult.status, 0); }).timeout(5000); - it('Should re-run tests if a test file is touched', (done) => { + it('Should re-run tests if files change', (done) => { const child = spawn( elmTestPath, ['--report=json', '--watch', path.join('tests', 'Passing', 'One.elm')], Object.assign({ encoding: 'utf-8', cwd: fixturesDir }, spawnOpts) ); - let hasRetriggered = false; + let runsExecuted = 0; child.on('close', (code, signal) => { // don't send error when killed after test passed @@ -427,22 +432,29 @@ describe('flags', () => { const reader = readline.createInterface({ input: child.stdout }); reader.on('line', (line) => { try { - const json = stripAnsi('' + line); - // skip expected non-json - if (json === 'Watching for changes...') return; - const parsedLine = JSON.parse(json); + const parsedLine = JSON.parse(stripAnsi('' + line)); if (parsedLine.event !== 'runComplete') return; - if (!hasRetriggered) { - const now = new Date(); - fs.utimesSync( - path.join(fixturesDir, 'tests', 'Passing', 'One.elm'), - now, - now - ); - hasRetriggered = true; - } else { - child.kill(); - done(); + runsExecuted++; + switch (runsExecuted) { + case 1: + touch(path.join(fixturesDir, 'tests', 'Passing', 'One.elm')); + break; + case 2: + touch(path.join(fixturesDir, 'src', 'Port1.elm')); + break; + case 3: + touch(path.join(fixturesDir, 'elm.json')); + setTimeout(() => touch(path.join(fixturesDir, 'elm.json')), 100); + break; + case 4: + child.kill(); + done(); + break; + default: + child.kill(); + done( + new Error(`More runs executed than expected: ${runsExecuted}`) + ); } } catch (e) { child.kill(); From 7e5901bc64f85b3837e030dba8dffd7045156797 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 23:11:01 +0100 Subject: [PATCH 39/67] Test adding and removing files --- tests/fixtures/tests/Passing/.gitignore | 1 + tests/flags.js | 31 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/tests/Passing/.gitignore diff --git a/tests/fixtures/tests/Passing/.gitignore b/tests/fixtures/tests/Passing/.gitignore new file mode 100644 index 00000000..fb307d7b --- /dev/null +++ b/tests/fixtures/tests/Passing/.gitignore @@ -0,0 +1 @@ +Generated.elm diff --git a/tests/flags.js b/tests/flags.js index d8ffea99..7d234c84 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -414,7 +414,17 @@ describe('flags', () => { assert.notStrictEqual(runResult.status, 0); }).timeout(5000); - it('Should re-run tests if files change', (done) => { + it('Should re-run tests when files are changed, added and removed', (done) => { + const addedFile = path.join( + fixturesDir, + 'tests', + 'Passing', + 'Generated.elm' + ); + if (fs.existsSync(addedFile)) { + fs.unlinkSync(addedFile); + } + const child = spawn( elmTestPath, ['--report=json', '--watch', path.join('tests', 'Passing', 'One.elm')], @@ -437,16 +447,31 @@ describe('flags', () => { runsExecuted++; switch (runsExecuted) { case 1: + // Imagine this adds `import Passing.Generated`… touch(path.join(fixturesDir, 'tests', 'Passing', 'One.elm')); break; case 2: - touch(path.join(fixturesDir, 'src', 'Port1.elm')); + // … then if Generated.elm is created we should re-run the tests. + // (A really smart implementation cound follow the import graph.) + fs.writeFileSync(addedFile, 'module Generated exposing (a)\na=1'); break; case 3: + // Same thing if we remove it again. + fs.unlinkSync(addedFile); + break; + case 4: + // Tests might depend on source files. (Again, a really smart + // implementation would know for sure.) + touch(path.join(fixturesDir, 'src', 'Port1.elm')); + break; + case 5: + // elm.json needs to be watched too. You might add source + // directories or install dependencies. touch(path.join(fixturesDir, 'elm.json')); + // Another change close after should be batched into the same run. setTimeout(() => touch(path.join(fixturesDir, 'elm.json')), 100); break; - case 4: + case 6: child.kill(); done(); break; From 53310e069efbe3ec8452b075647fdb05a90b873b Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 23:19:53 +0100 Subject: [PATCH 40/67] Test finding elm.json up the directory tree --- tests/flags.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/flags.js b/tests/flags.js index 7d234c84..090c400b 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -489,6 +489,16 @@ describe('flags', () => { }).timeout(60000); }); + describe('finding elm.json', () => { + it('find an elm.json up the directory tree', () => { + const runResult = execElmTest( + ['One.elm'], + path.join(fixturesDir, 'tests', 'Passing') + ); + assert.strictEqual(runResult.status, 0); + }).timeout(60000); + }); + describe('unknown flags', () => { it('Should fail on unknown short flag', () => { const runResult = execElmTest([ From 161182503ca7737e843120979385e61ada698d40 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 20 Nov 2020 23:42:10 +0100 Subject: [PATCH 41/67] Document how crazy big files are handled --- lib/RunTests.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/RunTests.js b/lib/RunTests.js index 24a54118..b45b404b 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -115,7 +115,10 @@ function runTests( // that, with a `stabilityThreshold` that is an amount of time in // milliseconds for a file size to remain constant before emitting its // event. Elm files aren’t that huge so it should be fine to just wait a - // fixed amount of time here instead. + // fixed amount of time here instead. I tried it with a 378 MB file. That + // resulted in first a "added" event and then a "changed" event 200 ms + // later. So the worst thing that can happen is that you get one run with + // half a file immediately followed by another run with the whole file. if (queue.length > 0) { await delay(200); } From 9945d4a660c6646ef80ab0cfba8fcf8390070b89 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 00:24:22 +0100 Subject: [PATCH 42/67] Add tests for watch + init --- tests/fixtures/templates/application/elm.json | 2 +- tests/flags.js | 96 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/templates/application/elm.json b/tests/fixtures/templates/application/elm.json index ceb16cf9..97d61ebc 100644 --- a/tests/fixtures/templates/application/elm.json +++ b/tests/fixtures/templates/application/elm.json @@ -1,7 +1,7 @@ { "type": "application", "source-directories": [ - "." + "src" ], "elm-version": "0.19.1", "dependencies": { diff --git a/tests/flags.js b/tests/flags.js index 090c400b..65ae5b71 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -431,15 +431,16 @@ describe('flags', () => { Object.assign({ encoding: 'utf-8', cwd: fixturesDir }, spawnOpts) ); - let runsExecuted = 0; - child.on('close', (code, signal) => { // don't send error when killed after test passed if (code !== null || signal !== 'SIGTERM') { done(new Error('elm-test --watch exited with status code: ' + code)); } }); + + let runsExecuted = 0; const reader = readline.createInterface({ input: child.stdout }); + reader.on('line', (line) => { try { const parsedLine = JSON.parse(stripAnsi('' + line)); @@ -478,7 +479,96 @@ describe('flags', () => { default: child.kill(); done( - new Error(`More runs executed than expected: ${runsExecuted}`) + new Error( + `More runs executed than expected: ${runsExecuted}\n${line}` + ) + ); + } + } catch (e) { + child.kill(); + done(e); + } + }); + }).timeout(60000); + + it('Should re-run tests after init and install', (done) => { + ensureEmptyDir(scratchDir); + + fs.copyFileSync( + path.join(fixturesDir, 'templates', 'application', 'elm.json'), + scratchElmJsonPath + ); + fs.mkdirSync(path.join(scratchDir, 'src')); + + // We start the watcher in a directory with only an elm.json, no tests dir. + const child = spawn( + elmTestPath, + ['--report=json', '--watch'], + Object.assign({ encoding: 'utf-8', cwd: scratchDir }, spawnOpts) + ); + + child.on('close', (code, signal) => { + // don't send error when killed after test passed + if (code !== null || signal !== 'SIGTERM') { + done(new Error('elm-test --watch exited with status code: ' + code)); + } + }); + + let runsExecuted = 0; + + child.stderr.on('data', (data) => { + switch (runsExecuted) { + case 0: { + // We got an error message saying that no tests were found. + // Let’s init the example tests! This should trigger a re-run. + elmTestWithYes(['init'], (code) => { + assert.strictEqual(code, 0); + runsExecuted++; + }); + break; + } + case 2: + child.kill(); + done(); + break; + default: + child.kill(); + done( + new Error( + `Unexpected stderr test run: ${runsExecuted}\n${data.toString()}` + ) + ); + } + }); + + const reader = readline.createInterface({ input: child.stdout }); + + reader.on('line', (line) => { + try { + const parsedLine = JSON.parse(stripAnsi('' + line)); + if (parsedLine.event !== 'runComplete') return; + runsExecuted++; + switch (runsExecuted) { + case 1: { + // elm/json is in the "indirect" dependencies – let’s move it to "direct". + // This should re-run the tests because we likely did this for a + // reason – some file depends on elm/json now. + elmTestWithYes(['install', 'elm/json'], (code) => { + assert.strictEqual(code, 0); + }); + break; + } + case 2: + // Remove the tests dir again. This should re-run and output + // messages about no tests found but not crash. + rimraf.sync(path.join(scratchDir, 'tests')); + break; + default: + child.kill(); + done( + new Error( + `Unexpected stdout test run: ${runsExecuted}\n${line}` + ) ); } } catch (e) { From dcdb863bdd1449e37b5c9a7c06633849db5b17fb Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 01:14:08 +0100 Subject: [PATCH 43/67] Improve error message when source-directories contains "." --- lib/FindTests.js | 30 ++++++++++++++++++++++++------ lib/RunTests.js | 6 +----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index 7915282c..78dcbdbb 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -65,12 +65,11 @@ function getGlobsToWatch( function findTests( testFilePaths /*: Array */, - sourceDirs /*: Array */, - isPackageProject /*: boolean */ + project /*: typeof Project.Project */ ) /*: Promise }>> */ { return Promise.all( testFilePaths.map((filePath) => { - const matchingSourceDirs = sourceDirs.filter((dir) => + const matchingSourceDirs = project.testsSourceDirs.filter((dir) => filePath.startsWith(`${dir}${path.sep}`) ); @@ -79,7 +78,12 @@ function findTests( switch (matchingSourceDirs.length) { case 0: return Promise.reject( - Error(missingSourceDirectoryError(filePath, isPackageProject)) + Error( + missingSourceDirectoryError( + filePath, + project.elmJson.type === 'package' + ) + ) ); case 1: @@ -90,7 +94,11 @@ function findTests( // This shouldn’t be possible for package projects. return Promise.reject( new Error( - multipleSourceDirectoriesError(filePath, matchingSourceDirs) + multipleSourceDirectoriesError( + filePath, + matchingSourceDirs, + project.testsDir + ) ) ); } @@ -140,7 +148,15 @@ ${ `.trim(); } -function multipleSourceDirectoriesError(filePath, matchingSourceDirs) { +function multipleSourceDirectoriesError( + filePath, + matchingSourceDirs, + testsDir +) { + const note = matchingSourceDirs.includes(testsDir) + ? 'Note: The tests/ folder counts as a source directory too (even if it isn’t listed in your elm.json)!' + : ''; + return ` This file: @@ -151,6 +167,8 @@ ${filePath} ${matchingSourceDirs.join('\n')} Edit "source-directories" in your elm.json and try to make it so no source directory contains another source directory! + +${note} `.trim(); } diff --git a/lib/RunTests.js b/lib/RunTests.js index b45b404b..88e912a4 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -168,11 +168,7 @@ function runTests( Generate.generateElmJson(project); - const testModules = await FindTests.findTests( - testFilePaths, - project.testsSourceDirs, - project.elmJson.type === 'package' - ); + const testModules = await FindTests.findTests(testFilePaths, project); const mainFile = Generate.generateMainModule( fuzz, From 9d87dcc11c75d5f27450a8be0982cf0c5200647f Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 01:30:38 +0100 Subject: [PATCH 44/67] Validate source directories --- lib/FindTests.js | 6 +++--- lib/Project.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/RunTests.js | 1 + 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index 78dcbdbb..c66780e8 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -138,7 +138,7 @@ This file: ${filePath} -…matches no source directory! Imports won’t work then. +…matches no source directory! Imports won't work then. ${ isPackageProject @@ -154,7 +154,7 @@ function multipleSourceDirectoriesError( testsDir ) { const note = matchingSourceDirs.includes(testsDir) - ? 'Note: The tests/ folder counts as a source directory too (even if it isn’t listed in your elm.json)!' + ? "Note: The tests/ folder counts as a source directory too (even if it isn't listed in your elm.json)!" : ''; return ` @@ -191,7 +191,7 @@ ${moduleName} Main Http.Helpers -Make sure that all parts start with an uppercase letter and don’t contain any spaces or anything like that. +Make sure that all parts start with an uppercase letter and don't contain any spaces or anything like that. `.trim(); } diff --git a/lib/Project.js b/lib/Project.js index 188d2f79..ff37529d 100644 --- a/lib/Project.js +++ b/lib/Project.js @@ -61,8 +61,57 @@ function init( }; } +/* We do this validation ourselves to avoid the ../../../../../ in Elm’s error message: + +-- MISSING SOURCE DIRECTORY ------------------------------------------- elm.json + +I need a valid elm.json file, but the "source-directories" field lists the +following directory: + + ../../../../../app + +I cannot find it though. Is it missing? Is there a typo? +*/ +function validateTestsSourceDirs(project /*: typeof Project */) /*: void */ { + for (const dir of project.testsSourceDirs) { + const fullDir = path.resolve(project.rootDir, dir); + let stats; + try { + stats = fs.statSync(fullDir); + } catch (error) { + throw new Error( + validateTestsSourceDirsError( + fullDir, + error.code === 'ENOENT' + ? "It doesn't exist though. Is it missing? Is there a typo?" + : `Failed to read that directory: ${error.message}` + ) + ); + } + if (!stats.isDirectory()) { + throw new Error( + validateTestsSourceDirsError( + fullDir, + `It exists but isn't a directory!` + ) + ); + } + } +} + +function validateTestsSourceDirsError(dir, message) { + return ` +The "source-directories" field in your elm.json lists the following directory: + +${dir} + +${message} + `.trim(); +} + module.exports = { Project, getTestsDir, init, + validateTestsSourceDirs, }; diff --git a/lib/RunTests.js b/lib/RunTests.js index 88e912a4..4bd80aa8 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -132,6 +132,7 @@ function runTests( // Files may be changed, added or removed so always re-create project info // from disk to stay fresh. const project = Project.init(projectRootDir, packageInfo.version); + Project.validateTestsSourceDirs(project); // Resolving globs is usually pretty fast. When running `elm-test` without // arguments it takes around 20-40 ms. Running `elm-test` with 100 files From 9778eb731cc27e48eb16d47f59836d126b18e5b9 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 01:38:29 +0100 Subject: [PATCH 45/67] Fix tests after added .gitignore --- tests/ci.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/ci.js b/tests/ci.js index 2311e064..e5e23d3a 100755 --- a/tests/ci.js +++ b/tests/ci.js @@ -75,6 +75,13 @@ function assertTestFailure(runResult) { ); } +function readdir(dir) { + return fs + .readdirSync(dir) + .filter((item) => !item.startsWith('.')) + .sort(); +} + describe('--help', () => { it('Should print the usage and exit indicating success', () => { const runResult = execElmTest(['--help']); @@ -166,8 +173,7 @@ describe('Testing elm-test on single Elm files', () => { } it(`Should run every file in tests/Passing`, () => { - const filesFound = fs.readdirSync(cwd + '/tests/Passing/'); - filesFound.sort(); + const filesFound = readdir(path.join(cwd, 'tests', 'Passing')); assert.deepStrictEqual(filesFound, passingTestFiles); }); @@ -192,8 +198,7 @@ describe('Testing elm-test on single Elm files', () => { } it(`Should run every file in tests/Failing`, () => { - const filesFound = fs.readdirSync(cwd + '/tests/Failing/'); - filesFound.sort(); + const filesFound = readdir(path.join(cwd, 'tests', 'Failing')); assert.deepStrictEqual(filesFound, failingTestFiles); }); @@ -209,8 +214,7 @@ describe('Testing elm-test on single Elm files', () => { } it(`Should run every file in tests/RuntimeException`, () => { - const filesFound = fs.readdirSync(cwd + '/tests/RuntimeException/'); - filesFound.sort(); + const filesFound = readdir(path.join(cwd, 'tests', 'RuntimeException')); assert.deepStrictEqual(filesFound, erroredTestFiles); }); }); From 76f0dec953bb27c5ee310767c79a2281eb98873c Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 11:49:43 +0100 Subject: [PATCH 46/67] Fix paths in test snapshots --- .../expected.txt | 2 +- .../invalid-elm-json/dependency-not-string/expected.txt | 2 +- .../package-with-application-style-dependencies/expected.txt | 2 +- .../invalid-elm-json/source-directory-not-string/expected.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt index 8664d236..6d942061 100644 --- a/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt +++ b/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt @@ -1,3 +1,3 @@ -/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json +/full/path/to/elm.json Failed to read elm.json: Expected test-dependencies->"direct" to be an object, but got: undefined diff --git a/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt b/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt index 986b9077..68d8ddea 100644 --- a/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt +++ b/tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt @@ -1,3 +1,3 @@ -/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/dependency-not-string/elm.json +/full/path/to/elm.json Failed to read elm.json: Expected test-dependencies->"direct"->"elm-explorations/test" to be a string, but got: 1.2 diff --git a/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt index 383493c0..48b25e7f 100644 --- a/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt +++ b/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt @@ -1,3 +1,3 @@ -/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json +/full/path/to/elm.json Failed to read elm.json: Expected dependencies->"direct" to be a string, but got: {"elm/core":"1.0.0"} diff --git a/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt b/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt index 9a95b1ef..eb087e58 100644 --- a/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt +++ b/tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt @@ -1,3 +1,3 @@ -/Users/lydell/forks/node-test-runner/tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json +/full/path/to/elm.json Failed to read elm.json: Expected "source-directories"->1 to be a string, but got: 1 From f4f6a6893cc2e2a1aa4a8c9871a00570e0c743c0 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 12:47:10 +0100 Subject: [PATCH 47/67] Try a different approach for resolving globs --- lib/FindTests.js | 31 ++++++++++++++++++------------- lib/RunTests.js | 3 ++- lib/elm-test.js | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index c66780e8..1f93a08e 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -17,19 +17,24 @@ function flatMap/*:: */( } // Resolve arguments that look like globs for shells that don’t support globs. -function resolveGlobs(fileGlobs /*: Array */) /*: Array */ { - return flatMap(flatMap(fileGlobs, globify), resolveFilePath); -} - -function globify(globString /*: string */) /*: Array */ { - // Without `path.resolve`, `../tests` gives 0 results even if `../tests` - // exists (at least on MacOS). - return glob.sync(path.resolve(globString), { - nocase: true, - ignore: '**/elm-stuff/**', - nodir: false, - absolute: true, - }); +function resolveGlobs( + fileGlobs /*: Array */, + projectRootDir /*: string */ +) /*: Array */ { + return flatMap( + flatMap(fileGlobs, (globString) => + // Globs passed as CLI arguments are relative to CWD, while elm-test + // operates from the project root dir. + glob.sync(path.relative(projectRootDir, path.resolve(globString)), { + cwd: projectRootDir, + nocase: true, + ignore: '**/elm-stuff/**', + nodir: false, + absolute: true, + }) + ), + resolveFilePath + ); } // Recursively search directories for *.elm files, excluding elm-stuff/ diff --git a/lib/RunTests.js b/lib/RunTests.js index 4bd80aa8..80ed8ed1 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -149,7 +149,8 @@ function runTests( queue = []; if (!onlyChanged) { testFilePaths = FindTests.resolveGlobs( - testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs + testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs, + project.rootDir ); } diff --git a/lib/elm-test.js b/lib/elm-test.js index 01f119b5..e4ef37fa 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -235,7 +235,7 @@ function main() { const project = getProject('make'); Generate.generateElmJson(project); Compile.compileSources( - FindTests.resolveGlobs(testFileGlobs), + FindTests.resolveGlobs(testFileGlobs, project.rootDir), project.generatedCodeDir, pathToElmBinary, options.report From 45d993f0bb24932dbb9a79c02d5dc910769f3c4a Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 13:28:52 +0100 Subject: [PATCH 48/67] =?UTF-8?q?Don=E2=80=99t=20trigger=20test=20runs=20o?= =?UTF-8?q?n=20non-elm=20file=20changes=20in=20tests/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/FindTests.js | 10 --------- lib/RunTests.js | 53 +++++++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index 1f93a08e..d504ba92 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -59,15 +59,6 @@ function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { ); } -function getGlobsToWatch( - project /*: typeof Project.Project */ -) /*: Array */ { - return project.testsSourceDirs.map((sourceDirectory) => - // TODO: Test this on Windows. - path.posix.join(sourceDirectory, '**', '*.elm') - ); -} - function findTests( testFilePaths /*: Array */, project /*: typeof Project.Project */ @@ -238,7 +229,6 @@ function noFilesFoundInTestsDir(projectRootDir) { module.exports = { findTests, - getGlobsToWatch, noFilesFoundError, resolveGlobs, }; diff --git a/lib/RunTests.js b/lib/RunTests.js index 80ed8ed1..c2b2ff39 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -99,7 +99,7 @@ function runTests( } */ ) /*: Promise */ { let watcher = undefined; - let watchedGlobs /*: Array */ = []; + let watchedPaths /*: Array */ = []; let testFilePaths /*: Array */ = []; let runsExecuted /*: number */ = 0; let currentRun /*: Promise | void */ = undefined; @@ -161,9 +161,8 @@ function runTests( } if (watcher !== undefined) { - const nextGlobsToWatch = FindTests.getGlobsToWatch(project); - const diff = diffArrays(watchedGlobs, nextGlobsToWatch); - watchedGlobs = nextGlobsToWatch; + const diff = diffArrays(watchedPaths, project.testsSourceDirs); + watchedPaths = project.testsSourceDirs; watcher.add(diff.added); watcher.unwatch(diff.removed); } @@ -227,36 +226,44 @@ function runTests( } }; - const rerun = (event) => (filePath) => { - queue.push({ event, filePath }); - if (currentRun === undefined) { - clearConsole(report); - infoLog(report, watcherEventMessage(queue)); - currentRun = run().then(onRunFinish); - } - }; - - // The globs to watch change over time and are added and removed as needed - // in `run`. We should always watch `elm.json` and `tests/`, though (see the - // 'addDir' event below). - const initialGlobsToWatch = [ + // The directories to watch change over time and are added and removed as + // needed in `run`. We should always watch `elm.json` and `tests/`, though + // (see the 'addDir' event below). + const alwaysWatched = [ ElmJson.getPath(projectRootDir), Project.getTestsDir(projectRootDir), ]; - watcher = chokidar.watch(initialGlobsToWatch, { + + const rerun = (event) => (absoluteFilePath) => { + if ( + absoluteFilePath.endsWith('.elm') || + alwaysWatched.includes(absoluteFilePath) + ) { + queue.push({ + event, + filePath: path.relative(projectRootDir, absoluteFilePath), + }); + if (currentRun === undefined) { + clearConsole(report); + infoLog(report, watcherEventMessage(queue)); + currentRun = run().then(onRunFinish); + } + } + }; + + watcher = chokidar.watch(alwaysWatched, { ignoreInitial: true, ignored: /(\/|^)elm-stuff(\/|$)/, - cwd: projectRootDir, + disableGlobbing: true, }); watcher.on('add', rerun('added')); watcher.on('change', rerun('changed')); watcher.on('unlink', rerun('removed')); - // The only time this event fires is when the `tests/` directory is added - // (all other glob patterns only match files, not directories). - // That’s useful if starting the watcher before `tests/` exists. - // There’s no need to listen for 'unlinkDir' – that makes no difference. + // The only time this event is interesting is when the `tests/` directory is + // created after the watcher was started. There’s no need to listen for + // 'unlinkDir' – that makes no difference. watcher.on('addDir', rerun('added')); // It’s unclear when this event occurrs. From a5604c24523369682d169df6005fea433d50c305 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 13:39:09 +0100 Subject: [PATCH 49/67] Update outdated comments --- lib/Generate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Generate.js b/lib/Generate.js index ae0bee77..c265d312 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -82,7 +82,7 @@ function generateElmJson(project /*: typeof Project.Project */) /*: void */ { }; testElmJson['source-directories'] = [ - // Include elm-stuff/generated-sources - since we'll be generating sources in there. + // Include the generated test application. generatedSrc, // NOTE: we must include node-test-runner's Elm source as a source-directory @@ -155,7 +155,7 @@ function generateMainModule( // We'll be putting the generated Main in something like this: // - // my-project-name/elm-stuff/generated-code/elm-community/elm-test/src/Test/Generated/Main123456.elm + // my-project-name/elm-stuff/generated-code/elm-community/elm-test/0.19.1-revisionX/src/Test/Generated/Main36138116.elm const testFileContents = `module Test.Generated.${moduleName} exposing (main)\n\n${testFileBody}`; // Make sure src/Test/Generated/ exists so we can write the file there. From 081dc67c1ad1e1ae7de061448a962bc56d9cfd7d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 14:59:14 +0100 Subject: [PATCH 50/67] Improve globbing --- lib/FindTests.js | 87 ++++++++++++++++++++++++++++++------------------ lib/RunTests.js | 38 ++++++++------------- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index d504ba92..c8ca5c35 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -16,47 +16,67 @@ function flatMap/*:: */( return array.reduce((result, item) => result.concat(f(item)), []); } -// Resolve arguments that look like globs for shells that don’t support globs. +// Double stars at the start and end is the correct way to ignore directories in +// the `glob` package. +// https://github.com/isaacs/node-glob/issues/270#issuecomment-273949982 +// https://github.com/isaacs/node-glob/blob/f5a57d3d6e19b324522a3fa5bdd5075fd1aa79d1/common.js#L222-L231 +const ignoredDirsGlobs = ['**/elm-stuff/**', '**/node_modules/**']; + function resolveGlobs( fileGlobs /*: Array */, projectRootDir /*: string */ +) /*: Array */ { + return flatMap(fileGlobs, (fileGlob) => { + const absolutePath = path.resolve(fileGlob); + try { + const stat = fs.statSync(absolutePath); + // If the CLI arg exists… + return stat.isDirectory() + ? // …and it’s a directory, find all .elm files in there… + findAllElmFilesInDir(absolutePath) + : // …otherwise use it as-is. + [absolutePath]; + } catch (error) { + // If the CLI arg does not exist… + return error.code === 'ENOENT' + ? // …resolve it as a glob for shells that don’t support globs. + resolveCliArgGlob(absolutePath, projectRootDir) + : // The glob package ignores other types of stat errors. + []; + } + }); +} + +function resolveCliArgGlob( + fileGlob /*: string */, + projectRootDir /*: string */ ) /*: Array */ { return flatMap( - flatMap(fileGlobs, (globString) => - // Globs passed as CLI arguments are relative to CWD, while elm-test - // operates from the project root dir. - glob.sync(path.relative(projectRootDir, path.resolve(globString)), { - cwd: projectRootDir, - nocase: true, - ignore: '**/elm-stuff/**', - nodir: false, - absolute: true, - }) - ), - resolveFilePath + // Globs passed as CLI arguments are relative to CWD, while elm-test + // operates from the project root dir. + glob.sync(path.relative(projectRootDir, path.resolve(fileGlob)), { + cwd: projectRootDir, + nocase: true, + absolute: true, + ignore: ignoredDirsGlobs, + // Match directories as well and mark them with a trailing slash. + nodir: false, + mark: true, + }), + (filePath) => + filePath.endsWith('/') ? findAllElmFilesInDir(filePath) : filePath ); } -// Recursively search directories for *.elm files, excluding elm-stuff/ -function resolveFilePath(elmFilePathOrDir /*: string */) /*: Array */ { - const candidates = !fs.existsSync(elmFilePathOrDir) - ? [] - : fs.lstatSync(elmFilePathOrDir).isDirectory() - ? flatMap( - glob.sync('/**/*.elm', { - root: elmFilePathOrDir, - nocase: true, - ignore: '/**/elm-stuff/**', - nodir: true, - }), - resolveFilePath - ) - : [path.resolve(elmFilePathOrDir)]; - - // Exclude everything having anything to do with elm-stuff - return candidates.filter( - (candidate) => !candidate.split(path.sep).includes('elm-stuff') - ); +// Recursively search for *.elm files. +function findAllElmFilesInDir(dir /*: string */) /*: Array */ { + return glob.sync('**/*.elm', { + cwd: dir, + nocase: true, + absolute: true, + ignore: ignoredDirsGlobs, + nodir: true, + }); } function findTests( @@ -229,6 +249,7 @@ function noFilesFoundInTestsDir(projectRootDir) { module.exports = { findTests, + ignoredDirsGlobs, noFilesFoundError, resolveGlobs, }; diff --git a/lib/RunTests.js b/lib/RunTests.js index c2b2ff39..e423b02c 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -100,7 +100,6 @@ function runTests( ) /*: Promise */ { let watcher = undefined; let watchedPaths /*: Array */ = []; - let testFilePaths /*: Array */ = []; let runsExecuted /*: number */ = 0; let currentRun /*: Promise | void */ = undefined; let queue /*: typeof Queue */ = []; @@ -121,38 +120,27 @@ function runTests( // half a file immediately followed by another run with the whole file. if (queue.length > 0) { await delay(200); - } - - // Re-print the message in case the queue has become longer while waiting. - if (queue.length > 0) { + // Re-print the message in case the queue has become longer while waiting. clearConsole(report); infoLog(report, watcherEventMessage(queue)); } + queue = []; + + // All operations down to `Generate.generateElmJson(project)` could + // potentially be avoided depending on what changed (checking what’s + // inside `queue`). But at the time of this writing all of them are fast + // (less than 20 ms, often less than 1 ms) so it’s not worth bothering. + // Files may be changed, added or removed so always re-create project info // from disk to stay fresh. const project = Project.init(projectRootDir, packageInfo.version); Project.validateTestsSourceDirs(project); - // Resolving globs is usually pretty fast. When running `elm-test` without - // arguments it takes around 20-40 ms. Running `elm-test` with 100 files - // as arguments it takes more than 100 ms (we still need to look for globs - // in all arguments). Still pretty fast, but it’s super easy to avoid - // recalculating them: Only when files are added or removed we need to - // re-calculate what files match the globs. In other words, if only files - // have changed there’s nothing to do. - // Actually, all operations down to `Generate.generateElmJson(project)` - // could potentially be avoided depending on what changed. But all of them - // are super fast (often less than 1 ms) so it’s not worth bothering. - const onlyChanged = - queue.length > 0 && queue.every(({ event }) => event === 'changed'); - queue = []; - if (!onlyChanged) { - testFilePaths = FindTests.resolveGlobs( - testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs, - project.rootDir - ); - } + const testFilePaths = FindTests.resolveGlobs( + testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs, + project.rootDir + ); if (testFilePaths.length === 0) { throw new Error( @@ -253,7 +241,7 @@ function runTests( watcher = chokidar.watch(alwaysWatched, { ignoreInitial: true, - ignored: /(\/|^)elm-stuff(\/|$)/, + ignored: FindTests.ignoredDirsGlobs, disableGlobbing: true, }); From 8e4535572c430b007b784d52aafc8e5ad7ac55d5 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 15:19:34 +0100 Subject: [PATCH 51/67] Deduplicate test files --- lib/FindTests.js | 42 ++++++++++++---------- tests/ci.js | 2 +- tests/fixtures/tests/Passing/Dedup/One.elm | 11 ++++++ tests/flags.js | 13 +++++-- 4 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/tests/Passing/Dedup/One.elm diff --git a/lib/FindTests.js b/lib/FindTests.js index c8ca5c35..cc07f4eb 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -26,25 +26,29 @@ function resolveGlobs( fileGlobs /*: Array */, projectRootDir /*: string */ ) /*: Array */ { - return flatMap(fileGlobs, (fileGlob) => { - const absolutePath = path.resolve(fileGlob); - try { - const stat = fs.statSync(absolutePath); - // If the CLI arg exists… - return stat.isDirectory() - ? // …and it’s a directory, find all .elm files in there… - findAllElmFilesInDir(absolutePath) - : // …otherwise use it as-is. - [absolutePath]; - } catch (error) { - // If the CLI arg does not exist… - return error.code === 'ENOENT' - ? // …resolve it as a glob for shells that don’t support globs. - resolveCliArgGlob(absolutePath, projectRootDir) - : // The glob package ignores other types of stat errors. - []; - } - }); + return Array.from( + new Set( + flatMap(fileGlobs, (fileGlob) => { + const absolutePath = path.resolve(fileGlob); + try { + const stat = fs.statSync(absolutePath); + // If the CLI arg exists… + return stat.isDirectory() + ? // …and it’s a directory, find all .elm files in there… + findAllElmFilesInDir(absolutePath) + : // …otherwise use it as-is. + [absolutePath]; + } catch (error) { + // If the CLI arg does not exist… + return error.code === 'ENOENT' + ? // …resolve it as a glob for shells that don’t support globs. + resolveCliArgGlob(absolutePath, projectRootDir) + : // The glob package ignores other types of stat errors. + []; + } + }) + ) + ); } function resolveCliArgGlob( diff --git a/tests/ci.js b/tests/ci.js index e5e23d3a..28800f39 100755 --- a/tests/ci.js +++ b/tests/ci.js @@ -78,7 +78,7 @@ function assertTestFailure(runResult) { function readdir(dir) { return fs .readdirSync(dir) - .filter((item) => !item.startsWith('.')) + .filter((item) => item.endsWith('.elm')) .sort(); } diff --git a/tests/fixtures/tests/Passing/Dedup/One.elm b/tests/fixtures/tests/Passing/Dedup/One.elm new file mode 100644 index 00000000..982e36ec --- /dev/null +++ b/tests/fixtures/tests/Passing/Dedup/One.elm @@ -0,0 +1,11 @@ +module Passing.Dedup.One exposing (..) + +import Expect +import Test exposing (..) + + +plainExpectation : Test +plainExpectation = + test "this should pass" <| + \() -> + Expect.equal "success" "success" diff --git a/tests/flags.js b/tests/flags.js index 65ae5b71..9e4130c5 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -579,14 +579,23 @@ describe('flags', () => { }).timeout(60000); }); - describe('finding elm.json', () => { - it('find an elm.json up the directory tree', () => { + describe('mixed', () => { + it('Should find an elm.json up the directory tree', () => { const runResult = execElmTest( ['One.elm'], path.join(fixturesDir, 'tests', 'Passing') ); assert.strictEqual(runResult.status, 0); }).timeout(60000); + + it('Should deduplicate test files', () => { + // This is nice if two globs accidentally intersect. + const runResult = execElmTest([ + 'tests/Passing/Dedup/*.elm', + 'tests/**/!(Failing)/**/One.elm', + ]); + assert.strictEqual(runResult.status, 0); + }).timeout(60000); }); describe('unknown flags', () => { From 46299d9335dad27da2ea09225fca79cf2c1c6b0d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 15:37:20 +0100 Subject: [PATCH 52/67] Normalize paths on Windows --- lib/FindTests.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index cc07f4eb..030011c0 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -47,7 +47,11 @@ function resolveGlobs( []; } }) - ) + ), + // The `glob` package returns absolute paths with slashes always, even on + // Windows. All other paths in elm-test use the native directory separator + // so normalize here. + (filePath) => path.normalize(filePath) ); } From 0f1e697dba98b0ba1797e772d687d855ba70f554 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 16:10:03 +0100 Subject: [PATCH 53/67] Reduce delay --- lib/RunTests.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/RunTests.js b/lib/RunTests.js index e423b02c..f47852e6 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -115,11 +115,14 @@ function runTests( // milliseconds for a file size to remain constant before emitting its // event. Elm files aren’t that huge so it should be fine to just wait a // fixed amount of time here instead. I tried it with a 378 MB file. That - // resulted in first a "added" event and then a "changed" event 200 ms + // resulted in first a "added" event and then a "changed" event a little // later. So the worst thing that can happen is that you get one run with // half a file immediately followed by another run with the whole file. + // I tested touching 100 files at a time. All of them produced events + // within 60 ms on Windows, and faster on Mac and Linux. So 100 ms sounds + // like a reasonable number – not too short, not too long of a wait. if (queue.length > 0) { - await delay(200); + await delay(100); // Re-print the message in case the queue has become longer while waiting. clearConsole(report); infoLog(report, watcherEventMessage(queue)); From 9e11a05345311513ee2583ab1c68590bf1d086a1 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 16:28:24 +0100 Subject: [PATCH 54/67] Use async/await in tests/Parser.js --- tests/Parser.js | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/Parser.js b/tests/Parser.js index ad42f1a8..1eee84e8 100644 --- a/tests/Parser.js +++ b/tests/Parser.js @@ -4,17 +4,19 @@ const assert = require('assert'); const stream = require('stream'); const Parser = require('../lib/Parser'); -function testParser(elmCode, expectedExposedNames) { - return Parser.extractExposedPossiblyTests('SomeFile.elm', (_, options) => { - const readable = stream.Readable.from([elmCode], { - ...options, - autoDestroy: true, - }); - readable.close = readable.destroy; - return readable; - }).then((exposed) => { - assert.deepStrictEqual(exposed, expectedExposedNames); - }); +async function testParser(elmCode, expectedExposedNames) { + const exposed = await Parser.extractExposedPossiblyTests( + 'SomeFile.elm', + (_, options) => { + const readable = stream.Readable.from([elmCode], { + ...options, + autoDestroy: true, + }); + readable.close = readable.destroy; + return readable; + } + ); + assert.deepStrictEqual(exposed, expectedExposedNames); } describe('Parser', () => { @@ -185,14 +187,15 @@ One = 1 testParser('module "string" Main exposing (one)', [])); it('treats `effect module` as a critical error', () => - testParser( - 'effect module Example where { subscription = MySub } exposing (..)', - ['should', 'not', 'succeed'] - ).catch((error) => { - assert.strictEqual( - error.message, - 'This file is problematic:\n\nSomeFile.elm\n\nIt starts with `effect module`. Effect modules can only exist inside src/ in elm and elm-explorations packages. They cannot contain tests.' - ); - })); + assert.rejects( + testParser( + 'effect module Example where { subscription = MySub } exposing (..)', + ['should', 'not', 'succeed'] + ), + { + message: + 'This file is problematic:\n\nSomeFile.elm\n\nIt starts with `effect module`. Effect modules can only exist inside src/ in elm and elm-explorations packages. They cannot contain tests.', + } + )); }); }); From 62007fb1b94e0113a8023b8f41ca22c53650ab6c Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 16:48:51 +0100 Subject: [PATCH 55/67] Fix tests on Windows --- tests/ElmJson.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ElmJson.js b/tests/ElmJson.js index 3d9494a3..a7335524 100644 --- a/tests/ElmJson.js +++ b/tests/ElmJson.js @@ -34,7 +34,8 @@ describe('handling invalid elm.json', () => { const expected = fs .readFileSync(path.join(fullPath, 'expected.txt'), 'utf8') .trim() - .replace('/full/path/to/elm.json', path.join(fullPath, 'elm.json')); + .replace('/full/path/to/elm.json', path.join(fullPath, 'elm.json')) + .replace(/\r\n/g, '\n'); assert.throws(() => ElmJson.read(fullPath), { message: expected, }); From d2c09cf80586e61f81c58e01654f16baa30c3029 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 21 Nov 2020 17:01:24 +0100 Subject: [PATCH 56/67] Fix another test on Windows --- tests/fixtures/invalid-elm-json/json-syntax-error/elm.json | 5 ++--- .../fixtures/invalid-elm-json/json-syntax-error/expected.txt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json b/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json index 277cb795..e9904f6d 100644 --- a/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json +++ b/tests/fixtures/invalid-elm-json/json-syntax-error/elm.json @@ -1,5 +1,4 @@ -{ - "type": "application", +{ "type" "application", "source-directories": [ "src" ], @@ -17,7 +16,7 @@ }, "indirect": { "elm/html": "1.0.0", - "elm/virtual-dom": "1.0.2", + "elm/virtual-dom": "1.0.2" } } } diff --git a/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt b/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt index ee983e94..9985b459 100644 --- a/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt +++ b/tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt @@ -1,3 +1,3 @@ /full/path/to/elm.json Failed to read elm.json: -Unexpected token } in JSON at position 462 +Unexpected string in JSON at position 11 From e9280de001bc97a06c92b4bbfac2b5dcd2d27993 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 28 Nov 2020 11:49:35 +0100 Subject: [PATCH 57/67] Code review --- lib/FindTests.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/FindTests.js b/lib/FindTests.js index 030011c0..49058899 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -9,6 +9,8 @@ const Project = require('./Project'); void Project; +// We can replace this with using `Array.prototype.flatMap` once Node.js 10 is +// EOL 2021-04-30 and support for Node.js 10 is dropped. function flatMap/*:: */( array /*: Array */, f /*: (T) => Array */ @@ -59,10 +61,14 @@ function resolveCliArgGlob( fileGlob /*: string */, projectRootDir /*: string */ ) /*: Array */ { + // Globs passed as CLI arguments are relative to CWD, while elm-test + // operates from the project root dir. + const globRelativeToProjectRoot = path.relative( + projectRootDir, + path.resolve(fileGlob) + ); return flatMap( - // Globs passed as CLI arguments are relative to CWD, while elm-test - // operates from the project root dir. - glob.sync(path.relative(projectRootDir, path.resolve(fileGlob)), { + glob.sync(globRelativeToProjectRoot, { cwd: projectRootDir, nocase: true, absolute: true, From a426c29e4b1a6c227968d135327a9eec9e185fa5 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 28 Nov 2020 12:41:15 +0100 Subject: [PATCH 58/67] Trigger build From 46d7f776ea7dadbc806cbc13b0ed7bb180fadca2 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Tue, 8 Dec 2020 21:11:28 +0100 Subject: [PATCH 59/67] =?UTF-8?q?Make=20sure=20elm-stuff=20doesn=E2=80=99t?= =?UTF-8?q?=20grow=20indefinitely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As a bonus, functions are now either pure or side-effect-only without return values. It is very clear now how the compilation steps work. --- lib/Generate.js | 57 +++++++++++++++++++++++------------------------ lib/RunTests.js | 19 ++++++++-------- package-lock.json | 5 ----- package.json | 1 - 4 files changed, 37 insertions(+), 45 deletions(-) diff --git a/lib/Generate.js b/lib/Generate.js index c265d312..ed894c1e 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -2,7 +2,6 @@ const { supportsColor } = require('chalk'); const fs = require('fs'); -const Murmur = require('murmur-hash-js'); const path = require('path'); const ElmJson = require('./ElmJson'); const Project = require('./Project'); @@ -121,6 +120,20 @@ function generateElmJson(project /*: typeof Project.Project */) /*: void */ { } } +function getMainModule( + generatedCodeDir /*: string */ +) /*: { moduleName: string, path: string } */ { + const moduleName = ['Test', 'Generated', 'Main']; + return { + moduleName: moduleName.join('.'), + path: + // We'll be putting the generated Main in something like this: + // + // my-project-name/elm-stuff/generated-code/elm-community/elm-test/0.19.1-revisionX/src/Test/Generated/Main.elm + path.join(getGeneratedSrcDir(generatedCodeDir), ...moduleName) + '.elm', + }; +} + function generateMainModule( fuzz /*: number */, seed /*: number */, @@ -131,43 +144,24 @@ function generateMainModule( moduleName: string, possiblyTests: Array, }> */, - generatedCodeDir /*: string */, + mainModule /*: { moduleName: string, path: string } */, processes /*: number */ -) /*: string */ { +) /*: void */ { const testFileBody = makeTestFileBody( testModules, makeOptsCode(fuzz, seed, report, testFileGlobs, testFilePaths, processes) ); - // Generate a filename that incorporates the hash of file contents. - // This way, if you run e.g. `elm-test Foo.elm` and then `elm-test Bar.elm` - // and then re-run `elm-test Foo.elm` we still have a cached `Main` for - // `Foo.elm` (assuming none of its necessary imports have changed - and - // why would they?) so we don't have to recompile it. - const salt = Murmur.murmur3(testFileBody); - const moduleName = 'Main' + salt; - const mainPath = path.join( - getGeneratedSrcDir(generatedCodeDir), - 'Test', - 'Generated' - ); - const mainFile = path.join(mainPath, moduleName + '.elm'); - - // We'll be putting the generated Main in something like this: - // - // my-project-name/elm-stuff/generated-code/elm-community/elm-test/0.19.1-revisionX/src/Test/Generated/Main36138116.elm - const testFileContents = `module Test.Generated.${moduleName} exposing (main)\n\n${testFileBody}`; + const testFileContents = `module ${mainModule.moduleName} exposing (main)\n\n${testFileBody}`; - // Make sure src/Test/Generated/ exists so we can write the file there. - fs.mkdirSync(mainPath, { recursive: true }); + // Make sure the generated code dir exists so we can write the file there. + fs.mkdirSync(path.dirname(mainModule.path), { recursive: true }); // Always write the file, in order to update its timestamp. This is important, - // because if we run `elm-make Main123456.elm` and that file's timestamp did - // not change, elm-make will short-circuit and not recompile *anything* - even + // because if we run `elm make Main.elm` and that file's timestamp did + // not change, `elm make` will short-circuit and not recompile *anything* - even // if some of Main's dependencies (such as an individual test file) changed. - fs.writeFileSync(mainFile, testFileContents); - - return mainFile; + fs.writeFileSync(mainModule.path, testFileContents); } function makeTestFileBody( @@ -280,4 +274,9 @@ function makeElmString(string) { .replace(/\r/g, '\\r')}"`; } -module.exports = { prepareCompiledJsFile, generateElmJson, generateMainModule }; +module.exports = { + generateElmJson, + generateMainModule, + getMainModule, + prepareCompiledJsFile, +}; diff --git a/lib/RunTests.js b/lib/RunTests.js index f47852e6..9358420d 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -158,29 +158,28 @@ function runTests( watcher.unwatch(diff.removed); } - Generate.generateElmJson(project); - + runsExecuted++; + const pipeFilename = getPipeFilename(runsExecuted); const testModules = await FindTests.findTests(testFilePaths, project); + const mainModule = Generate.getMainModule(project.generatedCodeDir); + const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); + + Generate.generateElmJson(project); - const mainFile = Generate.generateMainModule( + Generate.generateMainModule( fuzz, seed, report, testFileGlobs, testFilePaths, testModules, - project.generatedCodeDir, + mainModule, processes ); - const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); - - runsExecuted++; - const pipeFilename = getPipeFilename(runsExecuted); - await Compile.compile( project.generatedCodeDir, - mainFile, + mainModule.path, dest, pathToElmBinary, report diff --git a/package-lock.json b/package-lock.json index 906a2d7a..451d1da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1837,11 +1837,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "murmur-hash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmur-hash-js/-/murmur-hash-js-1.0.0.tgz", - "integrity": "sha1-UEEEkmnJZjPIZjhpYLL0KJ515bA=" - }, "mustache": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.2.1.tgz", diff --git a/package.json b/package.json index d881773b..b7b2faec 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "elm-json": "^0.2.8", "glob": "^7.1.6", "graceful-fs": "^4.2.4", - "murmur-hash-js": "^1.0.0", "rimraf": "^3.0.2", "split": "^1.0.1", "which": "^2.0.2", From 9ca9939bd3970f7281e0b771fdec9ffe370f24ab Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:21:11 +0100 Subject: [PATCH 60/67] Remove outdated comment --- lib/Generate.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/Generate.js b/lib/Generate.js index ed894c1e..eaad1401 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -154,13 +154,8 @@ function generateMainModule( const testFileContents = `module ${mainModule.moduleName} exposing (main)\n\n${testFileBody}`; - // Make sure the generated code dir exists so we can write the file there. fs.mkdirSync(path.dirname(mainModule.path), { recursive: true }); - // Always write the file, in order to update its timestamp. This is important, - // because if we run `elm make Main.elm` and that file's timestamp did - // not change, `elm make` will short-circuit and not recompile *anything* - even - // if some of Main's dependencies (such as an individual test file) changed. fs.writeFileSync(mainModule.path, testFileContents); } From e1a1a45037c86cd384f750d72a12920dc4794643 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:21:26 +0100 Subject: [PATCH 61/67] Simplify after.js now that we always know the module name --- templates/after.js | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/templates/after.js b/templates/after.js index bd8da95e..49f29e1b 100644 --- a/templates/after.js +++ b/templates/after.js @@ -1,19 +1,3 @@ -// Make sure necessary things are defined. -if (typeof Elm === 'undefined') { - throw 'test runner config error: Elm is not defined. Make sure you provide a file compiled by Elm!'; -} - -var potentialModuleNames = Object.keys(Elm.Test.Generated); - -if (potentialModuleNames.length !== 1) { - console.error( - 'Multiple potential generated modules to run in the Elm.Test.Generated namespace: ', - potentialModuleNames, - ' - this should never happen!' - ); - process.exit(1); -} - var net = require('net'), client = net.createConnection(pipeFilename); @@ -26,10 +10,8 @@ client.on('error', function (error) { client.setEncoding('utf8'); client.setNoDelay(true); -var testModule = Elm.Test.Generated[potentialModuleNames[0]]; - // Run the Elm app. -var app = testModule.init({ flags: Date.now() }); +var app = Elm.Test.Generated.Main.init({ flags: Date.now() }); client.on('data', function (msg) { app.ports.receive.send(JSON.parse(msg)); From be5aa8583a56db9dbd9c66ca9dfd517dc902ecba Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:33:26 +0100 Subject: [PATCH 62/67] Remove unnecessary mutation when creating elm.json --- lib/Generate.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/Generate.js b/lib/Generate.js index eaad1401..e623a9bf 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -69,18 +69,7 @@ function generateElmJson(project /*: typeof Project.Project */) /*: void */ { fs.mkdirSync(generatedSrc, { recursive: true }); - let testElmJson = { - type: 'application', - 'source-directories': [], // these are added below - 'elm-version': '0.19.1', - dependencies: Solve.getDependenciesCached(project), - 'test-dependencies': { - direct: {}, - indirect: {}, - }, - }; - - testElmJson['source-directories'] = [ + const sourceDirs = [ // Include the generated test application. generatedSrc, @@ -102,6 +91,17 @@ function generateElmJson(project /*: typeof Project.Project */) /*: void */ { path.relative(project.generatedCodeDir, absolutePath) ); + const testElmJson = { + type: 'application', + 'source-directories': sourceDirs, + 'elm-version': '0.19.1', + dependencies: Solve.getDependenciesCached(project), + 'test-dependencies': { + direct: {}, + indirect: {}, + }, + }; + // Generate the new elm.json, if necessary. const generatedContents = JSON.stringify(testElmJson, null, 4); const generatedPath = ElmJson.getPath(project.generatedCodeDir); From e1469b3d28db4ce596020f48965b1fe288d2072f Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:34:14 +0100 Subject: [PATCH 63/67] Better variable name: installationScratchDir --- lib/Install.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Install.js b/lib/Install.js index 342dffcd..943ef5ca 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -14,18 +14,18 @@ function install( pathToElmBinary /*: string */, packageName /*: string */ ) /*: 'SuccessfullyInstalled' | 'AlreadyInstalled' */ { - const generatedCodeDir = path.join(project.generatedCodeDir, 'install'); + const installationScratchDir = path.join(project.generatedCodeDir, 'install'); try { // Recreate the directory to remove any artifacts from the last time // someone ran `elm-test install`. We do not delete this directory after // the installation finishes in case the user needs to debug the test run. - if (fs.existsSync(generatedCodeDir)) { + if (fs.existsSync(installationScratchDir)) { // We can replace this with `fs.rmdirSync(dir, { recursive: true })` // once Node.js 10 is EOL 2021-04-30 and support for Node.js 10 is dropped. - rimraf.sync(generatedCodeDir); + rimraf.sync(installationScratchDir); } - fs.mkdirSync(generatedCodeDir, { recursive: true }); + fs.mkdirSync(installationScratchDir, { recursive: true }); } catch (error) { throw new Error( `Unable to create temporary directory for elm-test install: ${error.message}` @@ -52,18 +52,18 @@ function install( 'source-directories': ['.'], }; - ElmJson.write(generatedCodeDir, tmpElmJson); + ElmJson.write(installationScratchDir, tmpElmJson); const result = spawn.sync(pathToElmBinary, ['install', packageName], { stdio: 'inherit', - cwd: generatedCodeDir, + cwd: installationScratchDir, }); if (result.status !== 0) { process.exit(result.status); } - const newElmJson = ElmJson.read(generatedCodeDir); + const newElmJson = ElmJson.read(installationScratchDir); if (newElmJson.type === 'package') { Object.keys(newElmJson['dependencies']).forEach(function (key) { From b5f5b73e745548bb5f1e2e94c46e9cad6f1e6133 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:45:23 +0100 Subject: [PATCH 64/67] Add missing newline at the end of a file --- tests/fixtures/write-elm-json/elm.input.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/write-elm-json/elm.input.json b/tests/fixtures/write-elm-json/elm.input.json index b31a08e5..d08792c4 100644 --- a/tests/fixtures/write-elm-json/elm.input.json +++ b/tests/fixtures/write-elm-json/elm.input.json @@ -22,4 +22,4 @@ "a": 1, "b": 2 } -} \ No newline at end of file +} From 596eb623aec56aea7935e728eedbd988e9ff0375 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:48:30 +0100 Subject: [PATCH 65/67] Improve comment in elm-test.js --- lib/elm-test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index e4ef37fa..296d576a 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -247,9 +247,10 @@ function main() { }); program - // Hack: This command has a name that isn’t likely to exist as a directory. - // If the command where called for example “tests” then `elm-test tests src` - // would only run tests in `src/`, not `tests/`. + // Hack: This command has a name that isn’t likely to exist as a directory + // containing tests. If the command were instead called "tests" then + // commander would interpret the `tests` in `elm-test tests src` as a + // command and only run tests in `src/`, ignoring all files in `tests/`. .command('__elmTestCommand__ [globs...]', { hidden: true, isDefault: true }) .action((testFileGlobs) => { const options = program.opts(); From 261b1a9c3502a628ef02870ed1b992782d182ef4 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 12 Dec 2020 18:50:32 +0100 Subject: [PATCH 66/67] Replace `findClosest` with `findClosestElmJson` --- lib/elm-test.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/elm-test.js b/lib/elm-test.js index 296d576a..9157f2b0 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -53,20 +53,17 @@ const parseReport = (flag /*: string */) => ( } }; -function findClosest( - dir /*: string */, - dirToPath /*: (dir: string) => string */ -) /*: string | void */ { - const entry = dirToPath(dir); +function findClosestElmJson(dir /*: string */) /*: string | void */ { + const entry = ElmJson.getPath(dir); return fs.existsSync(entry) ? entry : dir === path.parse(dir).root ? undefined - : findClosest(path.dirname(dir), dirToPath); + : findClosestElmJson(path.dirname(dir)); } function getProjectRootDir(subcommand /*: string */) { - const elmJsonPath = findClosest(process.cwd(), ElmJson.getPath); + const elmJsonPath = findClosestElmJson(process.cwd()); if (elmJsonPath === undefined) { const command = subcommand === 'tests' ? 'elm-test' : `elm-test ${subcommand}`; From 43b76cc15b2c0b69941c30cb06a3bd4edf77c26d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 13 Dec 2020 00:04:25 +0100 Subject: [PATCH 67/67] =?UTF-8?q?Enable=20Flow=E2=80=99s=20unclear-type=20?= =?UTF-8?q?lint=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flowconfig | 1 - lib/Compile.js | 12 ++++++++---- lib/Parser.js | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index 68b196d2..c1d6060f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -5,4 +5,3 @@ exact_by_default=true [lints] all=error untyped-import=off -unclear-type=off diff --git a/lib/Compile.js b/lib/Compile.js index 6a7c8064..d57be4ab 100644 --- a/lib/Compile.js +++ b/lib/Compile.js @@ -61,22 +61,26 @@ function spawnCompiler({ ignoreStdout, cwd }) { return ( pathToElm /*: string */, processArgs /*: Array */, - processOpts /*: Object */ + processOpts /*: child_process$spawnOpts */ ) => { - const finalOpts = Object.assign({ env: process.env }, processOpts, { + const finalOpts = { + env: process.env, + ...processOpts, cwd, stdio: [ process.stdin, ignoreStdout ? 'ignore' : process.stdout, process.stderr, ], - }); + }; return spawn(pathToElm, processArgs, finalOpts); }; } -function processOptsForReporter(report /*: typeof Report.Report */) { +function processOptsForReporter( + report /*: typeof Report.Report */ +) /*: child_process$spawnOpts */ { if (Report.isMachineReadable(report)) { return { env: process.env, stdio: ['ignore', 'ignore', process.stderr] }; } else { diff --git a/lib/Parser.js b/lib/Parser.js index 79bf2af1..b87542e8 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -28,7 +28,7 @@ function extractExposedPossiblyTests( filePath /*: string */, createReadStream /*: ( path: string, - options?: Object + options?: { encoding?: string, ... } ) => stream$Readable & { close(): void } */ ) /*: Promise> */ { return new Promise((resolve, reject) => {