diff --git a/src/__mocks__/fs/index.ts b/src/__mocks__/fs/index.ts index 61bd51adf..f9a799dc8 100644 --- a/src/__mocks__/fs/index.ts +++ b/src/__mocks__/fs/index.ts @@ -18,6 +18,10 @@ export const readFile = async (filePath: string) => { throw new Error('file not found'); } + if (filePath.includes('fail-yaml')) { + throw new Error('file not found'); + } + if (filePath.includes('fail-csv-reader.csv')) { return ` cpu-cores-available,≈ç≈¬˚∆∑∂´®øˆ´cpu-cores-utilized, ---- cpu-manufacturer,cpu-model-name,cpu-tdp,gpu-count,gpu-model-name,Hardware Information on AWS Documentation & Comments,instance-class,instance-storage,memory-available,platform-memory,release-date,storage-drives @@ -76,6 +80,15 @@ export const writeFile = async (pathToFile: string, content: string) => { const fileContentObject = JSON.parse(fileContent); const parsedContent = JSON.parse(content); + for (const property in fileContentObject) { + expect(parsedContent).toHaveProperty(property); + } + } else if (pathToFile.includes('package.json-npm2')) { + const updatedPath = pathToFile.replace('-npm2', ''); + const fileContent = await fsAsync.readFile(updatedPath, 'utf8'); + const fileContentObject = JSON.parse(fileContent); + const parsedContent = JSON.parse(content); + for (const property in fileContentObject) { expect(parsedContent).toHaveProperty(property); } @@ -112,7 +125,7 @@ export const appendFile = (file: string, appendContent: string) => `${file}${appendContent}`; export const stat = async (filePath: string) => { - if (filePath === 'true') { + if (filePath === 'true' || filePath === 'mock-path') { return true; } else { throw new Error('File not found.'); @@ -128,7 +141,7 @@ export const access = async (directoryPath: string) => { }; export const unlink = async (filePath: string) => { - if (filePath === 'true') { + if (filePath === 'true' || filePath === 'mock-path') { return; } else { throw new Error('File not found.'); @@ -148,6 +161,10 @@ export const readdir = (directoryPath: string) => { return ['subdir/file2.yml', 'file1.yaml']; } + if (directoryPath.includes('mock-nested-directory')) { + return ['mock-sub-directory']; + } + return []; }; @@ -155,7 +172,8 @@ export const lstat = (filePath: string) => { if ( filePath.includes('mock-directory') || filePath.includes('mock-sub-directory/subdir') || - filePath === 'true' + filePath === 'true' || + filePath.includes('npm-node-test') ) { return { isDirectory: () => true, @@ -169,3 +187,11 @@ export const lstat = (filePath: string) => { } return; }; + +export const rm = (path: string) => { + if (path.includes('npm-node-test')) { + return; + } else { + throw new Error('File not found.'); + } +}; diff --git a/src/__mocks__/plugin/lib/index.ts b/src/__mocks__/plugin/lib/index.ts index 497dcef72..2d1fe48aa 100644 --- a/src/__mocks__/plugin/lib/index.ts +++ b/src/__mocks__/plugin/lib/index.ts @@ -1 +1,2 @@ export {Mockavizta} from './mockavizta'; +export {SciEmbodied} from './sci-embodied'; diff --git a/src/__mocks__/plugin/lib/sci-embodied/index.ts b/src/__mocks__/plugin/lib/sci-embodied/index.ts new file mode 100644 index 000000000..c97e2dc9a --- /dev/null +++ b/src/__mocks__/plugin/lib/sci-embodied/index.ts @@ -0,0 +1,28 @@ +import {PluginFactory} from '@grnsft/if-core/interfaces'; +import {PluginParams} from '@grnsft/if-core/types'; + +export const SciEmbodied = PluginFactory({ + metadata: { + inputs: { + vCPUs: { + description: 'number of CPUs allocated to an application', + unit: 'CPUs', + 'aggregation-method': { + time: 'copy', + component: 'copy', + }, + }, + }, + outputs: { + 'embodied-carbon': { + description: 'embodied carbon for a resource, scaled by usage', + unit: 'gCO2eq', + 'aggregation-method': { + time: 'sum', + component: 'sum', + }, + }, + }, + }, + implementation: async (inputs: PluginParams[]) => inputs, +}); diff --git a/src/__tests__/common/util/debug-logger.test.ts b/src/__tests__/common/util/debug-logger.test.ts index affdca6c5..4d597a8d9 100644 --- a/src/__tests__/common/util/debug-logger.test.ts +++ b/src/__tests__/common/util/debug-logger.test.ts @@ -93,4 +93,30 @@ describe('util/debug-logger: ', () => { expect(debugSpy).not.toHaveBeenCalled(); }); + + it('logs empty messages when the message is `\n`.', () => { + console.debug('\n'); + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith(); + }); + + it('logs messages when the message contains `**Computing`.', () => { + const logMessage = '**Computing some pipline message'; + console.debug(logMessage); + + expect(debugSpy).toHaveBeenCalledTimes(1); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(logMessage)); + }); + + it('logs messages when the message is a number.', () => { + const logMessage = 10; + console.debug(logMessage); + + expect(debugSpy).toHaveBeenCalledTimes(1); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('DEBUG:')); + expect(debugSpy).toHaveBeenCalledWith( + expect.stringContaining(logMessage.toString()) + ); + }); }); diff --git a/src/__tests__/common/util/fs.test.ts b/src/__tests__/common/util/fs.test.ts index 643d80df3..17c6db828 100644 --- a/src/__tests__/common/util/fs.test.ts +++ b/src/__tests__/common/util/fs.test.ts @@ -93,7 +93,7 @@ describe('util/fs: ', () => { expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-empty-directory'); }); - it('returns YAML files in the directory', async () => { + it('returns YAML files in the directory.', async () => { const fsReaddirSpy = jest.spyOn(fs, 'readdir'); jest .spyOn(fs, 'lstat') @@ -108,19 +108,29 @@ describe('util/fs: ', () => { expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); }); - it('recursively finds YAML files in nested directories.', async () => { + it('recursively finds YAML files if the directory exists.', async () => { const fsReaddirSpy = jest.spyOn(fs, 'readdir'); - jest - .spyOn(fs, 'lstat') - .mockResolvedValue({isDirectory: () => false} as any); - const result = await getYamlFiles('/mock-sub-directory'); + + jest.spyOn(fs, 'lstat').mockImplementation((path: any) => { + return Promise.resolve({ + isDirectory: () => { + if (path.endsWith('.yaml') || path.endsWith('.yml')) { + return false; + } + return true; + }, + } as any); + }); + + const path = '/mock-nested-directory'; + const result = await getYamlFiles(path); expect.assertions(2); expect(result).toEqual([ - '/mock-sub-directory/subdir/file2.yml', - '/mock-sub-directory/file1.yaml', + '/mock-nested-directory/mock-sub-directory/subdir/file2.yml', + '/mock-nested-directory/mock-sub-directory/file1.yaml', ]); - expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); + expect(fsReaddirSpy).toHaveBeenCalledWith(path); }); }); diff --git a/src/__tests__/common/util/validations.test.ts b/src/__tests__/common/util/validations.test.ts new file mode 100644 index 000000000..d5e961d7f --- /dev/null +++ b/src/__tests__/common/util/validations.test.ts @@ -0,0 +1,251 @@ +import {z} from 'zod'; + +import {ERRORS} from '@grnsft/if-core/utils'; + +import { + atLeastOneDefined, + allDefined, + validate, + validateManifest, + manifestSchema, +} from '../../../common/util/validations'; + +const {InputValidationError, ManifestValidationError} = ERRORS; + +describe('utils/validations: ', () => { + describe('atLeastOneDefined(): ', () => { + it('returns true if at least one value is defined.', () => { + const input = { + 'cpu/utilization': undefined, + users: 42, + carbon: undefined, + }; + expect(atLeastOneDefined(input)).toBe(true); + }); + + it('returns false if all values are undefined.', () => { + const input = { + 'cpu/utilization': undefined, + users: undefined, + carbon: undefined, + }; + + expect(atLeastOneDefined(input)).toBe(false); + }); + + it('returns true if all values are defined.', () => { + const input = {'cpu/utilization': 0.1, users: 100, carbon: 200}; + + expect(atLeastOneDefined(input)).toBe(true); + }); + + it('returns true if at least one value is `null` (null is defined).', () => { + const input = { + 'cpu/utilization': undefined, + users: null, + carbon: undefined, + }; + + expect(atLeastOneDefined(input)).toBe(true); + }); + + it('returns false for an empty object.', () => { + const input = {}; + + expect(atLeastOneDefined(input)).toBe(false); + }); + }); + + describe('allDefined(): ', () => { + it('returns true when all values are defined.', () => { + const input = { + 'cloud/instance-type': 'A1', + region: 'uk-west', + duration: 1, + 'cpu/utilization': 10, + 'network/energy': 10, + energy: 5, + }; + + expect(allDefined(input)).toBe(true); + }); + + it('returns false when any value is undefined.', () => { + const input = { + 'cloud/instance-type': 'A1', + region: undefined, + duration: 1, + 'cpu/utilization': 10, + 'network/energy': 10, + energy: 5, + }; + + expect(allDefined(input)).toBe(false); + }); + + it('returns true for an empty object.', () => { + expect(allDefined({})).toBe(true); + }); + }); + + describe('validate(): ', () => { + const schema = z.object({ + coefficient: z.number(), + 'input-parameter': z.string().min(1), + 'output-parameter': z.string().min(1), + }); + + it('successfully returns valid data.', () => { + const validObject = { + coefficient: 3, + 'input-parameter': 'cpu/memory', + 'output-parameter': 'result', + }; + const result = validate(schema, validObject); + + expect(result).toEqual(validObject); + }); + + it('throws an InputValidationError with a formatted message when validation fails.', () => { + const invalidObject = { + coefficient: 3, + 'input-parameter': 2, + 'output-parameter': 'result', + }; + + expect.assertions(2); + try { + validate(schema, invalidObject); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"input-parameter" parameter is expected string, received number. Error code: invalid_type.' + ) + ); + } + }); + + it('uses a custom error constructor when provided', () => { + const invalidObject = { + coefficient: 3, + 'input-parameter': 2, + 'output-parameter': 'result', + }; + + expect.assertions(1); + try { + validate(schema, invalidObject, undefined, ManifestValidationError); + } catch (error) { + expect(error).toBeInstanceOf(ManifestValidationError); + } + }); + + it('throws an InputValidationError for invalid_union issue and call prettifyErrorMessage.', () => { + const schema = z.object({ + data: z.union([z.string(), z.number().min(10)]), + }); + const invalidObject = {data: false}; + const index = 3; + + expect.assertions(2); + + try { + validate(schema, invalidObject, index); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"data" parameter is expected string, received boolean at index 3. Error code: invalid_union.' + ) + ); + } + }); + + it('returns only the error message when path is empty.', () => { + const schema = z.object({ + '': z.string().min(1), + }); + + const invalidObject = {}; + const index = 4; + + expect.assertions(2); + try { + validate(schema, invalidObject, index); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual(new InputValidationError('Required')); + } + }); + }); + + describe('validateManifest(): ', () => { + beforeEach(() => { + jest.spyOn(console, 'debug').mockImplementation(() => {}); + jest.clearAllMocks(); + }); + + it('logs the validation process and call validate with correct arguments.', () => { + const mockManifest = { + name: 'mock-name', + initialize: { + plugins: { + 'memory-energy-from-memory-util': { + path: 'builtin', + method: 'Coefficient', + }, + }, + }, + tree: { + children: { + child: { + pipeline: {compute: 'memory-energy-from-memory-util'}, + inputs: [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3600, + 'memory/utilization': 10, + }, + ], + }, + }, + }, + }; + const mockResult = {isValid: true}; + const validateSpy = jest + .spyOn(require('../../../common/util/validations'), 'validate') + .mockReturnValue(mockResult); + const result = validateManifest(mockManifest); + + expect(console.debug).toHaveBeenCalledWith('Validating manifest'); + expect(validateSpy).toHaveBeenCalledWith( + manifestSchema, + mockManifest, + undefined, + ManifestValidationError + ); + expect(result).toBe(mockResult); + }); + + it('throws an error if validation fails.', () => { + const mockManifest = {invalidKey: 'value'}; + const mockError = new ManifestValidationError('Validation failed'); + const validateSpy = jest + .spyOn(require('../../../common/util/validations'), 'validate') + .mockImplementation(() => { + throw mockError; + }); + + expect(() => validateManifest(mockManifest)).toThrow( + ManifestValidationError + ); + expect(validateSpy).toHaveBeenCalledWith( + manifestSchema, + mockManifest, + undefined, + ManifestValidationError + ); + }); + }); +}); diff --git a/src/__tests__/common/util/yaml.test.ts b/src/__tests__/common/util/yaml.test.ts index 7ad06e13f..aefca2ffc 100644 --- a/src/__tests__/common/util/yaml.test.ts +++ b/src/__tests__/common/util/yaml.test.ts @@ -1,3 +1,5 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + jest.mock('fs/promises', () => require('../../../__mocks__/fs')); import { @@ -6,6 +8,8 @@ import { saveYamlFileAs, } from '../../../common/util/yaml'; +const {ReadFileError} = ERRORS; + describe('util/yaml: ', () => { describe('checkIfFileIsYaml(): ', () => { it('returns false, in case of not yaml file.', () => { @@ -41,6 +45,17 @@ describe('util/yaml: ', () => { expect(typeof result).toBe(expectedType); expect(result.name).toBe(expectedYamlName); }); + + it('throws an error when file is not valid yaml.', async () => { + expect.assertions(2); + + try { + await openYamlFileAsObject('fail-yaml'); + } catch (error) { + expect(error).toBeInstanceOf(ReadFileError); + expect(error).toEqual(new ReadFileError('file not found')); + } + }); }); describe('saveYamlFileAs(): ', () => { diff --git a/src/__tests__/if-check/util/npm.test.ts b/src/__tests__/if-check/util/npm.test.ts index 4bdffc10f..6363ddf32 100644 --- a/src/__tests__/if-check/util/npm.test.ts +++ b/src/__tests__/if-check/util/npm.test.ts @@ -28,6 +28,21 @@ jest.mock('../../../common/util/helpers', () => { "npm run if-env -- -m ./src/__mocks__/mock-manifest.yaml && npm run if-run -- -m ./src/__mocks__/mock-manifest.yaml -o src/__mocks__/re-mock-manifest && node -p 'Boolean(process.stdout.isTTY)' | npm run if-diff -- -s src/__mocks__/re-mock-manifest.yaml -t ./src/__mocks__/mock-manifest.yaml" ); break; + case 'if-check-cwd': + expect(param).toEqual( + "npm run if-env -- -m ./src/__mocks__/mock-manifest.yaml -c && npm run if-run -- -m ./src/__mocks__/mock-manifest.yaml -o src/__mocks__/re-mock-manifest && node -p 'Boolean(process.stdout.isTTY)' | npm run if-diff -- -s src/__mocks__/re-mock-manifest.yaml -t ./src/__mocks__/mock-manifest.yaml" + ); + break; + case 'if-check-global': + expect(param).toEqual( + "if-env -m ./src/__mocks__/mock-manifest.yaml && if-run -m ./src/__mocks__/mock-manifest.yaml -o src/__mocks__/re-mock-manifest && node -p 'Boolean(process.stdout.isTTY)' | if-diff -s src/__mocks__/re-mock-manifest.yaml -t ./src/__mocks__/mock-manifest.yaml" + ); + break; + case 'if-check-prefix': + expect(param).toEqual( + "if-env --prefix=.. -m ./src/__mocks__/mock-manifest.yaml && if-run --prefix=.. -m ./src/__mocks__/mock-manifest.yaml -o src/__mocks__/re-mock-manifest && node -p 'Boolean(process.stdout.isTTY)' | if-diff --prefix=.. -s src/__mocks__/re-mock-manifest.yaml -t ./src/__mocks__/mock-manifest.yaml" + ); + break; } return; }, @@ -50,5 +65,47 @@ describe('if-check/util/npm: ', () => { '✔ if-check successfully verified mock-manifest.yaml\n' ); }); + + it('successfully executes with cwd command.', async () => { + process.env.NPM_INSTALL = 'if-check-cwd'; + const manifest = './src/__mocks__/mock-manifest.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + + await executeCommands(manifest, true); + + expect.assertions(2); + expect(logSpy).toHaveBeenCalledWith( + '✔ if-check successfully verified mock-manifest.yaml\n' + ); + }); + + it('successfully executes with correct commands when is running from global.', async () => { + process.env.npm_config_global = 'true'; + process.env.NPM_INSTALL = 'if-check-global'; + const manifest = './src/__mocks__/mock-manifest.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + + await executeCommands(manifest, false); + + expect.assertions(2); + expect(logSpy).toHaveBeenCalledWith( + '✔ if-check successfully verified mock-manifest.yaml\n' + ); + }); + + it('successfully executes with correct commands when CURRENT_DIR is provided.', async () => { + process.env.CURRENT_DIR = './mock-path'; + process.env.npm_config_global = 'true'; + process.env.NPM_INSTALL = 'if-check-prefix'; + const manifest = './src/__mocks__/mock-manifest.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + + await executeCommands(manifest, false); + + expect.assertions(2); + expect(logSpy).toHaveBeenCalledWith( + '✔ if-check successfully verified mock-manifest.yaml\n' + ); + }); }); }); diff --git a/src/__tests__/if-diff/lib/compare.test.ts b/src/__tests__/if-diff/lib/compare.test.ts index f43e7c590..aae92707c 100644 --- a/src/__tests__/if-diff/lib/compare.test.ts +++ b/src/__tests__/if-diff/lib/compare.test.ts @@ -110,5 +110,21 @@ describe('lib/compare: ', () => { const expectedResponse = {path: '1', source: undefined, target: 2}; expect(response).toEqual(expectedResponse); }); + + it('executes when path is the `initialize`.', () => { + const a = { + tree: { + inputs: [1, 2], + }, + }; + const b = { + tree: { + inputs: [1, 2], + }, + }; + + const response = compare(a, b, 'initialize'); + expect(Object.keys(response).length).toEqual(0); + }); }); }); diff --git a/src/__tests__/if-env/util/npm.test.ts b/src/__tests__/if-env/util/npm.test.ts index be922f07c..aae709967 100644 --- a/src/__tests__/if-env/util/npm.test.ts +++ b/src/__tests__/if-env/util/npm.test.ts @@ -14,6 +14,14 @@ jest.mock('../../../common/util/logger', () => ({ }, })); +jest.mock('../../../../package.json', () => ({ + description: 'Mocked description', + author: 'Mocked Author', + bugs: {}, + engines: {}, + homepage: 'Mocked homepage', +})); + jest.mock('../../../common/util/helpers', () => { const originalModule = jest.requireActual('../../../if-run/util/helpers'); @@ -75,6 +83,25 @@ describe('util/npm: ', () => { expect.assertions(2); expect(result).toBe(packageJsonPath); }); + + it('returns the node_modules path if it exists.', async () => { + const folderPath = path.resolve(__dirname, 'npm-node-test'); + const packageJsonPath = path.resolve(folderPath, 'package.json'); + const expectedPath = path.resolve( + __dirname, + 'npm-node-test/node_modules' + ); + const fsRmSpy = jest.spyOn(fs, 'rm'); + + isFileExists('false'); + + const result = await initPackageJsonIfNotExists(folderPath); + + expect.assertions(3); + + expect(result).toBe(packageJsonPath); + expect(fsRmSpy).toHaveBeenCalledWith(expectedPath, {recursive: true}); + }); }); describe('installDependencies(): ', () => { @@ -191,7 +218,29 @@ describe('util/npm: ', () => { }); }); - it('returns an empty object if no matches found', () => { + it('extracts a plugin path with its matching version from dependencies.', () => { + const plugins: ManifestPlugin = { + 'cloud-metadata': { + path: 'grnsft/if-plugins', + method: 'CloudMetadata', + }, + }; + const dependencies = [ + '@commitlint/config-conventional@18.6.0', + '@grnsft/if-core@0.0.7', + 'grnsft/if-plugins@v0.3.2', + '@jest/globals@29.7.0', + ]; + + const result = extractPathsWithVersion(plugins, dependencies); + + expect.assertions(1); + expect(result).toEqual({ + 'grnsft/if-plugins': '^v0.3.2', + }); + }); + + it('returns an empty object if no matches found.', () => { const plugins: ManifestPlugin = { 'cloud-metadata': { path: '@grnsft/if-plugins', @@ -219,6 +268,9 @@ describe('util/npm: ', () => { describe('updatePackageJsonProperties(): ', () => { it('updates the package.json properties correctly.', async () => { + process.env.PACKAGE = 'true'; + jest.unmock('../../../../package.json'); + const packageJsonPath = path.join(folderPath, 'package.json-npm1'); const expectedPackageJsonContent = JSON.stringify( @@ -246,5 +298,66 @@ describe('util/npm: ', () => { expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); }); + + it('updates the package.json properties when the name is missing.', async () => { + process.env.PACKAGE = 'false'; + + const packageJsonPath = path.join(folderPath, 'package.json-npm2'); + + const expectedPackageJsonContent = JSON.stringify( + { + description: 'mock-description', + author: {}, + bugs: {}, + engines: {}, + homepage: 'mock-homepage', + dependencies: { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }, + }, + null, + 2 + ); + + const fsReadSpy = jest + .spyOn(fs, 'readFile') + .mockResolvedValue(expectedPackageJsonContent); + await updatePackageJsonProperties(packageJsonPath, true); + + expect.assertions(8); + + expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + expect(fs.readFile).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + }); + + it('updates the package.json properties when package.json is empty.', async () => { + const packageJsonPath = path.join(folderPath, 'package.json-npm2'); + + jest.unmock('../../../../package.json'); + + const expectedPackageJsonContent = JSON.stringify( + { + description: 'mock-description', + author: {}, + bugs: {}, + engines: {}, + homepage: 'mock-homepage', + dependencies: { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }, + }, + null, + 2 + ); + + const fsReadSpy = jest + .spyOn(fs, 'readFile') + .mockResolvedValue(expectedPackageJsonContent); + await updatePackageJsonProperties(packageJsonPath, false); + + expect.assertions(7); + + expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + }); }); }); diff --git a/src/__tests__/if-run/builtins/divide.test.ts b/src/__tests__/if-run/builtins/divide.test.ts index ae48c4a61..885706219 100644 --- a/src/__tests__/if-run/builtins/divide.test.ts +++ b/src/__tests__/if-run/builtins/divide.test.ts @@ -137,6 +137,36 @@ describe('builtins/divide: ', () => { expect(response).toEqual(expectedResult); }); + it('returns a result when `denominator` is arithmetic expression.', async () => { + expect.assertions(1); + const config = { + numerator: 'vcpus-allocated', + denominator: '=duration*3', + output: 'vcpus-allocated-per-second', + }; + + const divide = Divide(config, parametersMetadata, {}); + const input = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]; + const response = await divide.execute(input); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + 'vcpus-allocated-per-second': 24 / (3600 * 3), + }, + ]; + + expect(response).toEqual(expectedResult); + }); + it('successfully executes when a parameter contains arithmetic expression.', async () => { expect.assertions(1); diff --git a/src/__tests__/if-run/builtins/export-yaml.test.ts b/src/__tests__/if-run/builtins/export-yaml.test.ts index 6a9a442e2..b3e8ca0cd 100644 --- a/src/__tests__/if-run/builtins/export-yaml.test.ts +++ b/src/__tests__/if-run/builtins/export-yaml.test.ts @@ -24,8 +24,8 @@ describe('builtins/export-yaml: ', () => { }); }); - describe('execute', () => { - it('returns result with correct arguments', async () => { + describe('execute(): ', () => { + it('returns result with correct arguments.', async () => { const outputPath = 'outputPath.yaml'; await exportYaml.execute(tree, context, outputPath); @@ -36,6 +36,17 @@ describe('builtins/export-yaml: ', () => { ); }); + it('returns result when path name is without extension.', async () => { + const outputPath = 'outputPath'; + + await exportYaml.execute(tree, context, outputPath); + + expect(saveYamlFileAs).toHaveBeenCalledWith( + {...context, tree}, + 'outputPath.yaml' + ); + }); + it('throws an error if outputPath is not provided.', async () => { expect.assertions(2); diff --git a/src/__tests__/if-run/builtins/interpolation.test.ts b/src/__tests__/if-run/builtins/interpolation.test.ts index dd5381529..71efc4f14 100644 --- a/src/__tests__/if-run/builtins/interpolation.test.ts +++ b/src/__tests__/if-run/builtins/interpolation.test.ts @@ -330,7 +330,7 @@ describe('builtins/interpolation: ', () => { } }); - it('throws an when the config is not provided.', async () => { + it('throws an error when the config is not provided.', async () => { const config = undefined; const plugin = Interpolation(config!, parametersMetadata, {}); diff --git a/src/__tests__/if-run/builtins/mock-observations.test.ts b/src/__tests__/if-run/builtins/mock-observations.test.ts index 046a066f6..5f950c79c 100644 --- a/src/__tests__/if-run/builtins/mock-observations.test.ts +++ b/src/__tests__/if-run/builtins/mock-observations.test.ts @@ -5,7 +5,7 @@ import {MockObservations} from '../../../if-run/builtins/mock-observations'; import {STRINGS} from '../../../if-run/config'; const {InputValidationError, ConfigError} = ERRORS; -const {INVALID_MIN_MAX} = STRINGS; +const {INVALID_MIN_MAX, MISSING_CONFIG} = STRINGS; describe('builtins/mock-observations: ', () => { const parametersMetadata = { @@ -412,5 +412,18 @@ describe('builtins/mock-observations: ', () => { ); } }); + + it('throws an error when the config is not provided.', async () => { + const config = undefined; + const plugin = MockObservations(config!, parametersMetadata, {}); + + expect.assertions(2); + try { + await plugin.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(ConfigError); + expect(error).toEqual(new ConfigError(MISSING_CONFIG)); + } + }); }); }); diff --git a/src/__tests__/if-run/builtins/shell.test.ts b/src/__tests__/if-run/builtins/shell.test.ts index d295b21a4..58f4f68f1 100644 --- a/src/__tests__/if-run/builtins/shell.test.ts +++ b/src/__tests__/if-run/builtins/shell.test.ts @@ -4,7 +4,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Shell} from '../../../if-run/builtins/shell'; -const {InputValidationError, ProcessExecutionError} = ERRORS; +import {STRINGS} from '../../../if-run/config'; + +const {InputValidationError, ProcessExecutionError, ConfigError} = ERRORS; + +const {MISSING_CONFIG} = STRINGS; jest.mock('child_process'); jest.mock('js-yaml'); @@ -66,12 +70,8 @@ describe('builtins/shell', () => { try { await shell.execute(invalidInputs); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); - expect(error).toStrictEqual( - new InputValidationError( - '"command" parameter is required. Error code: invalid_type.' - ) - ); + expect(error).toBeInstanceOf(ConfigError); + expect(error).toStrictEqual(new ConfigError(MISSING_CONFIG)); } }); @@ -99,5 +99,25 @@ describe('builtins/shell', () => { } }); }); + + it('throws an error when the config is not provided.', async () => { + const config = undefined; + const plugin = Shell(config!, parametersMetadata, {}); + const inputs = [ + { + duration: 3600, + timestamp: '2022-01-01T00:00:00Z', + }, + ]; + + expect.assertions(2); + + try { + await plugin.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(ConfigError); + expect(error).toEqual(new ConfigError(MISSING_CONFIG)); + } + }); }); }); diff --git a/src/__tests__/if-run/lib/compute.test.ts b/src/__tests__/if-run/lib/compute.test.ts index 9c883f961..a1d5f0a31 100644 --- a/src/__tests__/if-run/lib/compute.test.ts +++ b/src/__tests__/if-run/lib/compute.test.ts @@ -1,4 +1,28 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +const mockWarn = jest.fn(message => { + if (process.env.LOGGER === 'true') { + expect(message).toEqual( + 'You have included node-level config in your manifest to support `params` plugin. IF no longer supports node-level config. `params` plugin should be refactored to accept all its config from config or input data.' + ); + } else if (process.env.LOGGER === 'empty') { + expect(message).toEqual( + 'You have included node-level config in your manifest. IF no longer supports node-level config. The manifest should be refactored to accept all its node-level config from config or input data.' + ); + } else if (process.env.LOGGER === 'invalid') { + expect(message).toEqual( + `You're using an old style manifest. Please update for phased execution. More information can be found here: +https://if.greensoftware.foundation/major-concepts/manifest-file` + ); + } +}); + +jest.mock('../../../common/util/logger', () => ({ + logger: { + warn: mockWarn, + }, +})); + +import * as explainer from '../../../if-run/lib/explain'; import {compute} from '../../../if-run/lib/compute'; import {ComputeParams} from '../../../if-run/types/compute'; @@ -86,6 +110,27 @@ describe('lib/compute: ', () => { .set('mock-observe-time-sync', mockObservePluginTimeSync()) .set('time-sync', mockTimeSync()), }; + + const observeParamsExecute: ComputeParams = { + // @ts-ignore + observe: {}, + context: { + name: 'mock-name', + initialize: { + plugins: { + mock: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }, + }, + }, + pluginStorage: pluginStorage() + .set('mock', mockExecutePlugin()) + .set('mock-observe', mockObservePlugin()) + .set('mock-observe-time-sync', mockObservePluginTimeSync()) + .set('time-sync', mockTimeSync()), + }; const paramsExecuteWithAppend = {...paramsExecute, append: true}; describe('compute(): ', () => { @@ -326,9 +371,34 @@ describe('lib/compute: ', () => { ...tree.children.mockChild.outputs, ...mockExecutePlugin().execute(tree.children.mockChild.inputs), ]; + + expect.assertions(2); expect(response.children.mockChild.outputs).toHaveLength(4); expect(response.children.mockChild.outputs).toEqual(expectedResult); }); + + it('computes simple tree with append when outputs is null.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {compute: ['mock']}, + inputs: [ + {timestamp: 'mock-timestamp-1', region: 'eu-west'}, + {timestamp: 'mock-timestamp-2', region: 'eu-west'}, + ], + outputs: undefined, + }, + }, + }; + const response = await compute(tree, paramsExecuteWithAppend); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect.assertions(2); + expect(response.children.mockChild.outputs).toHaveLength(2); + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); }); it('computes simple tree with regroup and append, with existing outputs preserved and regrouped without re-computing.', async () => { @@ -363,6 +433,32 @@ describe('lib/compute: ', () => { expect(response.children.mockChild.children).toEqual(expectedResponse); }); + it('computes simple tree with regroup and append, with existing outputs preserved and without new outputs.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {regroup: ['region'], compute: ['mock']}, + inputs: [{timestamp: 'mock-timestamp-1', region: 'uk-east'}], + }, + }, + }; + const response = await compute(tree, paramsExecuteWithAppend); + const expectedResponse = { + 'uk-east': { + inputs: [{region: 'uk-east', timestamp: 'mock-timestamp-1'}], + outputs: [ + { + newField: 'mock-newField', + region: 'uk-east', + timestamp: 'mock-timestamp-1', + }, + ], + }, + }; + + expect(response.children.mockChild.children).toEqual(expectedResponse); + }); + it('computes simple tree with regroup and no append, with existing outputs that are removed.', async () => { const tree = { children: { @@ -406,6 +502,201 @@ describe('lib/compute: ', () => { {timestamp: '2024-09-03', duration: 60, 'cpu/utilization': 40}, ]; + expect.assertions(1); + expect(response.children.mockChild.inputs).toEqual(expectedResult); + }); + + it('computes simple tree with observe plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {observe: ['mock-observe']}, + }, + }, + }; + + const response = await compute(tree, paramsExecute); + const expectedResult = [ + {timestamp: '2024-09-02', duration: 40, 'cpu/utilization': 30}, + {timestamp: '2024-09-03', duration: 60, 'cpu/utilization': 40}, + ]; + + expect.assertions(1); expect(response.children.mockChild.inputs).toEqual(expectedResult); }); + + it('observes simple tree with observe plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {observe: ['mock-observe']}, + }, + }, + }; + + const response = await compute(tree, observeParamsExecute); + const expectedResult = [ + {timestamp: '2024-09-02', duration: 40, 'cpu/utilization': 30}, + {timestamp: '2024-09-03', duration: 60, 'cpu/utilization': 40}, + ]; + + expect.assertions(1); + expect(response.children.mockChild.inputs).toEqual(expectedResult); + }); + + it('observes simple tree with config.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {observe: ['mock-observe']}, + config: {}, + }, + }, + }; + + const response = await compute(tree, observeParamsExecute); + const expectedResult = [ + {timestamp: '2024-09-02', duration: 40, 'cpu/utilization': 30}, + {timestamp: '2024-09-03', duration: 60, 'cpu/utilization': 40}, + ]; + + expect.assertions(1); + expect(response.children.mockChild.inputs).toEqual(expectedResult); + }); + + it('warns when pipeline is null.', async () => { + process.env.LOGGER = 'invalid'; + const tree = { + children: { + mockChild: { + pipeline: null, + }, + }, + }; + paramsExecute.context.explainer = false; + const response = await compute(tree, paramsExecute); + + expect.assertions(2); + expect(response.children.mockChild.inputs).toEqual(undefined); + + process.env.LOGGER = undefined; + }); + + it('warns when pipeline is an empty object.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {}, + }, + }, + }; + paramsExecute.context.explainer = false; + const response = await compute(tree, paramsExecute); + + expect.assertions(1); + expect(response.children.mockChild.inputs).toEqual(undefined); + }); + + it('warns when config is provided in the tree.', async () => { + process.env.LOGGER = 'true'; + const tree = { + children: { + mockChild: { + pipeline: {compute: ['mock']}, + config: {params: 5}, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + paramsExecute.context.explainer = false; + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect.assertions(2); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + process.env.LOGGER = undefined; + }); + + it('warns when config is provided in the tree and it is null.', async () => { + process.env.LOGGER = 'empty'; + const tree = { + children: { + mockChild: { + pipeline: {compute: ['mock']}, + config: null, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + paramsExecute.context.explainer = false; + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect.assertions(2); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + process.env.LOGGER = undefined; + }); + + it('observes simple tree with execute plugin and explain property.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {observe: ['mock-observe']}, + }, + }, + }; + + paramsExecute.context.explainer = true; + + const explainerSpy = jest.spyOn(explainer, 'addExplainData'); + + const response = await compute(tree, paramsExecute); + const expectedResult = [ + {timestamp: '2024-09-02', duration: 40, 'cpu/utilization': 30}, + {timestamp: '2024-09-03', duration: 60, 'cpu/utilization': 40}, + ]; + + expect.assertions(2); + + expect(response.children.mockChild.inputs).toEqual(expectedResult); + expect(explainerSpy).toHaveBeenCalledWith({ + metadata: {}, + pluginName: 'mock-observe', + }); + }); + + it('computes simple tree with execute plugin and explain property.', async () => { + const tree = { + children: { + mockChild: { + pipeline: {compute: ['mock']}, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + + paramsExecute.context.explainer = true; + + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); }); diff --git a/src/__tests__/if-run/lib/environment.test.ts b/src/__tests__/if-run/lib/environment.test.ts index b7b6ab60b..7bf0c8b2f 100644 --- a/src/__tests__/if-run/lib/environment.test.ts +++ b/src/__tests__/if-run/lib/environment.test.ts @@ -1,4 +1,39 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import {execPromise} from '../../../common/util/helpers'; + +jest.mock('../../../common/util/helpers', () => { + const originalModule = jest.requireActual('../../../common/util/helpers'); + + return { + ...originalModule, + execPromise: jest.fn(async (...args) => { + if (process.env.EXECUTE === 'invalid') { + return { + stdout: JSON.stringify({ + version: '0.7.2', + name: '@grnsft/if', + }), + }; + } else if (process.env.EXECUTE === 'true') { + return { + stdout: JSON.stringify({ + version: '0.7.2', + name: '@grnsft/if', + dependencies: { + 'release-iterator': { + version: '0.7.2', + resolved: 'git@github.com:Green-Software-Foundation/if.git', + overridden: false, + }, + }, + }), + }; + } + + return originalModule.execPromise(...args); + }), + }; +}); import {injectEnvironment} from '../../../if-run/lib/environment'; @@ -9,6 +44,8 @@ describe('lib/environment: ', () => { it('checks response to have `execution` property.', async () => { // @ts-ignore const response = await injectEnvironment(context); + + expect.assertions(1); expect(response).toHaveProperty('execution'); }, 6000); @@ -17,15 +54,63 @@ describe('lib/environment: ', () => { const response = await injectEnvironment(context); const {execution} = response; + expect.assertions(2); expect(execution).toHaveProperty('command'); expect(execution).toHaveProperty('environment'); }); + it('checks if dependency has github link.', async () => { + process.env.EXECUTE = 'true'; + // @ts-ignore + const response = await injectEnvironment(context); + const {execution} = response; + + const mockStdout = JSON.stringify({version: '0.7.2', name: '@grnsft/if'}); + + // @ts-ignore + (execPromise as jest.Mock).mockResolvedValue({ + stdout: mockStdout, + stderr: '', + }); + + expect.assertions(3); + expect(execution).toHaveProperty('command'); + expect(execution?.environment).toHaveProperty('dependencies'); + expect(execution?.environment?.dependencies).toEqual([ + 'release-iterator@0.7.2 (git@github.com:Green-Software-Foundation/if.git)', + ]); + + process.env.EXECUTE = 'undefined'; + }); + + it('checks if stdout do not have dependency property.', async () => { + process.env.EXECUTE = 'invalid'; + // @ts-ignore + const response = await injectEnvironment(context); + const {execution} = response; + + const mockStdout = JSON.stringify({version: '0.7.2', name: '@grnsft/if'}); + + // @ts-ignore + (execPromise as jest.Mock).mockResolvedValue({ + stdout: mockStdout, + stderr: '', + }); + + expect.assertions(3); + expect(execution).toHaveProperty('command'); + expect(execution?.environment).toHaveProperty('dependencies'); + expect(execution?.environment?.dependencies).toEqual([]); + + process.env.EXECUTE = 'undefined'; + }); + it('checks environment response type.', async () => { // @ts-ignore const response = await injectEnvironment(context); const environment = response.execution!.environment!; + expect.assertions(5); expect(typeof environment['date-time']).toEqual('string'); expect(Array.isArray(environment.dependencies)).toBeTruthy(); expect(typeof environment['node-version']).toEqual('string'); diff --git a/src/__tests__/if-run/lib/explain.test.ts b/src/__tests__/if-run/lib/explain.test.ts index a6049f500..af2f6e770 100644 --- a/src/__tests__/if-run/lib/explain.test.ts +++ b/src/__tests__/if-run/lib/explain.test.ts @@ -9,10 +9,25 @@ const {ManifestValidationError} = ERRORS; const {AGGREGATION_UNITS_NOT_MATCH, AGGREGATION_METHODS_NOT_MATCH} = STRINGS; describe('lib/explain: ', () => { + it('returns empty object when `inputs` and `outputs` of `metadata` are empty.', () => { + const mockData = { + pluginName: 'coefficient', + metadata: { + inputs: {}, + outputs: {}, + }, + }; + + // @ts-ignore + addExplainData(mockData); + expect.assertions(1); + expect(explain()).toEqual({}); + }); + it('missing explain data if `inputs` and `outputs` of `metadata` are `undefined`.', () => { const mockData = { pluginName: 'divide', - metadata: {kind: 'execute', inputs: undefined, outputs: undefined}, + metadata: {inputs: undefined, outputs: undefined}, }; addExplainData(mockData); diff --git a/src/__tests__/if-run/lib/initialize.test.ts b/src/__tests__/if-run/lib/initialize.test.ts index 3b2c82fdc..d0461735e 100644 --- a/src/__tests__/if-run/lib/initialize.test.ts +++ b/src/__tests__/if-run/lib/initialize.test.ts @@ -2,6 +2,11 @@ jest.mock('mockavizta', () => require('../../../__mocks__/plugin'), { virtual: true, }); + +jest.mock('sci-embodied', () => require('../../../__mocks__/plugin'), { + virtual: true, +}); + jest.mock( '../../../if-run/builtins', () => require('../../../__mocks__/plugin'), @@ -26,8 +31,8 @@ const { } = ERRORS; const {MISSING_METHOD, MISSING_PATH, INVALID_MODULE_PATH} = STRINGS; -describe('lib/initalize: ', () => { - describe('initalize(): ', () => { +describe('lib/initialize: ', () => { + describe('initialize(): ', () => { it('creates instance with get and set methods.', async () => { const context = {initialize: {plugins: {}}}; // @ts-ignore @@ -211,5 +216,48 @@ describe('lib/initalize: ', () => { ); } }); + + it('checks if parameter-metadata is provided.', async () => { + const context = { + initialize: { + plugins: { + 'sci-embodied': { + path: 'sci-embodied', + method: 'SciEmbodied', + 'parameter-metadata': { + inputs: { + vCPUs: { + description: 'number of CPUs allocated to an application', + unit: 'CPUs', + 'aggregation-method': { + time: 'copy', + component: 'copy', + }, + }, + }, + outputs: { + 'embodied-carbon': { + description: + 'embodied carbon for a resource, scaled by usage', + unit: 'gCO2eq', + 'aggregation-method': { + time: 'sum', + component: 'sum', + }, + }, + }, + }, + }, + }, + }, + }; + // @ts-ignore + const storage = await initialize(context); + + const pluginName = Object.keys(context.initialize.plugins)[0]; + const module = storage.get(pluginName); + expect(module).toHaveProperty('execute'); + expect(module).toHaveProperty('metadata'); + }); }); }); diff --git a/src/__tests__/if-run/util/aggregation-helper.test.ts b/src/__tests__/if-run/util/aggregation-helper.test.ts index f27eb7874..1add75d88 100644 --- a/src/__tests__/if-run/util/aggregation-helper.test.ts +++ b/src/__tests__/if-run/util/aggregation-helper.test.ts @@ -126,5 +126,50 @@ describe('util/aggregation-helper: ', () => { expect(aggregated.timestamp).toBeUndefined(); expect(aggregated.duration).toBeUndefined(); }); + + it('executes when aggregation properties are `none`.', () => { + storeAggregationMetrics({ + carbon: { + time: 'none', + component: 'none', + }, + }); + const inputs: PluginParams[] = [ + {timestamp: '', duration: 10, carbon: 10}, + {timestamp: '', duration: 10, carbon: 20}, + ]; + const metrics: string[] = ['carbon']; + const isTemporal = true; + + const expectedValue = { + timestamp: '', + duration: 10, + }; + const aggregated = aggregateOutputsIntoOne(inputs, metrics, isTemporal); + expect(aggregated).toEqual(expectedValue); + }); + + it('executes when aggregation properties are `copy`.', () => { + storeAggregationMetrics({ + carbon: { + time: 'copy', + component: 'copy', + }, + }); + const inputs: PluginParams[] = [ + {timestamp: '', duration: 10, carbon: 10}, + {timestamp: '', duration: 10, carbon: 20}, + ]; + const metrics: string[] = ['carbon']; + const isTemporal = true; + + const expectedValue = { + timestamp: '', + duration: 10, + carbon: 20, + }; + const aggregated = aggregateOutputsIntoOne(inputs, metrics, isTemporal); + expect(aggregated).toEqual(expectedValue); + }); }); }); diff --git a/src/__tests__/if-run/util/args.test.ts b/src/__tests__/if-run/util/args.test.ts index c43d49eb9..5ad3d2ecd 100644 --- a/src/__tests__/if-run/util/args.test.ts +++ b/src/__tests__/if-run/util/args.test.ts @@ -30,6 +30,16 @@ jest.mock('ts-command-line-args', () => ({ manifest: 'manifest-mock.yaml', 'no-output': false, }; + case 'no-output-true': + return { + manifest: 'manifest-mock.yaml', + 'no-output': true, + }; + case 'append': + return { + manifest: 'manifest-mock.yaml', + append: true, + }; default: return { manifest: 'mock-manifest.yaml', @@ -39,6 +49,18 @@ jest.mock('ts-command-line-args', () => ({ }, })); +const mockWarn = jest.fn(message => { + if (process.env.LOGGER === 'true') { + expect(message).toEqual(NO_OUTPUT); + } +}); + +jest.mock('../../../common/util/logger', () => ({ + logger: { + warn: mockWarn, + }, +})); + const processRunningPath = process.cwd(); import * as path from 'node:path'; @@ -47,8 +69,10 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {parseIfRunProcessArgs} from '../../../if-run/util/args'; import {STRINGS as COMMON_STRINGS} from '../../../common/config'; +import {STRINGS} from '../../../if-run/config/strings'; const {SOURCE_IS_NOT_YAML, MANIFEST_IS_MISSING} = COMMON_STRINGS; +const {NO_OUTPUT} = STRINGS; const {CliSourceFileError, ParseCliParamsError} = ERRORS; describe('if-run/util/args: ', () => { @@ -160,5 +184,44 @@ describe('if-run/util/args: ', () => { expect(response).toEqual(expectedResult); }); + + it('executes when `no-output` and manifest persist.', () => { + process.env.LOGGER = 'true'; + process.env.result = 'no-output-true'; + const manifestPath = 'manifest-mock.yaml'; + + const response = parseIfRunProcessArgs(); + const expectedResult = { + inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), + compute: undefined, + debug: undefined, + observe: undefined, + outputOptions: { + noOutput: true, + }, + regroup: undefined, + }; + + expect.assertions(2); + expect(response).toEqual(expectedResult); + }); + + it('executes when `append` is provided.', () => { + process.env.result = 'append'; + const manifestPath = 'manifest-mock.yaml'; + + const response = parseIfRunProcessArgs(); + const expectedResult = { + append: true, + inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), + compute: undefined, + debug: undefined, + observe: undefined, + outputOptions: {}, + regroup: undefined, + }; + + expect(response).toEqual(expectedResult); + }); }); }); diff --git a/src/__tests__/if-run/util/helpers.test.ts b/src/__tests__/if-run/util/helpers.test.ts index dedc76634..b6df94e6a 100644 --- a/src/__tests__/if-run/util/helpers.test.ts +++ b/src/__tests__/if-run/util/helpers.test.ts @@ -2,12 +2,21 @@ const mockWarn = jest.fn(); const mockError = jest.fn(); +jest.mock('process'); + import {ERRORS} from '@grnsft/if-core/utils'; import {andHandle, mergeObjects} from '../../../if-run/util/helpers'; const {WriteFileError} = ERRORS; +class CustomError extends Error { + constructor(message: string) { + super(message); + this.name = 'CustomError'; + } +} + jest.mock('../../../common/util/logger', () => ({ logger: { warn: mockWarn, @@ -26,13 +35,30 @@ describe('if-run/util/helpers: ', () => { mockError.mockReset(); }); - it('logs error in case of error is unknown.', () => { + it('logs error in case of the error is known.', () => { const message = 'mock-message'; andHandle(new WriteFileError(message)); expect(mockWarn).toHaveBeenCalledTimes(0); expect(mockError).toHaveBeenCalledTimes(1); }); + + it('logs error in case of the error is unknown.', () => { + const exitSpy = jest + .spyOn(process, 'exit') + .mockImplementation((code?: number | undefined): never => { + throw new Error(`Mocked process.exit with code: ${code}`); + }); + + const message = 'mock-message'; + + try { + andHandle(new CustomError(message)); + } catch (error) { + expect(exitSpy).toHaveBeenCalledWith(2); + expect(mockError).toHaveBeenCalledTimes(2); + } + }); }); describe('mergeObjects(): ', () => { diff --git a/src/common/util/yaml.ts b/src/common/util/yaml.ts index 036fb4451..8faea65f1 100644 --- a/src/common/util/yaml.ts +++ b/src/common/util/yaml.ts @@ -28,7 +28,7 @@ export const saveYamlFileAs = async (object: any, pathToFile: string) => { await fs.mkdir(dirPath, {recursive: true}); const yamlString = YAML.dump(object, {noRefs: true}); - return fs.writeFile(pathToFile, yamlString); + return await fs.writeFile(pathToFile, yamlString); } catch (error: any) { throw new WriteFileError(error.message); } diff --git a/src/if-run/builtins/divide/index.ts b/src/if-run/builtins/divide/index.ts index 9252bc305..328efbe02 100644 --- a/src/if-run/builtins/divide/index.ts +++ b/src/if-run/builtins/divide/index.ts @@ -74,9 +74,7 @@ const calculateDivide = ( ) => { const {denominator, numerator} = params; const finalDenominator = - typeof denominator === 'number' - ? denominator - : input[denominator] || denominator; + typeof denominator === 'number' ? denominator : input[denominator]; const finalNumerator = typeof numerator === 'number' ? numerator : input[numerator]; diff --git a/src/if-run/builtins/exponent/index.ts b/src/if-run/builtins/exponent/index.ts index 464ce9367..b6e3c1732 100644 --- a/src/if-run/builtins/exponent/index.ts +++ b/src/if-run/builtins/exponent/index.ts @@ -31,10 +31,7 @@ export const Exponent = PluginFactory({ inputValidation: (input: PluginParams, config: ConfigParams) => { const inputParameter = config['input-parameter']; const inputData = { - [inputParameter]: - typeof inputParameter === 'number' - ? inputParameter - : input[inputParameter], + [inputParameter]: input[inputParameter], }; const validationSchema = z.record(z.string(), z.number()); @@ -43,7 +40,7 @@ export const Exponent = PluginFactory({ inputData ); }, - implementation: async (inputs: PluginParams[], config: ConfigParams = {}) => { + implementation: async (inputs: PluginParams[], config: ConfigParams) => { const { 'input-parameter': inputParameter, exponent, diff --git a/src/if-run/builtins/shell/index.ts b/src/if-run/builtins/shell/index.ts index a6752f3a0..7e253dffd 100644 --- a/src/if-run/builtins/shell/index.ts +++ b/src/if-run/builtins/shell/index.ts @@ -15,7 +15,7 @@ const {MISSING_CONFIG} = STRINGS; export const Shell = PluginFactory({ configValidation: (config: ConfigParams) => { - if (!config) { + if (!config || !Object.keys(config)?.length) { throw new ConfigError(MISSING_CONFIG); }