diff --git a/.eslintrc.js b/.eslintrc.js index d745936ef..943b3d18a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -162,7 +162,7 @@ module.exports = { }, overrides: [ { - files: ['./*.js'], + files: ['**/*.js', '**/*.mjs'], rules: { // Not compatible with JSDoc according https://github.com/typescript-eslint/typescript-eslint/issues/8955#issuecomment-2097518639 '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/documentation/tooling-integration.md b/documentation/tooling-integration.md index 07cef6304..0a26ddc57 100644 --- a/documentation/tooling-integration.md +++ b/documentation/tooling-integration.md @@ -169,7 +169,7 @@ In that case, we (should) still report errors as JSON, with the following format - `message`: The description of the problem. It is an array like `formatted` for review errors, even though at this moment it only contains a string that has been trimmed and where colors have been removed. - `stack` (optional): The original JavaScript runtime stacktrace. Only sent if you run with `--debug`. -You should only have to listen to the CLI's standard output (`stdout`), and should not have to listen to the standard error output (`stderr`). +You should only have to listen to the CLI's standard output stream (`stdout`), and should not listen to the standard error stream (`stderr`). ## Things that may help you diff --git a/lib/main.js b/lib/main.js index 15ae4bf49..5f333d99c 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,3 +1,4 @@ +// TODO(@lishaduck): Create a `setup` function. process.title = 'elm-review'; if (!process.argv.includes('--no-color')) { @@ -34,8 +35,10 @@ const Watch = require('./watch'); /** * @type {Options} */ +// TODO(@lishaduck): Centralize all calls to just this & move into main. const options = AppState.getOptions(); +// TODO(@lishaduck): Move to aforementioned `setup` function. process.on('uncaughtException', errorHandler); process.on('unhandledRejection', errorHandler); diff --git a/lib/types/json.ts b/lib/types/json.ts index d1f156d7c..0441a88c8 100644 --- a/lib/types/json.ts +++ b/lib/types/json.ts @@ -1,9 +1,9 @@ /** - * A replacer function for JSON.stringify. + * A replacer function for {@linkcode JSON.stringify}. */ export type Replacer = (key: string, value: unknown) => unknown; /** - * A reviver function for JSON.parse. + * A reviver function for {@linkcode JSON.parse}. */ export type Reviver = (key: string, value: unknown) => unknown; diff --git a/lib/types/options.ts b/lib/types/options.ts index 4abdfabde..03bcae078 100644 --- a/lib/types/options.ts +++ b/lib/types/options.ts @@ -7,7 +7,7 @@ export type OptionsBase = { enableExtract: boolean; unsuppress: boolean | string[]; detailsMode: DetailsMode; - report: ReportMode; + report: ReportMode | null; rulesFilter: string[] | null; ignoreProblematicDependencies: boolean; }; diff --git a/package-lock.json b/package-lock.json index f87c0b71e..2fc9ca388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,8 @@ "ls-engines": "^0.9.3", "prettier": "^2.8.8", "turbo": "^2.2.3", - "typescript": "~5.6.3" + "typescript": "~5.6.3", + "zx": "^8.2.4" }, "engines": { "node": "14 >=14.21 || 16 >=16.20 || 18 || 20 || >=22" @@ -2057,6 +2058,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -10414,6 +10425,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "optional": true + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -10799,6 +10817,43 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zx": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.2.4.tgz", + "integrity": "sha512-g9wVU+5+M+zVen/3IyAZfsZFmeqb6vDfjqFggakviz5uLK7OAejOirX+jeTOkyvAh/OYRlCgw+SdqzN7F61QVQ==", + "dev": true, + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + }, + "optionalDependencies": { + "@types/fs-extra": ">=11", + "@types/node": ">=20" + } + }, + "node_modules/zx/node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "optional": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/zx/node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "optional": true, + "dependencies": { + "undici-types": "~6.20.0" + } } }, "dependencies": { @@ -12333,6 +12388,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -18435,6 +18500,13 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "optional": true + }, "unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -18734,6 +18806,39 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zx": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.2.4.tgz", + "integrity": "sha512-g9wVU+5+M+zVen/3IyAZfsZFmeqb6vDfjqFggakviz5uLK7OAejOirX+jeTOkyvAh/OYRlCgw+SdqzN7F61QVQ==", + "dev": true, + "requires": { + "@types/fs-extra": ">=11", + "@types/node": ">=20" + }, + "dependencies": { + "@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "optional": true, + "requires": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "optional": true, + "requires": { + "undici-types": "~6.20.0" + } + } + } } } } diff --git a/package.json b/package.json index a8f6d6be9..31e8c059c 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,9 @@ "prettier-check": "prettier . --check --cache", "prettier-fix": "prettier . --write --cache", "test": "turbo run testing check-engines --continue", - "test-sync": "npm run test-run && npm run jest", - "test-run": "(cd test/ && ./run.sh)", - "test-run-record": "(cd test/ && ./run.sh record)", + "test-sync": "npm run jest && npm run test-run", + "test-run": "(cd test/ && node ./run.mjs)", + "test-run-record": "(cd test/ && node ./run.mjs record)", "tsc": "tsc", "tsc-watch": "tsc --watch" }, @@ -111,7 +111,8 @@ "ls-engines": "^0.9.3", "prettier": "^2.8.8", "turbo": "^2.2.3", - "typescript": "~5.6.3" + "typescript": "~5.6.3", + "zx": "^8.2.4" }, "packageManager": "npm@8.19.4+sha512.dc700d97c8bd0ca9d403cf4fe0a12054d376f048d27830a6bc4a9bcce02ec42143cdd059ce3525f7dce09c6a4e52e9af5b996f268d8729c8ebb1cfad7f2bf51f", "engines": { diff --git a/test/jest-helpers/cli.js b/test/jest-helpers/cli.js index 180a0333d..124cb33d8 100644 --- a/test/jest-helpers/cli.js +++ b/test/jest-helpers/cli.js @@ -55,6 +55,7 @@ async function internalExec(args, options = {}) { const colorFlag = 'FORCE_COLOR=' + (options.colors ? '1' : '0'); try { + // If this just uses child_process.exec, the shell scripts are pointless, and should be all migrated to Jest tests. const result = await exec( [colorFlag, cli, reportMode(options), colors(options), args].join(' '), { diff --git a/test/run-snapshots/init-project/src/Main.elm b/test/run-snapshots/init-project/src/Main.elm index ffe984ec6..b756e059c 100644 --- a/test/run-snapshots/init-project/src/Main.elm +++ b/test/run-snapshots/init-project/src/Main.elm @@ -1,4 +1,3 @@ module A exposing (..) import Html exposing (text) main = text "Hello!" - diff --git a/test/run-snapshots/init-template-project/src/Main.elm b/test/run-snapshots/init-template-project/src/Main.elm index ffe984ec6..b756e059c 100644 --- a/test/run-snapshots/init-template-project/src/Main.elm +++ b/test/run-snapshots/init-template-project/src/Main.elm @@ -1,4 +1,3 @@ module A exposing (..) import Html exposing (text) main = text "Hello!" - diff --git a/test/run.mjs b/test/run.mjs new file mode 100755 index 000000000..534d32116 --- /dev/null +++ b/test/run.mjs @@ -0,0 +1,532 @@ +#!/bin/node +/* eslint n/no-process-exit: "off" -- WIP */ +import * as fsp from 'node:fs/promises'; +import * as path from 'node:path'; +import * as process from 'node:process'; +import {fileURLToPath} from 'node:url'; +import {glob} from 'tinyglobby'; +import {$, cd} from 'zx'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +$.quiet = true; +$.stdio = 'pipe'; +$.preferLocal = [path.join(__dirname, '../.node_modules/.bin/')]; + +const BIN = 'elm-review'; +const TMP = path.join(__dirname, 'temporary'); +const ELM_HOME = path.join(TMP, 'elm-home'); +const SNAPSHOTS = path.join(__dirname, 'run-snapshots'); +/** @type {string | undefined} */ +const SUBCOMMAND = process.argv[2]; + +/** + * @param {string} data + * @returns {string} + */ +const replaceScript = (data) => { + const localPath = path.join(__dirname, '..'); + return data.replace(new RegExp(localPath, 'g'), ''); +}; + +const {AUTH_GITHUB, CI, REMOTE} = process.env; +const AUTH = + AUTH_GITHUB === undefined ? [] : [`--github-auth="${AUTH_GITHUB}"`]; + +const TEST_ARGS = ['--no-color', ...AUTH, '--FOR-TESTS']; + +const TEST_ARGS_REGEX = /--no-color \$'--github-auth="[\w:]+"' /; + +/** + * @param {string} title + * @param {string[]} args + * @param {string} file + * @param {string} [input] + * @returns {Promise} + */ +const runCommandAndCompareToSnapshot = async (title, args, file, input) => { + const snapshotPath = path.join(SNAPSHOTS, file); + const actualPath = path.join(TMP, file); + const fullArgs = [...TEST_ARGS, ...args]; + + const cmd = $({halt: true, input})`${BIN} ${fullArgs}`.nothrow(); + const censoredCommand = cmd.cmd.replace(TEST_ARGS_REGEX, ''); + + process.stdout.write(`- ${title}: \u001B[34m ${censoredCommand}\u001B[0m`); + try { + await fsp.access(snapshotPath); + } catch { + console.error( + `\n \u001B[31mThere is no snapshot recording for \u001B[33m${file}\u001B[31m\nRun \u001B[33m\n npm run test-run-record -s\n\u001B[31mto generate it.\u001B[0m` + ); + process.exit(1); + } + + const output = await cmd.run().text(); + const replacedOutput = replaceScript(output); + await fsp.writeFile(actualPath, replacedOutput); + + const diff = await $`diff ${actualPath} ${snapshotPath}`.nothrow(); + if (diff.exitCode === 0) { + console.log(` \u001B[92mOK\u001B[0m`); + } else { + const [snapshot, actual] = await Promise.all([ + fsp.readFile(snapshotPath, 'utf8'), + fsp.readFile(actualPath, 'utf8') + ]); + + console.error( + `\u001B[31m ERROR\n I found a different output than expected:\u001B[0m` + ); + console.error(`\n \u001B[31mExpected:\u001B[0m\n`); + console.error(snapshot); + console.error(`\n \u001B[31mbut got:\u001B[0m\n`); + console.error(actual); + console.error(`\n \u001B[31mHere is the difference:\u001B[0m\n`); + console.error(diff.text()); + process.exit(1); + } +}; + +/** + * @param {string} title + * @param {string[]} args + * @param {string} file + * @param {string} [input] + * @returns {Promise} + */ +const runAndRecord = async (title, args, file, input) => { + const snapshotPath = path.join(SNAPSHOTS, file); + const fullArgs = [...TEST_ARGS, ...args]; + + const cmd = $({halt: true, input})`${BIN} ${fullArgs}`.nothrow(); + const censoredCommand = cmd.cmd.replace(TEST_ARGS_REGEX, ''); + + process.stdout.write( + `\u001B[33m- ${title}\u001B[0m: \u001B[34m ${censoredCommand}\u001B[0m` + ); + + $.env.ELM_HOME = ELM_HOME; + + const output = await cmd.run().text(); + const replacedOutput = replaceScript(output); + await fsp.writeFile(snapshotPath, replacedOutput); +}; + +/** + * @param {string} title + * @param {string[]} args + * @param {string} file + * @returns {Promise} + */ +const createTestSuiteWithDifferentReportFormats = async (title, args, file) => { + await createTest(title, args, `${file}.txt`); + await createTest( + `${title} (JSON)`, + [...args, '--report=json'], + `${file}-json.txt` + ); + await createTest( + `${title} (Newline delimited JSON)`, + [...args, '--report=ndjson'], + `${file}-ndjson.txt` + ); +}; + +/** + * @param {string} title + * @param {string[]} args + * @param {string} file + * @returns {Promise} + */ +const createTestSuiteForHumanAndJson = async (title, args, file) => { + await createTest(title, args, `${file}.txt`); + await createTest( + `${title} (JSON)`, + [...args, '--report=json'], + `${file}-json.txt` + ); +}; + +const initElmProject = async () => { + await $({input: 'Y'})`elm init`; + await fsp.writeFile( + 'src/Main.elm', + 'module A exposing (..)\nimport Html exposing (text)\nmain = text "Hello!"\n' + ); +}; + +/** + * @param {string} folder + * @returns {Promise} + */ +const checkFolderContents = async (folder) => { + if (SUBCOMMAND === undefined) { + process.stdout.write(' Checking generated files are the same'); + + const snapshotFolder = path.join(SNAPSHOTS, folder); + const actualFolder = path.join(TMP, folder); + + const diff = + await $`diff -rq ${actualFolder} ${snapshotFolder} --exclude="elm-stuff"`.nothrow(); + if (diff.exitCode === 0) { + console.log(` \u001B[92mOK\u001B[0m`); + } else { + console.error( + `\u001B[31m ERROR\n The generated files are different:\u001B[0m` + ); + console.error(diff.text()); + process.exit(1); + } + } +}; + +/** + * @param {string} folder + * @returns {Promise} + */ +const createAndGoIntoFolder = async (folder) => { + const targetPath = + SUBCOMMAND === undefined + ? path.join(TMP, folder) + : path.join(SNAPSHOTS, folder); + await fsp.mkdir(targetPath, {recursive: true}); + cd(targetPath); +}; + +const cleanUp = async () => { + const elmStuffs = await glob(path.join(__dirname, '/*/elm-stuff'), { + ignore: path.join(__dirname, 'project-with-files-in-elm-stuff/'), + onlyDirectories: true, + expandDirectories: false + }); + + const pathsToRemove = [TMP, ...elmStuffs]; + + await Promise.all( + pathsToRemove.map(async (p) => { + await fsp.rm(p, {recursive: true, force: true}); + }) + ); +}; + +await cleanUp(); +await fsp.mkdir(TMP, {recursive: true}); + +const createTest = await (async () => { + if (SUBCOMMAND === 'record') { + await fsp.rm(SNAPSHOTS, {recursive: true, force: true}); + await fsp.mkdir(SNAPSHOTS, {recursive: true}); + return runAndRecord; + } + + console.log('\u001B[33m-- Testing runs\u001B[0m'); + return runCommandAndCompareToSnapshot; +})(); + +const PACK_OUTPUT = await $`npm pack -s ../`.pipe($`tail -n 1`); +const PACKAGE_PATH = PACK_OUTPUT.valueOf(); +console.log(`Package path is \`${PACKAGE_PATH}\`.`); +await $`npm install -g ${PACKAGE_PATH}`; + +// Init + +const INIT_PROJECT_NAME = 'init-project'; + +await createAndGoIntoFolder(INIT_PROJECT_NAME); + +await initElmProject(); +await createTest('Init a new configuration', ['init'], 'init.txt', 'Y'); + +await checkFolderContents(INIT_PROJECT_NAME); + +// Init with template + +const INIT_TEMPLATE_PROJECT_NAME = 'init-template-project'; + +await createAndGoIntoFolder(INIT_TEMPLATE_PROJECT_NAME); + +await initElmProject(); +await createTest( + 'Init a new configuration using a template', + ['init', '--template', 'jfmengels/elm-review-unused/example'], + 'init-template.txt' +); + +await checkFolderContents(INIT_TEMPLATE_PROJECT_NAME); + +// FIXES + +const projectPath = + SUBCOMMAND === undefined + ? path.join(TMP, 'project to fix') + : path.join(SNAPSHOTS, 'project to fix'); +await fsp.rm(projectPath, {recursive: true, force: true}); + +// @ts-expect-error(TS2339): CI runs on a newer Node.js. +// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- ^ +await fsp.cp(path.join(__dirname, 'project-with-errors'), projectPath, { + recursive: true +}); +cd(projectPath); + +await createTest( + 'Running with --fix-all-without-prompt', + ['--fix-all-without-prompt'], + 'fix-all.txt' +); + +if (SUBCOMMAND === undefined) { + const filesToCheck = [ + 'src/Main.elm', + 'src/Folder/Used.elm', + 'src/Folder/Unused.elm' + ]; + + /** + * @param {string} file + * @returns {Promise} + */ + const processFile = async (file) => { + const actualFile = path.join(TMP, 'project to fix', file); + const snapshotFile = path.join(SNAPSHOTS, 'project to fix', file); + + const diff = await $`diff ${actualFile} ${snapshotFile}`.nothrow(); + + if (diff.exitCode !== 0) { + console.error(`Running with --fix-all-without-prompt (looking at code)`); + console.error( + `\u001B[31m ERROR\n I found a different FIX output than expected for ${file}:\u001B[0m` + ); + console.error(`\n \u001B[31mHere is the difference:\u001B[0m\n`); + console.error( + await $`diff -py ${actualFile} ${snapshotFile}`.nothrow().text() + ); + process.exit(1); + } + }; + + await Promise.all(filesToCheck.map(processFile)); +} + +// Suppress + +cd(path.join(__dirname, 'project-with-suppressed-errors')); +await createTestSuiteForHumanAndJson( + 'Running with only suppressed errors should not report any errors', + [], + 'suppressed-errors-pass' +); + +await fsp.copyFile('fixed-elm.json', 'elm.json'); +await createTest( + 'Fixing all errors for an entire rule should remove the suppression file', + [], + 'suppressed-errors-after-fixed-errors-for-rule.txt' +); + +try { + await fsp.access('./review/suppressed/NoUnused.Dependencies.json'); + + // That should've thrown: thus the file still exists!? + console.error( + 'Expected project-with-suppressed-errors/review/suppressed/NoUnused.Dependencies.json to have been deleted' + ); + process.exit(1); +} catch { + // File not accessible, hopefully it got deleted. +} + +await $`git checkout HEAD elm.json review/suppressed/`; + +await fsp.rm('src/OtherFile.elm'); +await createTest( + 'Fixing all errors for an entire rule should update the suppression file', + [], + 'suppressed-errors-after-fixed-errors-for-file.txt' +); + +const diff = + await $`diff review/suppressed/NoUnused.Variables.json expected-NoUnused.Variables.json`.nothrow(); +if (diff.exitCode !== 0) { + console.error( + 'Expected project-with-suppressed-errors/review/suppressed/NoUnused.Variables.json to have been updated' + ); + process.exit(1); +} + +await $`git checkout HEAD src/OtherFile.elm review/suppressed/`; + +await fsp.copyFile('with-errors-OtherFile.elm', 'src/OtherFile.elm'); +await createTestSuiteForHumanAndJson( + 'Introducing new errors should show all related errors', + [], + 'suppressed-errors-introducing-new-errors' +); +await $`git checkout HEAD src/OtherFile.elm`; + +cd(__dirname); + +// New-package + +cd(SUBCOMMAND === 'record' ? SNAPSHOTS : TMP); + +const NEW_PACKAGE_NAME = 'elm-review-something'; +const NEW_PACKAGE_NAME_FOR_NEW_RULE = `${NEW_PACKAGE_NAME}-for-new-rule`; + +await createTest( + 'Creating a new package', + [ + 'new-package', + '--prefill', + `some-author,${NEW_PACKAGE_NAME},BSD-3-Clause`, + 'No.Doing.Foo', + '--rule-type', + 'module' + ], + 'new-package.txt' +); + +await checkFolderContents(NEW_PACKAGE_NAME); + +// New-rule (DEPENDS ON PREVIOUS STEP!) + +// @ts-expect-error(TS2339): CI runs on a newer Node.js. +// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- ^ +await fsp.cp(NEW_PACKAGE_NAME, NEW_PACKAGE_NAME_FOR_NEW_RULE, { + recursive: true +}); +cd(NEW_PACKAGE_NAME_FOR_NEW_RULE); + +await createTest( + 'Creating a new rule', + ['new-rule', 'SomeModuleRule', '--rule-type', 'module'], + 'new-module-rule.txt' +); +await createTest( + 'Creating a new rule', + ['new-rule', 'SomeProjectRule', '--rule-type', 'project'], + 'new-project-rule.txt' +); + +await checkFolderContents(NEW_PACKAGE_NAME_FOR_NEW_RULE); + +cd(path.join(__dirname, 'project-with-errors')); + +await createTestSuiteWithDifferentReportFormats( + 'Filter rules', + ['--rules', 'NoUnused.Variables'], + 'filter-rules' +); + +await createTest( + 'Filter rules with comma-separated list', + ['--rules', 'NoUnused.Variables,NoUnused.Exports'], + 'filter-rules-comma.txt' +); +await createTest( + 'Filter rules with multiple --rules calls', + ['--rules', 'NoUnused.Variables', '--rules', 'NoUnused.Exports'], + 'filter-rules-multiple-calls.txt' +); + +await createTestSuiteWithDifferentReportFormats( + 'Filter unknown rule', + ['--rules', 'NoUnused.Unknown'], + 'filter-unknown-rule' +); + +await createTest( + 'Ignore errors on directories', + ['--ignore-dirs', 'src/Folder/'], + 'ignore-dirs.txt' +); +await createTest( + 'Ignore errors on files', + ['--ignore-files', 'src/Folder/Unused.elm'], + 'ignore-files.txt' +); + +// Review with remote configuration + +if (!REMOTE && !SUBCOMMAND && (!CI || !AUTH_GITHUB)) { + process.exit(0); +} + +await createTest( + 'Running using remote GitHub configuration', + ['--template', 'jfmengels/elm-review-unused/example'], + 'remote-configuration.txt' +); +await createTest( + 'Running using remote GitHub configuration (no errors)', + [ + '--template', + 'jfmengels/node-elm-review/test/config-that-triggers-no-errors' + ], + 'remote-configuration-no-errors.txt' +); +await createTest( + 'Running using remote GitHub configuration without a path to the config', + ['--template', 'jfmengels/test-node-elm-review'], + 'remote-configuration-no-path.txt' +); + +await createTestSuiteWithDifferentReportFormats( + 'Using unknown remote GitHub configuration', + ['--template', 'jfmengels/unknown-repo-123'], + 'remote-configuration-unknown' +); +await createTestSuiteWithDifferentReportFormats( + 'Using unknown remote GitHub configuration with a branch', + ['--template', 'jfmengels/unknown-repo-123#some-branch'], + 'remote-configuration-unknown-with-branch' +); +await createTestSuiteWithDifferentReportFormats( + 'Using remote GitHub configuration with a non-existing branch and commit', + ['--template', 'jfmengels/elm-review-unused/example#unknown-branch'], + 'remote-configuration-with-unknown-branch' +); +await createTestSuiteWithDifferentReportFormats( + 'Using remote GitHub configuration with existing repo but that does not contain template folder', + ['--template', 'jfmengels/node-elm-review'], + 'remote-configuration-with-absent-folder' +); +await createTestSuiteWithDifferentReportFormats( + 'Using a remote configuration with a missing direct elm-review dependency', + ['--template', 'jfmengels/node-elm-review/test/config-without-elm-review'], + 'remote-without-elm-review' +); +await createTestSuiteWithDifferentReportFormats( + 'Using a remote configuration with an outdated elm-review', + [ + '--template', + 'jfmengels/node-elm-review/test/config-for-outdated-elm-review-version' + ], + 'remote-with-outdated-elm-review-version' +); +await createTestSuiteWithDifferentReportFormats( + 'Using a remote configuration with an salvageable (outdated but compatible) elm-review', + [ + '--template', + 'jfmengels/node-elm-review/test/config-for-salvageable-elm-review-version' + ], + 'remote-with-outdated-but-salvageable-elm-review-version' +); +await createTestSuiteWithDifferentReportFormats( + 'Using a remote configuration with unparsable elm.json', + ['--template', 'jfmengels/node-elm-review/test/config-unparsable-elmjson'], + 'remote-configuration-with-unparsable-elmjson' +); +await createTestSuiteWithDifferentReportFormats( + 'Using both --config and --template', + [ + '--config', + '../config-that-triggers-no-errors', + '--template jfmengels/test-node-elm-review' + ], + 'remote-configuration-with-config-flag' +); + +await fsp.rm(TMP, {recursive: true, force: true}); diff --git a/test/run.sh b/test/run.sh deleted file mode 100755 index 40102ec0a..000000000 --- a/test/run.sh +++ /dev/null @@ -1,426 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -CWD=$(pwd) -CMD="elm-review --no-color" -TMP="$CWD/temporary" -ELM_HOME="$TMP/elm-home" -SNAPSHOTS="$CWD/run-snapshots" -SUBCOMMAND="${1:-}" - -replace_script() { - sed "s|$(dirname "$(dirname "$(pwd)")")||g" -} - -# If you get errors like rate limit exceeded, you can run these tests -# with "AUTH_GITHUB=gitHubUserName:token" -# Follow this guide: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token -# to create an API token, and give it access to public repositories. -if [ -z "${AUTH_GITHUB:-}" ] -then - AUTH="" -else - AUTH=" --github-auth $AUTH_GITHUB" -fi - -runCommandAndCompareToSnapshot() { - local LOCAL_COMMAND=$1 - local TITLE=$2 - local ARGS=$3 - local FILE=$4 - - echo -ne "- $TITLE: \x1B[34m elm-review --FOR-TESTS $ARGS\x1B[0m" - if [ ! -f "$SNAPSHOTS/$FILE" ] - then - echo -e "\n \x1B[31mThere is no snapshot recording for \x1B[33m$FILE\x1B[31m\nRun \x1B[33m\n npm run test-run-record -s\n\x1B[31mto generate it.\x1B[0m" - exit 1 - fi - - (eval "$LOCAL_COMMAND$AUTH --FOR-TESTS $ARGS" || true) 2>&1 \ - | replace_script \ - > "$TMP/$FILE" - local diffed - diffed="$(diff "$TMP/$FILE" "$SNAPSHOTS/$FILE" || true)" - if [ "$diffed" != "" ] - then - echo -e "\x1B[31m ERROR\n I found a different output than expected:\x1B[0m" - echo -e "\n \x1B[31mExpected:\x1B[0m\n" - cat "$SNAPSHOTS/$FILE" - echo -e "\n \x1B[31mbut got:\x1B[0m\n" - cat "$TMP/$FILE" - echo -e "\n \x1B[31mHere is the difference:\x1B[0m\n" - diff -p "$TMP/$FILE" "$SNAPSHOTS/$FILE" - else - echo -e " \x1B[92mOK\x1B[0m" - fi -} - -runAndRecord() { - local LOCAL_COMMAND=$1 - local TITLE=$2 - local ARGS=$3 - local FILE=$4 - echo -e "\x1B[33m- $TITLE\x1B[0m: \x1B[34m elm-review --FOR-TESTS $ARGS\x1B[0m" - (eval "ELM_HOME=$ELM_HOME $LOCAL_COMMAND$AUTH --FOR-TESTS $ARGS" || true) 2>&1 \ - | replace_script \ - > "$SNAPSHOTS/$FILE" -} - -createExtensiveTestSuite() { - local LOCAL_COMMAND=$1 - local TITLE=$2 - local ARGS=$3 - local FILE=$4 - createTestSuiteWithDifferentReportFormats "$LOCAL_COMMAND" "$TITLE" "$ARGS" "$FILE" - createTestSuiteWithDifferentReportFormats "$LOCAL_COMMAND" "$TITLE (debug)" "$ARGS --debug" "$FILE-debug" -} - -createTestSuiteWithDifferentReportFormats() { - local LOCAL_COMMAND=$1 - local TITLE=$2 - local ARGS=$3 - local FILE=$4 - $createTest "$LOCAL_COMMAND" \ - "$TITLE" \ - "$ARGS" \ - "$FILE.txt" - $createTest "$LOCAL_COMMAND" \ - "$TITLE (JSON)" \ - "$ARGS --report=json" \ - "$FILE-json.txt" - $createTest "$LOCAL_COMMAND" \ - "$TITLE (Newline delimited JSON)" \ - "$ARGS --report=ndjson" \ - "$FILE-ndjson.txt" -} - -createTestSuiteForHumanAndJson() { - local LOCAL_COMMAND=$1 - local TITLE=$2 - local ARGS=$3 - local FILE=$4 - $createTest "$LOCAL_COMMAND" \ - "$TITLE" \ - "$ARGS" \ - "$FILE.txt" - $createTest "$LOCAL_COMMAND" \ - "$TITLE (JSON)" \ - "$ARGS --report=json" \ - "$FILE-json.txt" -} - -initElmProject() { - echo Y | npx --no-install elm init > /dev/null - echo -e 'module A exposing (..)\nimport Html exposing (text)\nmain = text "Hello!"\n' > src/Main.elm -} - -checkFolderContents() { - if [ "$SUBCOMMAND" != "record" ] - then - echo -n " Checking generated files are the same" - - local diffed - diffed="$(diff -rq "$TMP/$1/" "$SNAPSHOTS/$1/" --exclude="elm-stuff" || true)" - if [ "$diffed" != "" ] - then - echo -e "\x1B[31m ERROR\n The generated files are different:\x1B[0m" - diff -rq "$TMP/$1/" "$SNAPSHOTS/$1/" --exclude="elm-stuff" - else - echo -e " \x1B[92mOK\x1B[0m" - fi - fi -} - -createAndGoIntoFolder() { - if [ "$SUBCOMMAND" != "record" ] - then - mkdir -p "$TMP/$1" - cd "$TMP/$1" - else - mkdir -p "$SNAPSHOTS/$1" - cd "$SNAPSHOTS/$1" - fi -} - -rm -rf "$TMP" \ - "$CWD/config-empty/elm-stuff" \ - "$CWD/config-error-debug/elm-stuff" \ - "$CWD/config-error-unknown-module/elm-stuff" \ - "$CWD/config-for-outdated-elm-review-version/elm-stuff" \ - "$CWD/config-for-salvageable-elm-review-version/elm-stuff" \ - "$CWD/config-syntax-error/elm-stuff" \ - "$CWD/config-that-triggers-no-errors/elm-stuff" \ - "$CWD/config-unparsable-elmjson/elm-stuff" \ - "$CWD/config-without-elm-review/elm-stuff" \ - "$CWD/project-using-es2015-module/elm-stuff" \ - "$CWD/project-with-errors/elm-stuff" \ - "$CWD/project-with-suppressed-errors/elm-stuff" - -mkdir -p "$TMP" - -if [ "$SUBCOMMAND" == "record" ] -then - createTest=runAndRecord - rm -rf "$SNAPSHOTS" &> /dev/null - mkdir -p "$SNAPSHOTS" -else - createTest=runCommandAndCompareToSnapshot - echo -e '\x1B[33m-- Testing runs\x1B[0m' -fi - -PACKAGE_PATH=$(npm pack -s ../ | tail -n 1) -echo "Package path is $PACKAGE_PATH" -npm install -g "$PACKAGE_PATH" - -# init - -INIT_PROJECT_NAME="init-project" - -createAndGoIntoFolder $INIT_PROJECT_NAME - -initElmProject -$createTest "echo Y | $CMD" \ - "Init a new configuration" \ - "init" \ - "init.txt" - -checkFolderContents $INIT_PROJECT_NAME - -# init with template - -INIT_TEMPLATE_PROJECT_NAME="init-template-project" - -createAndGoIntoFolder $INIT_TEMPLATE_PROJECT_NAME - -initElmProject -$createTest "echo Y | $CMD" \ - "Init a new configuration using a template" \ - "init --template jfmengels/elm-review-unused/example" \ - "init-template.txt" - -checkFolderContents $INIT_TEMPLATE_PROJECT_NAME - -# FIXES - -if [ "$SUBCOMMAND" == "record" ] -then - rm -rf "$SNAPSHOTS/project to fix" - cp -r "$CWD/project-with-errors" "$SNAPSHOTS/project to fix" - cd "$SNAPSHOTS/project to fix" -else - rm -rf "$TMP/project to fix" - cp -r "$CWD/project-with-errors" "$TMP/project to fix" - cd "$TMP/project to fix" -fi - -$createTest "$CMD" \ - "Running with --fix-all-without-prompt" \ - "--fix-all-without-prompt" \ - "fix-all.txt" - -if [ "$SUBCOMMAND" != "record" ] -then - declare diffed - diffed="$(diff -q "$TMP/project to fix/src/Main.elm" "$SNAPSHOTS/project to fix/src/Main.elm" || true)" - if [ "$diffed" != "" ] - then - echo -e "Running with --fix-all-without-prompt (looking at code)" - echo -e "\x1B[31m ERROR\n I found a different FIX output for Main.elmthan expected:\x1B[0m" - echo -e "\n \x1B[31mHere is the difference:\x1B[0m\n" - diff -py "$TMP/project to fix/src/Main.elm" "$SNAPSHOTS/project to fix/src/Main.elm" - fi - declare diffed - diffed="$(diff -q "$TMP/project to fix/src/Folder/Used.elm" "$SNAPSHOTS/project to fix/src/Folder/Used.elm" || true)" - if [ "$diffed" != "" ] - then - echo -e "Running with --fix-all-without-prompt (looking at code)" - echo -e "\x1B[31m ERROR\n I found a different FIX output than expected for Folder/Used.elm:\x1B[0m" - echo -e "\n \x1B[31mHere is the difference:\x1B[0m\n" - diff -py "$TMP/project to fix/src/Folder/Used.elm" "$SNAPSHOTS/project to fix/src/Folder/Used.elm" - fi - declare diffed - diffed="$(diff -q "$TMP/project to fix/src/Folder/Unused.elm" "$SNAPSHOTS/project to fix/src/Folder/Unused.elm" || true)" - if [ "$diffed" != "" ] - then - echo -e "Running with --fix-all-without-prompt (looking at code)" - echo -e "\x1B[31m ERROR\n I found a different FIX output than expected for Folder/Unused.elm:\x1B[0m" - echo -e "\n \x1B[31mHere is the difference:\x1B[0m\n" - diff -py "$TMP/project to fix/src/Folder/Unused.elm" "$SNAPSHOTS/project to fix/src/Folder/Unused.elm" - fi -fi - -## suppress - -cd "$CWD/project-with-suppressed-errors" -createTestSuiteForHumanAndJson "$CMD" \ - "Running with only suppressed errors should not report any errors" \ - "" \ - "suppressed-errors-pass" - -cp fixed-elm.json elm.json -$createTest "$CMD" \ - "Fixing all errors for an entire rule should remove the suppression file" \ - "" \ - "suppressed-errors-after-fixed-errors-for-rule.txt" -if [ -f "./review/suppressed/NoUnused.Dependencies.json" ]; then - echo "Expected project-with-suppressed-errors/review/suppressed/NoUnused.Dependencies.json to have been deleted" - exit 1 -fi -git checkout HEAD elm.json review/suppressed/ > /dev/null - - -rm src/OtherFile.elm -$createTest "$CMD" \ - "Fixing all errors for an entire rule should update the suppression file" \ - "" \ - "suppressed-errors-after-fixed-errors-for-file.txt" - -declare diffed -diffed="$(diff review/suppressed/NoUnused.Variables.json expected-NoUnused.Variables.json || true)" -if [ "$diffed" != "" ]; then - echo "Expected project-with-suppressed-errors/review/suppressed/NoUnused.Variables.json to have been updated" - exit 1 -fi -git checkout HEAD src/OtherFile.elm review/suppressed/ > /dev/null - -cp with-errors-OtherFile.elm src/OtherFile.elm -createTestSuiteForHumanAndJson "$CMD" \ - "Introducing new errors should show all related errors" \ - "" \ - "suppressed-errors-introducing-new-errors" -git checkout HEAD src/OtherFile.elm > /dev/null - -cd "$CWD" - -# new-package - -if [ "$SUBCOMMAND" == "record" ] -then - cd "$SNAPSHOTS/" -else - cd "$TMP/" -fi - -NEW_PACKAGE_NAME="elm-review-something" -NEW_PACKAGE_NAME_FOR_NEW_RULE="$NEW_PACKAGE_NAME-for-new-rule" - -$createTest "$CMD" \ - "Creating a new package" \ - "new-package --prefill some-author,$NEW_PACKAGE_NAME,BSD-3-Clause No.Doing.Foo --rule-type module" \ - "new-package.txt" - -checkFolderContents $NEW_PACKAGE_NAME - -# new-rule (DEPENDS ON PREVIOUS STEP!) - -cp -r $NEW_PACKAGE_NAME $NEW_PACKAGE_NAME_FOR_NEW_RULE -cd $NEW_PACKAGE_NAME_FOR_NEW_RULE - -$createTest "$CMD" \ - "Creating a new rule" \ - "new-rule SomeModuleRule --rule-type module" \ - "new-module-rule.txt" - -$createTest "$CMD" \ - "Creating a new rule" \ - "new-rule SomeProjectRule --rule-type project" \ - "new-project-rule.txt" - -checkFolderContents $NEW_PACKAGE_NAME_FOR_NEW_RULE - -cd "$CWD/project-with-errors" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Filter rules" \ - "--rules NoUnused.Variables" \ - "filter-rules" - -$createTest "$CMD" \ - "Filter rules with comma-separated list" \ - "--rules NoUnused.Variables,NoUnused.Exports" \ - "filter-rules-comma.txt" - -$createTest "$CMD" \ - "Filter rules with multiple --rules calls" \ - "--rules NoUnused.Variables --rules NoUnused.Exports" \ - "filter-rules-multiple-calls.txt" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Filter unknown rule" \ - "--rules NoUnused.Unknown" \ - "filter-unknown-rule" - -$createTest "$CMD" \ - "Ignore errors on directories" \ - "--ignore-dirs src/Folder/" \ - "ignore-dirs.txt" - -$createTest "$CMD" \ - "Ignore errors on files" \ - "--ignore-files src/Folder/Unused.elm" \ - "ignore-files.txt" - -# Review with remote configuration - -$createTest "$CMD" \ - "Running using remote GitHub configuration" \ - "--template jfmengels/elm-review-unused/example" \ - "remote-configuration.txt" - -$createTest "$CMD" \ - "Running using remote GitHub configuration (no errors)" \ - "--template jfmengels/node-elm-review/test/config-that-triggers-no-errors" \ - "remote-configuration-no-errors.txt" - -$createTest "$CMD" \ - "Running using remote GitHub configuration without a path to the config" \ - "--template jfmengels/test-node-elm-review" \ - "remote-configuration-no-path.txt" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using unknown remote GitHub configuration" \ - "--template jfmengels/unknown-repo-123" \ - "remote-configuration-unknown" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using unknown remote GitHub configuration with a branch" \ - "--template jfmengels/unknown-repo-123#some-branch" \ - "remote-configuration-unknown-with-branch" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using remote GitHub configuration with a non-existing branch and commit" \ - "--template jfmengels/elm-review-unused/example#unknown-branch" \ - "remote-configuration-with-unknown-branch" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using remote GitHub configuration with existing repo but that does not contain template folder" \ - "--template jfmengels/node-elm-review" \ - "remote-configuration-with-absent-folder" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using a remote configuration with a missing direct elm-review dependency" \ - "--template jfmengels/node-elm-review/test/config-without-elm-review" \ - "remote-without-elm-review" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using a remote configuration with an outdated elm-review" \ - "--template jfmengels/node-elm-review/test/config-for-outdated-elm-review-version" \ - "remote-with-outdated-elm-review-version" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using a remote configuration with an salvageable (outdated but compatible) elm-review" \ - "--template jfmengels/node-elm-review/test/config-for-salvageable-elm-review-version" \ - "remote-with-outdated-but-salvageable-elm-review-version" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using a remote configuration with unparsable elm.json" \ - "--template jfmengels/node-elm-review/test/config-unparsable-elmjson" \ - "remote-configuration-with-unparsable-elmjson" - -createTestSuiteWithDifferentReportFormats "$CMD" \ - "Using both --config and --template" \ - "--config ../config-that-triggers-no-errors --template jfmengels/test-node-elm-review" \ - "remote-configuration-with-config-flag" - -rm -rf "$TMP" diff --git a/turbo.json b/turbo.json index 9b3fb25b8..794625a88 100644 --- a/turbo.json +++ b/turbo.json @@ -40,6 +40,7 @@ ] }, "test-sync": { + "env": ["CI", "REMOTE"], "inputs": [ "ast-codec/", "bin/", @@ -136,6 +137,7 @@ "inputs": ["lib/", "test/"] }, "test-run": { + "env": ["CI", "REMOTE"], "inputs": [ "ast-codec/", "bin/", @@ -150,6 +152,7 @@ ] }, "test-run-record": { + "env": ["CI", "REMOTE"], "inputs": [ "ast-codec/", "bin/",