UNPKG

elm-test

Version:
281 lines (244 loc) 8.28 kB
// @flow const { supportsColor } = require('chalk'); const fs = require('fs'); const path = require('path'); const ElmJson = require('./ElmJson'); const Project = require('./Project'); const Report = require('./Report'); const Solve = require('./Solve'); void Project; void Report; const before = fs.readFileSync( path.join(__dirname, '..', 'templates', 'before.js'), 'utf8' ); const after = fs.readFileSync( path.join(__dirname, '..', 'templates', 'after.js'), 'utf8' ); function prepareCompiledJsFile( pipeFilename /*: string */, dest /*: string */ ) /*: void */ { const content = fs.readFileSync(dest, 'utf8'); const finalContent = ` ${before} var Elm = (function(module) { ${addKernelTestChecking(content)} return this.Elm; })({}); var pipeFilename = ${JSON.stringify(pipeFilename)}; ${after} `.trim(); fs.writeFileSync(dest, finalContent); } // For older versions of elm-explorations/test we need to list every single // variant of the `Test` type. To avoid having to update this regex if a new // variant is added, newer versions of elm-explorations/test have prefixed all // variants with `ElmTestVariant__` so we can match just on that. const testVariantDefinition = /^var\s+\$elm_explorations\$test\$Test\$Internal\$(?:ElmTestVariant__\w+|UnitTest|FuzzTest|Labeled|Skipped|Only|Batch)\s*=\s*(?:\w+\(\s*)?function\s*\([\w, ]*\)\s*\{\s*return *\{/gm; const checkDefinition = /^(var\s+\$author\$project\$Test\$Runner\$Node\$check)\s*=\s*\$author\$project\$Test\$Runner\$Node\$checkHelperReplaceMe___;?$/m; // Create a symbol, tag all `Test` constructors with it and make the `check` // function look for it. function addKernelTestChecking(content) { return ( 'var __elmTestSymbol = Symbol("elmTestSymbol");\n' + content .replace(testVariantDefinition, '$&__elmTestSymbol: __elmTestSymbol, ') .replace( checkDefinition, '$1 = value => value && value.__elmTestSymbol === __elmTestSymbol ? $elm$core$Maybe$Just(value) : $elm$core$Maybe$Nothing;' ) ); } function getGeneratedSrcDir(generatedCodeDir /*: string */) /*: string */ { return path.join(generatedCodeDir, 'src'); } async function generateElmJson( project /*: typeof Project.Project */, onSolveProgress /*: typeof Solve.OnProgress */ ) /*: Promise<void> */ { const generatedSrc = getGeneratedSrcDir(project.generatedCodeDir); fs.mkdirSync(generatedSrc, { recursive: true }); const sourceDirs = [ // Include the generated test application. generatedSrc, // 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.join(__dirname, '..', 'elm', 'src'), ] .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. (value, index, self) => self.indexOf(value) === index ) .map((absolutePath) => // Relative paths have the nice benefit that if the user moves their // directory, this doesn't break. path.relative(project.generatedCodeDir, absolutePath) ); const testElmJson = { type: 'application', 'source-directories': sourceDirs, 'elm-version': '0.19.1', dependencies: await Solve.getDependenciesCached(project, onSolveProgress), 'test-dependencies': { direct: {}, indirect: {}, }, }; // Generate the new elm.json, if necessary. const generatedContents = JSON.stringify(testElmJson, null, 4); 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` // to do a bunch of unnecessary work. if ( !fs.existsSync(generatedPath) || generatedContents !== fs.readFileSync(generatedPath, 'utf8') ) { // package projects don't explicitly list their transitive dependencies, // to we have to figure out what they are. We write the elm.json that // we have so far, and run elm to see what it thinks is missing. fs.writeFileSync(generatedPath, generatedContents); } } 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 */, report /*: typeof Report.Report */, testFileGlobs /*: Array<string> */, testFilePaths /*: Array<string> */, testModules /*: Array<{ moduleName: string, possiblyTests: Array<string>, }> */, mainModule /*: { moduleName: string, path: string } */, processes /*: number */ ) /*: void */ { const testFileBody = makeTestFileBody( testModules, makeOptsCode(fuzz, seed, report, testFileGlobs, testFilePaths, processes) ); const testFileContents = `module ${mainModule.moduleName} exposing (main)\n\n${testFileBody}`; fs.mkdirSync(path.dirname(mainModule.path), { recursive: true }); fs.writeFileSync(mainModule.path, testFileContents); } function makeTestFileBody( testModules /*: Array<{ moduleName: string, possiblyTests: Array<string>, }> */, optsCode /*: string */ ) /*: string */ { const imports = testModules.map((mod) => `import ${mod.moduleName}`); const possiblyTestsList = makeList(testModules.map(makeModuleTuple)); return ` ${imports.join('\n')} import Test.Reporter.Reporter exposing (Report(..)) import Console.Text exposing (UseColor(..)) import Test.Runner.Node import Test main : Test.Runner.Node.TestProgram main = Test.Runner.Node.run ${indentAllButFirstLine(' ', optsCode)} ${indentAllButFirstLine(' ', possiblyTestsList)} `.trim(); } function makeModuleTuple(mod /*: { moduleName: string, possiblyTests: Array<string>, } */) /*: string */ { const list = mod.possiblyTests.map( (test) => `Test.Runner.Node.check ${mod.moduleName}.${test}` ); return ` ( "${mod.moduleName}" , ${indentAllButFirstLine(' ', makeList(list))} ) `.trim(); } function makeList(parts /*: Array<string> */) /*: string */ { if (parts.length === 0) { return '[]'; } const list = parts.map( (part, index) => `${index === 0 ? '' : ', '}${indentAllButFirstLine(' ', part)}` ); return ` [ ${list.join('\n')} ] `.trim(); } function indentAllButFirstLine(indent, string) { return string .split('\n') .map((line, index) => (index === 0 ? line : indent + line)) .join('\n'); } function makeOptsCode( fuzz /*: number */, seed /*: number */, report /*: typeof Report.Report */, testFileGlobs /*: Array<string> */, testFilePaths /*: Array<string> */, processes /*: number */ ) /*: string */ { return ` { runs = ${fuzz} , report = ${generateElmReportVariant(report)} , seed = ${seed} , processes = ${processes} , globs = ${indentAllButFirstLine(' ', makeList(testFileGlobs.map(makeElmString)))} , paths = ${indentAllButFirstLine(' ', makeList(testFilePaths.map(makeElmString)))} } `.trim(); } function generateElmReportVariant( report /*: typeof Report.Report */ ) /*: string */ { switch (report) { case 'json': return 'JsonReport'; case 'junit': return 'JUnitReport'; case 'console': if (supportsColor) { return 'ConsoleReport UseColor'; } else { return 'ConsoleReport Monochrome'; } } } function makeElmString(string) { return `"${string .replace(/[\\"]/g, '\\$&') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r')}"`; } module.exports = { generateElmJson, generateMainModule, getMainModule, prepareCompiledJsFile, };