test(cli): add cli tests

This commit is contained in:
Sam Tolmay 2020-10-30 10:50:04 +02:00
parent 1f27b7d5d2
commit ae097828d1
17 changed files with 565 additions and 48 deletions

View File

@ -17,6 +17,7 @@ jobs:
node-version: '12.x'
- uses: actions/checkout@v2
with:
# needed for yarn version check, checks out entire repo
fetch-depth: 0
- name: Check yarn cache integrity
run: yarn install --immutable --immutable-cache --check-cache

View File

@ -30,7 +30,8 @@
"clean": "lerna run clean",
"prepare": "lerna run prepare",
"prettier": "prettier --config .prettierrc --write **/*.js",
"test": "lerna run test"
"test": "lerna run test",
"test:ci": "yarn install --immutable --immutable-cache --check-cache && yarn version check && yarn build && yarn test --ignore='@lowdefy/format'"
},
"devDependencies": {
"@yarnpkg/pnpify": "2.3.3",

View File

@ -7,6 +7,7 @@
"node": "12"
}
}
]
],
"@babel/preset-react"
]
}

View File

@ -22,13 +22,15 @@ import { outputDirectoryPath } from '../../utils/directories';
async function build(options) {
const context = await createContext(options);
await getBuildScript(context);
const outputDirectory = path.resolve(context.baseDirectory, outputDirectoryPath);
context.print.info('Starting build.');
await context.buildScript({
logger: context.print,
cacheDirectory: context.cacheDirectory,
configDirectory: context.baseDirectory,
outputDirectory: path.resolve(context.baseDirectory, outputDirectoryPath),
outputDirectory,
});
context.print.info(`Build artifacts saved at ${outputDirectory}.`);
}
export default build;

View File

@ -0,0 +1,57 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import { cleanDirectory } from '@lowdefy/node-utils';
import cleanCache from './cleanCache';
import createPrint from '../../utils/print';
jest.mock('@lowdefy/node-utils', () => {
const cleanDirectory = jest.fn();
return { cleanDirectory };
});
jest.mock('../../utils/print', () => {
const info = jest.fn();
return () => ({
info,
});
});
const print = createPrint();
beforeEach(() => {
cleanDirectory.mockReset();
});
test('cleanCache', async () => {
await cleanCache({});
const cachePath = path.resolve(process.cwd(), './.lowdefy/.cache');
expect(cleanDirectory.mock.calls).toEqual([[cachePath]]);
expect(print.info.mock.calls).toEqual([
[`Cleaning cache at "${cachePath}".`],
['Cache cleaned.'],
]);
});
test('cleanCache baseDir', async () => {
await cleanCache({ baseDirectory: 'baseDir' });
const cachePath = path.resolve(process.cwd(), 'baseDir/.lowdefy/.cache');
expect(cleanDirectory.mock.calls).toEqual([[cachePath]]);
expect(print.info.mock.calls).toEqual([
[`Cleaning cache at "${cachePath}".`],
['Cache cleaned.'],
]);
});

View File

@ -1,3 +1,18 @@
<!--
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<!DOCTYPE html>
<html lang="en">
<head>

View File

