@loki/runner
Version:
Loki test runner
311 lines (298 loc) • 9.65 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const ciInfo = require('ci-info');
const map = require('ramda/src/map');
const groupBy = require('ramda/src/groupBy');
const toPairs = require('ramda/src/toPairs');
const fromPairs = require('ramda/src/fromPairs');
const mapObjIndexed = require('ramda/src/mapObjIndexed');
const { createChromeAppTarget } = require('@loki/target-chrome-app');
const { createChromeDockerTarget } = require('@loki/target-chrome-docker');
const {
createChromeAWSLambdaTarget,
} = require('@loki/target-chrome-aws-lambda');
const {
createIOSSimulatorTarget,
} = require('@loki/target-native-ios-simulator');
const {
createAndroidEmulatorTarget,
} = require('@loki/target-native-android-emulator');
const { die } = require('../../console');
const createBaselineLimitedBatchBuilder = require('./create-baseline-limited-batch-builder');
const testBatch = require('./test-batch');
const { TaskRunner } = require('./task-runner');
const {
renderInteractive,
renderVerbose,
renderNonInteractive,
renderSilent,
} = require('./renderers');
const {
TASK_TYPE_TARGET,
TASK_TYPE_PREPARE,
TASK_TYPE_START,
TASK_TYPE_FETCH_STORIES,
TASK_TYPE_TESTS,
TASK_TYPE_TEST,
TASK_TYPE_STOP,
} = require('./constants');
const getRendererForOptions = (options) => {
if (options.silent) {
return renderSilent;
}
if (options.verboseRenderer) {
return renderVerbose;
}
if (ciInfo.isCI) {
return renderNonInteractive;
}
return renderInteractive;
};
async function placeGitignore(pathsToIgnore) {
const parentDir = path.dirname(pathsToIgnore[0]);
const gitignorePath = `${parentDir}/.gitignore`;
if (!(await fs.pathExists(gitignorePath))) {
const relativeToParent = (p) => path.relative(parentDir, p);
const isDecendant = (p) => p.indexOf('..') !== 0;
const gitignore = pathsToIgnore
.map(relativeToParent)
.filter(isDecendant)
.concat(['']) // For last empty newline
.join('\n');
await fs.outputFile(gitignorePath, gitignore);
}
}
const groupByTarget = (configurations) =>
mapObjIndexed(
fromPairs,
groupBy(([, { target }]) => target, toPairs(configurations))
);
async function runTests(flatConfigurations, options) {
if (options.updateReference) {
await fs.ensureDir(options.referenceDir);
} else {
await fs.emptyDirSync(options.outputDir);
await fs.emptyDirSync(options.differenceDir);
await placeGitignore([options.outputDir, options.differenceDir]);
}
const getTargetTasks = (
targetName,
target,
configurations,
concurrency = 1,
tolerance = 0,
batchSize = 1,
batchBuilder
) => {
let storybook;
return {
id: targetName,
meta: {
type: TASK_TYPE_TARGET,
},
task: () =>
new TaskRunner([
{
id: `${targetName}/${TASK_TYPE_PREPARE}`,
meta: {
target: targetName,
type: TASK_TYPE_PREPARE,
},
task: async () => {
await target.prepare();
},
enabled: !!target.prepare,
},
{
id: `${targetName}/${TASK_TYPE_START}`,
meta: {
target: targetName,
type: TASK_TYPE_START,
},
task: async ({ activeTargets }) => {
await target.start();
activeTargets.push(target);
},
},
{
id: `${targetName}/${TASK_TYPE_FETCH_STORIES}`,
meta: {
target: targetName,
type: TASK_TYPE_FETCH_STORIES,
},
task: async () => {
storybook = await target.getStorybook();
if (storybook.length === 0 && !options.passWithNoStories) {
throw new Error('Error: No stories were found.');
}
},
},
{
id: `${targetName}/${TASK_TYPE_TESTS}`,
meta: {
target: targetName,
type: TASK_TYPE_TESTS,
},
task: () =>
new TaskRunner(
Object.keys(configurations).reduce(
(tasks, configurationName) => {
const configuration = configurations[configurationName];
const excludePattern =
options.skipStoriesPattern || configuration.skipStories;
const includePattern =
options.storiesFilter || configuration.storiesFilter;
return tasks.concat(
storybook
.filter(({ kind, story }) => {
const fullStoryName = `${kind} ${story}`;
const exclude =
excludePattern &&
new RegExp(excludePattern, 'i').test(fullStoryName);
const include =
!includePattern ||
new RegExp(includePattern, 'i').test(fullStoryName);
return !exclude && include;
})
.map(({ id, kind, story, parameters }) => ({
id: `${targetName}/${TASK_TYPE_TEST}/${configurationName}/${kind}/${story}`,
meta: {
target: targetName,
configuration: configurationName,
id,
kind,
story,
type: TASK_TYPE_TEST,
},
task: {
configuration,
configurationName,
id,
kind,
story,
parameters,
},
}))
);
},
[]
),
{
concurrency: Math.ceil(concurrency / batchSize),
exitOnError: false,
batchSize,
batchBuilder,
batchExector: (batch) =>
testBatch(
target,
batch.map(({ task }) => task),
options,
tolerance
),
}
),
},
{
id: `${targetName}/${TASK_TYPE_STOP}`,
meta: {
target: targetName,
type: TASK_TYPE_STOP,
},
task: async ({ activeTargets }) => {
await target.stop();
const index = activeTargets.indexOf(target);
if (index !== -1) {
activeTargets.splice(index, 1);
}
},
},
]),
};
};
const createTargetTask = (configurations) => {
const { target } = configurations[Object.keys(configurations)[0]];
switch (target) {
case 'chrome.app': {
return getTargetTasks(
target,
createChromeAppTarget({
baseUrl: options.reactUri,
chromeFlags: options.chromeFlags,
}),
configurations,
options.chromeConcurrency,
options.chromeTolerance
);
}
case 'chrome.aws-lambda': {
return getTargetTasks(
target,
createChromeAWSLambdaTarget({
baseUrl: options.reactUri,
chromeAwsLambdaFunctionName: options.chromeAwsLambdaFunctionName,
chromeAwsLambdaRetries: options.chromeAwsLambdaRetries,
}),
configurations,
options.chromeConcurrency,
options.chromeTolerance,
options.chromeAwsLambdaBatchSize,
createBaselineLimitedBatchBuilder(options)
);
}
case 'chrome.docker': {
return getTargetTasks(
target,
createChromeDockerTarget({
baseUrl: options.reactUri,
chromeDockerImage: options.chromeDockerImage,
chromeFlags: options.chromeFlags,
dockerNet: options.dockerNet,
dockerWithSudo: options.dockerWithSudo,
chromeDockerWithoutSeccomp: options.chromeDockerWithoutSeccomp,
}),
configurations,
options.chromeConcurrency,
options.chromeTolerance
);
}
case 'ios.simulator': {
return getTargetTasks(
target,
createIOSSimulatorTarget({
socketUri: options.reactNativeUri,
device: options.device,
}),
configurations
);
}
case 'android.emulator': {
return getTargetTasks(
target,
createAndroidEmulatorTarget({
socketUri: options.reactNativeUri,
device: options.device,
}),
configurations
);
}
default: {
return die(`Unknown target "${target}`);
}
}
};
const tasks = new TaskRunner(
Object.values(map(createTargetTask, groupByTarget(flatConfigurations)))
);
const context = { activeTargets: [] };
const render = getRendererForOptions(options);
const stopRendering = render(tasks);
try {
await tasks.run(context);
stopRendering();
} catch (err) {
stopRendering();
await Promise.all(context.activeTargets.map((target) => target.stop()));
throw err;
}
}
module.exports = runTests;