elm-test
Version:
Run elm-test suites.
272 lines (229 loc) • 7.98 kB
JavaScript
// @flow
const gracefulFs = require('graceful-fs');
const fs = require('fs');
const { globSync } = require('tinyglobby');
const path = require('path');
const Parser = require('./Parser');
const Project = require('./Project');
void Project;
// 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<string> */,
projectRootDir /*: string */
) /*: Array<string> */ {
return Array.from(
new Set(
fileGlobs.flatMap((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.
[];
}
})
),
// 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)
);
}
function resolveCliArgGlob(
fileGlob /*: string */,
projectRootDir /*: string */
) /*: Array<string> */ {
// 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)
);
// The globs _have_ to use forwards slash as path separator, regardless of
// platform, making it unambiguous which characters are separators and which
// are escapes.
// Note: As far I can tell, escaping glob syntax has _never_ worked on
// Windows. In Elm, needing to escape glob syntax should be very rare, since
// Elm file paths must match the module name (letters only). So it’s probably
// more worth supporting `some\folder\*Test.elm` rather than escaping.
const pattern =
process.platform === 'win32'
? globRelativeToProjectRoot.replace(/\\/g, '/')
: globRelativeToProjectRoot;
return globSync(pattern, {
cwd: projectRootDir,
caseSensitiveMatch: false,
absolute: true,
ignore: ignoredDirsGlobs,
// Match directories as well
onlyFiles: false,
}).flatMap((filePath) =>
// Directories have their path end with `/`
filePath.endsWith('/') ? findAllElmFilesInDir(filePath) : filePath
);
}
// Recursively search for *.elm files.
function findAllElmFilesInDir(dir /*: string */) /*: Array<string> */ {
return globSync('**/*.elm', {
cwd: dir,
caseSensitiveMatch: false,
absolute: true,
ignore: ignoredDirsGlobs,
onlyFiles: true,
});
}
function findTests(
testFilePaths /*: Array<string> */,
project /*: typeof Project.Project */
) /*: Promise<Array<{ moduleName: string, possiblyTests: Array<string> }>> */ {
return Promise.all(
testFilePaths.map((filePath) => {
const matchingSourceDirs = project.testsSourceDirs.filter((dir) =>
filePath.startsWith(`${dir}${path.sep}`)
);
// Tests must be in tests/ or in source-directories – otherwise they won’t
// compile. Elm won’t be able to find imports.
switch (matchingSourceDirs.length) {
case 0:
return Promise.reject(
Error(
missingSourceDirectoryError(
filePath,
project.elmJson.type === 'package'
)
)
);
case 1:
// Keep going.
break;
default:
// This shouldn’t be possible for package projects.
return Promise.reject(
new Error(
multipleSourceDirectoriesError(
filePath,
matchingSourceDirs,
project.testsDir
)
)
);
}
// By finding the module name from the file path we can import it even if
// the file is full of errors. Elm will then report what’s wrong.
const moduleNameParts = path
.relative(matchingSourceDirs[0], filePath)
.replace(/\.elm$/, '')
.split(path.sep);
const moduleName = moduleNameParts.join('.');
if (!moduleNameParts.every(Parser.isUpperName)) {
return Promise.reject(
new Error(
badModuleNameError(filePath, matchingSourceDirs[0], moduleName)
)
);
}
return Parser.extractExposedPossiblyTests(
filePath,
// We’re reading files asynchronously in a loop here, so it makes sense
// to use graceful-fs to avoid “too many open files” errors.
gracefulFs.createReadStream
).then((possiblyTests) => ({
moduleName,
possiblyTests,
}));
})
);
}
function missingSourceDirectoryError(filePath, isPackageProject) {
return `
This file:
${filePath}
…matches no source directory! Imports won't work then.
${
isPackageProject
? 'Move it to tests/ or src/ in your project root.'
: 'Move it to tests/ in your project root, or make sure it is covered by "source-directories" in your elm.json.'
}
`.trim();
}
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:
${filePath}
…matches more than one source directory:
${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();
}
function badModuleNameError(filePath, sourceDir, moduleName) {
return `
This file:
${filePath}
…located in this directory:
${sourceDir}
…is problematic. Trying to construct a module name from the parts after the directory gives:
${moduleName}
…but module names need to look like for example:
Main
Http.Helpers
Make sure that all parts start with an uppercase letter and don't contain any spaces or anything like that.
`.trim();
}
function noFilesFoundError(
projectRootDir /*: string */,
testFileGlobs /*: Array<string> */
) /*: 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,
ignoredDirsGlobs,
noFilesFoundError,
resolveGlobs,
};