mirror of https://github.com/status-im/metro.git
434 lines
11 KiB
JavaScript
434 lines
11 KiB
JavaScript
|
/**
|
||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||
|
*
|
||
|
* This source code is licensed under the MIT license found in the
|
||
|
* LICENSE file in the root directory of this source tree.
|
||
|
*
|
||
|
* @emails oncall+javascript_foundation
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
jest
|
||
|
.mock('console')
|
||
|
.mock('fs', () => new (require('metro-memory-fs'))())
|
||
|
.mock('temp', () => ({path() {
|
||
|
return '/tmp/repro.args';
|
||
|
}}))
|
||
|
.useRealTimers();
|
||
|
|
||
|
const JSONStream = require('JSONStream');
|
||
|
const buckWorker = require('../worker-tool');
|
||
|
const path = require('path');
|
||
|
const mkdirp = require('mkdirp');
|
||
|
|
||
|
// mocked
|
||
|
const {Console} = require('console');
|
||
|
const fs = require('fs');
|
||
|
|
||
|
const {any, anything} = jasmine;
|
||
|
|
||
|
const UNKNOWN_MESSAGE = 1;
|
||
|
const INVALID_MESSAGE = 2;
|
||
|
|
||
|
describe('Buck worker:', () => {
|
||
|
let commands, inStream, worker, written;
|
||
|
|
||
|
beforeEach(() => {
|
||
|
commands = {};
|
||
|
worker = buckWorker(commands);
|
||
|
|
||
|
inStream = JSONStream.stringify();
|
||
|
inStream.pipe(worker);
|
||
|
written = [];
|
||
|
worker.on('data', chunk => written.push(chunk));
|
||
|
});
|
||
|
|
||
|
describe('handshake:', () => {
|
||
|
it('responds to a correct handshake', () => {
|
||
|
inStream.write(handshake());
|
||
|
|
||
|
return end().then(data =>
|
||
|
expect(data).toEqual([handshake()])
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('responds to a handshake with a `protocol_version` different from "0"', () => {
|
||
|
inStream.write({
|
||
|
id: 0,
|
||
|
type: 'handshake',
|
||
|
protocol_version: '2',
|
||
|
capabilities: [],
|
||
|
});
|
||
|
|
||
|
return end().then(responses =>
|
||
|
expect(responses).toEqual([{
|
||
|
id: 0,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
}]));
|
||
|
});
|
||
|
|
||
|
it('errors for a second handshake', () => {
|
||
|
inStream.write(handshake());
|
||
|
inStream.write(handshake(1));
|
||
|
|
||
|
return end().then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 1,
|
||
|
type: 'error',
|
||
|
exit_code: UNKNOWN_MESSAGE,
|
||
|
}));
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('errors for unknown message types', () => {
|
||
|
inStream.write(handshake());
|
||
|
inStream.write({id: 1, type: 'arbitrary'});
|
||
|
return end().then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 1,
|
||
|
type: 'error',
|
||
|
exit_code: UNKNOWN_MESSAGE,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
describe('commands:', () => {
|
||
|
let createWriteStreamImpl, openedStreams;
|
||
|
|
||
|
function mockFiles(files) {
|
||
|
writeFiles(files, '/');
|
||
|
}
|
||
|
|
||
|
function writeFiles(files, dirPath) {
|
||
|
for (const key in files) {
|
||
|
const entry = files[key];
|
||
|
if (entry == null || typeof entry === 'string') {
|
||
|
fs.writeFileSync(path.join(dirPath, key), entry || '');
|
||
|
} else {
|
||
|
const subDirPath = path.join(dirPath, key);
|
||
|
mkdirp.sync(subDirPath);
|
||
|
writeFiles(entry, subDirPath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
beforeAll(() => {
|
||
|
createWriteStreamImpl = fs.createWriteStream;
|
||
|
fs.createWriteStream = (...args) => {
|
||
|
const writeStream = createWriteStreamImpl(...args);
|
||
|
++openedStreams;
|
||
|
writeStream.on('close', () => (--openedStreams));
|
||
|
return writeStream;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
afterAll(() => {
|
||
|
fs.createWriteStream = createWriteStreamImpl;
|
||
|
});
|
||
|
|
||
|
beforeEach(() => {
|
||
|
fs.reset();
|
||
|
openedStreams = 0;
|
||
|
mockFiles({
|
||
|
'arbitrary': {
|
||
|
'args': '',
|
||
|
'stdout': '',
|
||
|
'stderr': '',
|
||
|
},
|
||
|
// When an error happens, the worker writes a repro file to the
|
||
|
// temporary folder.
|
||
|
'tmp': {},
|
||
|
});
|
||
|
|
||
|
inStream.write(handshake());
|
||
|
});
|
||
|
|
||
|
afterEach(function assertThatAllWriteStreamsWereClosed() {
|
||
|
expect(openedStreams).toBe(0);
|
||
|
});
|
||
|
|
||
|
it('errors if `args_path` cannot be opened', () => {
|
||
|
mockFiles({some: {'args-path': undefined}});
|
||
|
inStream.write(command({id: 5, args_path: '/some/args-path'}));
|
||
|
return end(2).then(([, response]) => {
|
||
|
expect(response).toEqual({
|
||
|
id: 5,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('errors if `stdout_path` cannot be opened', () => {
|
||
|
const path = '/does/not/exist';
|
||
|
inStream.write(command({id: 5, stdout_path: path}));
|
||
|
return end(2).then(([, response]) => {
|
||
|
expect(response).toEqual({
|
||
|
id: 5,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('errors if `stderr_path` cannot be opened', () => {
|
||
|
const path = '/does/not/exist';
|
||
|
inStream.write(command({id: 5, stderr_path: path}));
|
||
|
return end(2).then(([, response]) => {
|
||
|
expect(response).toEqual({
|
||
|
id: 5,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('errors for unspecified commands', () => {
|
||
|
mockFiles({
|
||
|
arbitrary: {
|
||
|
file: '--flag-without-preceding-command',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
inStream.write(command({
|
||
|
id: 1,
|
||
|
args_path: '/arbitrary/file',
|
||
|
}));
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 1,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('errors for empty commands', () => {
|
||
|
mockFiles({
|
||
|
arbitrary: {
|
||
|
file: '',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
inStream.write(command({
|
||
|
id: 2,
|
||
|
args_path: '/arbitrary/file',
|
||
|
}));
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 2,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('errors for unknown commands', () => {
|
||
|
mockFiles({
|
||
|
arbitrary: {
|
||
|
file: 'arbitrary',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
inStream.write(command({
|
||
|
id: 3,
|
||
|
args_path: '/arbitrary/file',
|
||
|
}));
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 3,
|
||
|
type: 'error',
|
||
|
exit_code: INVALID_MESSAGE,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('errors if no `args_path` is specified', () => {
|
||
|
inStream.write({
|
||
|
id: 1,
|
||
|
type: 'command',
|
||
|
stdout_path: '/arbitrary',
|
||
|
stderr_path: '/arbitrary',
|
||
|
});
|
||
|
return end().then(([, response]) =>
|
||
|
expect(response)
|
||
|
.toEqual({id: 1, type: 'error', exit_code: INVALID_MESSAGE}));
|
||
|
});
|
||
|
|
||
|
it('errors if no `stdout_path` is specified', () => {
|
||
|
inStream.write({
|
||
|
id: 1,
|
||
|
type: 'command',
|
||
|
args_path: '/arbitrary',
|
||
|
stderr_path: '/arbitrary',
|
||
|
});
|
||
|
return end().then(([, response]) =>
|
||
|
expect(response)
|
||
|
.toEqual({id: 1, type: 'error', exit_code: INVALID_MESSAGE}));
|
||
|
});
|
||
|
|
||
|
it('errors if no `stderr_path` is specified', () => {
|
||
|
inStream.write({
|
||
|
id: 1,
|
||
|
type: 'command',
|
||
|
args_path: '/arbitrary',
|
||
|
stdout_path: '/arbitrary',
|
||
|
});
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response)
|
||
|
.toEqual({id: 1, type: 'error', exit_code: INVALID_MESSAGE}));
|
||
|
});
|
||
|
|
||
|
it('passes arguments to an existing command', () => {
|
||
|
commands.transform = jest.fn();
|
||
|
const args = 'foo bar baz\tmore';
|
||
|
mockFiles({
|
||
|
arbitrary: {
|
||
|
file: 'transform ' + args,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
inStream.write(command({
|
||
|
args_path: '/arbitrary/file',
|
||
|
}));
|
||
|
|
||
|
return end(1).then(() =>
|
||
|
expect(commands.transform)
|
||
|
.toBeCalledWith(args.split(/\s+/), null, anything()));
|
||
|
});
|
||
|
|
||
|
it('passes JSON/structured arguments to an existing command', async () => {
|
||
|
commands.transform = jest.fn();
|
||
|
const args = {foo: 'bar', baz: 'glo'};
|
||
|
mockFiles({
|
||
|
arbitrary: {
|
||
|
file: JSON.stringify({...args, command: 'transform'}),
|
||
|
},
|
||
|
});
|
||
|
|
||
|
inStream.write(command({
|
||
|
args_path: '/arbitrary/file',
|
||
|
}));
|
||
|
|
||
|
await end(1);
|
||
|
expect(commands.transform)
|
||
|
.toBeCalledWith([], args, anything());
|
||
|
});
|
||
|
|
||
|
it('passes a console object to the command', () => {
|
||
|
mockFiles({
|
||
|
args: 'transform',
|
||
|
stdio: {},
|
||
|
});
|
||
|
|
||
|
commands.transform = jest.fn();
|
||
|
|
||
|
inStream.write(command({
|
||
|
args_path: '/args',
|
||
|
stdout_path: '/stdio/out',
|
||
|
stderr_path: '/stdio/err',
|
||
|
}));
|
||
|
|
||
|
return end().then(() => {
|
||
|
const streams = last(Console.mock.calls);
|
||
|
expect(streams[0].path).toEqual('/stdio/out');
|
||
|
expect(streams[1].path).toEqual('/stdio/err');
|
||
|
expect(commands.transform)
|
||
|
.toBeCalledWith(anything(), null, any(Console));
|
||
|
});
|
||
|
|
||
|
});
|
||
|
|
||
|
it('responds with success if the command finishes succesfully', () => {
|
||
|
commands.transform = (args, _) => {};
|
||
|
mockFiles({path: {to: {args: 'transform'}}});
|
||
|
inStream.write(command({
|
||
|
id: 123,
|
||
|
args_path: '/path/to/args',
|
||
|
}));
|
||
|
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 123,
|
||
|
type: 'result',
|
||
|
exit_code: 0,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('responds with error if the command errors asynchronously', () => {
|
||
|
commands.transform =
|
||
|
jest.fn((args, _, callback) => Promise.reject(new Error('arbitrary')));
|
||
|
mockFiles({path: {to: {args: 'transform'}}});
|
||
|
inStream.write(command({
|
||
|
id: 123,
|
||
|
args_path: '/path/to/args',
|
||
|
}));
|
||
|
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 123,
|
||
|
type: 'error',
|
||
|
exit_code: 3,
|
||
|
}));
|
||
|
});
|
||
|
|
||
|
it('responds with error if the command throws synchronously', () => {
|
||
|
commands.transform = (args, _) => {
|
||
|
throw new Error('arbitrary');
|
||
|
};
|
||
|
mockFiles({path: {to: {args: 'transform'}}});
|
||
|
inStream.write(command({
|
||
|
id: 456,
|
||
|
args_path: '/path/to/args',
|
||
|
}));
|
||
|
|
||
|
return end(2).then(([, response]) =>
|
||
|
expect(response).toEqual({
|
||
|
id: 456,
|
||
|
type: 'error',
|
||
|
exit_code: 3,
|
||
|
}));
|
||
|
});
|
||
|
});
|
||
|
|
||
|
function end(afterMessages) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
worker
|
||
|
.once('error', reject)
|
||
|
.once('end', () => resolve(written.join('')));
|
||
|
|
||
|
if (afterMessages == null || written.length >= afterMessages) {
|
||
|
inStream.end();
|
||
|
} else {
|
||
|
worker.on('data', () => {
|
||
|
if (written.length === afterMessages) {
|
||
|
inStream.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}).then(JSON.parse);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function command(overrides) {
|
||
|
return {
|
||
|
id: 4, // chosen by fair dice roll
|
||
|
type: 'command',
|
||
|
args_path: '/arbitrary/args',
|
||
|
stdout_path: '/arbitrary/stdout',
|
||
|
stderr_path: '/arbitrary/stderr',
|
||
|
...overrides,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function handshake(id = 0) {
|
||
|
return {
|
||
|
id,
|
||
|
type: 'handshake',
|
||
|
protocol_version: '0',
|
||
|
capabilities: [],
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function last(arrayLike) {
|
||
|
return arrayLike[arrayLike.length - 1];
|
||
|
}
|