UNPKG

@devicecloud.dev/dcd

Version:

Better cloud maestro testing

216 lines (215 loc) 9.24 kB
"use strict"; 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, }; }