@ -0,0 +1,188 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import BatchChanges from './BatchChanges';
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const context = {};
test('BatchChanges calls the provided sync function', async () => {
const fn = jest.fn();
const batchChanges = new BatchChanges({ fn, context });
batchChanges.newChange();
await wait(600);
expect(fn).toHaveBeenCalledTimes(1);
});
test('BatchChanges calls the provided async function', async () => {
let done = false;
const fn = jest.fn(async () => {
await wait(100);
done = true;
});
const batchChanges = new BatchChanges({ fn, context });
batchChanges.newChange();
await wait(550);
expect(fn).toHaveBeenCalledTimes(1);
expect(done).toBe(false);
await wait(70);
expect(done).toBe(true);
});
test('BatchChanges calls the provided sync function only once if newChange is called multiple times', async () => {
const fn = jest.fn();
const batchChanges = new BatchChanges({ fn, context });
batchChanges.newChange();
batchChanges.newChange();
batchChanges.newChange();
await wait(600);
expect(fn).toHaveBeenCalledTimes(1);
});
test('BatchChanges has a default minDelay', async () => {
const fn = jest.fn();
const batchChanges = new BatchChanges({ fn, context });
expect(batchChanges.minDelay).toBe(500);
expect(batchChanges.delay).toBe(500);
});
test('BatchChanges set minDelay', async () => {
const fn = jest.fn();
const batchChanges = new BatchChanges({ fn, context, minDelay: 42 });
expect(batchChanges.minDelay).toBe(42);
expect(batchChanges.delay).toBe(42);
});
test('BatchChanges resets timer if newChange is called multiple times in delay window', async () => {
const fn = jest.fn();
const batchChanges = new BatchChanges({ fn, context });
batchChanges.newChange();
await wait(400);
batchChanges.newChange();
await wait(400);
batchChanges.newChange();
await wait(600);
expect(fn).toHaveBeenCalledTimes(1);
});
test('BatchChanges retries on errors, with back-off', async () => {
let count = 0;
let success = false;
const context = {
print: {
error: jest.fn(),
warn: jest.fn(),
},
};
const fn = jest.fn(() => {
if (count > 1) {
success = true;
return;
}
count += 1;
throw new Error(`Error: ${count}`);
});
const batchChanges = new BatchChanges({ fn, context, minDelay: 100 });
batchChanges.newChange();
await wait(120);
expect(fn).toHaveBeenCalledTimes(1);
expect(context.print.error.mock.calls).toEqual([
[
'Error: 1',
{
timestamp: true,
},
],
]);
expect(context.print.warn.mock.calls).toEqual([
[
'Retrying in 0.2s.',
{
timestamp: true,
},
],
]);
expect(batchChanges.delay).toBe(200);
expect(count).toBe(1);
await wait(200);
expect(fn).toHaveBeenCalledTimes(2);
expect(context.print.error.mock.calls).toEqual([
[
'Error: 1',
{
timestamp: true,
},
],
[
'Error: 2',
{
timestamp: true,
},
],
]);
expect(context.print.warn.mock.calls).toEqual([
[
'Retrying in 0.2s.',
{
timestamp: true,
},
],
[
'Retrying in 0.4s.',
{
timestamp: true,
},
],
]);
expect(batchChanges.delay).toBe(400);
expect(count).toBe(2);
await wait(400);
expect(fn).toHaveBeenCalledTimes(3);
expect(context.print.error.mock.calls).toEqual([
[
'Error: 1',
{
timestamp: true,
},
],
[
'Error: 2',
{
timestamp: true,
},
],
]);
expect(context.print.warn.mock.calls).toEqual([
[
'Retrying in 0.2s.',
{
timestamp: true,
},
],
[
'Retrying in 0.4s.',
{
timestamp: true,
},
],
]);
expect(success).toBe(true);
});

View File

