]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Integration tests for the eslint.js executable. | |
3 | * @author Teddy Katz | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | const childProcess = require("child_process"); | |
9 | const fs = require("fs"); | |
10 | const assert = require("chai").assert; | |
56c4a2cb DC |
11 | const path = require("path"); |
12 | ||
13 | const EXECUTABLE_PATH = path.resolve(path.join(__dirname, "../../bin/eslint.js")); | |
eb39fafa DC |
14 | |
15 | /** | |
16 | * Returns a Promise for when a child process exits | |
17 | * @param {ChildProcess} exitingProcess The child process | |
18 | * @returns {Promise<number>} A Promise that fulfills with the exit code when the child process exits | |
19 | */ | |
20 | function awaitExit(exitingProcess) { | |
21 | return new Promise(resolve => exitingProcess.once("exit", resolve)); | |
22 | } | |
23 | ||
24 | /** | |
25 | * Asserts that the exit code of a given child process will equal the given value. | |
26 | * @param {ChildProcess} exitingProcess The child process | |
27 | * @param {number} expectedExitCode The expected exit code of the child process | |
28 | * @returns {Promise} A Promise that fulfills if the exit code ends up matching, and rejects otherwise. | |
29 | */ | |
30 | function assertExitCode(exitingProcess, expectedExitCode) { | |
31 | return awaitExit(exitingProcess).then(exitCode => { | |
32 | assert.strictEqual(exitCode, expectedExitCode, `Expected an exit code of ${expectedExitCode} but got ${exitCode}.`); | |
33 | }); | |
34 | } | |
35 | ||
36 | /** | |
37 | * Returns a Promise for the stdout of a process. | |
38 | * @param {ChildProcess} runningProcess The child process | |
39 | * @returns {Promise<{stdout: string, stderr: string}>} A Promise that fulfills with all of the | |
40 | * stdout and stderr output produced by the process when it exits. | |
41 | */ | |
42 | function getOutput(runningProcess) { | |
43 | let stdout = ""; | |
44 | let stderr = ""; | |
45 | ||
46 | runningProcess.stdout.on("data", data => (stdout += data)); | |
47 | runningProcess.stderr.on("data", data => (stderr += data)); | |
48 | return awaitExit(runningProcess).then(() => ({ stdout, stderr })); | |
49 | } | |
50 | ||
51 | describe("bin/eslint.js", () => { | |
52 | const forkedProcesses = new Set(); | |
53 | ||
54 | /** | |
55 | * Forks the process to run an instance of ESLint. | |
56 | * @param {string[]} [args] An array of arguments | |
57 | * @param {Object} [options] An object containing options for the resulting child process | |
58 | * @returns {ChildProcess} The resulting child process | |
59 | */ | |
60 | function runESLint(args, options) { | |
61 | const newProcess = childProcess.fork(EXECUTABLE_PATH, args, Object.assign({ silent: true }, options)); | |
62 | ||
63 | forkedProcesses.add(newProcess); | |
64 | return newProcess; | |
65 | } | |
66 | ||
67 | describe("reading from stdin", () => { | |
68 | it("has exit code 0 if no linting errors are reported", () => { | |
69 | const child = runESLint(["--stdin", "--no-eslintrc"]); | |
70 | ||
71 | child.stdin.write("var foo = bar;\n"); | |
72 | child.stdin.end(); | |
73 | return assertExitCode(child, 0); | |
74 | }); | |
75 | ||
76 | it("has exit code 0 if no linting errors are reported", () => { | |
77 | const child = runESLint([ | |
78 | "--stdin", | |
79 | "--no-eslintrc", | |
80 | "--rule", | |
81 | "{'no-extra-semi': 2}", | |
82 | "--fix-dry-run", | |
83 | "--format", | |
84 | "json" | |
85 | ]); | |
86 | ||
87 | const expectedOutput = JSON.stringify([ | |
88 | { | |
89 | filePath: "<text>", | |
90 | messages: [], | |
91 | errorCount: 0, | |
92 | warningCount: 0, | |
93 | fixableErrorCount: 0, | |
94 | fixableWarningCount: 0, | |
56c4a2cb DC |
95 | output: "var foo = bar;\n", |
96 | usedDeprecatedRules: [] | |
eb39fafa DC |
97 | } |
98 | ]); | |
99 | ||
100 | const exitCodePromise = assertExitCode(child, 0); | |
101 | const stdoutPromise = getOutput(child).then(output => { | |
102 | assert.strictEqual(output.stdout.trim(), expectedOutput); | |
103 | assert.strictEqual(output.stderr, ""); | |
104 | }); | |
105 | ||
106 | child.stdin.write("var foo = bar;;\n"); | |
107 | child.stdin.end(); | |
108 | ||
109 | return Promise.all([exitCodePromise, stdoutPromise]); | |
110 | }); | |
111 | ||
112 | it("has exit code 1 if a syntax error is thrown", () => { | |
113 | const child = runESLint(["--stdin", "--no-eslintrc"]); | |
114 | ||
115 | child.stdin.write("This is not valid JS syntax.\n"); | |
116 | child.stdin.end(); | |
117 | return assertExitCode(child, 1); | |
118 | }); | |
119 | ||
120 | it("has exit code 1 if a linting error occurs", () => { | |
121 | const child = runESLint(["--stdin", "--no-eslintrc", "--rule", "semi:2"]); | |
122 | ||
123 | child.stdin.write("var foo = bar // <-- no semicolon\n"); | |
124 | child.stdin.end(); | |
125 | return assertExitCode(child, 1); | |
126 | }); | |
127 | ||
128 | it( | |
129 | "gives a detailed error message if no config file is found in /", | |
130 | () => { | |
131 | if ( | |
132 | fs.readdirSync("/").some( | |
133 | fileName => | |
134 | /^\.eslintrc(?:\.(?:js|yaml|yml|json))?$/u | |
135 | .test(fileName) | |
136 | ) | |
137 | ) { | |
138 | return Promise.resolve(true); | |
139 | } | |
140 | const child = runESLint( | |
141 | ["--stdin"], { cwd: "/", env: { HOME: "/" } } | |
142 | ); | |
143 | ||
144 | const exitCodePromise = assertExitCode(child, 2); | |
145 | const stderrPromise = getOutput(child).then(output => { | |
146 | assert.match( | |
147 | output.stderr, | |
148 | /ESLint couldn't find a configuration file/u | |
149 | ); | |
150 | }); | |
151 | ||
152 | child.stdin.write("1 < 3;\n"); | |
153 | child.stdin.end(); | |
154 | return Promise.all([exitCodePromise, stderrPromise]); | |
155 | } | |
156 | ); | |
157 | ||
158 | it("successfully reads from an asynchronous pipe", () => { | |
159 | const child = runESLint(["--stdin", "--no-eslintrc"]); | |
160 | ||
161 | child.stdin.write("var foo = bar;\n"); | |
162 | return new Promise(resolve => setTimeout(resolve, 300)).then(() => { | |
163 | child.stdin.write("var baz = qux;\n"); | |
164 | child.stdin.end(); | |
165 | ||
166 | return assertExitCode(child, 0); | |
167 | }); | |
168 | }); | |
169 | ||
170 | it("successfully handles more than 4k data via stdin", () => { | |
171 | const child = runESLint(["--stdin", "--no-eslintrc"]); | |
56c4a2cb | 172 | const large = fs.createReadStream(path.join(__dirname, "../bench/large.js"), "utf8"); |
eb39fafa DC |
173 | |
174 | large.pipe(child.stdin); | |
175 | ||
176 | return assertExitCode(child, 0); | |
177 | }); | |
178 | }); | |
179 | ||
180 | describe("running on files", () => { | |
181 | it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js"]), 0)); | |
56c4a2cb DC |
182 | it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [1, never]"]), 0)); |
183 | it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [2, never]"]), 1)); | |
eb39fafa DC |
184 | it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["README.md"]), 1)); |
185 | }); | |
186 | ||
187 | describe("automatically fixing files", () => { | |
56c4a2cb | 188 | const fixturesPath = path.join(__dirname, "../fixtures/autofix-integration"); |
eb39fafa DC |
189 | const tempFilePath = `${fixturesPath}/temp.js`; |
190 | const startingText = fs.readFileSync(`${fixturesPath}/left-pad.js`).toString(); | |
191 | const expectedFixedText = fs.readFileSync(`${fixturesPath}/left-pad-expected.js`).toString(); | |
192 | const expectedFixedTextQuiet = fs.readFileSync(`${fixturesPath}/left-pad-expected-quiet.js`).toString(); | |
193 | ||
194 | beforeEach(() => { | |
195 | fs.writeFileSync(tempFilePath, startingText); | |
196 | }); | |
197 | ||
198 | it("has exit code 0 and fixes a file if all rules can be fixed", () => { | |
199 | const child = runESLint(["--fix", "--no-eslintrc", "--no-ignore", tempFilePath]); | |
200 | const exitCodeAssertion = assertExitCode(child, 0); | |
201 | const outputFileAssertion = awaitExit(child).then(() => { | |
202 | assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedText); | |
203 | }); | |
204 | ||
205 | return Promise.all([exitCodeAssertion, outputFileAssertion]); | |
206 | }); | |
207 | ||
208 | it("has exit code 0, fixes errors in a file, and does not report or fix warnings if --quiet and --fix are used", () => { | |
209 | const child = runESLint(["--fix", "--quiet", "--no-eslintrc", "--no-ignore", tempFilePath]); | |
210 | const exitCodeAssertion = assertExitCode(child, 0); | |
211 | const stdoutAssertion = getOutput(child).then(output => assert.strictEqual(output.stdout, "")); | |
212 | const outputFileAssertion = awaitExit(child).then(() => { | |
213 | assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedTextQuiet); | |
214 | }); | |
215 | ||
216 | return Promise.all([exitCodeAssertion, stdoutAssertion, outputFileAssertion]); | |
217 | }); | |
218 | ||
219 | it("has exit code 1 and fixes a file if not all rules can be fixed", () => { | |
220 | const child = runESLint(["--fix", "--no-eslintrc", "--no-ignore", "--rule", "max-len: [2, 10]", tempFilePath]); | |
221 | const exitCodeAssertion = assertExitCode(child, 1); | |
222 | const outputFileAssertion = awaitExit(child).then(() => { | |
223 | assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedText); | |
224 | }); | |
225 | ||
226 | return Promise.all([exitCodeAssertion, outputFileAssertion]); | |
227 | }); | |
228 | ||
229 | afterEach(() => { | |
230 | fs.unlinkSync(tempFilePath); | |
231 | }); | |
232 | }); | |
233 | ||
234 | describe("cache files", () => { | |
235 | const CACHE_PATH = ".temp-eslintcache"; | |
236 | const SOURCE_PATH = "tests/fixtures/cache/src/test-file.js"; | |
237 | const ARGS_WITHOUT_CACHE = ["--no-eslintrc", "--no-ignore", SOURCE_PATH, "--cache-location", CACHE_PATH]; | |
238 | const ARGS_WITH_CACHE = ARGS_WITHOUT_CACHE.concat("--cache"); | |
239 | ||
240 | describe("when no cache file exists", () => { | |
241 | it("creates a cache file when the --cache flag is used", () => { | |
242 | const child = runESLint(ARGS_WITH_CACHE); | |
243 | ||
244 | return assertExitCode(child, 0).then(() => { | |
245 | assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location"); | |
246 | ||
247 | // Cache file should contain valid JSON | |
248 | JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); | |
249 | }); | |
250 | }); | |
251 | }); | |
252 | ||
253 | describe("when a valid cache file already exists", () => { | |
254 | beforeEach(() => { | |
255 | const child = runESLint(ARGS_WITH_CACHE); | |
256 | ||
257 | return assertExitCode(child, 0).then(() => { | |
258 | assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location"); | |
259 | }); | |
260 | }); | |
261 | it("can lint with an existing cache file and the --cache flag", () => { | |
262 | const child = runESLint(ARGS_WITH_CACHE); | |
263 | ||
264 | return assertExitCode(child, 0).then(() => { | |
265 | ||
266 | // Note: This doesn't actually verify that the cache file is used for anything. | |
267 | assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should still exist after linting with --cache"); | |
268 | }); | |
269 | }); | |
270 | it("updates the cache file when the source file is modified", () => { | |
271 | const initialCacheContent = fs.readFileSync(CACHE_PATH, "utf8"); | |
272 | ||
273 | // Update the file to change its mtime | |
274 | fs.writeFileSync(SOURCE_PATH, fs.readFileSync(SOURCE_PATH, "utf8")); | |
275 | ||
276 | const child = runESLint(ARGS_WITH_CACHE); | |
277 | ||
278 | return assertExitCode(child, 0).then(() => { | |
279 | const newCacheContent = fs.readFileSync(CACHE_PATH, "utf8"); | |
280 | ||
281 | assert.notStrictEqual(initialCacheContent, newCacheContent, "Cache file should change after source is modified"); | |
282 | }); | |
283 | }); | |
284 | it("deletes the cache file when run without the --cache argument", () => { | |
285 | const child = runESLint(ARGS_WITHOUT_CACHE); | |
286 | ||
287 | return assertExitCode(child, 0).then(() => { | |
288 | assert.isFalse(fs.existsSync(CACHE_PATH), "Cache file should be deleted after running ESLint without the --cache argument"); | |
289 | }); | |
290 | }); | |
291 | }); | |
292 | ||
293 | // https://github.com/eslint/eslint/issues/7748 | |
294 | describe("when an invalid cache file already exists", () => { | |
295 | beforeEach(() => { | |
296 | fs.writeFileSync(CACHE_PATH, "This is not valid JSON."); | |
297 | ||
298 | // Sanity check | |
299 | assert.throws( | |
300 | () => JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")), | |
301 | SyntaxError, | |
302 | /Unexpected token/u, | |
303 | "Cache file should not contain valid JSON at the start" | |
304 | ); | |
305 | }); | |
306 | ||
307 | it("overwrites the invalid cache file with a valid one when the --cache argument is used", () => { | |
308 | const child = runESLint(ARGS_WITH_CACHE); | |
309 | ||
310 | return assertExitCode(child, 0).then(() => { | |
311 | assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location"); | |
312 | ||
313 | // Cache file should contain valid JSON | |
314 | JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); | |
315 | }); | |
316 | }); | |
317 | ||
318 | it("deletes the invalid cache file when the --cache argument is not used", () => { | |
319 | const child = runESLint(ARGS_WITHOUT_CACHE); | |
320 | ||
321 | return assertExitCode(child, 0).then(() => { | |
322 | assert.isFalse(fs.existsSync(CACHE_PATH), "Cache file should be deleted after running ESLint without the --cache argument"); | |
323 | }); | |
324 | }); | |
325 | }); | |
326 | ||
327 | afterEach(() => { | |
328 | if (fs.existsSync(CACHE_PATH)) { | |
329 | fs.unlinkSync(CACHE_PATH); | |
330 | } | |
331 | }); | |
332 | }); | |
333 | ||
334 | describe("handling crashes", () => { | |
335 | it("prints the error message to stderr in the event of a crash", () => { | |
336 | const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "Makefile.js"]); | |
337 | const exitCodeAssertion = assertExitCode(child, 2); | |
338 | const outputAssertion = getOutput(child).then(output => { | |
339 | const expectedSubstring = "Syntax error in selector"; | |
340 | ||
341 | assert.strictEqual(output.stdout, ""); | |
342 | assert.include(output.stderr, expectedSubstring); | |
343 | }); | |
344 | ||
345 | return Promise.all([exitCodeAssertion, outputAssertion]); | |
346 | }); | |
347 | ||
348 | it("prints the error message exactly once to stderr in the event of a crash", () => { | |
349 | const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "Makefile.js"]); | |
350 | const exitCodeAssertion = assertExitCode(child, 2); | |
351 | const outputAssertion = getOutput(child).then(output => { | |
352 | const expectedSubstring = "Syntax error in selector"; | |
353 | ||
354 | assert.strictEqual(output.stdout, ""); | |
355 | assert.include(output.stderr, expectedSubstring); | |
356 | ||
357 | // The message should appear exactly once in stderr | |
358 | assert.strictEqual(output.stderr.indexOf(expectedSubstring), output.stderr.lastIndexOf(expectedSubstring)); | |
359 | }); | |
360 | ||
361 | return Promise.all([exitCodeAssertion, outputAssertion]); | |
362 | }); | |
363 | ||
364 | it("prints the error message pointing to line of code", () => { | |
56c4a2cb | 365 | const invalidConfig = path.join(__dirname, "../fixtures/bin/.eslintrc.yml"); |
eb39fafa DC |
366 | const child = runESLint(["--no-ignore", invalidConfig]); |
367 | const exitCodeAssertion = assertExitCode(child, 2); | |
368 | const outputAssertion = getOutput(child).then(output => { | |
369 | const expectedSubstring = ": bad indentation of a mapping entry at line"; | |
370 | ||
371 | assert.strictEqual(output.stdout, ""); | |
372 | assert.include(output.stderr, expectedSubstring); | |
373 | }); | |
374 | ||
375 | return Promise.all([exitCodeAssertion, outputAssertion]); | |
376 | }); | |
377 | }); | |
378 | ||
379 | ||
380 | describe("emitting a warning for ecmaFeatures", () => { | |
381 | it("does not emit a warning when it does not find an ecmaFeatures option", () => { | |
382 | const child = runESLint(["Makefile.js"]); | |
383 | ||
384 | const exitCodePromise = assertExitCode(child, 0); | |
385 | const outputPromise = getOutput(child).then(output => assert.strictEqual(output.stderr, "")); | |
386 | ||
387 | return Promise.all([exitCodePromise, outputPromise]); | |
388 | }); | |
389 | it("emits a warning when it finds an ecmaFeatures option", () => { | |
390 | const child = runESLint(["-c", "tests/fixtures/config-file/ecma-features/.eslintrc.yml", "Makefile.js"]); | |
391 | ||
392 | const exitCodePromise = assertExitCode(child, 0); | |
393 | const outputPromise = getOutput(child).then(output => { | |
394 | assert.include(output.stderr, "The 'ecmaFeatures' config file property is deprecated and has no effect."); | |
395 | }); | |
396 | ||
397 | return Promise.all([exitCodePromise, outputPromise]); | |
398 | }); | |
399 | }); | |
400 | ||
401 | afterEach(() => { | |
402 | ||
403 | // Clean up all the processes after every test. | |
404 | forkedProcesses.forEach(child => child.kill()); | |
405 | forkedProcesses.clear(); | |
406 | }); | |
407 | }); |