@devicecloud.dev/dcd
Version:
Better cloud maestro testing
216 lines (215 loc) • 9.24 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.plan = plan;
const glob_1 = require("glob");
const fs = require("node:fs");
const path = require("node:path");
const planMethods_1 = require("./planMethods");
async function checkDependencies(input) {
const checkedDependencies = [];
const uncheckedDependencies = [input];
while (uncheckedDependencies.length > 0) {
const fileToCheck = uncheckedDependencies.shift();
const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(fileToCheck);
const { allErrors, allFiles } = (0, planMethods_1.processDependencies)({
config,
input: fileToCheck,
testSteps,
});
if (allErrors.length > 0) {
throw new Error('The following flow files are not present in the filesystem: \n' +
allErrors.join('\n'));
}
for (const file of allFiles) {
if (!(0, planMethods_1.isFlowFile)(file)) {
// js/media files don't have dependencies
checkedDependencies.push(file);
}
if (!checkedDependencies.includes(file)) {
uncheckedDependencies.push(file);
}
}
if (!checkedDependencies.includes(fileToCheck)) {
checkedDependencies.push(fileToCheck);
}
}
return checkedDependencies;
}
function filterFlowFiles(unfilteredFlowFiles, excludeFlows) {
if (excludeFlows) {
return unfilteredFlowFiles.filter((file) => !excludeFlows.some((flow) => path.normalize(file).startsWith(path.normalize(path.resolve(flow)))));
}
return unfilteredFlowFiles;
}
function getWorkspaceConfig(input, unfilteredFlowFiles) {
const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
const config = configFilePath
? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
: {};
return config;
}
function extractDeviceCloudOverrides(config) {
if (!config || !config.env || typeof config.env !== 'object') {
return {};
}
const overrides = {};
const envVars = config.env;
for (const [key, value] of Object.entries(envVars)) {
if (key.startsWith('DEVICECLOUD_OVERRIDE_')) {
// Remove the DEVICECLOUD_OVERRIDE_ prefix and use the rest as the override key
const overrideKey = key.replace('DEVICECLOUD_OVERRIDE_', '');
overrides[overrideKey] = value;
}
}
return overrides;
}
async function plan(input, includeTags, excludeTags, excludeFlows, configFile) {
const normalizedInput = path.normalize(input);
const flowMetadata = {};
if (!fs.existsSync(normalizedInput)) {
throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
}
if (fs.lstatSync(normalizedInput).isFile()) {
if (normalizedInput.endsWith('config.yaml') ||
normalizedInput.endsWith('config.yml')) {
throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
}
const { config } = (0, planMethods_1.readTestYamlFileAsJson)(normalizedInput);
const flowOverrides = {};
if (config) {
flowMetadata[normalizedInput] = config;
flowOverrides[normalizedInput] = extractDeviceCloudOverrides(config);
}
const checkedDependancies = await checkDependencies(normalizedInput);
return {
flowMetadata,
flowOverrides,
flowsToRun: [normalizedInput],
referencedFiles: [...new Set(checkedDependancies)],
totalFlowFiles: 1,
};
}
let unfilteredFlowFiles = await (0, planMethods_1.readDirectory)(normalizedInput, planMethods_1.isFlowFile);
if (unfilteredFlowFiles.length === 0) {
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
}
unfilteredFlowFiles = filterFlowFiles(unfilteredFlowFiles, excludeFlows);
let workspaceConfig;
if (configFile) {
const configFilePath = path.resolve(process.cwd(), configFile);
if (!fs.existsSync(configFilePath)) {
throw new Error(`Config file does not exist: ${configFilePath}`);
}
workspaceConfig = (0, planMethods_1.readYamlFileAsJson)(configFilePath);
}
else {
workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
}
if (workspaceConfig.flows) {
const globs = workspaceConfig.flows.map((glob) => glob);
const matchedFiles = await (0, glob_1.glob)(globs, {
cwd: normalizedInput,
nodir: true,
});
// overwrite the list of files with the globbed ones
unfilteredFlowFiles = matchedFiles
.filter((file) => {
// Exclude config files
if (file === 'config.yaml' || file === 'config.yml') {
return false;
}
// Exclude config file if specified
if (configFile && file === path.basename(configFile)) {
return false;
}
// Check file extension
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
return false;
}
// Exclude files inside .app bundles
// Check if any directory in the path ends with .app
const pathParts = file.split(path.sep);
for (const part of pathParts) {
if (part.endsWith('.app')) {
return false;
}
}
return true;
})
.map((file) => path.resolve(normalizedInput, file));
}
else {
// workspace config has no flows, so we need to remove the config file from the test list
unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
!file.endsWith('config.yml') &&
(!configFile || !file.endsWith(configFile)));
}
if (unfilteredFlowFiles.length === 0) {
const error = workspaceConfig.flows
? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
: new Error(`Workspace does not contain any Flows: ${path.resolve(normalizedInput)}`);
throw error;
}
const configPerFlowFile =
// eslint-disable-next-line unicorn/no-array-reduce
unfilteredFlowFiles.reduce((acc, filePath) => {
const { config } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
acc[filePath] = config;
return acc;
}, {});
const allIncludeTags = [
...includeTags,
...(workspaceConfig.includeTags || []),
];
const allExcludeTags = [
...excludeTags,
...(workspaceConfig.excludeTags || []),
];
const flowOverrides = {};
const allFlows = unfilteredFlowFiles.filter((filePath) => {
const config = configPerFlowFile[filePath];
const tags = config?.tags || [];
if (config) {
flowMetadata[filePath] = config;
flowOverrides[filePath] = extractDeviceCloudOverrides(config);
}
return ((allIncludeTags.length === 0 ||
tags.some((tag) => allIncludeTags.includes(tag))) &&
(allExcludeTags.length === 0 ||
!tags.some((tag) => allExcludeTags.includes(tag))));
});
if (allFlows.length === 0) {
throw new Error(`Include / Exclude tags did not match any Flows:\n\nInclude Tags:\n${allIncludeTags.join('\n')}\n\nExclude Tags:\n${allExcludeTags.join('\n')}`);
}
// Check dependencies only for the filtered flows
const allFiles = await Promise.all(allFlows.map((filePath) => checkDependencies(filePath))).then((results) => [...new Set(results.flat())]);
// eslint-disable-next-line unicorn/no-array-reduce
const pathsByName = allFlows.reduce((acc, filePath) => {
const config = configPerFlowFile[filePath];
const name = config?.name || path.parse(filePath).name;
acc[name] = filePath;
return acc;
}, {});
const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
?.map((flowOrder) => flowOrder.replace('.yaml', '').replace('.yml', '')) // support case where ext is left on
?.map((flowOrder) => (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [flowOrder]))
.flat() || [];
const normalFlows = allFlows
.filter((flow) => !flowsToRunInSequence.includes(flow))
.sort((a, b) => a.localeCompare(b));
return {
allExcludeTags,
allIncludeTags,
flowMetadata,
flowOverrides,
flowsToRun: normalFlows,
referencedFiles: [...new Set(allFiles)],
sequence: {
continueOnFailure: workspaceConfig.executionOrder?.continueOnFailure,
flows: flowsToRunInSequence,
},
totalFlowFiles: unfilteredFlowFiles.length,
workspaceConfig,
};
}
;