diff --git a/package-lock.json b/package-lock.json index 591705438..0c4cd07c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "zod": "^3.22.4" }, "bin": { - "ie": "build/index.js" + "ie": "build/index.js", + "if-diff": "build/diff.js" }, "devDependencies": { "@babel/core": "^7.22.10", diff --git a/src/__mocks__/json.ts b/src/__mocks__/json.ts new file mode 100644 index 000000000..d6bb24d2c --- /dev/null +++ b/src/__mocks__/json.ts @@ -0,0 +1,14 @@ +export const readAndParseJson = async () => { + return { + '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', + }, + }; +}; diff --git a/src/__mocks__/readline/index.ts b/src/__mocks__/readline/index.ts new file mode 100644 index 000000000..c3af2fa16 --- /dev/null +++ b/src/__mocks__/readline/index.ts @@ -0,0 +1,20 @@ +export const createInterface = () => { + if (process.env.readline === 'no_manifest') { + return ` +mock message in console +`; + } + + if (process.env.readline === 'manifest') { + return (async function* (): any { + yield ` +# start +name: mock-name +description: mock-description +# end + `; + })(); + } + + return []; +}; diff --git a/src/__tests__/unit/lib/compare.test.ts b/src/__tests__/unit/lib/compare.test.ts new file mode 100644 index 000000000..dbac5a490 --- /dev/null +++ b/src/__tests__/unit/lib/compare.test.ts @@ -0,0 +1,114 @@ +import {compare} from '../../../lib/compare'; + +describe('lib/compare: ', () => { + describe('compare(): ', () => { + it('test if empty objects are equal.', () => { + const a = {}; + const b = {}; + + const response = compare(a, b); + expect(Object.keys(response).length).toEqual(0); + }); + + it('tests for nested objects with arrays.', () => { + const a = { + tree: { + inputs: [1, 2], + }, + }; + const b = { + tree: { + inputs: [1, 2], + }, + }; + + const response = compare(a, b); + expect(Object.keys(response).length).toEqual(0); + }); + + it('tests for nested objects with arrays (negative case).', () => { + const a = { + tree: { + inputs: [1, 2], + }, + }; + const b = { + tree: { + inputs: [1], + }, + }; + + const response = compare(a, b); + expect(response.path).toEqual('tree.inputs.1'); + expect(response.source).toEqual(2); + expect(response.target).toBeUndefined(); + }); + + it('checks if execution params are ignored.', () => { + const a = { + tree: { + inputs: [1, 2], + }, + execution: { + a: 'mock-a', + b: 'mock-b', + status: 'success', + }, + }; + const b = { + tree: { + inputs: [1, 2], + }, + execution: { + status: 'success', + }, + }; + + const response = compare(a, b); + expect(Object.keys(response).length).toEqual(0); + }); + + it('checks if error and status are in place, and others are ignored.', () => { + const a = { + tree: { + inputs: [1, 2], + }, + execution: { + a: 'a', + b: 'b', + error: 'mock-error-message', + status: 'fail', + }, + }; + const b = { + tree: { + inputs: [1, 2], + }, + execution: { + error: 'mock-error-message', + status: 'fail', + }, + }; + + const response = compare(a, b); + expect(Object.keys(response).length).toEqual(0); + }); + + it('checks if arrays are equal.', () => { + const a = [1, 2]; + const b = [1, 2]; + + const response = compare(a, b); + expect(Object.keys(response).length).toEqual(0); + }); + + it('checks if arrays are equal (first one is missing some items).', () => { + const a = [1]; + const b = [1, 2]; + + const response = compare(a, b); + const expectedResponse = {path: '1', source: undefined, target: 2}; + expect(response).toEqual(expectedResponse); + }); + }); +}); diff --git a/src/__tests__/unit/lib/environment.test.ts b/src/__tests__/unit/lib/environment.test.ts index e4d1c579b..c528eaee1 100644 --- a/src/__tests__/unit/lib/environment.test.ts +++ b/src/__tests__/unit/lib/environment.test.ts @@ -23,7 +23,7 @@ describe('lib/envirnoment: ', () => { it('checks environment response type.', async () => { // @ts-ignore const response = await injectEnvironment(context); - const {environment} = response.execution; + const environment = response.execution!.environment!; expect(typeof environment['date-time']).toEqual('string'); expect(Array.isArray(environment.dependencies)).toBeTruthy(); diff --git a/src/__tests__/unit/lib/load.test.ts b/src/__tests__/unit/lib/load.test.ts index fed158b92..f9ad3e73b 100644 --- a/src/__tests__/unit/lib/load.test.ts +++ b/src/__tests__/unit/lib/load.test.ts @@ -1,4 +1,4 @@ -jest.mock('fs/promises', () => require('../../../__mocks__/fs')); +jest.mock('../../../util/json', () => require('../../../__mocks__/json')); jest.mock( 'mockavizta', () => ({ @@ -12,64 +12,52 @@ jest.mock( }), {virtual: true} ); +jest.mock('../../../util/helpers', () => ({ + parseManifestFromStdin: () => { + if (process.env.readline === 'valid-source') { + return ` +name: 'mock-name' +description: 'mock-description' +`; + } + return ''; + }, +})); +jest.mock('../../../util/yaml', () => ({ + openYamlFileAsObject: (path: string) => { + switch (path) { + case 'load-default.yml': + return 'raw-manifest'; + case 'source-path.yml': + return 'source-manifest'; + case 'target-path.yml': + return 'target-manifest'; + default: + return ''; + } + }, +})); -import {load} from '../../../lib/load'; +import {load, loadIfDiffFiles} from '../../../lib/load'; import {PARAMETERS} from '../../../config'; import {PluginParams} from '../../../types/interface'; +import {STRINGS} from '../../../config'; + +const {INVALID_SOURCE} = STRINGS; + describe('lib/load: ', () => { describe('load(): ', () => { it('loads yaml with default parameters.', async () => { - const inputPath = 'mock.yaml'; + const inputPath = 'load-default.yml'; const paramPath = undefined; const result = await load(inputPath, paramPath); const expectedValue = { - rawManifest: { - name: 'gsf-demo', - description: 'Hello', - tags: { - kind: 'web', - complexity: 'moderate', - category: 'cloud', - }, - initialize: { - plugins: { - mockavizta: { - path: 'mockavizta', - method: 'Mockavizta', - }, - }, - }, - 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, - }, - ], - }, - }, - }, - }, + rawManifest: 'raw-manifest', parameters: PARAMETERS, }; @@ -77,54 +65,13 @@ describe('lib/load: ', () => { }); it('loads yaml with custom parameters.', async () => { - const inputPath = 'param-mock.yaml'; + const inputPath = 'load-default.yml'; const paramPath = 'param-mock.json'; const result = await load(inputPath, paramPath); const expectedValue = { - rawManifest: { - name: 'gsf-demo', - description: 'Hello', - tags: { - kind: 'web', - complexity: 'moderate', - category: 'cloud', - }, - initialize: { - plugins: { - mockavizta: { - path: 'mockavizta', - method: 'Mockavizta', - }, - }, - }, - 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, - }, - ], - }, - }, - }, - }, + rawManifest: 'raw-manifest', parameters: { 'mock-carbon': { description: 'an amount of carbon emitted into the atmosphere', @@ -142,4 +89,51 @@ describe('lib/load: ', () => { expect(result).toEqual(expectedValue); }); }); + + describe('loadIfDiffFiles(): ', () => { + it('rejects with invalid source error.', async () => { + const params = { + sourcePath: '', + targetPath: '', + }; + + try { + await loadIfDiffFiles(params); + } catch (error) { + if (error instanceof Error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe(INVALID_SOURCE); + } + } + }); + + it('successfully loads target, and source from stdin.', async () => { + process.env.readline = 'valid-source'; + const params = { + targetPath: 'target-path.yml', + }; + + const response = await loadIfDiffFiles(params); + const expectedSource = { + name: 'mock-name', + description: 'mock-description', + }; + const expectedTarget = 'target-manifest'; + expect(response.rawSourceManifest).toEqual(expectedSource); + expect(response.rawTargetManifest).toEqual(expectedTarget); + }); + + it('successfully loads target, and source from stdin.', async () => { + const params = { + targetPath: 'target-path.yml', + sourcePath: 'source-path.yml', + }; + + const response = await loadIfDiffFiles(params); + const expectedSource = 'source-manifest'; + const expectedTarget = 'target-manifest'; + expect(response.rawSourceManifest).toEqual(expectedSource); + expect(response.rawTargetManifest).toEqual(expectedTarget); + }); + }); }); diff --git a/src/__tests__/unit/util/args.test.ts b/src/__tests__/unit/util/args.test.ts index cefda328d..d40d36cae 100644 --- a/src/__tests__/unit/util/args.test.ts +++ b/src/__tests__/unit/util/args.test.ts @@ -28,15 +28,38 @@ jest.mock('ts-command-line-args', () => ({ manifest: 'manifest-mock.yml', 'override-params': 'override-params-mock.yml', }; - case 'help': - return { - manifest: path.normalize(`${processRunningPath}/manifest-mock.yml`), - help: true, - }; case 'not-yaml': return { manifest: 'mock.notyaml', }; + case 'stdout': + return { + manifest: 'manifest-mock.yaml', + stdout: true, + }; + /** If-diff mocks */ + case 'only-target': + return { + target: 'target-mock.yml', + }; + case 'target-is-not-yaml': + return { + target: 'target-mock', + }; + case 'source-is-not-yaml': + return { + target: 'target-mock.yml', + source: 'source-mock', + }; + case 'target-source': + return { + target: 'target-mock.yml', + source: 'source-mock.yml', + }; + case 'diff-throw-error': + throw new Error('mock-error'); + case 'diff-throw': + throw 'mock-error'; default: return { manifest: 'mock-manifest.yaml', @@ -48,14 +71,20 @@ jest.mock('ts-command-line-args', () => ({ import path = require('path'); -import {parseIEProcessArgs} from '../../../util/args'; +import {parseIEProcessArgs, parseIfDiffArgs} from '../../../util/args'; import {ERRORS} from '../../../util/errors'; import {STRINGS} from '../../../config'; const {CliInputError} = ERRORS; -const {MANIFEST_IS_MISSING, FILE_IS_NOT_YAML} = STRINGS; +const { + MANIFEST_IS_MISSING, + FILE_IS_NOT_YAML, + TARGET_IS_NOT_YAML, + INVALID_TARGET, + SOURCE_IS_NOT_YAML, +} = STRINGS; describe('util/args: ', () => { const originalEnv = process.env; @@ -170,6 +199,104 @@ describe('util/args: ', () => { expect(error).toEqual(new CliInputError(FILE_IS_NOT_YAML)); } }); + + it('returns stdout and manifest.', () => { + expect.assertions(1); + + process.env.result = 'stdout'; + const manifestPath = 'manifest-mock.yaml'; + + const response = parseIEProcessArgs(); + const expectedResult = { + inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), + outputOptions: { + stdout: true, + }, + }; + + expect(response).toEqual(expectedResult); + }); + }); + + describe('parseIfDiffArgs(): ', () => { + it('throws error if `target` is missing.', () => { + expect.assertions(1); + + try { + parseIfDiffArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliInputError(INVALID_TARGET)); + } + } + }); + + it('throws error if `target` is not a yaml.', () => { + process.env.result = 'target-is-not-yaml'; + expect.assertions(1); + + try { + parseIfDiffArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliInputError(TARGET_IS_NOT_YAML)); + } + } + }); + + it('returns `target`s full path.', () => { + process.env.result = 'only-target'; + expect.assertions(1); + + const response = parseIfDiffArgs(); + expect(response).toHaveProperty('targetPath'); + }); + + it('throws error if source is not a yaml.', () => { + process.env.result = 'source-is-not-yaml'; + expect.assertions(1); + + try { + parseIfDiffArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliInputError(SOURCE_IS_NOT_YAML)); + } + } + }); + + it('returns target and source full paths.', () => { + process.env.result = 'target-source'; + expect.assertions(2); + + const response = parseIfDiffArgs(); + expect(response).toHaveProperty('targetPath'); + expect(response).toHaveProperty('sourcePath'); + }); + + it('throws error if parsing failed.', () => { + process.env.result = 'diff-throw-error'; + expect.assertions(1); + + try { + parseIfDiffArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliInputError('mock-error')); + } + } + }); + + it('throws error if parsing failed (not instance of error).', () => { + process.env.result = 'diff-throw'; + expect.assertions(1); + + try { + parseIfDiffArgs(); + } catch (error) { + expect(error).toEqual('mock-error'); + } + }); }); process.env = originalEnv; diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 5bda8b752..d627790e4 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -1,14 +1,25 @@ const mockWarn = jest.fn(); const mockError = jest.fn(); +jest.mock('node:readline/promises', () => + require('../../../__mocks__/readline') +); jest.mock('../../../util/logger', () => ({ logger: { warn: mockWarn, error: mockError, }, })); -import {andHandle, mergeObjects} from '../../../util/helpers'; +import { + andHandle, + checkIfEqual, + formatNotMatchingLog, + mergeObjects, + oneIsPrimitive, + parseManifestFromStdin, +} from '../../../util/helpers'; import {ERRORS} from '../../../util/errors'; +import {Difference} from '../../../types/lib/compare'; const {WriteFileError} = ERRORS; @@ -175,4 +186,231 @@ describe('util/helpers: ', () => { expect(result).toEqual(expectedResult); }); }); + + describe('formatNotMatchingLog(): ', () => { + const actualLogger = console.log; + const mockLogger = jest.fn(); + console.log = mockLogger; + + beforeEach(() => { + mockLogger.mockReset(); + }); + + it('logs the message.', () => { + const difference: Difference = { + message: 'mock-message', + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(1); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + }); + + it('logs message and path.', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(2); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + }); + + it('logs message, path and formatted source/target (one is missing).', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: 'mock-source', + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith('target: missing'); + }); + + it('logs message, path and formatted source/target.', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: 'mock-source', + target: 'mock-target', + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith(`target: ${difference.target}`); + }); + + it('logs message, path and formatted source/target (numbers).', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: 10, + target: 0, + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith(`target: ${difference.target}`); + }); + + it('logs message, path and formatted source/target (booleans).', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: true, + target: false, + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith(`target: ${difference.target}`); + }); + + it('logs message, path and formatted source/target (objects).', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: {}, + target: false, + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith(`target: ${difference.target}`); + }); + + it('logs message, path and formatted source/target (empty string).', () => { + const difference: Difference = { + message: 'mock-message', + path: 'mock.path', + source: '', + target: false, + }; + + formatNotMatchingLog(difference); + expect(mockLogger).toHaveBeenCalledTimes(4); + expect(mockLogger).toHaveBeenCalledWith(difference.message); + expect(mockLogger).toHaveBeenCalledWith(difference.path); + expect(mockLogger).toHaveBeenCalledWith(`source: ${difference.source}`); + expect(mockLogger).toHaveBeenCalledWith(`target: ${difference.target}`); + }); + + afterAll(() => { + console.log = actualLogger; + }); + }); + + describe('oneIsPrimitive(): ', () => { + it('returns true if values are nullish.', () => { + const source = null; + const target = undefined; + + const result = oneIsPrimitive(source, target); + expect(result).toBeTruthy(); + }); + + it('returns true if values are string or number.', () => { + const source = 'string'; + const target = 10; + + const result = oneIsPrimitive(source, target); + expect(result).toBeTruthy(); + }); + + it('returns false if one of values is object.', () => { + const source = 'string'; + const target = {}; + + const result = oneIsPrimitive(source, target); + expect(result).toBeFalsy(); + }); + }); + + describe('parseManifestFromStdin(): ', () => { + it('returns empty string if there is no data in stdin.', async () => { + const response = await parseManifestFromStdin(); + const expectedResult = ''; + + expect(response).toEqual(expectedResult); + }); + + it('returns empty string if nothing is piped.', async () => { + const originalIsTTY = process.stdin.isTTY; + process.stdin.isTTY = true; + const response = await parseManifestFromStdin(); + const expectedResult = ''; + + expect(response).toEqual(expectedResult); + process.stdin.isTTY = originalIsTTY; + }); + + it('throws error if there is no manifest in stdin.', async () => { + process.env.readline = 'no_manifest'; + const expectedMessage = 'Manifest not found in STDIN.'; + expect.assertions(1); + + try { + await parseManifestFromStdin(); + } catch (error) { + if (error instanceof Error) { + expect(error.message).toEqual(expectedMessage); + } + } + }); + + it('returns empty string if there is no data in stdin.', async () => { + process.env.readline = 'manifest'; + const response = await parseManifestFromStdin(); + const expectedMessage = ` +name: mock-name +description: mock-description +`; + + expect(response).toEqual(expectedMessage); + }); + }); + + describe('checkIfEqual(): ', () => { + it('checks if values are equal.', () => { + const a = 'mock'; + const b = 'mock'; + + const response = checkIfEqual(a, b); + expect(response).toBeTruthy(); + }); + + it('returns true if one of the values is wildcard.', () => { + const a = 'mock'; + const b = '*'; + + const response = checkIfEqual(a, b); + expect(response).toBeTruthy(); + }); + + it('returns false for number and string with the same value.', () => { + const a = 5; + const b = '5'; + + const response = checkIfEqual(a, b); + expect(response).toBeFalsy(); + }); + }); }); diff --git a/src/config/strings.ts b/src/config/strings.ts index 5bfb7d134..04551b9b9 100644 --- a/src/config/strings.ts +++ b/src/config/strings.ts @@ -52,4 +52,8 @@ You have not selected an output method. To see your output data, you can choose --stdout: this will print your output data to the console --output : this will save your output data to the given filepath (do not provide file extension) Note that for the '--output' option you also need to define the output type in your manifest file. See https://if.greensoftware.foundation/major-concepts/manifest-file#initialize`, + SOURCE_IS_NOT_YAML: 'Given source file is not in yaml format.', + TARGET_IS_NOT_YAML: 'Given target is not in yaml format.', + INVALID_TARGET: 'Target is invalid.', + INVALID_SOURCE: 'Source is invalid.', }; diff --git a/src/index.ts b/src/index.ts index 041086559..692195c4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,8 +34,8 @@ const impactEngine = async () => { await exhaust(aggregatedTree, context, outputOptions); } catch (error) { if (error instanceof Error) { - envManifest.execution.status = 'fail'; - envManifest.execution.error = error.toString(); + envManifest.execution!.status = 'fail'; + envManifest.execution!.error = error.toString(); logger.error(error); const {tree, ...context} = envManifest; diff --git a/src/lib/compare.ts b/src/lib/compare.ts index 12bfb95b8..bf37c0d81 100644 --- a/src/lib/compare.ts +++ b/src/lib/compare.ts @@ -2,13 +2,23 @@ import {checkIfEqual, oneIsPrimitive} from '../util/helpers'; import {Difference} from '../types/lib/compare'; +/** + * Returns `status` and `exception` properties from execution context. + */ +const omitExecutionParams = (object: any) => ({ + status: object.status, + ...(object.error && { + error: object.error, + }), +}); + /** * 1. If objects are not of the same type or are primitive types, compares directly. * 2. Gets the keys from both objects. - * 3. Checks for keys present in both objects. - * 4. If both are arrays, checks their elements. - * 5. Checks for differences in values for common keys. - * 6. If all common keys are checked and no differences are found, return empty object. + * 3. If both are arrays, checks their elements. + * 4. Checks for keys present in both objects. + * If key is `execution`, omit unnecessary params. + * 5. If all keys are checked and no differences are found, return empty object. */ export const compare = (source: any, target: any, path = ''): Difference => { if (oneIsPrimitive(source, target)) { @@ -16,8 +26,8 @@ export const compare = (source: any, target: any, path = ''): Difference => { ? {} : { path, - source: source, - target: target, + source, + target, }; } @@ -27,17 +37,25 @@ export const compare = (source: any, target: any, path = ''): Difference => { const allKeys = new Set([...keys1, ...keys2]); if (Array.isArray(source) && Array.isArray(target)) { - for (let i = 0; i < source.length; i++) { + source.forEach((_record, i) => { compare(source[i], target[i], path ? `${path}[${i}]` : `${i}`); - } + }); } for (const key of allKeys) { - const result = compare( - source[key], - target[key], - path ? `${path}.${key}` : key - ); + let result: any = {}; + + if (key === 'execution') { + if (source[key] && target[key]) { + result = compare( + omitExecutionParams(source[key]), + omitExecutionParams(target[key]), + path ? `${path}.${key}` : key + ); + } + } else { + result = compare(source[key], target[key], path ? `${path}.${key}` : key); + } if (Object.keys(result).length) { return result; diff --git a/src/lib/load.ts b/src/lib/load.ts index b8ac3841b..447153068 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,15 +1,21 @@ import * as YAML from 'js-yaml'; +import {ERRORS} from '../util/errors'; import {openYamlFileAsObject} from '../util/yaml'; import {readAndParseJson} from '../util/json'; import {parseManifestFromStdin} from '../util/helpers'; import {PARAMETERS} from '../config'; +import {STRINGS} from '../config'; import {Parameters} from '../types/parameters'; import {LoadDiffParams} from '../types/util/args'; import {Manifest} from '../types/manifest'; +const {CliInputError} = ERRORS; + +const {INVALID_SOURCE} = STRINGS; + /** * Parses manifest file as an object. Checks if parameter file is passed via CLI, then loads it too. * Returns context, tree and parameters (either the default one, or from CLI). @@ -37,7 +43,7 @@ export const loadIfDiffFiles = async (params: LoadDiffParams) => { const pipedSourceManifest = await parseManifestFromStdin(); if (!sourcePath && !pipedSourceManifest) { - throw new Error('Source is invalid.'); + throw new CliInputError(INVALID_SOURCE); } const loadFromSource = diff --git a/src/types/lib/compare.ts b/src/types/lib/compare.ts index 756fca9eb..0f0cae8cf 100644 --- a/src/types/lib/compare.ts +++ b/src/types/lib/compare.ts @@ -2,5 +2,6 @@ export type Difference = { path?: string; source?: any; target?: any; + message?: string; [key: string]: any; }; diff --git a/src/util/args.ts b/src/util/args.ts index a78d0a507..56466fd48 100644 --- a/src/util/args.ts +++ b/src/util/args.ts @@ -14,7 +14,14 @@ const {CliInputError} = ERRORS; const {IE, IF_DIFF} = CONFIG; -const {FILE_IS_NOT_YAML, MANIFEST_IS_MISSING, NO_OUTPUT} = STRINGS; +const { + FILE_IS_NOT_YAML, + MANIFEST_IS_MISSING, + NO_OUTPUT, + SOURCE_IS_NOT_YAML, + TARGET_IS_NOT_YAML, + INVALID_TARGET, +} = STRINGS; /** * Validates `ie` process arguments. @@ -106,6 +113,10 @@ export const parseIfDiffArgs = () => { const {source, target} = validateAndParseIfDiffArgs(); if (target) { + if (source && !checkIfFileIsYaml(source)) { + throw new CliInputError(SOURCE_IS_NOT_YAML); + } + if (checkIfFileIsYaml(target)) { const response: LoadDiffParams = { targetPath: prependFullFilePath(target), @@ -118,8 +129,8 @@ export const parseIfDiffArgs = () => { return response; } - throw new CliInputError(FILE_IS_NOT_YAML); // change to one of the source or target parts are not a yaml + throw new CliInputError(TARGET_IS_NOT_YAML); } - throw new CliInputError(MANIFEST_IS_MISSING); // change to one of the source or target are missing + throw new CliInputError(INVALID_TARGET); }; diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 11c96b601..b15863cef 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -146,7 +146,7 @@ export const formatNotMatchingLog = (message: Difference) => { */ const collectPipedData = async () => { if (process.stdin.isTTY) { - return; + return ''; } const readline = createInterface({ diff --git a/src/util/validations.ts b/src/util/validations.ts index a7a601b81..28e42af8a 100644 --- a/src/util/validations.ts +++ b/src/util/validations.ts @@ -63,19 +63,23 @@ 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(), - }), + execution: z + .object({ + command: z.string().optional(), + 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()), + }) + .optional(), + status: z.string(), + error: z.string().optional(), + }) + .optional(), tree: z.record(z.string(), z.any()), });