diff --git a/src/__mocks__/builtins/export-csv.ts b/src/__mocks__/builtins/export-csv.ts index 3d815f1ad..44170e619 100644 --- a/src/__mocks__/builtins/export-csv.ts +++ b/src/__mocks__/builtins/export-csv.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import {Context} from '../../types/manifest'; export const tree = { @@ -117,6 +118,7 @@ export const tree = { }, }; +// @ts-ignore export const context: Context = { name: 'demo', description: '', diff --git a/src/__tests__/unit/builtins/export-csv-raw.test.ts b/src/__tests__/unit/builtins/export-csv-raw.test.ts index c47959704..1ebf9e45e 100644 --- a/src/__tests__/unit/builtins/export-csv-raw.test.ts +++ b/src/__tests__/unit/builtins/export-csv-raw.test.ts @@ -6,7 +6,7 @@ import {ERRORS} from '../../../util/errors'; import {tree, context, outputs} from '../../../__mocks__/builtins/export-csv'; -const {CliInputError, WriteFileError} = ERRORS; +const {ExhaustError} = ERRORS; jest.mock('fs/promises', () => ({ __esModule: true, @@ -50,7 +50,7 @@ describe('builtins/export-csv-raw: ', () => { await expect( exportCSVRaw.execute(tree, context, outputPath) ).rejects.toThrow( - new WriteFileError( + new ExhaustError( 'Failed to write CSV to output#carbon: Could not write CSV file.' ) ); @@ -65,8 +65,8 @@ describe('builtins/export-csv-raw: ', () => { try { await exportCSVRaw.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustError); + expect(error).toEqual(new ExhaustError('Output path is required.')); } }); }); diff --git a/src/__tests__/unit/builtins/export-csv.test.ts b/src/__tests__/unit/builtins/export-csv.test.ts index 26c1f194e..94414a970 100644 --- a/src/__tests__/unit/builtins/export-csv.test.ts +++ b/src/__tests__/unit/builtins/export-csv.test.ts @@ -13,7 +13,7 @@ import { aggregation, } from '../../../__mocks__/builtins/export-csv'; -const {CliInputError} = ERRORS; +const {ExhaustError} = ERRORS; jest.mock('fs/promises', () => ({ writeFile: jest.fn<() => Promise>().mockResolvedValue(), @@ -197,8 +197,8 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustError); + expect(error).toEqual(new ExhaustError('Output path is required.')); } }); @@ -210,9 +210,9 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); + expect(error).toBeInstanceOf(ExhaustError); expect(error).toEqual( - new CliInputError('Output path should contains `#`.') + new ExhaustError('Output path should contain `#`.') ); } }); @@ -225,9 +225,9 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); + expect(error).toBeInstanceOf(ExhaustError); expect(error).toEqual( - new CliInputError( + new ExhaustError( 'CSV export criteria is not found in output path. Please append it after --output #.' ) ); diff --git a/src/__tests__/unit/builtins/export-yaml.test.ts b/src/__tests__/unit/builtins/export-yaml.test.ts index e54c22972..a2157e4ef 100644 --- a/src/__tests__/unit/builtins/export-yaml.test.ts +++ b/src/__tests__/unit/builtins/export-yaml.test.ts @@ -8,7 +8,7 @@ jest.mock('../../../util/yaml', () => ({ saveYamlFileAs: jest.fn(), })); -const {CliInputError} = ERRORS; +const {ExhaustError} = ERRORS; describe('builtins/export-yaml: ', () => { describe('ExportYaml: ', () => { @@ -38,8 +38,8 @@ describe('builtins/export-yaml: ', () => { try { await exportYaml.execute({}, context, ''); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustError); + expect(error).toEqual(new ExhaustError('Output path is required.')); } }); }); diff --git a/src/__tests__/unit/lib/compute.test.ts b/src/__tests__/unit/lib/compute.test.ts index cb719c4b7..9ff951586 100644 --- a/src/__tests__/unit/lib/compute.test.ts +++ b/src/__tests__/unit/lib/compute.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import {compute} from '../../../lib/compute'; import {ComputeParams} from '../../../types/compute'; @@ -28,6 +29,7 @@ describe('lib/compute: ', () => { * Compute params. */ const paramsExecute: ComputeParams = { + // @ts-ignore context: { name: 'mock-name', initialize: { @@ -42,6 +44,7 @@ describe('lib/compute: ', () => { pluginStorage: pluginStorage().set('mock', mockExecutePlugin()), }; const params: ComputeParams = { + // @ts-ignore context: { name: 'mock-name', initialize: { diff --git a/src/__tests__/unit/lib/exhaust.test.ts b/src/__tests__/unit/lib/exhaust.test.ts index 371ae9c0a..97db0b9dc 100644 --- a/src/__tests__/unit/lib/exhaust.test.ts +++ b/src/__tests__/unit/lib/exhaust.test.ts @@ -7,7 +7,7 @@ import {ERRORS} from '../../../util/errors'; import {STRINGS} from '../../../config'; -const {CliInputError, ModuleInitializationError} = ERRORS; +const {ExhaustError} = ERRORS; const {INVALID_EXHAUST_PLUGIN} = STRINGS; describe('lib/exhaust: ', () => { @@ -58,9 +58,9 @@ describe('lib/exhaust: ', () => { // @ts-ignore await exhaust(tree, context, {}); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); + expect(error).toBeInstanceOf(ExhaustError); - if (error instanceof CliInputError) { + if (error instanceof ExhaustError) { expect(error.message).toEqual(expectedMessage); } } @@ -83,9 +83,9 @@ describe('lib/exhaust: ', () => { // @ts-ignore await exhaust(tree, context, {}); } catch (error) { - expect(error).toBeInstanceOf(ModuleInitializationError); + expect(error).toBeInstanceOf(ExhaustError); - if (error instanceof ModuleInitializationError) { + if (error instanceof ExhaustError) { expect(error.message).toEqual(expectedMessage); } } diff --git a/src/__tests__/unit/lib/initalize.test.ts b/src/__tests__/unit/lib/initialize.test.ts similarity index 92% rename from src/__tests__/unit/lib/initalize.test.ts rename to src/__tests__/unit/lib/initialize.test.ts index fcd4a10d0..ad5541684 100644 --- a/src/__tests__/unit/lib/initalize.test.ts +++ b/src/__tests__/unit/lib/initialize.test.ts @@ -10,7 +10,7 @@ jest.mock('../../../util/log-memoize', () => ({ memoizedLog: mockLog, })); -import {initalize} from '../../../lib/initialize'; +import {initialize} from '../../../lib/initialize'; import {ERRORS} from '../../../util/errors'; @@ -25,7 +25,7 @@ describe('lib/initalize: ', () => { describe('initalize(): ', () => { it('creates instance with get and set methods.', async () => { const plugins = {}; - const response = await initalize(plugins); + const response = await initialize(plugins); expect(response).toHaveProperty('get'); expect(response).toHaveProperty('set'); @@ -40,7 +40,7 @@ describe('lib/initalize: ', () => { method: 'Mockavizta', }, }; - const storage = await initalize(plugins); + const storage = await initialize(plugins); const pluginName = Object.keys(plugins)[0]; const module = storage.get(pluginName); @@ -59,7 +59,7 @@ describe('lib/initalize: ', () => { }, }, }; - const storage = await initalize(plugins); + const storage = await initialize(plugins); const pluginName = Object.keys(plugins)[0]; const module = storage.get(pluginName); @@ -79,7 +79,7 @@ describe('lib/initalize: ', () => { }; try { - await initalize(plugins); + await initialize(plugins); } catch (error) { expect(error).toBeInstanceOf(PluginCredentialError); @@ -101,7 +101,7 @@ describe('lib/initalize: ', () => { }; try { - await initalize(plugins); + await initialize(plugins); } catch (error) { expect(error).toBeInstanceOf(PluginCredentialError); @@ -121,7 +121,7 @@ describe('lib/initalize: ', () => { }, }, }; - const storage = await initalize(plugins); + const storage = await initialize(plugins); const pluginName = Object.keys(plugins)[0]; const module = storage.get(pluginName); @@ -139,7 +139,7 @@ describe('lib/initalize: ', () => { }, }, }; - const storage = await initalize(plugins); + const storage = await initialize(plugins); const pluginName = Object.keys(plugins)[0]; const module = storage.get(pluginName); @@ -159,7 +159,7 @@ describe('lib/initalize: ', () => { }; try { - await initalize(plugins); + await initialize(plugins); } catch (error) { expect(error).toBeInstanceOf(ModuleInitializationError); diff --git a/src/__tests__/unit/lib/load.test.ts b/src/__tests__/unit/lib/load.test.ts index 0e40636dc..fed158b92 100644 --- a/src/__tests__/unit/lib/load.test.ts +++ b/src/__tests__/unit/lib/load.test.ts @@ -28,32 +28,7 @@ describe('lib/load: ', () => { const result = await load(inputPath, paramPath); const expectedValue = { - tree: { - children: { - 'front-end': { - pipeline: ['boavizta-cpu'], - config: { - 'boavizta-cpu': { - 'core-units': 24, - processor: 'Intel® Core™ i7-1185G7', - }, - }, - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 3600, - 'cpu/utilization': 18.392, - }, - { - timestamp: '2023-08-06T00:00', - duration: 3600, - 'cpu/utilization': 16, - }, - ], - }, - }, - }, - rawContext: { + rawManifest: { name: 'gsf-demo', description: 'Hello', tags: { @@ -69,6 +44,31 @@ describe('lib/load: ', () => { }, }, }, + tree: { + children: { + 'front-end': { + pipeline: ['boavizta-cpu'], + config: { + 'boavizta-cpu': { + 'core-units': 24, + processor: 'Intel® Core™ i7-1185G7', + }, + }, + inputs: [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/utilization': 18.392, + }, + { + timestamp: '2023-08-06T00:00', + duration: 3600, + 'cpu/utilization': 16, + }, + ], + }, + }, + }, }, parameters: PARAMETERS, }; @@ -82,45 +82,8 @@ describe('lib/load: ', () => { const result = await load(inputPath, paramPath); - const expectedParameters = { - 'mock-carbon': { - description: 'an amount of carbon emitted into the atmosphere', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'mock-cpu': { - description: 'number of cores available', - unit: 'cores', - aggregation: 'none', - }, - }; const expectedValue = { - tree: { - children: { - 'front-end': { - pipeline: ['boavizta-cpu'], - config: { - 'boavizta-cpu': { - 'core-units': 24, - processor: 'Intel® Core™ i7-1185G7', - }, - }, - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 3600, - 'cpu/utilization': 18.392, - }, - { - timestamp: '2023-08-06T00:00', - duration: 3600, - 'cpu/utilization': 16, - }, - ], - }, - }, - }, - rawContext: { + rawManifest: { name: 'gsf-demo', description: 'Hello', tags: { @@ -136,8 +99,44 @@ describe('lib/load: ', () => { }, }, }, + tree: { + children: { + 'front-end': { + pipeline: ['boavizta-cpu'], + config: { + 'boavizta-cpu': { + 'core-units': 24, + processor: 'Intel® Core™ i7-1185G7', + }, + }, + inputs: [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/utilization': 18.392, + }, + { + timestamp: '2023-08-06T00:00', + duration: 3600, + 'cpu/utilization': 16, + }, + ], + }, + }, + }, + }, + parameters: { + 'mock-carbon': { + description: 'an amount of carbon emitted into the atmosphere', + unit: 'gCO2e', + aggregation: 'sum', + }, + 'mock-cpu': { + description: 'number of cores available', + unit: 'cores', + aggregation: 'none', + }, }, - parameters: expectedParameters, }; expect(result).toEqual(expectedValue); diff --git a/src/builtins/export-csv-raw.ts b/src/builtins/export-csv-raw.ts index 3e2420ab5..429d99c85 100644 --- a/src/builtins/export-csv-raw.ts +++ b/src/builtins/export-csv-raw.ts @@ -5,7 +5,7 @@ import {ERRORS} from '../util/errors'; import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface'; import {Context} from '../types/manifest'; -const {WriteFileError, CliInputError} = ERRORS; +const {ExhaustError} = ERRORS; export const ExportCSVRaw = (): ExhaustPluginInterface => { /** @@ -127,9 +127,7 @@ export const ExportCSVRaw = (): ExhaustPluginInterface => { try { await fs.writeFile(`${outputPath}.csv`, content); } catch (error) { - throw new WriteFileError( - `Failed to write CSV to ${outputPath}: ${error}` - ); + throw new ExhaustError(`Failed to write CSV to ${outputPath}: ${error}`); } }; @@ -138,7 +136,7 @@ export const ExportCSVRaw = (): ExhaustPluginInterface => { */ const execute = async (tree: any, _context: Context, outputPath: string) => { if (!outputPath) { - throw new CliInputError('Output path is required.'); + throw new ExhaustError('Output path is required.'); } const [extractredFlatMap, extractedHeaders] = diff --git a/src/builtins/export-csv.ts b/src/builtins/export-csv.ts index 9e48ad1b0..efd2cc3cb 100644 --- a/src/builtins/export-csv.ts +++ b/src/builtins/export-csv.ts @@ -6,7 +6,7 @@ import {ERRORS} from '../util/errors'; import {Context} from '../types/manifest'; import {PluginParams} from '../types/interface'; -const {CliInputError} = ERRORS; +const {ExhaustError} = ERRORS; /** * Extension to IF that outputs the tree in a CSV format. @@ -20,7 +20,7 @@ export const ExportCSV = () => { const criteria = paths[paths.length - 1]; if (paths.length <= 1 || !criteria) { - throw new CliInputError( + throw new ExhaustError( 'CSV export criteria is not found in output path. Please append it after --output #.' ); } @@ -36,11 +36,11 @@ export const ExportCSV = () => { */ const validateOutputPath = (outputPath: string) => { if (!outputPath) { - throw new CliInputError('Output path is required.'); + throw new ExhaustError('Output path is required.'); } if (!outputPath.includes('#')) { - throw new CliInputError('Output path should contains `#`.'); + throw new ExhaustError('Output path should contain `#`.'); } return outputPath; diff --git a/src/builtins/export-yaml.ts b/src/builtins/export-yaml.ts index bedab254e..e242bbcc2 100644 --- a/src/builtins/export-yaml.ts +++ b/src/builtins/export-yaml.ts @@ -1,10 +1,9 @@ import {saveYamlFileAs} from '../util/yaml'; - import {ERRORS} from '../util/errors'; import {Context} from '../types/manifest'; -const {CliInputError} = ERRORS; +const {ExhaustError} = ERRORS; export const ExportYaml = () => { /** Takes string before hashtag. */ @@ -15,7 +14,7 @@ export const ExportYaml = () => { */ const execute = async (tree: any, context: Context, outputPath: string) => { if (!outputPath) { - throw new CliInputError('Output path is required.'); + throw new ExhaustError('Output path is required.'); } const outputFile = { diff --git a/src/index.ts b/src/index.ts index 11fa4e928..17eec0334 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,14 @@ import {aggregate} from './lib/aggregate'; import {compute} from './lib/compute'; import {injectEnvironment} from './lib/environment'; import {exhaust} from './lib/exhaust'; -import {initalize} from './lib/initialize'; +import {initialize} from './lib/initialize'; import {load} from './lib/load'; import {parameterize} from './lib/parameterize'; import {parseArgs} from './util/args'; import {andHandle} from './util/helpers'; import {logger} from './util/logger'; +import {validateManifest} from './util/validations'; import {STRINGS} from './config'; @@ -21,13 +22,28 @@ const impactEngine = async () => { logger.info(DISCLAIMER_MESSAGE); const {inputPath, paramPath, outputOptions} = options; - const {tree, rawContext, parameters} = await load(inputPath, paramPath); - const context = await injectEnvironment(rawContext); - parameterize.combine(context.params, parameters); - const pluginStorage = await initalize(context.initialize.plugins); - const computedTree = await compute(tree, {context, pluginStorage}); - const aggregatedTree = aggregate(computedTree, context.aggregation); - exhaust(aggregatedTree, context, outputOptions); + const {rawManifest, parameters} = await load(inputPath, paramPath); + const envManifest = await injectEnvironment(rawManifest); + + try { + const {tree, ...context} = validateManifest(envManifest); + parameterize.combine(context.params, parameters); + const pluginStorage = await initialize(context.initialize.plugins); + const computedTree = await compute(tree, {context, pluginStorage}); + const aggregatedTree = aggregate(computedTree, context.aggregation); + await exhaust(aggregatedTree, context, outputOptions); + } catch (error) { + if (error instanceof Error) { + envManifest.execution.status = 'fail'; + envManifest.execution.error = error.toString(); + logger.error(error); + const {tree, ...context} = envManifest; + + if (error.name !== 'ExhaustError') { + exhaust(tree, context, outputOptions); + } + } + } }; impactEngine().catch(andHandle); diff --git a/src/lib/environment.ts b/src/lib/environment.ts index b1f421bb4..c66baa516 100644 --- a/src/lib/environment.ts +++ b/src/lib/environment.ts @@ -2,7 +2,7 @@ import {DateTime} from 'luxon'; import {execPromise} from '../util/helpers'; -import {Context, ContextWithExec} from '../types/manifest'; +import {Manifest} from '../types/manifest'; import {NpmListResponse, PackageDependency} from '../types/environment'; import {osInfo} from '../util/os-checker'; @@ -59,16 +59,19 @@ const listDependencies = async () => { }; /** - * Injects execution information (command, environment) to existing context. + * Injects execution information (command, environment) to existing manifest. */ -export const injectEnvironment = async (context: Context) => { +export const injectEnvironment = async ( + manifest: Manifest +): Promise => { const dependencies = await listDependencies(); const info = await osInfo(); const dateTime = `${getProcessStartingTimestamp()} (UTC)`; - const contextWithExec: ContextWithExec = { - ...context, + return { + ...manifest, execution: { + status: 'success', command: process.argv.join(' '), environment: { 'if-version': packageJson.version, @@ -80,6 +83,4 @@ export const injectEnvironment = async (context: Context) => { }, }, }; - - return contextWithExec; }; diff --git a/src/lib/exhaust.ts b/src/lib/exhaust.ts index 5ee631d91..5c80b0a5a 100644 --- a/src/lib/exhaust.ts +++ b/src/lib/exhaust.ts @@ -14,7 +14,7 @@ import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface'; import {Context} from '../types/manifest'; import {Options} from '../types/process-args'; -const {ModuleInitializationError} = ERRORS; +const {ExhaustError} = ERRORS; const {INVALID_EXHAUST_PLUGIN} = STRINGS; /** @@ -35,7 +35,7 @@ const initializeExhaustPlugin = (name: string): ExhaustPluginInterface => { case 'csv-raw': return ExportCSVRaw(); default: - throw new ModuleInitializationError(INVALID_EXHAUST_PLUGIN(name)); + throw new ExhaustError(INVALID_EXHAUST_PLUGIN(name)); } }; diff --git a/src/lib/initialize.ts b/src/lib/initialize.ts index 5f603ac2a..d6bf7c184 100644 --- a/src/lib/initialize.ts +++ b/src/lib/initialize.ts @@ -84,9 +84,9 @@ const initPlugin = async ( }; /** - * Registers all plugins from `manifest`.`initalize` property. + * Registers all plugins from `manifest`.`initialize` property. */ -export const initalize = async ( +export const initialize = async ( plugins: GlobalPlugins ): Promise => { const storage = pluginStorage(); diff --git a/src/lib/load.ts b/src/lib/load.ts index c3c3912ec..a024444df 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,4 +1,3 @@ -import {validateManifest} from '../util/validations'; import {openYamlFileAsObject} from '../util/yaml'; import {readAndParseJson} from '../util/json'; @@ -11,7 +10,6 @@ import {Parameters} from '../types/parameters'; */ export const load = async (inputPath: string, paramPath?: string) => { const rawManifest = await openYamlFileAsObject(inputPath); - const {tree, ...rawContext} = validateManifest(rawManifest); const parametersFromCli = paramPath && (await readAndParseJson(paramPath)); /** @todo validate json */ @@ -20,8 +18,7 @@ export const load = async (inputPath: string, paramPath?: string) => { PARAMETERS; /** @todo PARAMETERS should be specified in parameterize only */ return { - tree, - rawContext, + rawManifest, parameters, }; }; diff --git a/src/types/manifest.ts b/src/types/manifest.ts index ce096f779..2359a3433 100644 --- a/src/types/manifest.ts +++ b/src/types/manifest.ts @@ -12,18 +12,7 @@ export type AggregationParams = Manifest['aggregation']; export type AggregationParamsSure = Extract; export type Context = Omit; -export type ContextWithExec = Omit & { - execution: { - command: string; - environment: { - 'if-version': string; - os: string; - 'os-version': string; - 'node-version': string; - 'date-time': string; - dependencies: string[]; - }; - }; -}; + +export type ContextWithExec = Omit; export type ManifestParameter = Extract[number]; diff --git a/src/util/errors.ts b/src/util/errors.ts index 9781b1f5d..9c9714ed2 100644 --- a/src/util/errors.ts +++ b/src/util/errors.ts @@ -1,5 +1,6 @@ const CUSTOM_ERRORS = [ 'CliInputError', + 'ExhaustError', 'ManifestValidationError', 'ModuleInitializationError', 'InputValidationError', diff --git a/src/util/validations.ts b/src/util/validations.ts index df947b589..5b1aff0cf 100644 --- a/src/util/validations.ts +++ b/src/util/validations.ts @@ -50,8 +50,20 @@ export const manifestSchema = z.object({ ), outputs: z.array(z.string()).optional(), }), + execution: z.object({ + command: z.string(), + environment: z.object({ + 'if-version': z.string(), + os: z.string(), + 'os-version': z.string(), + 'node-version': z.string(), + 'date-time': z.string(), + dependencies: z.array(z.string()), + }), + status: z.string(), + error: z.string().optional(), + }), tree: z.record(z.string(), z.any()), - 'if-version': z.string().optional().nullable(), }); /**