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