diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 02648f3084ceb..3225f18718de1 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -176,7 +176,7 @@ exports.Cluster = class Cluster { this._process.once('exit', code => { // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat them as errors if (code > 0 && !(code === 143 || code === 130)) { - reject(createCliError(`ES exitted with code ${code}`)); + reject(createCliError(`ES exited with code ${code}`)); } else { resolve(); } diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js new file mode 100644 index 0000000000000..f1dc0e736a0bf --- /dev/null +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +const { exitCode, start } = JSON.parse(process.argv[2]); + +if (start) { + console.log('started'); +} + +process.exitCode = exitCode; diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js new file mode 100644 index 0000000000000..42ccae8ad0f45 --- /dev/null +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -0,0 +1,330 @@ +const { createToolingLog } = require('@kbn/dev-utils'); +const execa = require('execa'); +const { Cluster } = require('../cluster'); +const { + installSource, + installSnapshot, + installArchive, +} = require('../install'); + +jest.mock('../install', () => ({ + installSource: jest.fn(), + installSnapshot: jest.fn(), + installArchive: jest.fn(), +})); + +jest.mock('execa', () => jest.fn()); + +const log = createToolingLog('verbose'); +log.onData = jest.fn(); +log.on('data', log.onData); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function ensureNoResolve(promise) { + await Promise.race([ + sleep(100), + promise.then(() => { + throw new Error('promise was not supposed to resolve'); + }), + ]); +} + +async function ensureResolve(promise) { + return await Promise.race([ + promise, + sleep(100).then(() => { + throw new Error( + 'promise was supposed to resolve with installSource() resolution' + ); + }), + ]); +} + +function mockEsBin({ exitCode, start }) { + execa.mockImplementationOnce((cmd, args, options) => + require.requireActual('execa')( + process.execPath, + [ + require.resolve('./__fixtures__/es_bin.js'), + JSON.stringify({ + exitCode, + start, + }), + ], + options + ) + ); +} + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('#installSource()', () => { + it('awaits installSource() promise and returns { installPath }', async () => { + let resolveInstallSource; + installSource.mockImplementationOnce( + () => + new Promise(resolve => { + resolveInstallSource = () => { + resolve({ installPath: 'foo' }); + }; + }) + ); + + const cluster = new Cluster(log); + const promise = cluster.installSource(); + await ensureNoResolve(promise); + resolveInstallSource(); + await expect(ensureResolve(promise)).resolves.toEqual({ + installPath: 'foo', + }); + }); + + it('passes through all options+log to installSource()', async () => { + installSource.mockResolvedValue({}); + const cluster = new Cluster(log); + await cluster.installSource({ foo: 'bar' }); + expect(installSource).toHaveBeenCalledTimes(1); + expect(installSource).toHaveBeenCalledWith({ + log, + foo: 'bar', + }); + }); + + it('rejects if installSource() rejects', async () => { + installSource.mockRejectedValue(new Error('foo')); + const cluster = new Cluster(log); + await expect(cluster.installSource()).rejects.toThrowError('foo'); + }); +}); + +describe('#installSnapshot()', () => { + it('awaits installSnapshot() promise and returns { installPath }', async () => { + let resolveInstallSnapshot; + installSnapshot.mockImplementationOnce( + () => + new Promise(resolve => { + resolveInstallSnapshot = () => { + resolve({ installPath: 'foo' }); + }; + }) + ); + + const cluster = new Cluster(log); + const promise = cluster.installSnapshot(); + await ensureNoResolve(promise); + resolveInstallSnapshot(); + await expect(ensureResolve(promise)).resolves.toEqual({ + installPath: 'foo', + }); + }); + + it('passes through all options+log to installSnapshot()', async () => { + installSnapshot.mockResolvedValue({}); + const cluster = new Cluster(log); + await cluster.installSnapshot({ foo: 'bar' }); + expect(installSnapshot).toHaveBeenCalledTimes(1); + expect(installSnapshot).toHaveBeenCalledWith({ + log, + foo: 'bar', + }); + }); + + it('rejects if installSnapshot() rejects', async () => { + installSnapshot.mockRejectedValue(new Error('foo')); + const cluster = new Cluster(log); + await expect(cluster.installSnapshot()).rejects.toThrowError('foo'); + }); +}); + +describe('#installArchive(path)', () => { + it('awaits installArchive() promise and returns { installPath }', async () => { + let resolveInstallArchive; + installArchive.mockImplementationOnce( + () => + new Promise(resolve => { + resolveInstallArchive = () => { + resolve({ installPath: 'foo' }); + }; + }) + ); + + const cluster = new Cluster(log); + const promise = cluster.installArchive(); + await ensureNoResolve(promise); + resolveInstallArchive(); + await expect(ensureResolve(promise)).resolves.toEqual({ + installPath: 'foo', + }); + }); + + it('passes through path and all options+log to installArchive()', async () => { + installArchive.mockResolvedValue({}); + const cluster = new Cluster(log); + await cluster.installArchive('path', { foo: 'bar' }); + expect(installArchive).toHaveBeenCalledTimes(1); + expect(installArchive).toHaveBeenCalledWith('path', { + log, + foo: 'bar', + }); + }); + + it('rejects if installArchive() rejects', async () => { + installArchive.mockRejectedValue(new Error('foo')); + const cluster = new Cluster(log); + await expect(cluster.installArchive()).rejects.toThrowError('foo'); + }); +}); + +describe('#start(installPath)', () => { + it('rejects when bin/elasticsearch exists with 0 before starting', async () => { + mockEsBin({ exitCode: 0, start: false }); + + await expect(new Cluster(log).start()).rejects.toThrowError( + 'ES exited without starting' + ); + }); + + it('rejects when bin/elasticsearch exists with 143 before starting', async () => { + mockEsBin({ exitCode: 143, start: false }); + + await expect(new Cluster(log).start()).rejects.toThrowError( + 'ES exited without starting' + ); + }); + + it('rejects when bin/elasticsearch exists with 130 before starting', async () => { + mockEsBin({ exitCode: 130, start: false }); + + await expect(new Cluster(log).start()).rejects.toThrowError( + 'ES exited without starting' + ); + }); + + it('rejects when bin/elasticsearch exists with 1 before starting', async () => { + mockEsBin({ exitCode: 1, start: false }); + + await expect(new Cluster(log).start()).rejects.toThrowError( + 'ES exited with code 1' + ); + }); + + it('resolves when bin/elasticsearch logs "started"', async () => { + mockEsBin({ start: true }); + + await new Cluster(log).start(); + }); + + it('rejects if #start() was called previously', async () => { + mockEsBin({ start: true }); + + const cluster = new Cluster(log); + await cluster.start(); + await expect(cluster.start()).rejects.toThrowError( + 'ES has already been started' + ); + }); + + it('rejects if #run() was called previously', async () => { + mockEsBin({ start: true }); + + const cluster = new Cluster(log); + await cluster.run(); + await expect(cluster.start()).rejects.toThrowError( + 'ES has already been started' + ); + }); +}); + +describe('#run()', () => { + it('resolves when bin/elasticsearch exists with 0', async () => { + mockEsBin({ exitCode: 0 }); + + await new Cluster(log).run(); + }); + + it('resolves when bin/elasticsearch exists with 143', async () => { + mockEsBin({ exitCode: 143 }); + + await new Cluster(log).run(); + }); + + it('resolves when bin/elasticsearch exists with 130', async () => { + mockEsBin({ exitCode: 130 }); + + await new Cluster(log).run(); + }); + + it('rejects when bin/elasticsearch exists with 1', async () => { + mockEsBin({ exitCode: 1 }); + + await expect(new Cluster(log).run()).rejects.toThrowError( + 'ES exited with code 1' + ); + }); + + it('rejects if #start() was called previously', async () => { + mockEsBin({ exitCode: 0, start: true }); + + const cluster = new Cluster(log); + await cluster.start(); + await expect(cluster.run()).rejects.toThrowError( + 'ES has already been started' + ); + }); + + it('rejects if #run() was called previously', async () => { + mockEsBin({ exitCode: 0 }); + + const cluster = new Cluster(log); + await cluster.run(); + await expect(cluster.run()).rejects.toThrowError( + 'ES has already been started' + ); + }); +}); + +describe('#stop()', () => { + it('rejects if #run() or #start() was not called', async () => { + const cluster = new Cluster(log); + await expect(cluster.stop()).rejects.toThrowError( + 'ES has not been started' + ); + }); + + it('resolves when ES exits with 0', async () => { + mockEsBin({ exitCode: 0, start: true }); + + const cluster = new Cluster(log); + await cluster.start(); + await cluster.stop(); + }); + + it('resolves when ES exits with 143', async () => { + mockEsBin({ exitCode: 143, start: true }); + + const cluster = new Cluster(log); + await cluster.start(); + await cluster.stop(); + }); + + it('resolves when ES exits with 130', async () => { + mockEsBin({ exitCode: 130, start: true }); + + const cluster = new Cluster(log); + await cluster.start(); + await cluster.stop(); + }); + + it('rejects when ES exits with 1', async () => { + mockEsBin({ exitCode: 1, start: true }); + + const cluster = new Cluster(log); + await expect(cluster.run()).rejects.toThrowError('ES exited with code 1'); + await expect(cluster.stop()).rejects.toThrowError('ES exited with code 1'); + }); +});