diff --git a/README.md b/README.md index ec510fcb..8c2146c3 100644 --- a/README.md +++ b/README.md @@ -221,3 +221,37 @@ To run a tool installed locally using `npm` you can use `npx`: `npx` adds the local `node_modules/.bin/` folder to `$PATH` when it executes the command passed to it. This means that if you have installed `elm` locally, `elm-test` will automatically find that local installation. As mentioned in [Installation](#installation) we recommend installing elm-test locally in every project. This ensures all contributors and CI use the same version, to avoid nasty “works on my computer” issues. + +### --dependencies + +This is useful when developing an Elm Package (`"type": "package"` in elm.json). + + elm-test --dependencies oldest + +Let’s say your package has the dependency `"elm/json": "1.0.0 <= v < 2.0.0"`. That allows a whole range of `elm/json` versions to be used with your package. Exactly which version of `elm/json` is going to be used in tests? Does it matter? + +Turns it it _does_ matter sometimes. For example, `elm/json` 1.1.0 added the `Json.Decode.oneOrMore` function. Let’s say you start using `oneOrMore` in your package. If the tests run with 1.1.0 or later, they are going to pass. But if they run with 1.0.0 (also allowed by the range), the tests are not even going to compile, because `oneOrMore` does not exist. Oops! + +When running `elm make` in a package project, the Elm compiler uses the _latest_ version permitted by the dependency ranges and makes sure that your project compiles. But it does not stop you from publishing a package with a too low version boundary. + +Tests to the rescue! By using `elm-test --dependencies oldest` your tests are going to be compiled and run with the _oldest_ permitted versions of your dependencies. If your use of the `oneOrMore` function has test coverage, the tests are going to fail (not compile)! The solution is to bump the lower bound: `"elm-test": "1.1.0 <= v < 2.0.0"`. + +`--dependencies` defaults to `newest`, because that was the behavior before the flag was added, and it is less surprising since it does the same thing as a plain `elm make`. But not now that you know about this little gotcha, go ahead and start using `--dependencies oldest` for your package! + +If you do start using `--dependencies oldest`, remember that your tests could fail due to bugs in a dependency that have been fixed in a later version. If that turns out to be the case, bump the lower bound. + +Note: Even with `--dependencies oldest` there are still edge cases. In the example above, let’s say your package also has another dependency, and that dependency in turn also depends on `elm/json`. But it has already specified that it wants at least 1.1.0. Then `--dependencies oldest` has no choice but installing 1.1.0, even if _your_ range allows 1.0.0. So `--dependencies oldest` is no guarantee that your lower version bounds are correct, but it does make it more likely. + +The flag is ignored for applications (`"type": "application"` in elm.json), because for applications all dependency versions are specified exactly (no ranges). (In rare edge cases, there _can_ be situations where your pinned _indirect_ dependencies can’t be honored perfectly, due to the merge between regular dependencies, test dependencies and the test runner dependencies that elm-test has to perform. But then we let the solver pick a working version and don’t consider the `--dependencies` flag.) + +If you use this together with `--offline`, beware that “oldest” and “newest” refer to what you packages you have on disk on your computer, not what the actually oldest and newest versions available on the package site are. Going back to the example with `"elm-json": "1.0.0 <= v < 2.0.0"`, if the only `elm/json` version you have on your computer is 1.1.0 then that’s what you’re gonna get with `--dependencies oldest --offline`. Even though 1.0.0 exists on the Internet, the tests are going to use 1.1.0 and therefore _not_ fail (as they would have with 1.0.0). + +### --offline + +Tell elm-test to fail instead of making HTTP request when “solving dependencies:” + + elm-test --offline + +Before running tests, elm-test needs to merge your regular dependencies, test dependencies and dependencies of the test runner, and find a working set of versions. When doing so, elm-test needs to ask the package server for available versions of packages. The results are cached in `~/.elm` (`$ELM_HOME`). If you already have a cache that is supposed to be up-to-date cache, and want elm-test to fail instead of making HTTP requests to the package server if it isn’t, pass `--offline`. + +Note: `--offline` only controls HTTP requests that elm-test makes directly. The Elm compiler might still make HTTP requests. diff --git a/example-package/elm.json b/example-package/elm.json index 18360111..dc6b7997 100644 --- a/example-package/elm.json +++ b/example-package/elm.json @@ -10,6 +10,7 @@ "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { "elm/core": "1.0.0 <= v < 2.0.0", + "elm/json": "1.0.0 <= v < 2.0.0", "elm/project-metadata-utils": "1.0.0 <= v < 2.0.0" }, "test-dependencies": { diff --git a/example-package/tests/DependenciesTest.elm b/example-package/tests/DependenciesTest.elm new file mode 100644 index 00000000..ad2727f9 --- /dev/null +++ b/example-package/tests/DependenciesTest.elm @@ -0,0 +1,22 @@ +module DependenciesTest exposing (testOneOrMore) + +import Expect +import Json.Decode as Decode +import Test exposing (Test, test) + + +{-| This test should pass with `--dependencies newest`, +but fail with `--dependencies oldest`, since the range +in elm.json is: `"elm/json": "1.0.0 <= v < 2.0.0"` and +`oneOrMore` was added in 1.1.0. +-} +testOneOrMore : Test +testOneOrMore = + test "Json.Decode.oneOrMore (added in elm/json 1.1.0) works" <| + \() -> + let + decoder = + Decode.oneOrMore (::) Decode.int + in + Decode.decodeString decoder "[1, 2, 3]" + |> Expect.equal (Ok [ 1, 2, 3 ]) diff --git a/lib/DependencyProvider.js b/lib/DependencyProvider.js index 8a334295..627f677c 100644 --- a/lib/DependencyProvider.js +++ b/lib/DependencyProvider.js @@ -119,7 +119,7 @@ class OnlineVersionsCache { } } -class OnlineAvailableVersionLister { +class AvailableVersionLister { /** * Memoization cache to avoid doing the same work twice in list. * @type { Map> } @@ -132,19 +132,18 @@ class OnlineAvailableVersionLister { * @param {OnlineVersionsCache} onlineCache */ constructor(onlineCache) { - onlineCache.update(); this.onlineCache = onlineCache; } /** * @param { string } pkg - * @param { undefined | string } pinnedVersion + * @param { OrderConstraints } orderConstraints * @returns { Array } */ - list(pkg, pinnedVersion) { + list(pkg, orderConstraints) { const memoVersions = this.memoCache.get(pkg); if (memoVersions !== undefined) { - return prioritizePinnedIndirectVersion(memoVersions, pinnedVersion); + return applyOrderConstraints(memoVersions, orderConstraints); } const offlineVersions = readVersionsInElmHomeAndSort(pkg); const allVersionsSet = new Set(this.onlineCache.getVersions(pkg)); @@ -154,32 +153,7 @@ class OnlineAvailableVersionLister { } const allVersions = [...allVersionsSet].sort(flippedSemverCompare); this.memoCache.set(pkg, allVersions); - return prioritizePinnedIndirectVersion(allVersions, pinnedVersion); - } -} - -class OfflineAvailableVersionLister { - /** - * Memoization cache to avoid doing the same work twice in list. - * @type { Map> } - */ - cache = new Map(); - - /** - * @param { string } pkg - * @param { undefined | string } pinnedVersion - * @returns { Array } - */ - list(pkg, pinnedVersion) { - const memoVersions = this.cache.get(pkg); - if (memoVersions !== undefined) { - return prioritizePinnedIndirectVersion(memoVersions, pinnedVersion); - } - - const offlineVersions = readVersionsInElmHomeAndSort(pkg); - - this.cache.set(pkg, offlineVersions); - return prioritizePinnedIndirectVersion(offlineVersions, pinnedVersion); + return applyOrderConstraints(allVersions, orderConstraints); } } @@ -206,7 +180,7 @@ function readVersionsInElmHomeAndSort(pkg) { * When doing `import('./DependencyProvider').DependencyProvider`, * TypeScript says `DependencyProvider` is not exported for some reason. * But `import('./DependencyProvider').DependencyProviderType` works. - * @typedef {DependencyProvider} DependencyProviderType + * @typedef { DependencyProvider } DependencyProviderType */ class DependencyProvider { @@ -219,67 +193,30 @@ class DependencyProvider { } /** - * Solve dependencies completely offline, without any http request. - * - * @param { string } elmJson + * @param { boolean } offline + * @param { import('./ElmJson').ElmJson } elmJson * @param { boolean } useTest * @param { Record } extra + * @param { PackageStrategy } packageStrategy * @returns { string } */ - solveOffline(elmJson, useTest, extra) { - const lister = new OfflineAvailableVersionLister(); - const dependencies = JSON.parse(elmJson).dependencies; - const indirectDeps = - dependencies === undefined ? undefined : dependencies.indirect; + solve(offline, elmJson, useTest, extra, packageStrategy) { + const lister = new AvailableVersionLister(this.cache); try { return wasm.solve_deps( - elmJson, + JSON.stringify(elmJson), useTest, extra, - fetchElmJsonOffline, + offline + ? fetchElmJsonOffline + : fetchElmJsonOnline.bind(null, this.syncHttpGet), /** @type { (pkg: string) => Array } */ (pkg) => - lister.list( - pkg, - indirectDeps === undefined ? undefined : indirectDeps[pkg] - ) + lister.list(pkg, toOrderConstraints(elmJson, packageStrategy, pkg)) ); } catch (errorMessage) { - throw new Error(errorMessage); - } - } - - /** - * Solve dependencies with http requests when required. - * - * @param { string } elmJson - * @param { boolean } useTest - * @param { Record } extra - * @returns { string } - */ - solveOnline(elmJson, useTest, extra) { - const lister = new OnlineAvailableVersionLister(this.cache); - const dependencies = JSON.parse(elmJson).dependencies; - const indirectDeps = - dependencies === undefined ? undefined : dependencies.indirect; - - try { - return wasm.solve_deps( - elmJson, - useTest, - extra, - /** @type { (pkg: string, version: string) => string } */ - (pkg, version) => fetchElmJsonOnline(this.syncHttpGet, pkg, version), - /** @type { (pkg: string) => Array } */ - (pkg) => - lister.list( - pkg, - indirectDeps === undefined ? undefined : indirectDeps[pkg] - ) - ); - } catch (errorMessage) { - throw new Error(errorMessage); + throw new Error(`Failed to solve dependencies:\n${errorMessage}`); } } } @@ -350,6 +287,59 @@ function onlineVersionsFromScratch(syncHttpGet, cachePath, remotePackagesUrl) { // Helper functions ################################################## +/** + * @typedef { + | { for: 'application', pinnedVersion: string | undefined } + | { for: 'package', strategy: PackageStrategy } + } OrderConstraints + * + * @typedef { 'newest' | 'oldest' } PackageStrategy + * + * @param { import('./ElmJson').ElmJson } elmJson + * @param { PackageStrategy } packageStrategy + * @param { string } pkg + * @returns { OrderConstraints } + */ +function toOrderConstraints(elmJson, packageStrategy, pkg) { + switch (elmJson.type) { + case 'application': + return { + for: 'application', + pinnedVersion: elmJson.dependencies.indirect[pkg], + }; + + case 'package': + return { + for: 'package', + strategy: packageStrategy, + }; + } +} + +/** + * @param { Array } versions + * @param { OrderConstraints } orderConstraints + * @returns { Array } + */ +function applyOrderConstraints(versions, orderConstraints) { + switch (orderConstraints.for) { + case 'application': + return prioritizePinnedIndirectVersion( + versions, + orderConstraints.pinnedVersion + ); + + case 'package': + switch (orderConstraints.strategy) { + case 'newest': + return versions; + + case 'oldest': + return versions.slice().reverse(); + } + } +} + /** * Enforces respecting pinned indirect dependencies. * diff --git a/lib/ElmJson.js b/lib/ElmJson.js index acf789e7..1fdca3e1 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -253,9 +253,25 @@ function getElmExplorationsTestPackageVersionOrRange(elmJson) { } } +/** + * @param { string } dir + * @returns { number } + */ +function mtime(dir) { + const elmJsonPath = getPath(dir); + + try { + const stats = fs.statSync(elmJsonPath); + return stats.mtimeMs; + } catch (_) { + return 0; + } +} + module.exports = { ELM_TEST_PACKAGE: ELM_TEST_PACKAGE, getPath: getPath, + mtime: mtime, parseDirectAndIndirectDependencies: parseDirectAndIndirectDependencies, read: read, requireElmTestPackage: requireElmTestPackage, diff --git a/lib/Generate.js b/lib/Generate.js index 982c1124..143043f4 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -87,10 +87,19 @@ function getGeneratedSrcDir(generatedCodeDir) { /** * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./DependencyProvider').PackageStrategy } packageStrategy * @param { import('./Project').Project } project + * @param { boolean } offline + * @param { () => void } onBeforeSolve * @returns { void } */ -function generateElmJson(dependencyProvider, project) { +function generateElmJson( + dependencyProvider, + packageStrategy, + project, + offline, + onBeforeSolve +) { const generatedSrc = getGeneratedSrcDir(project.generatedCodeDir); fs.mkdirSync(generatedSrc, { recursive: true }); @@ -121,7 +130,13 @@ function generateElmJson(dependencyProvider, project) { type: 'application', 'source-directories': sourceDirs, 'elm-version': '0.19.2', - dependencies: Solve.getDependenciesCached(dependencyProvider, project), + dependencies: Solve.getDependenciesCached( + dependencyProvider, + packageStrategy, + project, + offline, + onBeforeSolve + ), 'test-dependencies': { direct: {}, indirect: {}, diff --git a/lib/Project.js b/lib/Project.js index 2d01bf92..4e49cfbc 100644 --- a/lib/Project.js +++ b/lib/Project.js @@ -9,6 +9,7 @@ const ElmJson = require('./ElmJson'); generatedCodeDir: string, testsSourceDirs: Array, elmJson: import('./ElmJson').ElmJson, + elmJsonMtime: number, } } Project */ @@ -33,6 +34,7 @@ function init(rootDir, version) { const shouldAddTestsDirAsSource = fs.existsSync(testsDir); const elmJson = ElmJson.read(rootDir); + const elmJsonMtime = ElmJson.mtime(rootDir); const projectSourceDirs = elmJson.type === 'package' ? ['src'] : elmJson['source-directories']; @@ -47,14 +49,7 @@ function init(rootDir, version) { ? resolvedSourceDirs.concat([testsDir]) : resolvedSourceDirs; - const generatedCodeDir = path.join( - rootDir, - 'elm-stuff', - 'generated-code', - 'elm-community', - 'elm-test', - version - ); + const generatedCodeDir = getGeneratedCodeDir(rootDir, version); return { rootDir, @@ -62,9 +57,26 @@ function init(rootDir, version) { generatedCodeDir, testsSourceDirs, elmJson, + elmJsonMtime, }; } +/** + * @param { string } rootDir + * @param { string } version + * @returns { string } + */ +function getGeneratedCodeDir(rootDir, version) { + return path.join( + rootDir, + 'elm-stuff', + 'generated-code', + 'elm-community', + 'elm-test', + version + ); +} + /* We do this validation ourselves to avoid the ../../../../../ in Elm’s error message: -- MISSING SOURCE DIRECTORY ------------------------------------------- elm.json @@ -123,6 +135,7 @@ ${message} } module.exports = { + getGeneratedCodeDir: getGeneratedCodeDir, getTestsDir: getTestsDir, init: init, validateTestsSourceDirs: validateTestsSourceDirs, diff --git a/lib/RunTests.js b/lib/RunTests.js index 2f4e54b3..fc257a4d 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -144,6 +144,8 @@ function watcherEventMessage(queue) { report: import('./Report').Report, seed: number, fuzz: number, + dependencies: import('./DependencyProvider').PackageStrategy, + offline: boolean, } } Options * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider @@ -160,7 +162,15 @@ function runTests( pathToElmBinary, testFileGlobs, processes, - { watch, clearConsole, report, seed, fuzz } + { + watch, + clearConsole, + report, + seed, + fuzz, + dependencies: packageStrategy, + offline, + } ) { /** @type { import('chokidar').FSWatcher | undefined } */ let watcher = undefined; @@ -242,7 +252,15 @@ function runTests( const mainModule = Generate.getMainModule(project.generatedCodeDir); const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); - Generate.generateElmJson(dependencyProvider, project); + Generate.generateElmJson( + dependencyProvider, + packageStrategy, + project, + offline, + () => { + progressLogger.log('Solving dependencies'); + } + ); progressLogger.log('Compiling'); diff --git a/lib/Solve.js b/lib/Solve.js index 00e8d974..c9a4eb51 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -13,35 +13,76 @@ function sha256(string) { /** * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./DependencyProvider').PackageStrategy } packageStrategy * @param { import('./Project').Project } project + * @param { boolean } offline + * @param { () => void } onBeforeSolve * @returns { import('./ElmJson').DirectAndIndirectDependencies } */ -function getDependenciesCached(dependencyProvider, project) { - const hash = sha256( - JSON.stringify({ - dependencies: project.elmJson.dependencies, - 'test-dependencies': project.elmJson['test-dependencies'], - }) - ); +function getDependenciesCached( + dependencyProvider, + packageStrategy, + project, + offline, + onBeforeSolve +) { + /** @type { string | undefined } */ + let cacheFile = undefined; - const cacheFile = path.join( - project.generatedCodeDir, - `dependencies.${hash}.json` - ); + // For packages, in offline mode we get _some_ version + // that happened to be available offline and happened to + // be newest or oldest at the time. So we can’t cache that + // result. An online run between two offline runs can alter + // what the second offline run results in. + if (!(project.elmJson.type === 'package' && offline)) { + const hash = sha256( + JSON.stringify({ + // For packages, when we want the newest version available + // for each dependency’s range, we in theory can’t cache + // anything and must look for potential new versions on the + // package site at every build. But when running the tests + // a hundred times to fix a bug, that is a waste of time + // and server resources. We therefore do the same thing as + // the Elm compiler itself: We _do_ cache, and use the mtime + // of elm.json in the cache key. That is not perfect, but it + // is what the compiler does, so we following along. + mtime: + project.elmJson.type === 'package' && packageStrategy === 'newest' + ? project.elmJsonMtime + : undefined, + dependencies: project.elmJson.dependencies, + 'test-dependencies': project.elmJson['test-dependencies'], + }) + ); - try { - return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); - } catch (error) { - if (error.code !== 'ENOENT') { - console.warn( - `Ignoring bad dependencies cache file:\n\n${error.message}\n\nPlease report this issue: https://github.com/rtfeldman/node-test-runner/issues/new` - ); + cacheFile = path.join( + project.generatedCodeDir, + `dependencies.${hash}.json` + ); + + try { + return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') { + console.warn( + `Ignoring bad dependencies cache file:\n\n${error.message}\n\nPlease report this issue: https://github.com/rtfeldman/node-test-runner/issues/new` + ); + } } } - const dependencies = getDependencies(dependencyProvider, project.elmJson); + onBeforeSolve(); - fs.writeFileSync(cacheFile, dependencies); + const dependencies = getDependencies( + dependencyProvider, + packageStrategy, + project.elmJson, + offline + ); + + if (cacheFile !== undefined) { + fs.writeFileSync(cacheFile, dependencies); + } return ElmJson.parseDirectAndIndirectDependencies( JSON.parse(dependencies), @@ -51,11 +92,31 @@ function getDependenciesCached(dependencyProvider, project) { /** * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./DependencyProvider').PackageStrategy } packageStrategy * @param { import('./ElmJson').ElmJson } elmJson + * @param { boolean } offline * @returns { string } */ -function getDependencies(dependencyProvider, elmJson) { +function getDependencies( + dependencyProvider, + packageStrategy, + elmJson, + offline +) { + // Applications have exact versions for all dependencies. If we find a solution + // using local information, that is THE solution and there is no need to ask the + // package site for anything. + // Packages, on the other hand, have _ranges_ for their dependencies. We can still + // find a solution using only local information, but we can’t know that it is the + // oldest or newest version out of all versions on the package site – only out of + // the versions available offline. So for packages we need to ask the package server + // about things _before_ trying to solve anything. + if (elmJson.type === 'package' && !offline) { + dependencyProvider.cache.update(); + } + const useTest = true; + // Note: These are the dependencies listed in `elm/elm.json`, except // `elm-explorations/test`. `elm/elm.json` is only used during development of // this CLI (for editor integrations and unit tests). When running `elm-test` @@ -67,11 +128,33 @@ function getDependencies(dependencyProvider, elmJson) { 'elm/time': '1.0.0 <= v < 2.0.0', 'elm/random': '1.0.0 <= v < 2.0.0', }; - const elmJsonStr = JSON.stringify(elmJson); + try { - return dependencyProvider.solveOffline(elmJsonStr, useTest, extra); - } catch (_) { - return dependencyProvider.solveOnline(elmJsonStr, useTest, extra); + return dependencyProvider.solve( + /* offline */ true, + elmJson, + useTest, + extra, + packageStrategy + ); + } catch (error) { + if (offline) { + throw error; + } + + // As mentioned above, for applications we only need to ask the package server + // for information if the local information wasn’t enough. + if (elmJson.type === 'application') { + dependencyProvider.cache.update(); + } + + return dependencyProvider.solve( + /* offline */ false, + elmJson, + useTest, + extra, + packageStrategy + ); } } diff --git a/lib/elm-test.js b/lib/elm-test.js index fd1f1982..865f4dba 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -208,6 +208,18 @@ function main() { 'Use a custom path to an Elm executable (default: elm)', undefined ) + .addOption( + new Option( + '--dependencies ', + 'Choose newest or oldest compatible dependencies for a package project (ignored for applications)' + ) + .default('newest') + .choices(['newest', 'oldest']) + ) + .option( + '--offline', + 'Do not make any HTTP requests when solving dependencies' + ) // Ensure compatibility with editor plugins setting --output=/dev/null when // running `make`. This has caused issues with Emacs' flycheck-elm package. .addOption( @@ -280,7 +292,15 @@ function main() { const pathToElmBinary = getPathToElmBinary(options['compiler']); const project = getProject('make'); const make = async () => { - Generate.generateElmJson(dependencyProvider, project); + Generate.generateElmJson( + dependencyProvider, + options['dependencies'], + project, + options['offline'], + () => { + // Don’t log that we are solving dependencies in `make` mode. + } + ); await Compile.compileSources( FindTests.resolveGlobs( testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs, diff --git a/tests/flags.js b/tests/flags.js index 4a01c48f..30ee4582 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -8,6 +8,9 @@ const os = require('os'); const readline = require('readline'); const which = require('which'); const { fixturesDir, spawnOpts, dummyBinPath } = require('./util'); +const Project = require('../lib/Project'); +const packageInfo = require('../package.json'); +const elmTestVersion = packageInfo.version; const rootDir = path.join(__dirname, '..'); const elmTestPath = path.join(rootDir, 'bin', 'elm-test'); @@ -42,7 +45,7 @@ function elmTestWithYes(args, callback) { * @param { Array } args * @param { string } [cwd] * @param { import('child_process').SpawnOptions } extraOpts - * @returns + * @returns { import('child_process').SpawnSyncReturns } */ function execElmTest(args, cwd = fixturesDir, extraOpts = {}) { return spawnSync( @@ -536,6 +539,94 @@ describe('flags', () => { }); }); + describe('--dependencies', () => { + /** + * @param { Array} args + * @returns { import('child_process').SpawnSyncReturns } + */ + const runTest = (args) => + execElmTest( + [...args, 'tests/DependenciesTest.elm'], + path.join(rootDir, 'example-package') + ); + + describe('run tests', () => { + it('Should detect a too low version boundary by not compiling when using --dependencies oldest', () => { + const runResult = runTest(['--dependencies=oldest']); + assert.strictEqual(runResult.status, 1); + assert.strictEqual(runResult.stderr.includes('oneOrMore'), true); + }); + + it('Should compile when using --dependencies newest', () => { + const runResult = runTest(['--dependencies=newest']); + assert.strictEqual(runResult.status, 0); + }); + + it('Should also compile when not using the flag since it defaults to latest', () => { + const runResult = runTest(['--dependencies=newest']); + assert.strictEqual(runResult.status, 0); + }); + }); + + describe('make', () => { + it('Should detect a too low version boundary by not compiling when using --dependencies oldest', () => { + const runResult = runTest(['make', '--dependencies=oldest']); + assert.strictEqual(runResult.status, 1); + assert.strictEqual(runResult.stderr.includes('oneOrMore'), true); + }); + + it('Should compile when using --dependencies newest', () => { + const runResult = runTest(['make', '--dependencies=newest']); + assert.strictEqual(runResult.status, 0); + }); + + it('Should also compile when not using the flag since it defaults to latest', () => { + const runResult = runTest(['make', '--dependencies=newest']); + assert.strictEqual(runResult.status, 0); + }); + }); + }); + + describe('--offline', () => { + it('Should fail if ELM_HOME is empty', () => { + const elmHome = path.join(fixturesDir, 'elm-stuff', 'elm-home'); + const generatedCodeDir = Project.getGeneratedCodeDir( + fixturesDir, + elmTestVersion + ); + for (const name of fs.readdirSync(generatedCodeDir)) { + if (name.startsWith('dependencies.') && name.endsWith('.json')) { + fs.unlinkSync(path.join(generatedCodeDir, name)); + } + } + + const runResult = execElmTest( + ['--offline', path.join('tests', 'Passing', 'One.elm')], + fixturesDir, + { + env: Object.assign({}, spawnOpts.env, { ELM_HOME: elmHome }), + } + ); + assert.strictEqual(runResult.status, 1); + assert.strictEqual( + runResult.stderr.includes('Failed to solve dependencies'), + true + ); + }); + + it('Should succeed if a previous run succeeded', () => { + const runResult1 = execElmTest([ + path.join('tests', 'Passing', 'One.elm'), + ]); + assert.strictEqual(runResult1.status, 0); + const runResult2 = execElmTest([ + '--offline', + path.join('tests', 'Passing', 'One.elm'), + ]); + assert.strictEqual(runResult2.status, 0); + }); + }); + describe('--watch', () => { it('Should fail if given a value', () => { const runResult = execElmTest([