@ -19,7 +19,7 @@ import getLowdefyVersion from './getLowdefyVersion';
import createPrint from './print';
import { cacheDirectoryPath } from './directories';
async function createContext(options) {
async function createContext(options = {}) {
const context = {
baseDirectory: path.resolve(options.baseDirectory || process.cwd()),
print: createPrint({ timestamp: true }),

View File

@ -0,0 +1,55 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import createContext from './context';
// eslint-disable-next-line no-unused-vars
import getLowdefyVersion from './getLowdefyVersion';
// eslint-disable-next-line no-unused-vars
import createPrint from './print';
jest.mock('./getLowdefyVersion', () => async () => Promise.resolve('lowdefy-version'));
jest.mock('./print', () => () => 'print');
test('createContext, options undefined', async () => {
const context = await createContext();
expect(context).toEqual({
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
version: 'lowdefy-version',
print: 'print',
});
});
test('createContext, options empty', async () => {
const context = await createContext({});
expect(context).toEqual({
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
version: 'lowdefy-version',
print: 'print',
});
});
test('createContext, options baseDir', async () => {
const context = await createContext({ baseDirectory: 'baseDir' });
expect(context).toEqual({
baseDirectory: path.resolve(process.cwd(), 'baseDir'),
cacheDirectory: path.resolve(process.cwd(), 'baseDir/.lowdefy/.cache'),
version: 'lowdefy-version',
print: 'print',
});
});

View File

@ -1,3 +1,19 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const cacheDirectoryPath = './.lowdefy/.cache';
const outputDirectoryPath = './.lowdefy/build';

View File

@ -0,0 +1,24 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as directories from './directories';
test('directories', () => {
expect(directories).toEqual({
cacheDirectoryPath: './.lowdefy/.cache',
outputDirectoryPath: './.lowdefy/build',
});
});

View File

@ -24,6 +24,7 @@ function errorHandler(fn, options = {}) {
} catch (error) {
const print = createPrint();
print.error(error.message);
// TODO: Stay alive feature
}
}
return run;

View File

@ -62,33 +62,7 @@ test('Pass args to synchronous function', async () => {
expect(res).toEqual({ arg1: '1', arg2: '2' });
});
test('Catch error synchronous function, stay alive', async () => {
const fn = jest.fn(() => {
throw new Error('Error');
});
const wrapped = errorHandler(fn, { stayAlive: true });
const res = await wrapped();
expect(res).toBe(undefined);
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Error']]);
});
test('Catch error asynchronous function, stay alive', async () => {
const fn = jest.fn(async () => {
await wait(3);
throw new Error('Async Error');
});
const wrapped = errorHandler(fn, { stayAlive: true });
const res = await wrapped();
expect(res).toBe(undefined);
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Async Error']]);
});
test('Catch error synchronous function, exit process', async () => {
const realExit = process.exit;
const mockExit = jest.fn();
process.exit = mockExit;
test('Catch error synchronous function', async () => {
const fn = jest.fn(() => {
throw new Error('Error');
});
@ -96,14 +70,9 @@ test('Catch error synchronous function, exit process', async () => {
await wrapped();
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Error']]);
expect(mockExit).toHaveBeenCalled();
process.exit = realExit;
});
test('Catch error asynchronous function, exit process', async () => {
const realExit = process.exit;
const mockExit = jest.fn();
process.exit = mockExit;
test('Catch error asynchronous function', async () => {
const fn = jest.fn(async () => {
await wait(3);
throw new Error('Async Error');
@ -112,6 +81,27 @@ test('Catch error asynchronous function, exit process', async () => {
await wrapped();
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Async Error']]);
expect(mockExit).toHaveBeenCalled();
process.exit = realExit;
});
// test('Catch error synchronous function, stay alive', async () => {
// const fn = jest.fn(() => {
// throw new Error('Error');
// });
// const wrapped = errorHandler(fn, { stayAlive: true });
// const res = await wrapped();
// expect(res).toBe(undefined);
// expect(fn).toHaveBeenCalled();
// expect(print.error.mock.calls).toEqual([['Error']]);
// });
// test('Catch error asynchronous function, stay alive', async () => {
// const fn = jest.fn(async () => {
// await wait(3);
// throw new Error('Async Error');
// });
// const wrapped = errorHandler(fn, { stayAlive: true });
// const res = await wrapped();
// expect(res).toBe(undefined);
// expect(fn).toHaveBeenCalled();
// expect(print.error.mock.calls).toEqual([['Async Error']]);
// });

View File

@ -19,7 +19,7 @@ import { type } from '@lowdefy/helpers';
import { readFile } from '@lowdefy/node-utils';
import YAML from 'js-yaml';
async function getLowdefyVersion(context) {
async function getLowdefyVersion(context = {}) {
const lowdefyYaml = await readFile(
path.resolve(context.baseDirectory || process.cwd(), 'lowdefy.yaml')
);

View File

@ -30,6 +30,19 @@ beforeEach(() => {
});
test('get version from yaml file', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
version: 1.0.0
`;
}
return null;
});
const version = await getLowdefyVersion({});
expect(version).toEqual('1.0.0');
});
test('get version from yaml file, context default value', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
@ -51,7 +64,7 @@ test('get version from yaml file, base dir specified', async () => {
}
return null;
});
const version = await getLowdefyVersion('./baseDir');
const version = await getLowdefyVersion({ baseDirectory: './baseDir' });
expect(version).toEqual('1.0.0');
});
@ -64,7 +77,7 @@ test('could not find lowdefy.yaml in cwd', async () => {
version: 1.0.0
`;
});
await expect(getLowdefyVersion()).rejects.toThrow(
await expect(getLowdefyVersion({})).rejects.toThrow(
'Could not find "lowdefy.yaml" file in current working directory. Change directory to a Lowdefy project, or specify a base directory.'
);
});
@ -78,7 +91,7 @@ test('could not find lowdefy.yaml in base dir', async () => {
version: 1.0.0
`;
});
await expect(getLowdefyVersion('./baseDir')).rejects.toThrow(
await expect(getLowdefyVersion({ baseDirectory: './baseDir' })).rejects.toThrow(
'Could not find "lowdefy.yaml" file in specified base directory'
);
});
@ -94,7 +107,7 @@ test('lowdefy.yaml is invalid yaml', async () => {
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
await expect(getLowdefyVersion({})).rejects.toThrow(
'Could not parse "lowdefy.yaml" file. Received error '
);
});
@ -110,7 +123,7 @@ test('No version specified', async () => {
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
await expect(getLowdefyVersion({})).rejects.toThrow(
'No version specified in "lowdefy.yaml" file. Specify a version in the "version field".'
);
});
@ -124,7 +137,7 @@ test('Version is not a string', async () => {
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
await expect(getLowdefyVersion({})).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received 1.'
);
});
@ -138,7 +151,7 @@ test('Version is not a valid version number', async () => {
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
await expect(getLowdefyVersion({})).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received "v1-0-3".'
);
});

View File

@ -19,7 +19,7 @@ import chalk from 'chalk';
const printToTerminal = (color, options = {}) => (text) => {
let message;
if (options.timestamp) {
const time = options.timestamp === true ? new Date(Date.now()) : new Date(options.timestamp);
const time = new Date(Date.now());
const h = time.getHours();
const m = time.getMinutes();
const s = time.getSeconds();

View File

@ -0,0 +1,153 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import createPrint from './print';
const mockConsoleLog = jest.fn();
const mockGetHours = jest.fn();
const mockGetMinutes = jest.fn();
const mockGetSeconds = jest.fn();
// const realLog = console.log;
// const realNow = Date.now;
console.log = mockConsoleLog;
// eslint-disable-next-line no-global-assign
Date = jest.fn(() => ({
getHours: mockGetHours,
getMinutes: mockGetMinutes,
getSeconds: mockGetSeconds,
}));
Date.now = () => {};
beforeEach(() => {
mockConsoleLog.mockReset();
mockGetHours.mockReset();
mockGetMinutes.mockReset();
mockGetSeconds.mockReset();
});
// afterAll(() => {
// console.log = realLog;
// Date.now = realNow;
// });
test('create print', () => {
const print = createPrint();
expect(print).toMatchInlineSnapshot(`
Object {
"error": [Function],
"info": [Function],
"log": [Function],
"warn": [Function],
}
`);
});
test('print info', () => {
const print = createPrint();
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['Test info']]);
});
test('print log', () => {
const print = createPrint();
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['Test log']]);
});
test('print warn', () => {
const print = createPrint();
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['Test warn']]);
});
test('print error', () => {
const print = createPrint();
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['Test error']]);
});
test('print info with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test info']]);
});
test('print log with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test log']]);
});
test('print warn with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test warn']]);
});
test('print error with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test error']]);
});
test('print info with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test info']]);
});
test('print log with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test log']]);
});
test('print warn with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test warn']]);
});
test('print error with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test error']]);
});