eslint_d
Version:
Speed up eslint to accelerate your development workflow
513 lines (431 loc) • 17.6 kB
JavaScript
import net from 'node:net';
import child_process from 'node:child_process';
import EventEmitter from 'node:events';
import { PassThrough } from 'node:stream';
import fs from 'node:fs';
import fs_promises from 'node:fs/promises';
import supportsColor from 'supports-color';
import { assert, refute, match, sinon } from '@sinonjs/referee-sinon';
import { forwardToDaemon, isAlive } from './forwarder.js';
import { createResolver } from './resolver.js';
import { LINT_COMMAND } from './commands.js';
describe('lib/forwarder', () => {
const resolver = createResolver();
if (resolver === 'fail' || resolver === 'ignore') {
throw new Error('Failed to create resolver');
}
const config = { token: 'token', port: 123, pid: 456, hash: 'hash' };
const color_level = supportsColor.stdout?.['level'] || 0;
let socket;
let argv;
let child;
let read_file_promise;
function returnThis() {
// @ts-ignore
return this;
}
let watcher;
beforeEach(() => {
watcher = new EventEmitter();
watcher['close'] = sinon.fake();
sinon.replace(fs, 'watch', sinon.fake.returns(watcher));
});
/**
* @param {string} text
*/
function fakeStdin(text) {
const stdin = new PassThrough();
stdin.end(text);
sinon.replaceGetter(process, 'stdin', () => stdin);
}
beforeEach(() => {
socket = new PassThrough();
sinon.replace(socket, 'write', sinon.fake());
sinon.replace(socket, 'end', sinon.fake());
sinon.replace(socket, 'on', sinon.fake(returnThis));
sinon.replace(net, 'connect', sinon.fake.returns(socket));
sinon.replace(console, 'error', sinon.fake());
argv = [process.argv0, 'eslint_d'];
sinon.replace(process, 'argv', argv);
});
beforeEach(() => {
read_file_promise = sinon.promise();
sinon.replace(
fs_promises,
'readFile',
sinon.fake.returns(read_file_promise)
);
child = { unref: sinon.fake() };
sinon.replace(child_process, 'spawn', sinon.fake.returns(child));
});
context('isAlive', () => {
it('connects to 127.0.0.1 on port from config', () => {
isAlive(config);
assert.calledOnceWith(net.connect, config.port, '127.0.0.1');
});
it('handles response without errors', async () => {
const chunks = [];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
const result = isAlive(config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
assert.equals(await result, true);
});
it('handles ECONNREFUSED', async () => {
const result = isAlive(config);
const err = new Error('Connection refused');
err['code'] = 'ECONNREFUSED';
socket.on.getCall(1).callback(err); // error
assert.equals(await result, false);
});
});
context('forwardToDaemon', () => {
it('connects to 127.0.0.1 on port from config', () => {
forwardToDaemon(resolver, config);
assert.calledOnceWith(net.connect, config.port, '127.0.0.1');
});
it('writes token, color level, cwd and argv to socket', async () => {
sinon.replace(process, 'cwd', sinon.fake.returns('the/cwd'));
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
assert.calledOnceWith(
socket.write,
`["token","${LINT_COMMAND}",${color_level},"the/cwd",[${JSON.stringify(process.argv0)},"eslint_d"],""]`
);
assert.calledOnce(socket.end);
});
it('writes DEBUG env to socket', async () => {
sinon.replace(process, 'cwd', sinon.fake.returns('the/cwd'));
sinon.define(process.env, 'DEBUG', 'eslint_d:*');
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
assert.calledOnceWith(
socket.write,
`["token","${LINT_COMMAND}",${color_level},"the/cwd",[${JSON.stringify(process.argv0)},"eslint_d"],"eslint_d:*"]`
);
assert.calledOnce(socket.end);
});
it('does not read from stdin by default', () => {
sinon.replace(process.stdin, 'on', sinon.fake(returnThis));
forwardToDaemon(resolver, config);
refute.called(process.stdin.on);
});
it('reads from stdin if --stdin is in argv', () => {
sinon.replace(process.stdin, 'on', sinon.fake(returnThis));
argv.push('--stdin');
forwardToDaemon(resolver, config);
assert.calledThrice(process.stdin.on);
assert.calledWith(process.stdin.on, 'readable', match.func);
assert.calledWith(process.stdin.on, 'end', match.func);
assert.calledWith(process.stdin.on, 'error', match.func);
});
it('writes text from stdin to socket', async () => {
fakeStdin('text from stdin');
argv.push('--stdin');
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
assert.calledThrice(socket.write);
assert.calledWith(socket.write, '\n');
assert.calledWith(socket.write, 'text from stdin');
});
it('forwards socket response to stdout', async () => {
const chunks = ['response ', 'from daemon'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledThrice(process.stdout.write);
assert.calledWith(process.stdout.write, 're');
assert.calledWith(process.stdout.write, 'sponse from');
assert.calledWith(process.stdout.write, ' daemon');
});
it('launch daemon on first connection refused', async () => {
const chunks = ['response ', 'from daemon'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
sinon.replace(fs_promises, 'unlink', sinon.fake.resolves());
forwardToDaemon(resolver, config);
const err = new Error('Connection refused');
err['code'] = 'ECONNREFUSED';
socket.on.getCall(1).callback(err); // connectionAttemptFailed
await new Promise(setImmediate);
// removeConfig
assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`);
await new Promise(setImmediate);
// spawn
assert.calledOnce(child_process.spawn);
// watch config
watcher.emit('change', 'rename', '.eslint_d');
await new Promise(setImmediate);
// read config
read_file_promise.resolve(JSON.stringify(config));
await new Promise(setImmediate);
assert.calledTwice(net.connect);
socket.on.getCall(2).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(4).callback(); // readable
socket.on.getCall(5).callback(); // end
assert.calledThrice(process.stdout.write);
assert.calledWith(process.stdout.write, 're');
assert.calledWith(process.stdout.write, 'sponse from');
assert.calledWith(process.stdout.write, ' daemon');
});
it('handles "EXIT000" from response', async () => {
const chunks = ['response from daemonEXIT000'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from daemon');
assert.equals(process.exitCode, 0);
refute.called(console.error);
});
it('handles "EXIT001" from response', async () => {
const chunks = ['response from daemonEXIT001'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from daemon');
assert.equals(process.exitCode, 1);
refute.called(console.error);
});
it('handles "EXIT123" from response', async () => {
const chunks = ['response from daemonEXIT123'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from daemon');
assert.equals(process.exitCode, 123);
refute.called(console.error);
});
it('handles "EXIT001" inside response', async () => {
const chunks = ['response EXIT001', ' from daemonEXIT002'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response ');
assert.calledWith(process.stdout.write, 'EXIT001 from daemon');
assert.equals(process.exitCode, 2);
refute.called(console.error);
});
it('logs error and sets exitCode to 1 if response does not end with EXIT marker', async () => {
const chunks = ['response from daemon'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from');
assert.calledWith(process.stdout.write, ' daemon');
assert.equals(process.exitCode, 1);
assert.calledOnceWith(console.error, 'eslint_d: unexpected response');
});
it('logs error from stream', async () => {
sinon.replace(fs_promises, 'unlink', sinon.fake.resolves());
forwardToDaemon(resolver, config);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(4).callback(new Error('Ouch!')); // error
assert.calledOnceWith(console.error, 'eslint_d: Error: Ouch!');
assert.equals(process.exitCode, 1);
refute.called(fs_promises.unlink);
});
it('logs error on second ECONNREFUSED', async () => {
sinon.replace(fs_promises, 'unlink', sinon.fake.resolves());
forwardToDaemon(resolver, config);
const err = new Error('Connection refused');
err['code'] = 'ECONNREFUSED';
socket.on.getCall(1).callback(err); // connectionAttemptFailed
await new Promise(setImmediate);
// removeConfig
assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`);
await new Promise(setImmediate);
// spawn
assert.calledOnce(child_process.spawn);
// watch config
watcher.emit('change', 'rename', '.eslint_d');
await new Promise(setImmediate);
// read config
read_file_promise.resolve(JSON.stringify(config));
await new Promise(setImmediate);
assert.calledTwice(net.connect);
socket.on.getCall(3).callback(err); // connectionAttemptFailed
await new Promise(setImmediate);
assert.calledOnceWith(
console.error,
'eslint_d: Error: Connection refused - removing config'
);
assert.equals(process.exitCode, 1);
assert.calledTwice(fs_promises.unlink);
});
context('--fix-to-stdout', () => {
beforeEach(() => {
sinon.replace(process, 'cwd', sinon.fake.returns('cwd'));
fakeStdin('text from stdin');
});
it('throws if --stdin is absent', async () => {
argv.push('--fix-to-stdout');
await forwardToDaemon(resolver, config);
assert.equals(process.exitCode, 1);
assert.calledOnceWith(
console.error,
'--fix-to-stdout requires passing --stdin as well'
);
});
it('replaces the option with --fix-dry-run --format json', async () => {
argv.push('--stdin', '--fix-to-stdout', '--other', '--options');
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
assert.calledThrice(socket.write);
assert.calledWith(
socket.write,
`["token","${LINT_COMMAND}",${color_level},"cwd",[${JSON.stringify(process.argv0)},"eslint_d","--stdin","--fix-dry-run","--format","json","--other","--options"],""]`
);
assert.calledWith(socket.write, '\n');
assert.calledWith(socket.write, 'text from stdin');
assert.calledOnce(socket.end);
});
it('prints fixed output to stdout', async () => {
argv.push('--stdin', '--fix-to-stdout');
const chunks = ['[{"output":"response from daemon"}]EXIT001'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from daemon');
assert.equals(process.exitCode, 0);
refute.called(console.error);
});
it('prints original input to stdout if no output', async () => {
argv.push('--stdin', '--fix-to-stdout');
const chunks = ['[{}]EXIT000'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'text from stdin');
assert.equals(process.exitCode, 0);
refute.called(console.error);
});
it('prints error to stderr and original input to stdout if output cannot be parsed', async () => {
argv.push('--stdin', '--fix-to-stdout');
const chunks = ['NotJSON!EXIT000'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'text from stdin');
assert.equals(process.exitCode, 1);
let error;
try {
JSON.parse('NotJSON!');
} catch (err) {
error = err;
}
assert.calledOnceWith(console.error, `eslint_d: ${error}`);
});
it('logs error and sets exitCode to 1 if response does not end with EXIT marker', async () => {
argv.push('--stdin', '--fix-to-stdout');
const chunks = ['response from daemon'];
sinon.replace(
socket,
'read',
sinon.fake(() => (chunks.length ? chunks.shift() : null))
);
sinon.replace(process.stdout, 'write', sinon.fake());
forwardToDaemon(resolver, config);
await new Promise(setImmediate);
socket.on.getCall(0).callback(); // connect
await new Promise(setImmediate);
socket.on.getCall(2).callback(); // readable
socket.on.getCall(3).callback(); // end
assert.calledWith(process.stdout.write, 'response from daemon');
assert.equals(process.exitCode, 1);
assert.calledOnceWith(console.error, 'eslint_d: unexpected response');
});
});
});
});