From ff10e0ad86923ecef5435e509bc59f11956bd4aa Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:12:36 +0000 Subject: [PATCH 01/65] docs(src): add github processes docs --- github-processes.md | 136 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 github-processes.md diff --git a/github-processes.md b/github-processes.md new file mode 100644 index 000000000..d18af27b5 --- /dev/null +++ b/github-processes.md @@ -0,0 +1,136 @@ + +## IF Repositories + +- [`if`](https://github.com/Green-Software-Foundation/if) + - source code for the IF +- [`if-plugins`](https://github.com/Green-Software-Foundation/if-plugins) + - source code for standard library of plugins + - IF core team commit to maintaining these plugins +- [`if-unofficial-plugins`](https://github.com/Green-Software-Foundation/if-unofficial-plugins) + - source code for plugins relying on third party data/APIs + - intended to be deprecated and removed in mid-term future + - plugins int his repo should be handed over to relevant orgs to maintain +- [`if-plugin-template`](https://github.com/Green-Software-Foundation/if-plugin-template) + - template for new plugins + - intended for builders to bootstrap IF-compatible plugin development +- [`if-standards`](https://github.com/Green-Software-Foundation/if-standards) + - not currently used, but intended to be the home of params.ts + - will have a dedicated discussion board and governance to set IF standards +- [`if-exhaust plugins`](https://github.com/Green-Software-Foundation/if-exhaust-plugins) + - not currently used + - intended to become a separate repo just for exhaust plugins + - requires strict rules from if + + +## Branch names and purposes + +Our main repositories all have two branches: `main` and `release`. +Here are the rules applied to each branch: + +### `if`, `if-plugins`, `if-unofficial-plugins` and `if-exhaust-plugins` + +#### `main` +- target branch for PRs +- PRs can be merged into `main` with two core team reviews, one being QA +- `main` regularly advances ahead of the `release` branch +- merged into `release` periodically, only after full QA approval +- pushing directly to `main` is forbidden - all changes are by PR +- PRs will not be merged if they do not pass CI/CD + +#### `release` + +- release is our stable branch +- it is protected and only merged into when we have a fully QA-approved `main` state that we want to release +- npm packages are released using `release` branch +- PRs into `release` are forbidden except to update tests, README, Github config, CI/CD or release config. +- Pushing directly to `release` is forbidden +- `release` should always satisfy a basic set of regression and scenario tests +- merging `main` into `release` requires that all automated tests pass and two core team members have approved, one being QA. + +### `if-plugin-template` + +- we only maintain a single branch: `main` +- PRs to `main` can be mrged after one approval from core team +- Pushing directly to `main` is possible but discouraged + + +### `if-docs` + +- we only maintain a single branch - `main` +- PRs to `main` can be mrged after one approval from core team +- Pushing directly to `main` is possible but discouraged + +### When can we break our rules? + +- `release` branches have the strictest rules. We should never override the process outlined above for `release` branches in any repository. +- On `main` we can be slightly more flexible. It is acceptable to skip QA for PRs that only change typos, documentation or comments. Any changes to source code or tests should be QA approved before merge. +- In emergency scenarios where an urgent hotfix is required it might be required to skip QA review on `main`branches - this should only happen with QA authorization so QA can retroactively test as soon as possible. + +## DCO + +We require contributors to conform to the DCO agreement on our repositories. This means either signing commits or explicitly adding a DCO commit message. This ensures all contributors agree to the conditions imposed by our licenses and adhere to our expected practises. The DCO must be satisfied in order to PRs to be merged. + +## Commits + +Commits are expected to conform to the [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/) syntax. +Commits are expected to be signed, and have descriptive commit messages. +We might ask people to provide new messages in some circumstances, or to break their PRs into smaller logical units. + +## PR triage + +We run a weekly PR triage call on Thursdays. The most important outcome of this call is to examine open PRs and issues across all the IF repositories and determine which ones include changes that we might want to merge. This initial sift for changes that are or are not aligned with our goals for the IF should be prioritized above technical comments and fixes. + +Some reasons why a PR might not pass triage: + +- changes are not aligned with our vision for IF +- we do not see a strong reason for making a change +- changes are obviously technically incorrect +- changes are too large to properly review (e.g. covering too many files) +- too many changes are made in too few commits +- commit messages are ambiguous +- PR description is too short, vague or imprecise +- PR would for some other reason take too long for core team to assess +- PR makes changes that do not conform to our license +- PR makes use of a third party API or dataset in an illegitimate way (e.g. exposing data that should be paywalled) + +After a PR has passed triage, it can be assigned to a core team member to review. Developer review precedes QA review. Community PRs (PRs raised from outside the core team) will *always* go through a full QA vetting procedure before being merged. + +We intend to respond to new PRs and issues within 3 days of the ticket being opened, even if only with a brief message thanking the OP and explaining the triage process. + + +## Labels + +``` +//review status labels +awaiting-triage +triage-pass +awaiting-review +review-pass +awaiting-qa +qa-pass +changes-needed + +//change type labels +fix +feature +docs +package +other + +// size labels +small +medium +large + +// priority labels +high-priority +med-priority +low-priority + +//delay explanation labels +abandoned +blocked +backlogged +``` + +Each PR is expected to have one label from each category assigned to it at all times. From 1c9fc39a2d8428ecedbccdc2b9da204a5403a776 Mon Sep 17 00:00:00 2001 From: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:31:19 +0000 Subject: [PATCH 02/65] Apply suggestions from code review Signed-off-by: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> --- github-processes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github-processes.md b/github-processes.md index d18af27b5..815e8941c 100644 --- a/github-processes.md +++ b/github-processes.md @@ -9,7 +9,7 @@ - [`if-unofficial-plugins`](https://github.com/Green-Software-Foundation/if-unofficial-plugins) - source code for plugins relying on third party data/APIs - intended to be deprecated and removed in mid-term future - - plugins int his repo should be handed over to relevant orgs to maintain + - plugins in this repo should be handed over to relevant orgs to maintain - [`if-plugin-template`](https://github.com/Green-Software-Foundation/if-plugin-template) - template for new plugins - intended for builders to bootstrap IF-compatible plugin development @@ -50,7 +50,7 @@ Here are the rules applied to each branch: ### `if-plugin-template` - we only maintain a single branch: `main` -- PRs to `main` can be mrged after one approval from core team +- PRs to `main` can be merged after one approval from core team - Pushing directly to `main` is possible but discouraged From 6024b400913817602ae4572c7d2f8d5e62278786 Mon Sep 17 00:00:00 2001 From: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:31:46 +0000 Subject: [PATCH 03/65] Update github-processes.md Signed-off-by: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> --- github-processes.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/github-processes.md b/github-processes.md index 815e8941c..5f8019985 100644 --- a/github-processes.md +++ b/github-processes.md @@ -131,6 +131,13 @@ low-priority abandoned blocked backlogged + +// other labels +project-management +agenda +discussion +help-wanted +good-first-issue ``` Each PR is expected to have one label from each category assigned to it at all times. From f45d4585416ad15aa0b2142404874751a4f3bbc3 Mon Sep 17 00:00:00 2001 From: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:32:18 +0000 Subject: [PATCH 04/65] Apply suggestions from code review Signed-off-by: Joseph Cook <33655003+jmcook1186@users.noreply.github.com> --- github-processes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github-processes.md b/github-processes.md index 5f8019985..61b9d0900 100644 --- a/github-processes.md +++ b/github-processes.md @@ -140,4 +140,4 @@ help-wanted good-first-issue ``` -Each PR is expected to have one label from each category assigned to it at all times. +Each PR is expected to have one label from each category (except `other`) assigned to it at all times. From f000d2d661b47617fd029a83b9ee5f704d65a050 Mon Sep 17 00:00:00 2001 From: alexzurbonsen Date: Sat, 9 Mar 2024 09:03:33 +0100 Subject: [PATCH 05/65] fix(util): fix mergeObject function Currently the mergeObject function has a couple of problems. E.g. it overrides an input if its value is false and the default is true. It does not deep merge objects. Added a test suite that the current function would not pass. And fixed it. Signed-off-by: alexzurbonsen --- package-lock.json | 11 ++- package.json | 2 + src/__tests__/unit/lib/compute.test.ts | 44 +++++++++ src/__tests__/unit/util/helpers.test.ts | 114 +++++++++++++++++++++++- src/lib/compute.ts | 4 +- src/util/helpers.ts | 26 +++--- 6 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 src/__tests__/unit/lib/compute.test.ts diff --git a/package-lock.json b/package-lock.json index 2cf63cf8d..49260a512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@commitlint/config-conventional": "^18.6.0", "csv-stringify": "^6.4.6", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "luxon": "^3.4.4", "ts-command-line-args": "^2.5.1", "typescript": "^5.1.6", @@ -28,6 +29,7 @@ "@jest/globals": "^29.6.1", "@types/jest": "^29.5.7", "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "fixpack": "^4.0.0", @@ -1869,6 +1871,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/luxon": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", @@ -5635,7 +5643,8 @@ }, "node_modules/lodash": { "version": "4.17.21", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", diff --git a/package.json b/package.json index 9535275f5..cdc5b1b8a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@commitlint/config-conventional": "^18.6.0", "csv-stringify": "^6.4.6", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "luxon": "^3.4.4", "ts-command-line-args": "^2.5.1", "typescript": "^5.1.6", @@ -29,6 +30,7 @@ "@jest/globals": "^29.6.1", "@types/jest": "^29.5.7", "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "fixpack": "^4.0.0", diff --git a/src/__tests__/unit/lib/compute.test.ts b/src/__tests__/unit/lib/compute.test.ts new file mode 100644 index 000000000..dae8b4e0f --- /dev/null +++ b/src/__tests__/unit/lib/compute.test.ts @@ -0,0 +1,44 @@ +import {PluginParams} from '../../../types/interface'; +import {mergeDefaults} from '../../../lib/compute'; + +describe('lib/compute:', () => { + describe('compute(): ', () => { + it('merges inputs array and defaults correctly', () => { + const input1 = { + a: 1, + b: false, + c: 'testInput1', + d: 100, + }; + const input2 = { + a: 2, + b: true, + c: 'testInput2', + }; + const inputs = [input1, input2]; + + const defaults = { + b: true, + c: 'testDefault', + d: 25, + }; + + const expectedResult: PluginParams[] = [ + { + a: 1, + b: false, + c: 'testInput1', + d: 100, + }, + { + a: 2, + b: true, + c: 'testInput2', + d: 25, + }, + ]; + const result = mergeDefaults(inputs, defaults); + expect(result).toEqual(expectedResult); + }); + }); +}); diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 9feffc5f3..7b1ac6769 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -7,8 +7,8 @@ jest.mock('../../../util/logger', () => ({ error: mockError, }, })); - import {andHandle} from '../../../util/helpers'; +import {mergeObjects} from '../../../util/helpers'; import {ERRORS} from '../../../util/errors'; const {WriteFileError} = ERRORS; @@ -38,3 +38,115 @@ describe('util/helpers: ', () => { }); }); }); + +describe('util/helpers: ', () => { + describe('mergeObjects(): ', () => { + it('does not override input', () => { + expect.assertions(1); + + const input = { + a: 1, + b: false, + c: 'testInput', + }; + + const defaults = { + c: 'testDefault', + }; + const result = mergeObjects(defaults, input); + + expect(result).toEqual(input); + }); + + it('adds only properties missing in input', () => { + expect.assertions(1); + + const input = { + a: 1, + b: false, + c: 'testInput', + }; + + const defaults = { + b: true, + c: 'testDefault', + d: 25, + }; + + const result = mergeObjects(defaults, input); + const expectedResult = { + a: 1, + b: false, + c: 'testInput', + d: 25, + }; + + expect(result).toEqual(expectedResult); + }); + + it('handles object values correctly', () => { + expect.assertions(1); + + const input = { + a: 1, + b: false, + c: 'testInput', + d: { + e: 1, + }, + }; + + const defaults = { + b: true, + c: 'testDefault1', + d: { + e: 25, + f: 'testDefault2', + }, + }; + + const result = mergeObjects(defaults, input); + const expectedResult = { + a: 1, + b: false, + c: 'testInput', + d: { + e: 1, + f: 'testDefault2', + }, + }; + + expect(result).toEqual(expectedResult); + }); + + it('handles array values correctly', () => { + expect.assertions(1); + + const input = { + a: 1, + b: false, + c: 'testInput1', + d: [1, 2, 3, 4], + e: 'testInput2', + }; + + const defaults = { + b: true, + c: 'testDefault1', + d: [5, 6, 7, 8, 9], + e: [1, 2, 3], + }; + + const result = mergeObjects(defaults, input); + const expectedResult = { + a: 1, + b: false, + c: 'testInput1', + d: [1, 2, 3, 4], + e: 'testInput2', + }; + + expect(result).toEqual(expectedResult); + }); + }); +}); diff --git a/src/lib/compute.ts b/src/lib/compute.ts index 343bfcc97..ba4a67513 100644 --- a/src/lib/compute.ts +++ b/src/lib/compute.ts @@ -16,13 +16,13 @@ const traverse = async (children: any, params: Params) => { /** * Appends `default` values to `inputs`. */ -const mergeDefaults = ( +export const mergeDefaults = ( inputs: PluginParams[], defaults: PluginParams | undefined ) => { if (inputs) { const response = defaults - ? inputs.map(input => mergeObjects(input, defaults)) + ? inputs.map(input => mergeObjects({...defaults}, input)) : inputs; return response; diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 3ef81c037..80f0df024 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -1,6 +1,7 @@ -import {ERRORS} from './errors'; +import {mergeWith} from 'lodash'; import {STRINGS} from '../config'; +import {ERRORS} from './errors'; import {logger} from './logger'; const {ISSUE_TEMPLATE} = STRINGS; @@ -19,22 +20,15 @@ export const andHandle = (error: Error) => { }; /** - * Mergers two objects, omitting null values. + * Merge destination and source recursively. */ -export const mergeObjects = (object1: any, object2: any) => { - const merged: Record = {}; - - const keys1 = Object.keys(object1); - keys1.forEach(key1 => { - merged[key1] = object1[key1] || object2[key1]; - }); - - const keys2 = Object.keys(object2); - keys2.forEach(key2 => { - if (!keys1.includes(key2)) { - merged[key2] = object2[key2]; +export const mergeObjects = (destination: any, source: any) => { + const handleArrays = (objValue: any, srcValue: any) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return srcValue; } - }); + return; + }; - return merged; + return mergeWith(destination, source, handleArrays); }; From 7d3622a1d987640b6ed8201fdec2f7c59c8eeed4 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 21 Mar 2024 08:31:12 +0400 Subject: [PATCH 06/65] fix(lib): add types to parameterize, fix get method --- src/lib/parameterize.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/parameterize.ts b/src/lib/parameterize.ts index ac4c3c549..3389cd7c5 100644 --- a/src/lib/parameterize.ts +++ b/src/lib/parameterize.ts @@ -1,9 +1,10 @@ import {logger} from '../util/logger'; +import {memoizedLog} from '../util/log-memoize'; import {STRINGS, PARAMETERS} from '../config'; import {ManifestParameter} from '../types/manifest'; -import {memoizedLog} from '../util/log-memoize'; +import {Parameters} from '../types/parameters'; const {REJECTING_OVERRIDE, UNKNOWN_PARAM} = STRINGS; @@ -18,7 +19,7 @@ const Parametrize = () => { */ const getAggregationMethod = (unitName: string) => { if (`${unitName}` in parametersStorage) { - return PARAMETERS[unitName as keyof typeof PARAMETERS].aggregation; + return parametersStorage[unitName as keyof typeof PARAMETERS].aggregation; } memoizedLog(logger.warn, UNKNOWN_PARAM(unitName)); @@ -33,7 +34,7 @@ const Parametrize = () => { */ const combine = ( contextParameters: ManifestParameter[] | null | undefined, - parameters: any + parameters: Parameters ) => { if (contextParameters) { contextParameters.forEach(param => { From 01305073f2863d570203f06861d967e6225b3b70 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 21 Mar 2024 08:31:48 +0400 Subject: [PATCH 07/65] chore(lib): add todo to load for moving parameters --- src/lib/load.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/load.ts b/src/lib/load.ts index aa2fc9da6..568981860 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -13,8 +13,11 @@ export const load = async (inputPath: string, paramPath?: string) => { const rawManifest = await openYamlFileAsObject(inputPath); const {tree, ...context} = validateManifest(rawManifest); const parametersFromCli = - paramPath && (await readAndParseJson(paramPath)); // todo: validate json - const parameters = parametersFromCli || PARAMETERS; + paramPath && + (await readAndParseJson(paramPath)); /** @todo validate json */ + const parameters = + parametersFromCli || + PARAMETERS; /** @todo PARAMETERS should be specified in parameterize only */ return { tree, From 819d54d7836feb9e0333df48db73b3d566e119e2 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 21 Mar 2024 08:32:25 +0400 Subject: [PATCH 08/65] test(lib): add units for parameterize --- src/__tests__/unit/lib/parameterize.test.ts | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/__tests__/unit/lib/parameterize.test.ts diff --git a/src/__tests__/unit/lib/parameterize.test.ts b/src/__tests__/unit/lib/parameterize.test.ts new file mode 100644 index 000000000..f94bb8025 --- /dev/null +++ b/src/__tests__/unit/lib/parameterize.test.ts @@ -0,0 +1,65 @@ +const mockLog = jest.fn(); + +jest.mock('../../../util/log-memoize', () => ({ + memoizedLog: mockLog, +})); + +import {parameterize} from '../../../lib/parameterize'; +import {ManifestParameter} from '../../../types/manifest'; + +describe('lib/parameterize: ', () => { + describe('getAggregationMethod(): ', () => { + it('returns method for average aggregation method metric.', () => { + const metric = 'cpu/utilization'; + const method = parameterize.getAggregationMethod(metric); + + const expectedMethod = 'avg'; + + expect(method).toEqual(expectedMethod); + }); + + it('returns method for unknown aggregation method metric.', () => { + const metric = 'mock/metric'; + const method = parameterize.getAggregationMethod(metric); + + const expectedMethod = 'sum'; + + expect(method).toEqual(expectedMethod); + expect(mockLog).toHaveBeenCalledTimes(1); + }); + }); + + describe('combine(): ', () => { + it('checks if return type is undefined.', () => { + const params = {}; + const response = parameterize.combine(null, params); + + expect(response).toBeUndefined(); + }); + + it('checks if uninitialized custom param is requested, then returns fallback `sum` method.', () => { + const name = 'mock-name'; + const method = parameterize.getAggregationMethod(name); + + const expectedMethodName = 'sum'; + expect(method).toEqual(expectedMethodName); + }); + + it('checks if custom params are inserted successfully.', () => { + const params = [ + { + name: 'mock-name', + description: 'mock-description', + unit: 'mock/sq', + aggregation: 'none', + }, + ] as ManifestParameter[]; + const object = {}; + + parameterize.combine(params, object); + const method = parameterize.getAggregationMethod(params[0].name); + + expect(method).toEqual(params[0].aggregation); + }); + }); +}); From fb7e70571f0c5c73e161cc922b7d064a1807e248 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 21 Mar 2024 09:03:16 +0400 Subject: [PATCH 09/65] fix(lib): in parameterize fix reject override warn log --- src/lib/parameterize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/parameterize.ts b/src/lib/parameterize.ts index 3389cd7c5..35e965ec6 100644 --- a/src/lib/parameterize.ts +++ b/src/lib/parameterize.ts @@ -39,7 +39,7 @@ const Parametrize = () => { if (contextParameters) { contextParameters.forEach(param => { if (`${param.name}` in parameters) { - logger.warn(REJECTING_OVERRIDE); + logger.warn(REJECTING_OVERRIDE(param)); return; } From a18d3996e54b0a99714d4ec4e4603e344214d0a1 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 21 Mar 2024 09:04:30 +0400 Subject: [PATCH 10/65] test(lib): add case to parameterize units --- src/__tests__/unit/lib/parameterize.test.ts | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/__tests__/unit/lib/parameterize.test.ts b/src/__tests__/unit/lib/parameterize.test.ts index f94bb8025..297bf90f2 100644 --- a/src/__tests__/unit/lib/parameterize.test.ts +++ b/src/__tests__/unit/lib/parameterize.test.ts @@ -3,11 +3,26 @@ const mockLog = jest.fn(); jest.mock('../../../util/log-memoize', () => ({ memoizedLog: mockLog, })); +jest.mock('../../../util/logger', () => ({ + logger: { + warn: mockLog, + }, +})); +import {PARAMETERS} from '../../../config'; import {parameterize} from '../../../lib/parameterize'; + +import {STRINGS} from '../../../config'; + import {ManifestParameter} from '../../../types/manifest'; +const {REJECTING_OVERRIDE} = STRINGS; + describe('lib/parameterize: ', () => { + afterEach(() => { + mockLog.mockReset(); + }); + describe('getAggregationMethod(): ', () => { it('returns method for average aggregation method metric.', () => { const metric = 'cpu/utilization'; @@ -61,5 +76,25 @@ describe('lib/parameterize: ', () => { expect(method).toEqual(params[0].aggregation); }); + + it('rejects on default param override.', () => { + const params = [ + { + name: 'carbon', + description: 'mock-description', + unit: 'mock/co', + aggregation: 'none', + }, + ] as ManifestParameter[]; + + parameterize.combine(params, PARAMETERS); + const method = parameterize.getAggregationMethod(params[0].name); + + const expectedMethodName = 'sum'; + const expectedMessage = REJECTING_OVERRIDE(params[0]); + + expect(method).toEqual(expectedMethodName); + expect(mockLog).toHaveBeenCalledWith(expectedMessage); + }); }); }); From 9904d2c5e10882bd355f2d9d09d2f3332fee93c4 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 21 Mar 2024 10:43:46 +0400 Subject: [PATCH 11/65] test(builtins): add test coverage for time-sync plugin --- src/__tests__/unit/builtins/time-sync.test.ts | 139 +++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index b2583e649..8c81d79d1 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -5,9 +5,9 @@ import {STRINGS} from '../../../config'; const {InputValidationError} = ERRORS; -const {INVALID_OBSERVATION_OVERLAP} = STRINGS; +const {INVALID_OBSERVATION_OVERLAP, INVALID_TIME_NORMALIZATION} = STRINGS; -describe('lib/time-sync:', () => { +describe('builtins/time-sync:', () => { describe('time-sync: ', () => { const basicConfig = { 'start-time': '2023-12-12T00:01:00.000Z', @@ -146,6 +146,32 @@ describe('execute(): ', () => { } }); + it('throws error on missing global config.', async () => { + const config = undefined; + const timeModel = TimeSync(config!); + + expect.assertions(1); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 15, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(INVALID_TIME_NORMALIZATION) + ); + } + }); + it('throws error if interval is invalid.', async () => { const invalidIntervalConfig = { 'start-time': '2023-12-12T00:00:00.000Z', @@ -208,6 +234,33 @@ describe('execute(): ', () => { } }); + it('throws an error if the `timestamp` is not valid date.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 10, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + expect.assertions(2); + + try { + await timeModel.execute([ + { + timestamp: 45, + duration: 10, + 'cpu/utilization': 10, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError('Unexpected date datatype: number: 45') + ); + } + }); + it('throws error if end is before start in global config.', async () => { const basicConfig = { 'start-time': '2023-12-12T00:00:10.000Z', @@ -393,6 +446,88 @@ describe('execute(): ', () => { expect(result).toStrictEqual(expectedResult); }); + it('returns a result when `time-reserved` persists.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:09.000Z', + interval: 5, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3, + 'time-reserved': 5, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3, + 'time-reserved': 5, + 'resources-total': 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + 'resources-total': 10, + 'time-reserved': 3.2, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + 'resources-total': 10, + 'time-reserved': 3.2, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); + + it('returns a result when the first timestamp in the input has time padding.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:09.000Z', + interval: 5, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 3, + 'resources-total': 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + 'resources-total': 10, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); + it('throws error if padding is required at start while allow-padding = false.', async () => { const basicConfig = { 'start-time': '2023-12-12T00:00:00.000Z', From 99054006ef91bc8d8543baf87c193083addf6727 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 21 Mar 2024 10:45:23 +0400 Subject: [PATCH 12/65] test(builtins): add test for group-by plugin --- src/__tests__/unit/builtins/group-by.test.ts | 175 +++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/__tests__/unit/builtins/group-by.test.ts diff --git a/src/__tests__/unit/builtins/group-by.test.ts b/src/__tests__/unit/builtins/group-by.test.ts new file mode 100644 index 000000000..9c99ee700 --- /dev/null +++ b/src/__tests__/unit/builtins/group-by.test.ts @@ -0,0 +1,175 @@ +import {GroupBy} from '../../../builtins/group-by'; +import {ERRORS} from '../../../util/errors'; + +const {InvalidGroupingError, InputValidationError} = ERRORS; + +describe('builtins/group-by: ', () => { + describe('GroupBy: ', () => { + const plugin = GroupBy(); + + describe('init GroupBy: ', () => { + it('initalizes object with properties.', async () => { + expect(plugin).toHaveProperty('metadata'); + expect(plugin).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('groups inputs correctly.', () => { + const inputs = [ + { + timestamp: '2023-07-06T00:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T05:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T10:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + ]; + const config = { + group: ['region', 'cloud/instance-type'], + }; + + const expectedOutput = { + 'uk-west': { + children: { + A1: { + inputs: [ + { + 'cloud/instance-type': 'A1', + region: 'uk-west', + timestamp: '2023-07-06T00:00', + }, + { + 'cloud/instance-type': 'A1', + region: 'uk-west', + timestamp: '2023-07-06T05:00', + }, + { + 'cloud/instance-type': 'A1', + region: 'uk-west', + timestamp: '2023-07-06T10:00', + }, + ], + }, + }, + }, + }; + + const result = plugin.execute(inputs, config); + expect(result).toEqual(expectedOutput); + }); + + it('throws an error when config is not provided.', () => { + const inputs = [ + { + timestamp: '2023-07-06T00:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T05:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T10:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + ]; + + const config = undefined; + + expect.assertions(2); + try { + plugin.execute(inputs, config!); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError('Config is not provided.') + ); + } + }); + + it('throws an error if `group` is an empty array.', () => { + const inputs = [ + { + timestamp: '2023-07-06T00:00', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T05:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + { + timestamp: '2023-07-06T10:00', + region: 'uk-west', + 'cloud/instance-type': 'A1', + }, + ]; + const config = { + group: ['region', 'cloud/instance-type'], + }; + + expect.assertions(2); + try { + plugin.execute(inputs, config); + } catch (error) { + expect(error).toBeInstanceOf(InvalidGroupingError); + expect(error).toEqual( + new InvalidGroupingError('Invalid group region.') + ); + } + }); + + it('throws an error if group type is missing from the input.', () => { + const inputs = [ + {timestamp: 1, region: 'uk-west', 'cloud/instance-type': 'A1'}, + ]; + const config = { + group: [], + }; + + expect.assertions(2); + try { + plugin.execute(inputs, config); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"group" parameter is array must contain at least 1 element(s). Error code: too_small.' + ) + ); + } + }); + + it('throws an error if input does not have required group type.', () => { + const inputs = [ + {timestamp: 1, region: 'uk-west', 'cloud/instance-type': 'A1'}, + ]; + const config = { + group: ['region', 'cloud/instance-type', 'unknown'], + }; + + expect.assertions(2); + try { + plugin.execute(inputs, config); + } catch (error) { + expect(error).toBeInstanceOf(InvalidGroupingError); + expect(error).toEqual( + new InvalidGroupingError('Invalid group unknown.') + ); + } + }); + }); + }); +}); From 65085a1949dcd35a14308ab1aa485bb1f03300cd Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 21 Mar 2024 10:47:50 +0400 Subject: [PATCH 13/65] fix(builtins): add missing validation for config in group-by plugin --- src/builtins/group-by.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/builtins/group-by.ts b/src/builtins/group-by.ts index 436e309ef..6a7036adf 100644 --- a/src/builtins/group-by.ts +++ b/src/builtins/group-by.ts @@ -1,11 +1,14 @@ -import {ERRORS} from '../util/errors'; +import {z} from 'zod'; import {STRINGS} from '../config'; import {GroupByPlugin, PluginParams} from '../types/interface'; import {GroupByConfig} from '../types/group-by'; -const {InvalidGroupingError} = ERRORS; +import {ERRORS} from '../util/errors'; +import {validate} from '../util/validations'; + +const {InvalidGroupingError, InputValidationError} = ERRORS; const {INVALID_GROUP_BY} = STRINGS; @@ -54,7 +57,8 @@ export const GroupBy = (): GroupByPlugin => { */ const execute = (inputs: PluginParams[], config: GroupByConfig) => inputs.reduce((acc, input) => { - const groups = config.group.map(groupType => { + const validatedConfig = validateConfig(config); + const groups = validatedConfig.group.map(groupType => { if (!input[groupType]) { throw new InvalidGroupingError(INVALID_GROUP_BY(groupType)); } @@ -70,5 +74,20 @@ export const GroupBy = (): GroupByPlugin => { return acc; }, {} as any).children; + /** + * Validates config parameter. + */ + const validateConfig = (config: GroupByConfig) => { + if (!config) { + throw new InputValidationError('Config is not provided.'); + } + + const schema = z.object({ + group: z.array(z.string()).min(1), + }); + + return validate>(schema, config); + }; + return {metadata, execute}; }; From 0450adb72e04e29397fbe0a6e533782e275372c1 Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 23 Mar 2024 16:54:53 +0400 Subject: [PATCH 14/65] feat(mocks): create mock data for export-csv plugin --- src/__mocks__/builtins/export-csv.ts | 171 +++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/__mocks__/builtins/export-csv.ts diff --git a/src/__mocks__/builtins/export-csv.ts b/src/__mocks__/builtins/export-csv.ts new file mode 100644 index 000000000..3d815f1ad --- /dev/null +++ b/src/__mocks__/builtins/export-csv.ts @@ -0,0 +1,171 @@ +import {Context} from '../../types/manifest'; + +export const tree = { + children: { + 'child-1': { + pipeline: ['teads-curve', 'sum', 'sci-m', 'sci-o', 'sci'], + config: null, + defaults: { + 'cpu/thermal-design-power': 100, + 'grid/carbon-intensity': 800, + 'device/emissions-embodied': 1533.12, + 'time-reserved': 3600, + 'device/expected-lifespan': 94608000, + 'resources-reserved': 1, + 'resources-total': 8, + 'functional-unit-time': '1 min', + }, + inputs: [ + { + timestamp: '2023-12-12T00:00:00.000Z', + 'cloud/instance-type': 'A1', + region: 'uk-west', + duration: 1, + 'cpu/utilization': 10, + 'network/energy': 10, + energy: 5, + }, + ], + outputs: [ + { + timestamp: '2023-12-12T00:00:00.000Z', + 'cloud/instance-type': 'A1', + region: 'uk-west', + duration: 1, + 'cpu/utilization': 10, + 'network/energy': 10, + energy: 5, + 'cpu/thermal-design-power': 100, + 'grid/carbon-intensity': 800, + 'device/emissions-embodied': 1533.12, + 'time-reserved': 3600, + 'device/expected-lifespan': 94608000, + 'resources-reserved': 1, + 'resources-total': 8, + 'functional-unit-time': '1 min', + 'cpu/energy': 0.000008888888888888888, + "carbon-plus-energy'": 10.000008888888889, + 'carbon-embodied': 0.0000020256215119228817, + 'carbon-operational': 4000, + carbon: 4000.0000020256216, + sci: 240000.0001215373, + }, + ], + aggregated: undefined, + }, + 'child-2': { + pipeline: ['teads-curve', 'sum', 'sci-m', 'sci-o', 'sci'], + config: null, + defaults: { + 'cpu/thermal-design-power': 100, + 'grid/carbon-intensity': 800, + 'device/emissions-embodied': 1533.12, + 'time-reserved': 3600, + 'device/expected-lifespan': 94608000, + 'resources-reserved': 1, + 'resources-total': 8, + 'functional-unit-time': '1 min', + }, + inputs: [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + 'cpu/utilization': 30, + 'cloud/instance-type': 'A1', + region: 'uk-west', + 'network/energy': 10, + energy: 5, + }, + ], + outputs: [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + 'cpu/utilization': 30, + 'cloud/instance-type': 'A1', + region: 'uk-west', + 'network/energy': 10, + energy: 5, + 'cpu/thermal-design-power': 100, + 'grid/carbon-intensity': 800, + 'device/emissions-embodied': 1533.12, + 'time-reserved': 3600, + 'device/expected-lifespan': 94608000, + 'resources-reserved': 1, + 'resources-total': 8, + 'functional-unit-time': '1 min', + 'cpu/energy': 0.00001650338753387534, + "carbon-plus-energy'": 10.000016503387533, + 'carbon-embodied': 0.0000020256215119228817, + 'carbon-operational': 4000, + carbon: 4000.0000020256216, + sci: 240000.0001215373, + }, + ], + aggregated: undefined, + }, + }, + outputs: [ + { + carbon: 8000.000004051243, + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + }, + ], + aggregated: { + carbon: 8000.000004051243, + }, +}; + +export const context: Context = { + name: 'demo', + description: '', + tags: null, + aggregation: undefined, + initialize: { + plugins: { + 'teads-curve': { + path: '@grnsft/if-unofficial-plugins', + method: 'TeadsCurve', + 'global-config': { + interpolation: 'spline', + }, + }, + sum: { + path: '@grnsft/if-plugins', + method: 'Sum', + 'global-config': { + 'input-parameters': ['cpu/energy', 'network/energy'], + 'output-parameter': "carbon-plus-energy'", + }, + }, + 'sci-m': { + path: '@grnsft/if-plugins', + method: 'SciM', + }, + 'sci-o': { + path: '@grnsft/if-plugins', + method: 'SciO', + }, + sci: { + path: '@grnsft/if-plugins', + method: 'Sci', + 'global-config': { + 'functional-unit': 'requests', + 'functional-unit-time': '1 minute', + }, + }, + }, + }, +}; + +export const aggregated = { + carbon: 4000.0000020256216, +}; + +export const aggregation = { + metrics: ['carbon'], + type: 'both', +}; + +export const outputs = ['csv']; From cff37724bcf61e935edd94929adc6007c3c9b3bc Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 23 Mar 2024 16:57:10 +0400 Subject: [PATCH 15/65] fix(builtins): add validation for output path in export-csv --- src/builtins/export-csv.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/builtins/export-csv.ts b/src/builtins/export-csv.ts index dbf7dc1ae..9e48ad1b0 100644 --- a/src/builtins/export-csv.ts +++ b/src/builtins/export-csv.ts @@ -13,11 +13,9 @@ const {CliInputError} = ERRORS; */ export const ExportCSV = () => { const parseOutputAndField = (outputPath: string) => { - if (!outputPath) { - throw new CliInputError('Output path is required.'); - } + const validatedPath = validateOutputPath(outputPath); - const paths = outputPath.split('#'); + const paths = validatedPath.split('#'); const output = paths.slice(0, paths.length - 1).join(''); const criteria = paths[paths.length - 1]; @@ -33,6 +31,21 @@ export const ExportCSV = () => { }; }; + /** + * Validates output path. + */ + const validateOutputPath = (outputPath: string) => { + if (!outputPath) { + throw new CliInputError('Output path is required.'); + } + + if (!outputPath.includes('#')) { + throw new CliInputError('Output path should contains `#`.'); + } + + return outputPath; + }; + /** * Grabs output and criteria from cli args, then call tree walker to collect csv data. */ From bc6448406ef2f1faff065071e22bef942bdb71e2 Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 23 Mar 2024 16:59:12 +0400 Subject: [PATCH 16/65] test(builtins): add test file for export-csv plugin --- .../unit/builtins/export-csv.test.ts | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/__tests__/unit/builtins/export-csv.test.ts diff --git a/src/__tests__/unit/builtins/export-csv.test.ts b/src/__tests__/unit/builtins/export-csv.test.ts new file mode 100644 index 000000000..eafe492f1 --- /dev/null +++ b/src/__tests__/unit/builtins/export-csv.test.ts @@ -0,0 +1,238 @@ +import * as fs from 'fs/promises'; +import {stringify} from 'csv-stringify/sync'; +import {jest} from '@jest/globals'; + +import {ExportCSV} from '../../../builtins/export-csv'; +import {ERRORS} from '../../../util/errors'; + +import { + tree, + context, + outputs, + aggregated, + aggregation, +} from '../../../__mocks__/builtins/export-csv'; + +const {CliInputError} = ERRORS; + +jest.mock('fs/promises', () => ({ + writeFile: jest.fn<() => Promise>().mockResolvedValue(), +})); + +describe('builtins//export-csv: ', () => { + describe('ExportCSV: ', () => { + const exportCSV = ExportCSV(); + + describe('init GroupBy: ', () => { + it('initalizes object with properties.', async () => { + expect(exportCSV).toMatchObject({metadata: {kind: 'exhaust'}}); + expect(exportCSV).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('generates CSV file with correct data.', async () => { + const outputPath = 'output#carbon'; + const columns = ['Path', 'Aggregated', '2023-12-12T00:00:00.000Z']; + const matrix = [ + columns, + ['tree.carbon', 8000.000004051243, 8000.000004051243], + [ + 'tree.children.child-1.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + [ + 'tree.children.child-2.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + ]; + const reformedContext = Object.assign({}, context, {outputs}); + const reformedTree = Object.assign({}, tree, { + children: { + ...tree.children, + 'child-1': { + ...tree.children['child-1'], + aggregated, + }, + 'child-2': { + ...tree.children['child-2'], + aggregated, + }, + }, + }); + + await exportCSV.execute(reformedTree, reformedContext, outputPath); + + expect(fs.writeFile).toHaveBeenCalledWith( + 'output.csv', + stringify(matrix, {columns}) + ); + }); + + it('generates CSV file when the `outputs` type is missing.', async () => { + const outputPath = 'output#carbon'; + const columns = ['Path', 'Aggregated', '2023-12-12T00:00:00.000Z']; + const matrix = [ + columns, + ['tree.carbon', 8000.000004051243, 8000.000004051243], + [ + 'tree.children.child-1.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + [ + 'tree.children.child-2.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + ]; + + const reformedTree = Object.assign({}, tree, { + children: { + ...tree.children, + 'child-1': { + ...tree.children['child-1'], + aggregated, + }, + 'child-2': { + ...tree.children['child-2'], + aggregated, + }, + }, + }); + + await exportCSV.execute(reformedTree, context, outputPath); + + expect.assertions(1); + + expect(fs.writeFile).toHaveBeenCalledWith( + 'output.csv', + stringify(matrix, {columns}) + ); + }); + + it('generates CSV file when `aggregation` persists.', async () => { + const outputPath = 'output#carbon'; + const columns = ['Path', 'Aggregated', '2023-12-12T00:00:00.000Z']; + const matrix = [ + columns, + ['tree.carbon', 8000.000004051243, 8000.000004051243], + [ + 'tree.children.child-1.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + [ + 'tree.children.child-2.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + ]; + + const reformedContext = Object.assign( + {}, + context, + {outputs}, + {aggregation} + ); + const reformedTree = Object.assign({}, tree, { + children: { + ...tree.children, + 'child-1': { + ...tree.children['child-1'], + aggregated, + }, + 'child-2': { + ...tree.children['child-2'], + aggregated, + }, + }, + }); + + await exportCSV.execute(reformedTree, reformedContext, outputPath); + + expect.assertions(1); + expect(fs.writeFile).toHaveBeenCalledWith( + 'output.csv', + stringify(matrix, {columns}) + ); + }); + + it('generates CSV file when `aggregation` is missing.', async () => { + const outputPath = 'output#carbon'; + const columns = ['Path', 'Aggregated', '2023-12-12T00:00:00.000Z']; + const matrix = [ + columns, + ['tree.carbon', 8000.000004051243, 8000.000004051243], + [ + 'tree.children.child-1.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + [ + 'tree.children.child-2.carbon', + 4000.0000020256216, + 4000.0000020256216, + ], + ]; + + const reformedContext = Object.assign({}, context, {outputs}); + + await exportCSV.execute(tree, reformedContext, outputPath); + + expect.assertions(1); + expect(fs.writeFile).toHaveBeenCalledWith( + 'output.csv', + stringify(matrix, {columns}) + ); + }); + + it('throws an error when output path is empty.', async () => { + const outputPath = ''; + + context.initialize = Object.assign({}, context.initialize, outputs); + + try { + await exportCSV.execute(tree, context, outputPath); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError('Output path is required.')); + } + }); + + it('throws an error when output path does not contains `#`.', async () => { + const outputPath = 'output.csv'; + + context.initialize = Object.assign({}, context.initialize, outputs); + + try { + await exportCSV.execute(tree, context, outputPath); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual( + new CliInputError('Output path should contains `#`.') + ); + } + }); + + it('throws an error when output path does not contains a criteria.', async () => { + const outputPath = 'output.csv#'; + + context.initialize = Object.assign({}, context.initialize, outputs); + + try { + await exportCSV.execute(tree, context, outputPath); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual( + new CliInputError( + 'CSV export criteria is not found in output path. Please append it after --output #.' + ) + ); + } + }); + }); + }); +}); From e0142da2bfcd0b93d02df247ab1da72058f84e65 Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 23 Mar 2024 17:09:45 +0400 Subject: [PATCH 17/65] test(builtins): fix test description --- src/__tests__/unit/builtins/export-csv.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/builtins/export-csv.test.ts b/src/__tests__/unit/builtins/export-csv.test.ts index eafe492f1..26c1f194e 100644 --- a/src/__tests__/unit/builtins/export-csv.test.ts +++ b/src/__tests__/unit/builtins/export-csv.test.ts @@ -19,7 +19,7 @@ jest.mock('fs/promises', () => ({ writeFile: jest.fn<() => Promise>().mockResolvedValue(), })); -describe('builtins//export-csv: ', () => { +describe('builtins/export-csv: ', () => { describe('ExportCSV: ', () => { const exportCSV = ExportCSV(); From 45bd59aebc18ccd88e0e15f96cd9684e590bba51 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:17:33 +0000 Subject: [PATCH 18/65] docs(package): add to github process doc --- github-processes.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/github-processes.md b/github-processes.md index 61b9d0900..65aa2f3f9 100644 --- a/github-processes.md +++ b/github-processes.md @@ -141,3 +141,24 @@ good-first-issue ``` Each PR is expected to have one label from each category (except `other`) assigned to it at all times. + + +## Releases + +We aim to release fortnightly, every other Tuesday. We release npm packages for `if`, `if-plugins`, `if-unofficial-plugins`, and `if-plugin-template`. + +## Hotfixes + +We will hotfix by raising PRs into `release` when necessary. These PRs require sign off by a core developer and our QA engineer. + +If more than two hotfixes are required on a particular release, the team will call a spike meeting to determine the causes of the bugs, identify any changes required to our QA process and determine next steps for fixing the release. + +Hotfixes on release can be merged back into `main` when they have been fully QA tested. + +We intend for hotfixes to be as infrequent as possible. + + +## Schematic + +Our Github branch management process is summarized in the foillowing schematic + From ee94c3d8546cd4f52b5bd642d13de65fefb64cc3 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:23:00 +0000 Subject: [PATCH 19/65] docs(package): fix typois and remove schematic --- github-processes.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/github-processes.md b/github-processes.md index 65aa2f3f9..5c8b37471 100644 --- a/github-processes.md +++ b/github-processes.md @@ -7,9 +7,9 @@ - source code for standard library of plugins - IF core team commit to maintaining these plugins - [`if-unofficial-plugins`](https://github.com/Green-Software-Foundation/if-unofficial-plugins) - - source code for plugins relying on third party data/APIs + - source code for plugins relying on third-party data/APIs - intended to be deprecated and removed in mid-term future - - plugins in this repo should be handed over to relevant orgs to maintain + - plugins in this repo should be handed over to relevant organizations to maintain - [`if-plugin-template`](https://github.com/Green-Software-Foundation/if-plugin-template) - template for new plugins - intended for builders to bootstrap IF-compatible plugin development @@ -57,24 +57,24 @@ Here are the rules applied to each branch: ### `if-docs` - we only maintain a single branch - `main` -- PRs to `main` can be mrged after one approval from core team +- PRs to `main` can be merged after one approval from core team - Pushing directly to `main` is possible but discouraged ### When can we break our rules? - `release` branches have the strictest rules. We should never override the process outlined above for `release` branches in any repository. - On `main` we can be slightly more flexible. It is acceptable to skip QA for PRs that only change typos, documentation or comments. Any changes to source code or tests should be QA approved before merge. -- In emergency scenarios where an urgent hotfix is required it might be required to skip QA review on `main`branches - this should only happen with QA authorization so QA can retroactively test as soon as possible. +- In emergency scenarios where an urgent hotfix is required it might be required to skip QA review on `main` branches - this should only happen with QA authorization so QA can retroactively test as soon as possible. ## DCO -We require contributors to conform to the DCO agreement on our repositories. This means either signing commits or explicitly adding a DCO commit message. This ensures all contributors agree to the conditions imposed by our licenses and adhere to our expected practises. The DCO must be satisfied in order to PRs to be merged. +We require contributors to conform to the DCO agreement on our repositories. This means either signing commits or explicitly adding a DCO commit message. This ensures all contributors agree to the conditions imposed by our licenses and adhere to our expected practices. The DCO must be satisfied in order to PRs to be merged. ## Commits Commits are expected to conform to the [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/) syntax. -Commits are expected to be signed, and have descriptive commit messages. -We might ask people to provide new messages in some circumstances, or to break their PRs into smaller logical units. +Commits are expected to be signed and have descriptive commit messages. +We might ask people to provide new messages in some circumstances or to break their PRs into smaller logical units. ## PR triage @@ -91,7 +91,7 @@ Some reasons why a PR might not pass triage: - PR description is too short, vague or imprecise - PR would for some other reason take too long for core team to assess - PR makes changes that do not conform to our license -- PR makes use of a third party API or dataset in an illegitimate way (e.g. exposing data that should be paywalled) +- PR makes use of a third-party API or dataset in an illegitimate way (e.g. exposing data that should be paywalled) After a PR has passed triage, it can be assigned to a core team member to review. Developer review precedes QA review. Community PRs (PRs raised from outside the core team) will *always* go through a full QA vetting procedure before being merged. @@ -149,16 +149,10 @@ We aim to release fortnightly, every other Tuesday. We release npm packages for ## Hotfixes -We will hotfix by raising PRs into `release` when necessary. These PRs require sign off by a core developer and our QA engineer. +We will hotfix by raising PRs into `release` when necessary. These PRs require sign-off by a core developer and our QA engineer. If more than two hotfixes are required on a particular release, the team will call a spike meeting to determine the causes of the bugs, identify any changes required to our QA process and determine next steps for fixing the release. Hotfixes on release can be merged back into `main` when they have been fully QA tested. We intend for hotfixes to be as infrequent as possible. - - -## Schematic - -Our Github branch management process is summarized in the foillowing schematic - From faee04fb11c0b545b0f7f495e7c870172295efc6 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 14:58:47 +0400 Subject: [PATCH 20/65] fix(builtins): fix an issue in export-scv-raw --- src/builtins/export-csv-raw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins/export-csv-raw.ts b/src/builtins/export-csv-raw.ts index a9981a089..3e2420ab5 100644 --- a/src/builtins/export-csv-raw.ts +++ b/src/builtins/export-csv-raw.ts @@ -148,7 +148,7 @@ export const ExportCSVRaw = (): ExhaustPluginInterface => { ); const csvString = getCsvString(extractredFlatMap, extractedHeaders, ids); - writeOutputFile(csvString, outputPath); + await writeOutputFile(csvString, outputPath); }; return {execute}; From 7590d03948daa02c9562adfd5ba1f77bbe1d777c Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 15:00:03 +0400 Subject: [PATCH 21/65] test(builtins): add tests for csv-export-raw --- .../unit/builtins/export-csv-raw.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/__tests__/unit/builtins/export-csv-raw.test.ts diff --git a/src/__tests__/unit/builtins/export-csv-raw.test.ts b/src/__tests__/unit/builtins/export-csv-raw.test.ts new file mode 100644 index 000000000..c47959704 --- /dev/null +++ b/src/__tests__/unit/builtins/export-csv-raw.test.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs/promises'; +import {jest} from '@jest/globals'; + +import {ExportCSVRaw} from '../../../builtins/export-csv-raw'; +import {ERRORS} from '../../../util/errors'; + +import {tree, context, outputs} from '../../../__mocks__/builtins/export-csv'; + +const {CliInputError, WriteFileError} = ERRORS; + +jest.mock('fs/promises', () => ({ + __esModule: true, + writeFile: jest.fn<() => Promise>().mockResolvedValue(), +})); + +describe('builtins/export-csv-raw: ', () => { + describe('ExportCSVRaw: ', () => { + const exportCSVRaw = ExportCSVRaw(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('init GroupBy: ', () => { + it('initalizes object with properties.', async () => { + expect(exportCSVRaw).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('generates CSV file with correct data.', async () => { + const outputPath = 'output#carbon'; + const content = + "id,timestamp,cloud/instance-type,region,duration,cpu/utilization,network/energy,energy,cpu/thermal-design-power,grid/carbon-intensity,device/emissions-embodied,time-reserved,device/expected-lifespan,resources-reserved,resources-total,functional-unit-time,cpu/energy,carbon-plus-energy',carbon-embodied,carbon-operational,carbon,sci\nchildren.child-1.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,10,10,5,100,800,1533.12,3600,94608000,1,8,1 min,0.000008888888888888888,10.000008888888889,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\nchildren.child-2.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,30,10,5,100,800,1533.12,3600,94608000,1,8,1 min,0.00001650338753387534,10.000016503387533,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\noutputs.0,2023-12-12T00:00:00.000Z,,,1,,,,,,,,,,,,,,,,8000.000004051243,"; + + await exportCSVRaw.execute(tree, context, outputPath); + + expect(fs.writeFile).toHaveBeenCalledWith(`${outputPath}.csv`, content); + }); + + it('throws an error when the CSV file could not be created.', async () => { + const outputPath = 'output#carbon'; + + expect.assertions(1); + + jest + .spyOn(fs, 'writeFile') + .mockRejectedValue('Could not write CSV file.'); + + await expect( + exportCSVRaw.execute(tree, context, outputPath) + ).rejects.toThrow( + new WriteFileError( + 'Failed to write CSV to output#carbon: Could not write CSV file.' + ) + ); + }); + + it('throws an error when output path is empty.', async () => { + const outputPath = ''; + + context.initialize = Object.assign({}, context.initialize, outputs); + + expect.assertions(2); + try { + await exportCSVRaw.execute(tree, context, outputPath); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError('Output path is required.')); + } + }); + }); + }); +}); From 00ad01dac9b0654bc536039010c619afed20e949 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:01:31 +0000 Subject: [PATCH 22/65] docs(package): update contributions doc --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a38b3139..13b12ed80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ ## Contribution Guidelines +> **HACKATHON PARTICIPANTS**: please note that these contribution guidelines are intended for community contributions to IF, and NOT hackathon submissions. If you are building plugins, tooling or other add-ons to IF, please maintain them in your own repositories and submit links to the hackathon judges.** + First off, thanks for taking the time to contribute! 🎉 @@ -7,6 +9,7 @@ The following document is a rule set of guidelines for contributing. ## Table of Contents +- [What and when to contribute](#what-and-when-to-contribute) - [Code Contributions](#code-contributions) - [Step 1: Fork](#step-1-fork) - [Step 2: Branch](#step-2-branch) @@ -20,6 +23,11 @@ The following document is a rule set of guidelines for contributing. - [Documentation](#documentation) - [Writing tests](#writing-tests) +## What and when to contribute + +You can contribute anything to the IF, but we are likely to close out unsolicited PRs without merging them. Our issue board is completely open and we have tags (`help-wanted`, `good-first-issue`) to help contributors to choose tasks to work on. We recommend speaking to the core team on Github before starting working on an issue. You can do this by raising an issue or commenting on an existing issue. This helps us to direct your energy is directions that are aligned with our roadmap, prevent multiple people working on the same task, and better manage our board. This all makes it much more likely that your work will get merged. + + ## Code Contributions ### Step 1: Fork From 98d30537634ad156c26c2eebc259a787a95ffd24 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:03:13 +0000 Subject: [PATCH 23/65] docs(package): add note about hackathon prs to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0b3b1e654..0b6b81699 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,12 @@ > Find out more about CarbonHack 2024 on the [CarbonHack website](https://grnsft.org/hack/github). Check out the [FAQ on GitHub](https://grnsft.org/hack/faq). > > Registration opens 15th January! +> +> **PLEASE NOTE** hackathon projects are not supposed to be raised as pull requests to this rpeository! Please keep your hackathon plugins in your own repositories and submit links instead of raising PRs! ---------------------------- + # Impact Framework From 018d00bb56787d29d0fe8f84df76452b3fdc9b09 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 15:25:08 +0400 Subject: [PATCH 24/65] test(builtins): add test for export-log --- .../unit/builtins/export-log.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/__tests__/unit/builtins/export-log.test.ts diff --git a/src/__tests__/unit/builtins/export-log.test.ts b/src/__tests__/unit/builtins/export-log.test.ts new file mode 100644 index 000000000..d649e51bd --- /dev/null +++ b/src/__tests__/unit/builtins/export-log.test.ts @@ -0,0 +1,30 @@ +import {ExportLog} from '../../../builtins/export-log'; +// import {ERRORS} from '../../../util/errors'; + +import {tree, context} from '../../../__mocks__/builtins/export-csv'; + +describe('ExportLog', () => { + it('successfully logs output manifest in console.', async () => { + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + await ExportLog().execute(tree, context); + + expect(mockConsoleLog).toHaveBeenCalled(); + expect(mockConsoleLog).toHaveBeenCalledWith( + JSON.stringify({...context, tree}, null, 2) + ); + + mockConsoleLog.mockRestore(); + }); + + it('successfully logs output manifest if tree is an empty object.', async () => { + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + await ExportLog().execute({}, context); + + expect(mockConsoleLog).toHaveBeenCalled(); + expect(mockConsoleLog).toHaveBeenCalledWith( + JSON.stringify({...context, tree: {}}, null, 2) + ); + + mockConsoleLog.mockRestore(); + }); +}); From 0c9d604d656158c3eb6950a2a071c8bdf85a38e3 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 15:37:36 +0400 Subject: [PATCH 25/65] test(builtins): remove unneccesary comment --- src/__tests__/unit/builtins/export-log.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/unit/builtins/export-log.test.ts b/src/__tests__/unit/builtins/export-log.test.ts index d649e51bd..01798d28b 100644 --- a/src/__tests__/unit/builtins/export-log.test.ts +++ b/src/__tests__/unit/builtins/export-log.test.ts @@ -1,5 +1,4 @@ import {ExportLog} from '../../../builtins/export-log'; -// import {ERRORS} from '../../../util/errors'; import {tree, context} from '../../../__mocks__/builtins/export-csv'; From ecda359806afe9bfc74a8641ae0f8e1d4b5c2b69 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 15:54:19 +0400 Subject: [PATCH 26/65] test(builtins): add describtion for the plugin --- .../unit/builtins/export-log.test.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/__tests__/unit/builtins/export-log.test.ts b/src/__tests__/unit/builtins/export-log.test.ts index 01798d28b..7ff7aae8d 100644 --- a/src/__tests__/unit/builtins/export-log.test.ts +++ b/src/__tests__/unit/builtins/export-log.test.ts @@ -2,28 +2,30 @@ import {ExportLog} from '../../../builtins/export-log'; import {tree, context} from '../../../__mocks__/builtins/export-csv'; -describe('ExportLog', () => { - it('successfully logs output manifest in console.', async () => { - const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); - await ExportLog().execute(tree, context); +describe('builtins/export-log:', () => { + describe('ExportLog: ', () => { + it('successfully logs output manifest in console.', async () => { + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + await ExportLog().execute(tree, context); - expect(mockConsoleLog).toHaveBeenCalled(); - expect(mockConsoleLog).toHaveBeenCalledWith( - JSON.stringify({...context, tree}, null, 2) - ); + expect(mockConsoleLog).toHaveBeenCalled(); + expect(mockConsoleLog).toHaveBeenCalledWith( + JSON.stringify({...context, tree}, null, 2) + ); - mockConsoleLog.mockRestore(); - }); + mockConsoleLog.mockRestore(); + }); - it('successfully logs output manifest if tree is an empty object.', async () => { - const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); - await ExportLog().execute({}, context); + it('successfully logs output manifest if tree is an empty object.', async () => { + const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); + await ExportLog().execute({}, context); - expect(mockConsoleLog).toHaveBeenCalled(); - expect(mockConsoleLog).toHaveBeenCalledWith( - JSON.stringify({...context, tree: {}}, null, 2) - ); + expect(mockConsoleLog).toHaveBeenCalled(); + expect(mockConsoleLog).toHaveBeenCalledWith( + JSON.stringify({...context, tree: {}}, null, 2) + ); - mockConsoleLog.mockRestore(); + mockConsoleLog.mockRestore(); + }); }); }); From db65a64d34c98d6025c413296d824f2b133334fd Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 25 Mar 2024 16:18:36 +0400 Subject: [PATCH 27/65] test(builtins): add test for export-yaml --- .../unit/builtins/export-yaml.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/__tests__/unit/builtins/export-yaml.test.ts diff --git a/src/__tests__/unit/builtins/export-yaml.test.ts b/src/__tests__/unit/builtins/export-yaml.test.ts new file mode 100644 index 000000000..e54c22972 --- /dev/null +++ b/src/__tests__/unit/builtins/export-yaml.test.ts @@ -0,0 +1,47 @@ +import {ExportYaml} from '../../../builtins/export-yaml'; +import {ERRORS} from '../../../util/errors'; +import {saveYamlFileAs} from '../../../util/yaml'; + +import {tree, context} from '../../../__mocks__/builtins/export-csv'; + +jest.mock('../../../util/yaml', () => ({ + saveYamlFileAs: jest.fn(), +})); + +const {CliInputError} = ERRORS; + +describe('builtins/export-yaml: ', () => { + describe('ExportYaml: ', () => { + const exportYaml = ExportYaml(); + + describe('init ExportYaml: ', () => { + it('initalizes object with properties.', async () => { + expect(exportYaml).toHaveProperty('execute'); + }); + }); + + describe('execute', () => { + it('returns result with correct arguments', async () => { + const outputPath = 'outputPath.yaml'; + + await exportYaml.execute(tree, context, outputPath); + + expect(saveYamlFileAs).toHaveBeenCalledWith( + {...context, tree}, + `${outputPath.split('#')[0]}.yaml` + ); + }); + + it('throws an error if outputPath is not provided.', async () => { + expect.assertions(2); + + try { + await exportYaml.execute({}, context, ''); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError('Output path is required.')); + } + }); + }); + }); +}); From 66f2bcc7b51014b9af0d04ece2180d2b239ba732 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:25:39 +0000 Subject: [PATCH 28/65] fix(lib): add global utc setting to time-sync --- src/builtins/time-sync.ts | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/builtins/time-sync.ts b/src/builtins/time-sync.ts index 5da11f268..ba39f5e41 100644 --- a/src/builtins/time-sync.ts +++ b/src/builtins/time-sync.ts @@ -1,22 +1,24 @@ -import {isDate} from 'node:util/types'; -import {DateTime, DateTimeMaybeValid, Interval} from 'luxon'; -import {z} from 'zod'; +import { isDate } from 'node:util/types'; +import { Settings, DateTime, DateTimeMaybeValid, Interval } from 'luxon'; +import { z } from 'zod'; -import {parameterize} from '../lib/parameterize'; +import { parameterize } from '../lib/parameterize'; -import {ERRORS} from '../util/errors'; +import { ERRORS } from '../util/errors'; -import {STRINGS} from '../config'; +import { STRINGS } from '../config'; -import {ExecutePlugin, PluginParams} from '../types/interface'; +import { ExecutePlugin, PluginParams } from '../types/interface'; import { PaddingReceipt, TimeNormalizerConfig, TimeParams, } from '../types/time-sync'; -import {validate} from '../util/validations'; +import { validate } from '../util/validations'; -const {InputValidationError} = ERRORS; +Settings.defaultZone = "utc"; + +const { InputValidationError } = ERRORS; const { INVALID_TIME_NORMALIZATION, @@ -174,7 +176,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { i: number ) => { const thisMoment = parseDate(currentRoundMoment).startOf('second'); - return thisMoment.plus({seconds: i}); + return thisMoment.plus({ seconds: i }); }; /** * Breaks down input per minimal time unit. @@ -255,7 +257,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { * Checks if `error on padding` is enabled and padding is needed. If so, then throws error. */ const validatePadding = (pad: PaddingReceipt, params: TimeParams): void => { - const {start, end} = pad; + const { start, end } = pad; const isPaddingNeeded = start || end; if (!params.allowPadding && isPaddingNeeded) { throw new InputValidationError(AVOIDING_PADDING_BY_EDGES(start, end)); @@ -276,7 +278,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const lastInput = inputs[inputs.length - 1]; const endDiffInSeconds = parseDate(lastInput.timestamp) - .plus({second: lastInput.duration}) + .plus({ second: lastInput.duration }) .diff(params.endTime) .as('seconds'); @@ -364,7 +366,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { pad: PaddingReceipt, params: TimeParams ): PluginParams[] => { - const {start, end} = pad; + const { start, end } = pad; const paddedFromBeginning = []; if (start) { @@ -387,7 +389,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { paddedArray.push( ...getZeroishInputPerSecondBetweenRange( lastInputEnd, - params.endTime.plus({seconds: 1}), + params.endTime.plus({ seconds: 1 }), lastInput ) ); @@ -402,7 +404,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { ) => { const array: PluginParams[] = []; const dateRange = Interval.fromDateTimes(startDate, endDate); - for (const interval of dateRange.splitBy({second: 1})) { + for (const interval of dateRange.splitBy({ second: 1 })) { array.push( fillWithZeroishInput( templateInput, @@ -424,7 +426,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { params: TimeParams ): PluginParams[] => { return inputs.reduce((acc: PluginParams[], item) => { - const {timestamp} = item; + const { timestamp } = item; if ( parseDate(timestamp) >= params.startTime && @@ -437,5 +439,5 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { }, [] as PluginParams[]); }; - return {metadata, execute}; + return { metadata, execute }; }; From ad42e861f1b40f57aaf837f42702160792feb269 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:26:42 +0000 Subject: [PATCH 29/65] fix(lib): run fix to satisfy linter --- src/builtins/time-sync.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/builtins/time-sync.ts b/src/builtins/time-sync.ts index ba39f5e41..67d5c9079 100644 --- a/src/builtins/time-sync.ts +++ b/src/builtins/time-sync.ts @@ -1,24 +1,24 @@ -import { isDate } from 'node:util/types'; -import { Settings, DateTime, DateTimeMaybeValid, Interval } from 'luxon'; -import { z } from 'zod'; +import {isDate} from 'node:util/types'; +import {Settings, DateTime, DateTimeMaybeValid, Interval} from 'luxon'; +import {z} from 'zod'; -import { parameterize } from '../lib/parameterize'; +import {parameterize} from '../lib/parameterize'; -import { ERRORS } from '../util/errors'; +import {ERRORS} from '../util/errors'; -import { STRINGS } from '../config'; +import {STRINGS} from '../config'; -import { ExecutePlugin, PluginParams } from '../types/interface'; +import {ExecutePlugin, PluginParams} from '../types/interface'; import { PaddingReceipt, TimeNormalizerConfig, TimeParams, } from '../types/time-sync'; -import { validate } from '../util/validations'; +import {validate} from '../util/validations'; -Settings.defaultZone = "utc"; +Settings.defaultZone = 'utc'; -const { InputValidationError } = ERRORS; +const {InputValidationError} = ERRORS; const { INVALID_TIME_NORMALIZATION, @@ -176,7 +176,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { i: number ) => { const thisMoment = parseDate(currentRoundMoment).startOf('second'); - return thisMoment.plus({ seconds: i }); + return thisMoment.plus({seconds: i}); }; /** * Breaks down input per minimal time unit. @@ -257,7 +257,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { * Checks if `error on padding` is enabled and padding is needed. If so, then throws error. */ const validatePadding = (pad: PaddingReceipt, params: TimeParams): void => { - const { start, end } = pad; + const {start, end} = pad; const isPaddingNeeded = start || end; if (!params.allowPadding && isPaddingNeeded) { throw new InputValidationError(AVOIDING_PADDING_BY_EDGES(start, end)); @@ -278,7 +278,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const lastInput = inputs[inputs.length - 1]; const endDiffInSeconds = parseDate(lastInput.timestamp) - .plus({ second: lastInput.duration }) + .plus({second: lastInput.duration}) .diff(params.endTime) .as('seconds'); @@ -366,7 +366,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { pad: PaddingReceipt, params: TimeParams ): PluginParams[] => { - const { start, end } = pad; + const {start, end} = pad; const paddedFromBeginning = []; if (start) { @@ -389,7 +389,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { paddedArray.push( ...getZeroishInputPerSecondBetweenRange( lastInputEnd, - params.endTime.plus({ seconds: 1 }), + params.endTime.plus({seconds: 1}), lastInput ) ); @@ -404,7 +404,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { ) => { const array: PluginParams[] = []; const dateRange = Interval.fromDateTimes(startDate, endDate); - for (const interval of dateRange.splitBy({ second: 1 })) { + for (const interval of dateRange.splitBy({second: 1})) { array.push( fillWithZeroishInput( templateInput, @@ -426,7 +426,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { params: TimeParams ): PluginParams[] => { return inputs.reduce((acc: PluginParams[], item) => { - const { timestamp } = item; + const {timestamp} = item; if ( parseDate(timestamp) >= params.startTime && @@ -439,5 +439,5 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { }, [] as PluginParams[]); }; - return { metadata, execute }; + return {metadata, execute}; }; From ea64ac9237737280e4c069c9b2eb500516a0876c Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:27:49 +0000 Subject: [PATCH 30/65] test(lib): check iso format and offset in time sync --- src/__tests__/unit/builtins/time-sync.test.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index 8c81d79d1..dbb3a7756 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -1,8 +1,8 @@ import {TimeSync} from '../../../builtins/time-sync'; import {ERRORS} from '../../../util/errors'; - +import {Settings, DateTime} from 'luxon'; import {STRINGS} from '../../../config'; - +Settings.defaultZone = 'utc'; const {InputValidationError} = ERRORS; const {INVALID_OBSERVATION_OVERLAP, INVALID_TIME_NORMALIZATION} = STRINGS; @@ -617,4 +617,28 @@ describe('execute(): ', () => { ); } }); + + it('checks that timestamps in return object are ISO 8061 and timezone UTC.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:03.000Z', + interval: 1, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + carbon: 1, + }, + ]); + console.log(DateTime.fromISO(result[0].timestamp).zone.valueOf()); + expect( + DateTime.fromISO(result[0].timestamp).zone.valueOf() === + 'FixedOffsetZone { fixed: 0 }' + ); + expect(DateTime.fromISO(result[0].timestamp).offset === 0); + }); }); From 8eafd9652dc0ce8b15686da3f97f1d6b3d998154 Mon Sep 17 00:00:00 2001 From: jmc <33655003+jmcook1186@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:18:33 +0000 Subject: [PATCH 31/65] test(lib): remove log statement --- src/__tests__/unit/builtins/time-sync.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index dbb3a7756..4b0507efa 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -634,7 +634,6 @@ describe('execute(): ', () => { carbon: 1, }, ]); - console.log(DateTime.fromISO(result[0].timestamp).zone.valueOf()); expect( DateTime.fromISO(result[0].timestamp).zone.valueOf() === 'FixedOffsetZone { fixed: 0 }' From 6dc1794527cbee09918c8e3e4b2330878717763f Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 26 Mar 2024 15:35:15 +0400 Subject: [PATCH 32/65] test(lib): write units for load --- src/__tests__/unit/lib/load.test.ts | 146 ++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/__tests__/unit/lib/load.test.ts diff --git a/src/__tests__/unit/lib/load.test.ts b/src/__tests__/unit/lib/load.test.ts new file mode 100644 index 000000000..d4f65f063 --- /dev/null +++ b/src/__tests__/unit/lib/load.test.ts @@ -0,0 +1,146 @@ +jest.mock('fs/promises', () => require('../../../__mocks__/fs')); +jest.mock( + 'mockavizta', + () => ({ + __esmodule: true, + Mockavizta: () => ({ + execute: (input: PluginParams) => input, + metadata: { + kind: 'execute', + }, + }), + }), + {virtual: true} +); + +import {load} from '../../../lib/load'; + +import {PARAMETERS} from '../../../config'; + +import {PluginParams} from '../../../types/interface'; + +describe('lib/load: ', () => { + describe('load(): ', () => { + it('loads yaml with default parameters.', async () => { + const inputPath = 'mock.yaml'; + const paramPath = undefined; + + 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, + }, + ], + }, + }, + }, + context: { + name: 'gsf-demo', + description: 'Hello', + tags: { + kind: 'web', + complexity: 'moderate', + category: 'cloud', + }, + initialize: { + plugins: { + mockavizta: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }, + }, + }, + parameters: PARAMETERS, + }; + + expect(result).toEqual(expectedValue); + }); + + it('loads yaml with custom parameters.', async () => { + const inputPath = 'param-mock.yaml'; + const paramPath = 'param-mock.json'; + + 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, + }, + ], + }, + }, + }, + context: { + name: 'gsf-demo', + description: 'Hello', + tags: { + kind: 'web', + complexity: 'moderate', + category: 'cloud', + }, + initialize: { + plugins: { + mockavizta: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }, + }, + }, + parameters: expectedParameters, + }; + + expect(result).toEqual(expectedValue); + }); + }); +}); From e0788b424e96af6479c49ba0779e3f08364d1dbd Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 26 Mar 2024 15:36:28 +0400 Subject: [PATCH 33/65] test(mocks): handle parameterize --- src/__mocks__/fs/index.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/__mocks__/fs/index.ts b/src/__mocks__/fs/index.ts index e6370223d..7a139a188 100644 --- a/src/__mocks__/fs/index.ts +++ b/src/__mocks__/fs/index.ts @@ -6,6 +6,21 @@ export const readFile = async (filePath: string) => { return Promise.reject(new Error('rejected')); } if (filePath.includes('json')) { + if (filePath.includes('param')) { + return JSON.stringify({ + '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', + }, + }); + } + return JSON.stringify(filePath); } @@ -19,8 +34,10 @@ export const readFile = async (filePath: string) => { complexity: moderate category: cloud initialize: - models: - boavizta-cpu: + plugins: + mockavizta: + path: mockavizta + method: Mockavizta tree: children: front-end: From 7f6a25714b9110ef06c570ca1c42072b53342f2c Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 28 Mar 2024 15:38:18 +0400 Subject: [PATCH 34/65] chore(package): drop lodash from dependencies --- package-lock.json | 8 -------- package.json | 2 -- 2 files changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49260a512..1947f04ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@commitlint/config-conventional": "^18.6.0", "csv-stringify": "^6.4.6", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", "luxon": "^3.4.4", "ts-command-line-args": "^2.5.1", "typescript": "^5.1.6", @@ -29,7 +28,6 @@ "@jest/globals": "^29.6.1", "@types/jest": "^29.5.7", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "fixpack": "^4.0.0", @@ -1871,12 +1869,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", - "dev": true - }, "node_modules/@types/luxon": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", diff --git a/package.json b/package.json index cdc5b1b8a..9535275f5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@commitlint/config-conventional": "^18.6.0", "csv-stringify": "^6.4.6", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", "luxon": "^3.4.4", "ts-command-line-args": "^2.5.1", "typescript": "^5.1.6", @@ -30,7 +29,6 @@ "@jest/globals": "^29.6.1", "@types/jest": "^29.5.7", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "fixpack": "^4.0.0", From 2b4a59117c49d358b8b0864978ab9648b355b1bd Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 28 Mar 2024 15:40:39 +0400 Subject: [PATCH 35/65] test(util): tune mergeObjects cases --- src/__tests__/unit/util/helpers.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 7b1ac6769..8e1107405 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -58,7 +58,7 @@ describe('util/helpers: ', () => { expect(result).toEqual(input); }); - it('adds only properties missing in input', () => { + it('adds only properties missing in input.', () => { expect.assertions(1); const input = { @@ -84,7 +84,7 @@ describe('util/helpers: ', () => { expect(result).toEqual(expectedResult); }); - it('handles object values correctly', () => { + it('keeps values from input in case of nested objects.', () => { expect.assertions(1); const input = { @@ -112,14 +112,13 @@ describe('util/helpers: ', () => { c: 'testInput', d: { e: 1, - f: 'testDefault2', }, }; expect(result).toEqual(expectedResult); }); - it('handles array values correctly', () => { + it('keeps value from input in case of arrays.', () => { expect.assertions(1); const input = { From 96e1c1ff6d37239d366dcb2c94c27e65335b6666 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 28 Mar 2024 16:34:58 +0400 Subject: [PATCH 36/65] refactor(util): rewrite merge object helper --- src/util/helpers.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 80f0df024..02d308b12 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -1,5 +1,3 @@ -import {mergeWith} from 'lodash'; - import {STRINGS} from '../config'; import {ERRORS} from './errors'; import {logger} from './logger'; @@ -20,15 +18,20 @@ export const andHandle = (error: Error) => { }; /** - * Merge destination and source recursively. + * Append entries from defaults which are missing from inputs. */ -export const mergeObjects = (destination: any, source: any) => { - const handleArrays = (objValue: any, srcValue: any) => { - if (Array.isArray(objValue) && Array.isArray(srcValue)) { - return srcValue; +export const mergeObjects = (defaults: any, input: any) => { + const merged: Record = {...input}; + + for (const key in defaults) { + if (Array.isArray(defaults[key])) { + merged[key] = input[key] !== undefined ? input[key] : defaults[key]; } - return; - }; - return mergeWith(destination, source, handleArrays); + if (!(key in input)) { + merged[key] = defaults[key]; + } + } + + return merged; }; From 3c2ac7e8211fc5f8299657ab95ff2660e99cfd90 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 28 Mar 2024 16:55:28 +0400 Subject: [PATCH 37/65] test(lib): implement units for initalize --- src/__tests__/unit/lib/initalize.test.ts | 174 +++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/__tests__/unit/lib/initalize.test.ts diff --git a/src/__tests__/unit/lib/initalize.test.ts b/src/__tests__/unit/lib/initalize.test.ts new file mode 100644 index 000000000..e792a2c6f --- /dev/null +++ b/src/__tests__/unit/lib/initalize.test.ts @@ -0,0 +1,174 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +jest.mock('mockavizta', () => require('../../../__mocks__/plugin'), { + virtual: true, +}); +jest.mock('../../../builtins', () => require('../../../__mocks__/plugin'), { + virtual: true, +}); +const mockLog = jest.fn(); +jest.mock('../../../util/log-memoize', () => ({ + memoizedLog: mockLog, +})); + +import {initalize} from '../../../lib/initialize'; + +import {ERRORS} from '../../../util/errors'; + +import {STRINGS} from '../../../config'; + +import {GlobalPlugins} from '../../../types/manifest'; + +const {PluginCredentialError, ModuleInitializationError} = ERRORS; +const {MISSING_METHOD, MISSING_PATH, INVALID_MODULE_PATH} = STRINGS; + +describe('lib/initalize: ', () => { + describe('initalize(): ', () => { + it('creates instance with get and set methods.', async () => { + const plugins = {}; + const response = await initalize(plugins); + + expect(response).toHaveProperty('get'); + expect(response).toHaveProperty('set'); + expect(typeof response.get).toEqual('function'); + expect(typeof response.set).toEqual('function'); + }); + + it('checks if plugin is initalized, warning is logged and has module has execute and metadata props.', async () => { + const plugins: GlobalPlugins = { + mockavizta: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }; + const storage = await initalize(plugins); + + const pluginName = Object.keys(plugins)[0]; + const module = storage.get(pluginName); + expect(module).toHaveProperty('execute'); + expect(module).toHaveProperty('metadata'); + expect(mockLog).toHaveBeenCalledTimes(1); // checks if logger is called + }); + + it('checks if plugin is initalized with global config and has execute and metadata.', async () => { + const plugins: GlobalPlugins = { + mockavizta: { + path: 'mockavizta', + method: 'Mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + const storage = await initalize(plugins); + + const pluginName = Object.keys(plugins)[0]; + const module = storage.get(pluginName); + expect(module).toHaveProperty('execute'); + expect(module).toHaveProperty('metadata'); + }); + + it('throws error if plugin does not have path property.', async () => { + const plugins: GlobalPlugins = { + // @ts-ignore + mockavizta: { + method: 'Mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + + try { + await initalize(plugins); + } catch (error) { + expect(error).toBeInstanceOf(PluginCredentialError); + + if (error instanceof PluginCredentialError) { + expect(error.message).toEqual(MISSING_PATH); + } + } + }); + + it('throws error if plugin does not have path property.', async () => { + const plugins: GlobalPlugins = { + // @ts-ignore + mockavizta: { + path: 'mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + + try { + await initalize(plugins); + } catch (error) { + expect(error).toBeInstanceOf(PluginCredentialError); + + if (error instanceof PluginCredentialError) { + expect(error.message).toEqual(MISSING_METHOD); + } + } + }); + + it('checks if builtin plugin is initalized.', async () => { + const plugins: GlobalPlugins = { + mockavizta: { + path: 'builtin', + method: 'Mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + const storage = await initalize(plugins); + + const pluginName = Object.keys(plugins)[0]; + const module = storage.get(pluginName); + expect(module).toHaveProperty('execute'); + expect(module).toHaveProperty('metadata'); + }); + + it('checks if github plugin is initalized.', async () => { + const plugins: GlobalPlugins = { + mockavizta: { + path: 'https://github.com/mockavizta', + method: 'Mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + const storage = await initalize(plugins); + + const pluginName = Object.keys(plugins)[0]; + const module = storage.get(pluginName); + expect(module).toHaveProperty('execute'); + expect(module).toHaveProperty('metadata'); + }); + + it('', async () => { + const plugins: GlobalPlugins = { + mockavizta: { + path: 'failing-mock', + method: 'Mockavizta', + 'global-config': { + verbose: true, + }, + }, + }; + + try { + await initalize(plugins); + } catch (error) { + expect(error).toBeInstanceOf(ModuleInitializationError); + + if (error instanceof ModuleInitializationError) { + expect(error.message).toEqual( + INVALID_MODULE_PATH(plugins.mockavizta.path) + ); + } + } + }); + }); +}); From 20e9e64f38b19c4f729c5081494e3ed0e01e4ece Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 28 Mar 2024 16:56:08 +0400 Subject: [PATCH 38/65] test(mocks): restructure mockavizta --- src/__mocks__/plugin/index.ts | 1 + src/__mocks__/plugin/lib/index.ts | 1 + src/__mocks__/plugin/lib/mockavizta/index.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 src/__mocks__/plugin/index.ts create mode 100644 src/__mocks__/plugin/lib/index.ts create mode 100644 src/__mocks__/plugin/lib/mockavizta/index.ts diff --git a/src/__mocks__/plugin/index.ts b/src/__mocks__/plugin/index.ts new file mode 100644 index 000000000..f41a696fd --- /dev/null +++ b/src/__mocks__/plugin/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/src/__mocks__/plugin/lib/index.ts b/src/__mocks__/plugin/lib/index.ts new file mode 100644 index 000000000..497dcef72 --- /dev/null +++ b/src/__mocks__/plugin/lib/index.ts @@ -0,0 +1 @@ +export {Mockavizta} from './mockavizta'; diff --git a/src/__mocks__/plugin/lib/mockavizta/index.ts b/src/__mocks__/plugin/lib/mockavizta/index.ts new file mode 100644 index 000000000..163a0cd24 --- /dev/null +++ b/src/__mocks__/plugin/lib/mockavizta/index.ts @@ -0,0 +1,18 @@ +import {PluginParams} from '../../../../types/interface'; + +/** + * Mock model for testing. + * Be sure when using this mock to specify that it's virtual. + * @example + * jest.mock( + * 'mockavizta', + * Mockavizta, + * {virtual: true} + * ); + */ +export const Mockavizta = () => ({ + execute: (input: PluginParams) => input, + metadata: { + kind: 'execute', + }, +}); From 4afd411ca3d4cc9e2b0861313518117837796acf Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Fri, 29 Mar 2024 00:34:35 +0400 Subject: [PATCH 39/65] test(lib): implement units for aggregation --- src/__tests__/unit/lib/aggregate.test.ts | 201 +++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/__tests__/unit/lib/aggregate.test.ts diff --git a/src/__tests__/unit/lib/aggregate.test.ts b/src/__tests__/unit/lib/aggregate.test.ts new file mode 100644 index 000000000..fb652e219 --- /dev/null +++ b/src/__tests__/unit/lib/aggregate.test.ts @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import {aggregate} from '../../../lib/aggregate'; + +describe('lib/aggregate: ', () => { + describe('aggregate(): ', () => { + it('returns tree if aggregation is missing.', () => { + const tree = {}; + const aggregation = undefined; + + const aggregatedTree = aggregate(tree, aggregation); + expect(aggregatedTree).toEqual(tree); + }); + + it('returns tree if aggregation.type is missing.', () => { + const tree = {}; + const aggregation = { + metrics: [], + }; + + // @ts-ignore + const aggregatedTree = aggregate(tree, aggregation); + expect(aggregatedTree).toEqual(tree); + }); + + it('does horizontal aggregation.', () => { + const tree = { + children: { + mocks: { + outputs: [ + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + ], + }, + }, + }; + + const aggregatedTree = aggregate(tree, { + metrics: ['carbon'], + type: 'horizontal', + }); + const expectedAggregated = { + carbon: + tree.children.mocks.outputs[0].carbon + + tree.children.mocks.outputs[1].carbon, + }; + expect(aggregatedTree.children.mocks.aggregated).toEqual( + expectedAggregated + ); + }); + + it('does vertical aggregation.', () => { + const tree = { + children: { + 'mocks-1': { + outputs: [ + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + ], + }, + 'mocks-2': { + outputs: [ + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 20, + }, + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 20, + }, + ], + }, + }, + }; + + const aggregatedTree = aggregate(tree, { + metrics: ['carbon'], + type: 'vertical', + }); + const expectedOutputs = [ + { + carbon: 30, + timestamp: 'mock-timestamp', + duration: 'mock-duration', + }, + { + carbon: 30, + timestamp: 'mock-timestamp', + duration: 'mock-duration', + }, + ]; + const expectedAggregated = { + carbon: + tree.children['mocks-1'].outputs[0].carbon + + tree.children['mocks-2'].outputs[0].carbon + + tree.children['mocks-1'].outputs[1].carbon + + tree.children['mocks-2'].outputs[1].carbon, + }; + expect(aggregatedTree.outputs).toEqual(expectedOutputs); + expect(aggregatedTree.aggregated).toEqual(expectedAggregated); + }); + + it('does both aggregations.', () => { + const tree = { + children: { + 'mocks-1': { + outputs: [ + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 10, + }, + ], + }, + 'mocks-2': { + outputs: [ + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 20, + }, + { + timestamp: 'mock-timestamp', + duration: 'mock-duration', + carbon: 20, + }, + ], + }, + }, + }; + + const aggregatedTree = aggregate(tree, { + metrics: ['carbon'], + type: 'both', + }); + + // horizontal aggregation + const expectedNode1Horizontal = { + carbon: + tree.children['mocks-1'].outputs[0].carbon + + tree.children['mocks-1'].outputs[1].carbon, + }; + expect(aggregatedTree.children['mocks-1'].aggregated).toEqual( + expectedNode1Horizontal + ); + const expectedNode2Horizontal = { + carbon: + tree.children['mocks-2'].outputs[0].carbon + + tree.children['mocks-2'].outputs[1].carbon, + }; + expect(aggregatedTree.children['mocks-2'].aggregated).toEqual( + expectedNode2Horizontal + ); + + // vertical aggregation + const expectedOutputs = [ + { + carbon: 30, + timestamp: 'mock-timestamp', + duration: 'mock-duration', + }, + { + carbon: 30, + timestamp: 'mock-timestamp', + duration: 'mock-duration', + }, + ]; + expect(aggregatedTree.outputs).toEqual(expectedOutputs); + const expectedAggregated = { + carbon: + tree.children['mocks-1'].outputs[0].carbon + + tree.children['mocks-2'].outputs[0].carbon + + tree.children['mocks-1'].outputs[1].carbon + + tree.children['mocks-2'].outputs[1].carbon, + }; + expect(aggregatedTree.aggregated).toEqual(expectedAggregated); + }); + }); +}); From 2b50b712bb878831f4cfee7b667ce1024fc40b4b Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 29 Mar 2024 15:38:27 +0400 Subject: [PATCH 40/65] test(builtins): add missing test coverage of time-sync plugin --- src/__tests__/unit/builtins/time-sync.test.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index 4b0507efa..d4b21a13d 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -7,6 +7,35 @@ const {InputValidationError} = ERRORS; const {INVALID_OBSERVATION_OVERLAP, INVALID_TIME_NORMALIZATION} = STRINGS; +jest.mock('luxon', () => { + const originalModule = jest.requireActual('luxon'); + return { + ...originalModule, + Interval: { + ...originalModule.Interval, + fromDateTimes: jest.fn((start, end) => ({ + start, + end, + splitBy: jest.fn(_options => { + const intervals = []; + let current = start; + + while (current < end) { + intervals.push({ + start: process.env.MOCK_INTERVAL === 'true' ? null : current, + end: current.plus({seconds: 1}), + }); + + current = current.plus({seconds: 1}); + } + + return intervals; + }), + })), + }, + }; +}); + describe('builtins/time-sync:', () => { describe('time-sync: ', () => { const basicConfig = { @@ -234,6 +263,71 @@ describe('execute(): ', () => { } }); + it('throws error if `timestamp` is missing.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 5, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + + try { + await timeModel.execute([ + { + duration: 15, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + '"timestamp" parameter is invalid input. Error code: invalid_union.' + ) + ); + } + }); + + it('throws error if the seconds `timestamp` is above 60.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 5, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:90.000Z', + duration: 15, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + '"timestamp" parameter is invalid input. Error code: invalid_union.' + ) + ); + } + }); + it('throws an error if the `timestamp` is not valid date.', async () => { const basicConfig = { 'start-time': '2023-12-12T00:00:00.000Z', @@ -489,7 +583,37 @@ describe('execute(): ', () => { expect(result).toStrictEqual(expectedResult); }); + it('throws an error when `start-time` is wrong.', async () => { + process.env.MOCK_INTERVAL = 'true'; + const basicConfig = { + 'start-time': '2023-12-12T00:00:90.000Z', + 'end-time': '2023-12-12T00:01:09.000Z', + interval: 5, + 'allow-padding': true, + }; + + const timeModel = TimeSync(basicConfig); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + '"timestamp" parameter is invalid datetime in input[1]. Error code: invalid_string.' + ) + ); + } + }); + it('returns a result when the first timestamp in the input has time padding.', async () => { + process.env.MOCK_INTERVAL = 'false'; const basicConfig = { 'start-time': '2023-12-12T00:00:00.000Z', 'end-time': '2023-12-12T00:00:09.000Z', @@ -619,6 +743,7 @@ describe('execute(): ', () => { }); it('checks that timestamps in return object are ISO 8061 and timezone UTC.', async () => { + process.env.MOCK_INTERVAL = 'false'; const basicConfig = { 'start-time': '2023-12-12T00:00:00.000Z', 'end-time': '2023-12-12T00:00:03.000Z', From 21749bee6d9a7b8d162e51d6a611acdf1592b768 Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 29 Mar 2024 15:50:22 +0400 Subject: [PATCH 41/65] test(builtins): remove mock function's unused argument --- src/__tests__/unit/builtins/time-sync.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index d4b21a13d..2a6c904e4 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -16,7 +16,7 @@ jest.mock('luxon', () => { fromDateTimes: jest.fn((start, end) => ({ start, end, - splitBy: jest.fn(_options => { + splitBy: jest.fn(() => { const intervals = []; let current = start; From 18d534e1a7deaa08e98e092516f81273a3f4b5d9 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 30 Mar 2024 14:12:27 +0400 Subject: [PATCH 42/65] refactor(src): rename plugins to pluginStorage in index --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 83e0db500..9a5eebe42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,8 +28,8 @@ const impactEngine = async () => { const {tree, context, parameters} = await load(inputPath, paramPath); parameterize.combine(context.params, parameters); - const plugins = await initalize(context.initialize.plugins); - const computedTree = await compute(tree, {context, plugins}); + const pluginStorage = await initalize(context.initialize.plugins); + const computedTree = await compute(tree, {context, pluginStorage}); const aggregatedTree = aggregate(computedTree, context.aggregation); context['if-version'] = packageJson.version; exhaust(aggregatedTree, context, outputPath); From c82d77459c294743e8796d006817b36c1835b5ae Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 30 Mar 2024 14:13:37 +0400 Subject: [PATCH 43/65] test(lib): implement cases for compute --- src/__tests__/unit/lib/compute.test.ts | 234 +++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/__tests__/unit/lib/compute.test.ts diff --git a/src/__tests__/unit/lib/compute.test.ts b/src/__tests__/unit/lib/compute.test.ts new file mode 100644 index 000000000..cb719c4b7 --- /dev/null +++ b/src/__tests__/unit/lib/compute.test.ts @@ -0,0 +1,234 @@ +import {compute} from '../../../lib/compute'; + +import {ComputeParams} from '../../../types/compute'; +import {pluginStorage} from '../../../util/plugin-storage'; + +describe('lib/compute: ', () => { + /** + * Mock plugins. + */ + const mockExecutePlugin = () => ({ + execute: (inputs: any) => + inputs.map((input: any) => { + input.newField = 'mock-newField'; + + return input; + }), + metadata: { + kind: 'execute', + }, + }); + const mockGroupByPlugin = () => ({ + execute: (inputs: any) => ({children: inputs}), + metadata: { + kind: 'groupby', + }, + }); + /** + * Compute params. + */ + const paramsExecute: ComputeParams = { + context: { + name: 'mock-name', + initialize: { + plugins: { + mock: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }, + }, + }, + pluginStorage: pluginStorage().set('mock', mockExecutePlugin()), + }; + const params: ComputeParams = { + context: { + name: 'mock-name', + initialize: { + plugins: { + mock: { + path: 'mockavizta', + method: 'Mockavizta', + }, + }, + }, + }, + pluginStorage: pluginStorage().set('mock', mockGroupByPlugin()), + }; + + describe('compute(): ', () => { + it('computes simple tree with execute plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); + + it('computes simple tree with groupby plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + const response = await compute(tree, params); + const expectedResult = mockGroupByPlugin().execute( + tree.children.mockChild.inputs + ); + + expect(response.children.mockChild.children).toEqual(expectedResult); + }); + + it('computes simple tree with defaults and execute plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + defaults: { + 'cpu/name': 'Intel CPU', + }, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs.map((input: any) => { + input['cpu/name'] = 'Intel CPU'; + + return input; + }) + ); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); + + it('computes nested tree with defaults and execute plugin.', async () => { + const tree = { + children: { + mockChild1: { + pipeline: ['mock'], + defaults: { + 'cpu/name': 'Intel CPU', + }, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + mockChild2: { + children: { + mockChild21: { + pipeline: ['mock'], + defaults: { + 'cpu/name': 'Intel CPU', + }, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }, + }, + }; + const response = await compute(tree, paramsExecute); + + const mapper = (input: any) => { + input['cpu/name'] = 'Intel CPU'; + + return input; + }; + const expectedResult1 = mockExecutePlugin().execute( + tree.children.mockChild1.inputs.map(mapper) + ); + const expectedResult21 = mockExecutePlugin().execute( + tree.children.mockChild2.children.mockChild21.inputs.map(mapper) + ); + + expect(response.children.mockChild1.outputs).toEqual(expectedResult1); + expect(response.children.mockChild2.children.mockChild21.outputs).toEqual( + expectedResult21 + ); + }); + + it('computes simple tree with no defaults and no inputs with execue plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + defaults: {}, + inputs: [], + }, + }, + }; + const response = await compute(tree, paramsExecute); + const expectedResult: any[] = []; + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); + + it('computes simple tree with defaults and no inputs with execue plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + defaults: { + carbon: 10, + }, + input: [], + }, + }, + }; + const response = await compute(tree, paramsExecute); + const expectedResult: any[] = [{carbon: 10, newField: 'mock-newField'}]; + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); + + it('computes simple tree with node config and execute plugin.', async () => { + const tree = { + children: { + mockChild: { + pipeline: ['mock'], + config: { + 'cpu/name': 'Intel CPU', + }, + inputs: [ + {timestamp: 'mock-timestamp-1', duration: 10}, + {timestamp: 'mock-timestamp-2', duration: 10}, + ], + }, + }, + }; + const response = await compute(tree, paramsExecute); + const expectedResult = mockExecutePlugin().execute( + tree.children.mockChild.inputs + ); + + expect(response.children.mockChild.outputs).toEqual(expectedResult); + }); + }); +}); From e96daa6ac39d198dc0674722004aaa83edbb59b5 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 30 Mar 2024 14:14:32 +0400 Subject: [PATCH 44/65] fix(lib): create clone of inputs --- src/lib/compute.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/compute.ts b/src/lib/compute.ts index 343bfcc97..d04197311 100644 --- a/src/lib/compute.ts +++ b/src/lib/compute.ts @@ -56,23 +56,23 @@ const computeNode = async (node: Node, params: Params): Promise => { }); } - let storage = node.inputs as PluginParams[]; - storage = mergeDefaults(storage, defaults); + let inputStorage = structuredClone(node.inputs) as PluginParams[]; + inputStorage = mergeDefaults(inputStorage, defaults); const pipelineCopy = structuredClone(pipeline); while (pipelineCopy.length !== 0) { const pluginName = pipelineCopy.shift() as string; - const plugin = params.plugins.get(pluginName); + const plugin = params.pluginStorage.get(pluginName); const nodeConfig = config && config[pluginName]; if (isExecute(plugin)) { - storage = await plugin.execute(storage, nodeConfig); - node.outputs = storage; + inputStorage = await plugin.execute(inputStorage, nodeConfig); + node.outputs = inputStorage; } if (isGroupBy(plugin)) { node.children = await plugin.execute( - storage, + inputStorage, nodeConfig as GroupByConfig ); delete node.inputs; From bc9a89ced6c2aab81f5c3702371b6d3c464417c4 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sat, 30 Mar 2024 14:15:03 +0400 Subject: [PATCH 45/65] refactor(types): rename plugins to pluginStorage --- src/types/compute.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/compute.ts b/src/types/compute.ts index 22ab36537..739d86742 100644 --- a/src/types/compute.ts +++ b/src/types/compute.ts @@ -7,7 +7,7 @@ export type NodeConfig = { }; export type Params = { - plugins: PluginStorageInterface; + pluginStorage: PluginStorageInterface; context: Context; pipeline?: string[]; config?: NodeConfig; @@ -25,5 +25,5 @@ export type Node = { export type ComputeParams = { context: Context; - plugins: PluginStorageInterface; + pluginStorage: PluginStorageInterface; }; From b542aa7c440b39d0768556bc4bc5d47c4c1a82ea Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 09:20:57 +0400 Subject: [PATCH 46/65] test(lib): fix initalize case headlines --- src/__tests__/unit/lib/initalize.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/unit/lib/initalize.test.ts b/src/__tests__/unit/lib/initalize.test.ts index e792a2c6f..fcd4a10d0 100644 --- a/src/__tests__/unit/lib/initalize.test.ts +++ b/src/__tests__/unit/lib/initalize.test.ts @@ -33,7 +33,7 @@ describe('lib/initalize: ', () => { expect(typeof response.set).toEqual('function'); }); - it('checks if plugin is initalized, warning is logged and has module has execute and metadata props.', async () => { + it('checks if plugin is initalized, warning is logged and plugin has execute and metadata props.', async () => { const plugins: GlobalPlugins = { mockavizta: { path: 'mockavizta', @@ -147,7 +147,7 @@ describe('lib/initalize: ', () => { expect(module).toHaveProperty('metadata'); }); - it('', async () => { + it('throws error if plugin path is invalid.', async () => { const plugins: GlobalPlugins = { mockavizta: { path: 'failing-mock', From a95e4aa22d585f03f30c0adfecbc3479c4b6b41e Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 09:46:31 +0400 Subject: [PATCH 47/65] test(lib): drop compute merge test --- src/__tests__/unit/lib/compute.test.ts | 44 -------------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/__tests__/unit/lib/compute.test.ts diff --git a/src/__tests__/unit/lib/compute.test.ts b/src/__tests__/unit/lib/compute.test.ts deleted file mode 100644 index dae8b4e0f..000000000 --- a/src/__tests__/unit/lib/compute.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {PluginParams} from '../../../types/interface'; -import {mergeDefaults} from '../../../lib/compute'; - -describe('lib/compute:', () => { - describe('compute(): ', () => { - it('merges inputs array and defaults correctly', () => { - const input1 = { - a: 1, - b: false, - c: 'testInput1', - d: 100, - }; - const input2 = { - a: 2, - b: true, - c: 'testInput2', - }; - const inputs = [input1, input2]; - - const defaults = { - b: true, - c: 'testDefault', - d: 25, - }; - - const expectedResult: PluginParams[] = [ - { - a: 1, - b: false, - c: 'testInput1', - d: 100, - }, - { - a: 2, - b: true, - c: 'testInput2', - d: 25, - }, - ]; - const result = mergeDefaults(inputs, defaults); - expect(result).toEqual(expectedResult); - }); - }); -}); From 4451adae8c76c165f198be644a2859584225c3e6 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 09:47:35 +0400 Subject: [PATCH 48/65] test(util): require mergeObjects from helpers --- src/__tests__/unit/util/helpers.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 8e1107405..4e8f2faab 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -7,8 +7,7 @@ jest.mock('../../../util/logger', () => ({ error: mockError, }, })); -import {andHandle} from '../../../util/helpers'; -import {mergeObjects} from '../../../util/helpers'; +import {andHandle, mergeObjects} from '../../../util/helpers'; import {ERRORS} from '../../../util/errors'; const {WriteFileError} = ERRORS; From 9cc3e82795157ffc2897d4d5200154348f79dfcc Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 09:48:35 +0400 Subject: [PATCH 49/65] fix(lib): make mergeDefaults private, pass original object to merge --- src/lib/compute.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/compute.ts b/src/lib/compute.ts index ba4a67513..b86ff02dc 100644 --- a/src/lib/compute.ts +++ b/src/lib/compute.ts @@ -16,13 +16,13 @@ const traverse = async (children: any, params: Params) => { /** * Appends `default` values to `inputs`. */ -export const mergeDefaults = ( +const mergeDefaults = ( inputs: PluginParams[], defaults: PluginParams | undefined ) => { if (inputs) { const response = defaults - ? inputs.map(input => mergeObjects({...defaults}, input)) + ? inputs.map(input => mergeObjects(defaults, input)) : inputs; return response; From f5e437ddf802c7132ea87a39d6b578ba6b193f91 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 09:49:06 +0400 Subject: [PATCH 50/65] refactor(util): simplify merge objects helper --- src/util/helpers.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 02d308b12..a19587a36 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -20,18 +20,7 @@ export const andHandle = (error: Error) => { /** * Append entries from defaults which are missing from inputs. */ -export const mergeObjects = (defaults: any, input: any) => { - const merged: Record = {...input}; - - for (const key in defaults) { - if (Array.isArray(defaults[key])) { - merged[key] = input[key] !== undefined ? input[key] : defaults[key]; - } - - if (!(key in input)) { - merged[key] = defaults[key]; - } - } - - return merged; -}; +export const mergeObjects = (defaults: any, input: any) => ({ + ...defaults, + ...input, +}); From ba6858fe906872293e9598e29ce337fe2756b286 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 10:35:12 +0400 Subject: [PATCH 51/65] fix(util): handle null, undefined case on merge --- src/util/helpers.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/util/helpers.ts b/src/util/helpers.ts index a19587a36..83c0cfb36 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -20,7 +20,18 @@ export const andHandle = (error: Error) => { /** * Append entries from defaults which are missing from inputs. */ -export const mergeObjects = (defaults: any, input: any) => ({ - ...defaults, - ...input, -}); +export const mergeObjects = (defaults: any, input: any) => { + const merged: Record = {...input}; + + for (const key in defaults) { + if (!(key in input)) { + merged[key] = defaults[key]; + } + + if (merged[key] === undefined || merged[key] === null) { + merged[key] = defaults[key]; + } + } + + return merged; +}; From 5cfa445709e312dcbc277714d99614ef94dfa73a Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Sun, 31 Mar 2024 10:35:34 +0400 Subject: [PATCH 52/65] test(util): add units to merge objects for null, undefined cases --- src/__tests__/unit/util/helpers.test.ts | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 4e8f2faab..5bda8b752 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -40,7 +40,7 @@ describe('util/helpers: ', () => { describe('util/helpers: ', () => { describe('mergeObjects(): ', () => { - it('does not override input', () => { + it('does not override input.', () => { expect.assertions(1); const input = { @@ -57,6 +57,34 @@ describe('util/helpers: ', () => { expect(result).toEqual(input); }); + it('overrides null/undefined inputs.', () => { + expect.assertions(1); + + const input = { + a: 1, + b: false, + c: 'testInput', + d: null, + e: undefined, + }; + + const defaults = { + c: 'testDefault', + d: 'testDefault', + e: 'testDefault', + }; + const result = mergeObjects(defaults, input); + const expectedResult = { + a: 1, + b: false, + c: 'testInput', + d: 'testDefault', + e: 'testDefault', + }; + + expect(result).toEqual(expectedResult); + }); + it('adds only properties missing in input.', () => { expect.assertions(1); From 0360edc45490a378cbdebeb4ef5972e44dd83f86 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 1 Apr 2024 00:13:10 +0400 Subject: [PATCH 53/65] fix(util): in helpers use structured clone --- src/util/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 83c0cfb36..88050c268 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -21,7 +21,7 @@ export const andHandle = (error: Error) => { * Append entries from defaults which are missing from inputs. */ export const mergeObjects = (defaults: any, input: any) => { - const merged: Record = {...input}; + const merged: Record = structuredClone(input); for (const key in defaults) { if (!(key in input)) { From 6ac2477e6fc2540e7a18c94171cf5431ff5e5523 Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:18:21 +0400 Subject: [PATCH 54/65] fix(examples): remove deprecated teads-aws manifest --- examples/manifests/teads-aws.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 examples/manifests/teads-aws.yml diff --git a/examples/manifests/teads-aws.yml b/examples/manifests/teads-aws.yml deleted file mode 100644 index 89e712569..000000000 --- a/examples/manifests/teads-aws.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: teads-aws -description: simple demo invoking TeadsAWS model -tags: -initialize: - plugins: - teads-aws: - method: TeadsAWS - path: "@grnsft/if-unofficial-plugins" - global-config: - interpolation: linear -tree: - children: - child: - pipeline: - - teads-aws # duration & config -> embodied - defaults: - cloud/instance-type: m5n.large - cpu/expected-lifespan: 252288000 - inputs: - - timestamp: 2023-07-06T00:00 - duration: 3600 - cpu/utilization: 10 From 813946c40677f4936a65c22d98c3bc5b9ead03cf Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:20:38 +0400 Subject: [PATCH 55/65] fix(examples): delete cim manifest - there is another manifest demostrating the plugin --- examples/manifests/cim.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 examples/manifests/cim.yml diff --git a/examples/manifests/cim.yml b/examples/manifests/cim.yml deleted file mode 100644 index a1b527c53..000000000 --- a/examples/manifests/cim.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: cloud-instance-metadata -description: example impl invoking Cloud Instance Metadata plugin -tags: -initialize: - plugins: - cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" -tree: - children: - child: - pipeline: - - cloud-metadata - config: - inputs: - - timestamp: 2023-07-06T00:00 # [KEYWORD] [NO-SUBFIELDS] time when measurement occurred - cloud/vendor: aws - cloud/instance-type: m5n.large - duration: 100 - cpu/utilization: 10 From ab96b70331f48753dcaab8daafb0eed72cea987e Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:21:52 +0400 Subject: [PATCH 56/65] fix(examples): update cloud-metadata plugin name --- examples/manifests/cloud-metadata.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/manifests/cloud-metadata.yml b/examples/manifests/cloud-metadata.yml index b19fc19e1..a4fb7e092 100644 --- a/examples/manifests/cloud-metadata.yml +++ b/examples/manifests/cloud-metadata.yml @@ -1,15 +1,15 @@ -name: coud-instanmce-metadata-demo +name: cloud-metadata-demo description: null tags: null initialize: plugins: teads-curve: - path: '@grnsft/if-unofficial-plugins' + path: "@grnsft/if-unofficial-plugins" method: TeadsCurve global-config: interpolation: spline - cloud-instance-metadata: - method: CloudInstanceMetadata + cloud-metadata: + method: CloudMetadata path: "@grnsft/if-plugins" tree: children: @@ -20,7 +20,7 @@ tree: cloud/instance-type: m5n.large pipeline: - teads-curve - - cloud-instance-metadata + - cloud-metadata inputs: - timestamp: 2023-07-06T00:00 duration: 10 From 62eb1a407b23bc55ccecdc47d63358cb23e207f7 Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:55:32 +0400 Subject: [PATCH 57/65] fix(examples): rename region into cloud/region --- examples/manifests/boavizta-pipeline.yml | 26 +++---- examples/manifests/generics.yml | 28 ++++---- examples/manifests/group-by.yml | 42 +++++------ examples/manifests/nesting.yml | 32 ++++----- examples/manifests/pipeline-demo-1.yml | 8 +-- examples/manifests/pipeline-demo.yml | 20 +++--- examples/manifests/pipeline-teads-sci.yml | 24 +++---- examples/manifests/pipeline-with-mocks.yml | 46 ++++++------ src/builtins/README.md | 84 +++++++++++----------- 9 files changed, 153 insertions(+), 157 deletions(-) diff --git a/examples/manifests/boavizta-pipeline.yml b/examples/manifests/boavizta-pipeline.yml index e44cb2898..404fdabec 100644 --- a/examples/manifests/boavizta-pipeline.yml +++ b/examples/manifests/boavizta-pipeline.yml @@ -5,7 +5,7 @@ initialize: plugins: boavizta-cpu: method: BoaviztaCpuOutput - path: '@grnsft/if-unofficial-plugins' + path: "@grnsft/if-unofficial-plugins" global-config: allocation: LINEAR verbose: true @@ -13,10 +13,10 @@ initialize: path: "@grnsft/if-plugins" method: Sum global-config: - input-parameters: + input-parameters: - cpu/energy - network/energy - output-parameter: energy + output-parameter: energy "sci-m": path: "@grnsft/if-plugins" method: SciM @@ -27,8 +27,8 @@ initialize: path: "@grnsft/if-plugins" method: Sci global-config: - functional-unit: '' - functional-unit-time: '1-day' + functional-unit: "" + functional-unit-time: "1-day" "time-sync": method: TimeSync path: "builtin" @@ -60,26 +60,26 @@ tree: cpu/name: Intel® Core™ i7-1185G7 inputs: - timestamp: "2023-12-12T00:00:00.000Z" - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 diff --git a/examples/manifests/generics.yml b/examples/manifests/generics.yml index d3bb5085d..058ee5b4c 100644 --- a/examples/manifests/generics.yml +++ b/examples/manifests/generics.yml @@ -4,7 +4,7 @@ tags: initialize: plugins: teads-curve: - path: '@grnsft/if-unofficial-plugins' + path: "@grnsft/if-unofficial-plugins" method: TeadsCurve global-config: interpolation: spline @@ -12,10 +12,10 @@ initialize: path: "@grnsft/if-plugins" method: Sum global-config: - input-parameters: + input-parameters: - cpu/energy - network/energy - output-parameter: energy-sum + output-parameter: energy-sum "coefficient": path: "@grnsft/if-plugins" method: Coefficient @@ -24,11 +24,11 @@ initialize: coefficient: 2 output-parameter: energy-doubled "multiply": - path: '@grnsft/if-plugins' + path: "@grnsft/if-plugins" method: Multiply global-config: - input-parameters: ['cpu/utilization', 'duration'] - output-parameter: 'cpu-times-duration' + input-parameters: ["cpu/utilization", "duration"] + output-parameter: "cpu-times-duration" tree: children: child-1: @@ -42,8 +42,8 @@ tree: cpu/thermal-design-power: 100 inputs: - timestamp: "2023-12-12T00:00:00.000Z" - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 10 @@ -51,21 +51,21 @@ tree: - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 15 network/energy: 10 energy: 5 diff --git a/examples/manifests/group-by.yml b/examples/manifests/group-by.yml index 7bb26b259..53e56b895 100644 --- a/examples/manifests/group-by.yml +++ b/examples/manifests/group-by.yml @@ -2,47 +2,47 @@ name: groupby-demo description: demo pipeline initialize: plugins: - group-by: - path: 'builtin' + group-by: + path: "builtin" method: GroupBy tree: children: my-app: - pipeline: + pipeline: - group-by config: group-by: group: - - region + - cloud/region - cloud/instance-type inputs: - timestamp: 2023-07-06T00:00 - duration: 300 - cloud/instance-type: A1 - region: uk-west + duration: 300 + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 99 - - timestamp: 2023-07-06T05:00 - duration: 300 - cloud/instance-type: A1 - region: uk-west - cpu/utilization: 23 + - timestamp: 2023-07-06T05:00 + duration: 300 + cloud/instance-type: A1 + cloud/region: uk-west + cpu/utilization: 23 - timestamp: 2023-07-06T10:00 duration: 300 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 12 - timestamp: 2023-07-06T00:00 # note this time restarts at the start timstamp - duration: 300 + duration: 300 cloud/instance-type: B1 - region: uk-west + cloud/region: uk-west cpu/utilization: 11 - - timestamp: 2023-07-06T05:00 - duration: 300 + - timestamp: 2023-07-06T05:00 + duration: 300 cloud/instance-type: B1 - region: uk-west + cloud/region: uk-west cpu/utilization: 67 - timestamp: 2023-07-06T10:00 - duration: 300 + duration: 300 cloud/instance-type: B1 - region: uk-west + cloud/region: uk-west cpu/utilization: 1 diff --git a/examples/manifests/nesting.yml b/examples/manifests/nesting.yml index a6351dce3..bf739f567 100644 --- a/examples/manifests/nesting.yml +++ b/examples/manifests/nesting.yml @@ -66,7 +66,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 @@ -74,18 +74,18 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 child-1: @@ -107,7 +107,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 @@ -115,18 +115,18 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 child-2: @@ -150,7 +150,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 @@ -158,18 +158,18 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 child-2-1: @@ -191,7 +191,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 @@ -199,17 +199,17 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 diff --git a/examples/manifests/pipeline-demo-1.yml b/examples/manifests/pipeline-demo-1.yml index 3c0096074..e4cbca771 100644 --- a/examples/manifests/pipeline-demo-1.yml +++ b/examples/manifests/pipeline-demo-1.yml @@ -61,7 +61,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 @@ -69,17 +69,17 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 diff --git a/examples/manifests/pipeline-demo.yml b/examples/manifests/pipeline-demo.yml index b5479ccbf..ea88d52a6 100644 --- a/examples/manifests/pipeline-demo.yml +++ b/examples/manifests/pipeline-demo.yml @@ -51,7 +51,7 @@ tree: config: group-by: group: - - region + - cloud/region - cloud/instance-type defaults: cpu/thermal-design-power: 100 @@ -65,23 +65,23 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 10 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 child-2: pipeline: @@ -94,7 +94,7 @@ tree: config: group-by: group: - - region + - cloud/region - cloud/instance-type defaults: cpu/thermal-design-power: 100 @@ -110,19 +110,19 @@ tree: duration: 1 cpu/utilization: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 28 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 40 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cpu/utilization: 33 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west diff --git a/examples/manifests/pipeline-teads-sci.yml b/examples/manifests/pipeline-teads-sci.yml index f8885b67a..aabab231a 100644 --- a/examples/manifests/pipeline-teads-sci.yml +++ b/examples/manifests/pipeline-teads-sci.yml @@ -12,10 +12,10 @@ initialize: path: "@grnsft/if-plugins" method: Sum global-config: - input-parameters: + input-parameters: - cpu/energy - network/energy - output-parameter: energy + output-parameter: energy "sci-m": path: "@grnsft/if-plugins" method: SciM @@ -26,8 +26,8 @@ initialize: path: "@grnsft/if-plugins" method: Sci global-config: - functional-unit: '' - functional-unit-time: '1-day' + functional-unit: "" + functional-unit-time: "1-day" "time-sync": method: TimeSync path: "builtin" @@ -57,26 +57,26 @@ tree: resources-total: 8 inputs: - timestamp: "2023-12-12T00:00:00.000Z" - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 0.000001 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 0.000001 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 15 network/energy: 0.000001 diff --git a/examples/manifests/pipeline-with-mocks.yml b/examples/manifests/pipeline-with-mocks.yml index a8081429d..806fd9554 100644 --- a/examples/manifests/pipeline-with-mocks.yml +++ b/examples/manifests/pipeline-with-mocks.yml @@ -3,14 +3,14 @@ description: tags: aggregation: metrics: - - 'carbon' - type: 'both' + - "carbon" + type: "both" initialize: plugins: mock-observations: kind: plugin method: MockObservations - path: '@grnsft/if-plugins' + path: "@grnsft/if-plugins" global-config: timestamp-from: 2023-07-06T00:00 timestamp-to: 2023-07-06T00:10 @@ -19,7 +19,7 @@ initialize: - cloud/instance-type: A1 generators: common: - region: uk-west + cloud/region: uk-west common-key: common-val randint: cpu/utilization: @@ -53,7 +53,7 @@ initialize: end-time: "2023-12-12T00:01:00.000Z" interval: 5 allow-padding: true - 'group-by': + "group-by": path: builtin method: GroupBy tree: @@ -69,7 +69,7 @@ tree: config: group-by: group: - - region + - cloud/region - instance-type defaults: cpu/thermal-design-power: 100 @@ -82,24 +82,24 @@ tree: functional-unit-time: "1 min" inputs: - timestamp: "2023-12-12T00:00:00.000Z" - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west duration: 1 cpu/utilization: 10 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 15 child-2: pipeline: @@ -112,7 +112,7 @@ tree: config: group-by: group: - - region + - cloud/region - cloud/instance-type defaults: cpu/thermal-design-power: 100 @@ -127,20 +127,20 @@ tree: - timestamp: "2023-12-12T00:00:00.000Z" duration: 1 cpu/utilization: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 28 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 40 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cpu/utilization: 33 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west diff --git a/src/builtins/README.md b/src/builtins/README.md index 6850f5c7e..33de59585 100644 --- a/src/builtins/README.md +++ b/src/builtins/README.md @@ -279,7 +279,6 @@ tree: carbon: 200 energy: 200 requests: 380 - ``` @@ -673,44 +672,43 @@ graph: - teads-curve config: group-by: - - cloud-region + - cloud/region - instance-type inputs: - timestamp: 2023-07-06T00:00 - duration: 300 - instance-type: A1 - region: uk-west + duration: 300 + instance-type: A1 + cloud/region: uk-west cpu-util: 99 - - timestamp: 2023-07-06T05:00 - duration: 300 - instance-type: A1 - region: uk-west - cpu-util: 23 + - timestamp: 2023-07-06T05:00 + duration: 300 + instance-type: A1 + cloud/region: uk-west + cpu-util: 23 - timestamp: 2023-07-06T10:00 duration: 300 - instance-type: A1 - region: uk-west + instance-type: A1 + cloud/region: uk-west cpu-util: 12 - timestamp: 2023-07-06T00:00 # note this time restarts at the start timstamp duration: 300 instance-type: B1 - region: uk-west + cloud/region: uk-west cpu-util: 11 - - timestamp: 2023-07-06T05:00 - duration: 300 + - timestamp: 2023-07-06T05:00 + duration: 300 instance-type: B1 - region: uk-west + cloud/region: uk-west cpu-util: 67 - timestamp: 2023-07-06T10:00 duration: 300 instance-type: B1 - region: uk-west - cpu-util: 1 + cloud/region: uk-west + cpu-util: 1 ``` However, each observation contains an `instance-type` field that varies between observations. There are two instance types being represented in this array of observations. This means there are duplicate entries for the same timestamp in this array. This is the problem that `group-by` solves. You provide `instance-type` as a key to the `group-by` plugin and it extracts the data belonging to the different instances and separates them into independent arrays. The above example would be restructured so that instance types `A1` and `B1` have their own data, as follows: - ```yaml graph: children: @@ -721,7 +719,7 @@ graph: config: group-by: groups: - - cloud-region + - cloud/region - instance-type children: A1: @@ -729,35 +727,35 @@ graph: - timestamp: 2023-07-06T00:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 99 - timestamp: 2023-07-06T05:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 23 - timestamp: 2023-07-06T10:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 12 B1: inputs: - timestamp: 2023-07-06T00:00 duration: 300 instance-type: B1 - region: uk-east + cloud/region: uk-east cpu-util: 11 - timestamp: 2023-07-06T05:00 duration: 300 instance-type: B1 - region: uk-east + cloud/region: uk-east cpu-util: 67 - timestamp: 2023-07-06T10:00 duration: 300 instance-type: B1 - region: uk-east - cpu-util: 1 + cloud/region: uk-east + cpu-util: 1 ``` ### Using `group-by` @@ -769,9 +767,9 @@ The initialization looks as follows: ```yaml initialize: plugins: -group-by: - path: 'builtin' - method: GroupBy +group-by: + path: 'builtin' + method: GroupBy ``` You then have to provide config defining which keys to group by in each component. This is done at the component level (i.e. not global config). @@ -782,19 +780,18 @@ For example: tree: children: my-app: - pipeline: + pipeline: - group-by config: group-by: group: - - region + - cloud/region - instance-type ``` -In the example above, the plugin would regroup the input data for the specific component by `region` and by `instance-type`. - -Assuming the values `A1` and `B1` are found for `instance-type` and the values `uk-east` and `uk-west` are found for `region`, the result of `group-by` would look similar to the following: +In the example above, the plugin would regroup the input data for the specific component by `cloud/region` and by `instance-type`. +Assuming the values `A1` and `B1` are found for `instance-type` and the values `uk-east` and `uk-west` are found for `cloud/region`, the result of `group-by` would look similar to the following: ```yaml tree: @@ -805,7 +802,7 @@ tree: config: group-by: groups: - - region + - cloud/region - instance-type children: uk-west: @@ -815,17 +812,17 @@ tree: - timestamp: 2023-07-06T00:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 99 - timestamp: 2023-07-06T05:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 23 - timestamp: 2023-07-06T10:00 duration: 300 instance-type: A1 - region: uk-west + cloud/region: uk-west cpu-util: 12 uk-east: children: @@ -834,19 +831,18 @@ tree: - timestamp: 2023-07-06T00:00 duration: 300 instance-type: B1 - region: uk-east + cloud/region: uk-east cpu-util: 11 - timestamp: 2023-07-06T05:00 duration: 300 instance-type: B1 - region: uk-east + cloud/region: uk-east cpu-util: 67 - timestamp: 2023-07-06T10:00 duration: 300 instance-type: B1 - region: uk-east - cpu-util: 1 + cloud/region: uk-east + cpu-util: 1 ``` This reorganized data can then be used to feed the rest of a computation pipeline. - From 8eaa1ee99cd43ea4ced729618922a1ace9c08865 Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:57:31 +0400 Subject: [PATCH 58/65] fix(examples): rename region into cloud/region and add missing params --- examples/manifests/pipeline-demo-2.yml | 27 ++++++----- examples/manifests/pipeline-with-generics.yml | 47 ++++++++++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/examples/manifests/pipeline-demo-2.yml b/examples/manifests/pipeline-demo-2.yml index 81b552d6a..c706bbb92 100644 --- a/examples/manifests/pipeline-demo-2.yml +++ b/examples/manifests/pipeline-demo-2.yml @@ -5,6 +5,11 @@ aggregation: metrics: - "carbon" type: "both" +params: + - name: carbon-plus-energy + description: "sum of energies" + unit: "kWh" + aggregation: sum initialize: plugins: "teads-curve": @@ -19,7 +24,7 @@ initialize: input-parameters: - cpu/energy - network/energy - output-parameter: carbon-plus-energy' + output-parameter: carbon-plus-energy "sci-m": path: "@grnsft/if-plugins" method: SciM @@ -56,7 +61,7 @@ tree: config: group-by: group: - - region + - cloud/region - cloud/instance-type defaults: cpu/thermal-design-power: 100 @@ -70,7 +75,7 @@ tree: inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west duration: 1 cpu/utilization: 10 network/energy: 10 @@ -79,20 +84,20 @@ tree: duration: 5 cpu/utilization: 20 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west cpu/utilization: 15 network/energy: 10 energy: 5 @@ -107,7 +112,7 @@ tree: config: group-by: group: - - region + - cloud/region - instance-type defaults: cpu/thermal-design-power: 100 @@ -123,27 +128,27 @@ tree: duration: 1 cpu/utilization: 30 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 28 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 40 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cpu/utilization: 33 cloud/instance-type: A1 - region: uk-west + cloud/region: uk-west network/energy: 10 energy: 5 diff --git a/examples/manifests/pipeline-with-generics.yml b/examples/manifests/pipeline-with-generics.yml index 2d966dba8..5a4eee159 100644 --- a/examples/manifests/pipeline-with-generics.yml +++ b/examples/manifests/pipeline-with-generics.yml @@ -3,8 +3,13 @@ description: tags: aggregation: metrics: - - 'carbon' - type: 'both' + - "carbon" + type: "both" +params: + - name: energy-sum + description: "sum of energies" + unit: "kWh" + aggregation: sum initialize: plugins: "teads-curve": @@ -16,17 +21,17 @@ initialize: path: "@grnsft/if-plugins" method: Sum global-config: - input-parameters: + input-parameters: - cpu/energy - network/energy - output-parameter: energy-sum + output-parameter: energy-sum "coefficient": path: "@grnsft/if-plugins" method: Coefficient global-config: input-parameter: energy coefficient: 2 - output-parameter: energy-doubled + output-parameter: energy-doubled "sci-m": path: "@grnsft/if-plugins" method: SciM @@ -70,8 +75,8 @@ tree: functional-unit-time: "1 min" inputs: - timestamp: "2023-12-12T00:00:00.000Z" - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west duration: 1 cpu/utilization: 50 network/energy: 10 @@ -79,21 +84,21 @@ tree: - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west cpu/utilization: 15 network/energy: 10 energy: 5 @@ -120,28 +125,28 @@ tree: - timestamp: "2023-12-12T00:00:00.000Z" duration: 1 cpu/utilization: 30 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 28 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 40 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cpu/utilization: 33 - cloud/instance-type: A1 - region: uk-west + cloud/instance-type: A1 + cloud/region: uk-west network/energy: 10 energy: 5 From b3aab35472e8496eba5806016d7b03ff4ec140fc Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 20:59:03 +0400 Subject: [PATCH 59/65] fix(examples): update the name of the e-net manifest --- examples/manifests/e-net.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/manifests/e-net.yml b/examples/manifests/e-net.yml index 2359f3d62..238f64902 100644 --- a/examples/manifests/e-net.yml +++ b/examples/manifests/e-net.yml @@ -1,4 +1,4 @@ -name: sci-e-demo +name: e-net-demo description: tags: initialize: From 161239b78c43d572a66f8e729b2bda68697982c6 Mon Sep 17 00:00:00 2001 From: manushak Date: Wed, 3 Apr 2024 21:00:13 +0400 Subject: [PATCH 60/65] fix(examples): update watt-time manifest to execute with free plan credentials --- examples/manifests/watt-time.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/manifests/watt-time.yml b/examples/manifests/watt-time.yml index 04019c6ef..f5dcd81e0 100644 --- a/examples/manifests/watt-time.yml +++ b/examples/manifests/watt-time.yml @@ -7,17 +7,17 @@ initialize: path: "@grnsft/if-plugins" method: MockObservations global-config: - timestamp-from: 2024-02-26T00:00 - timestamp-to: 2024-02-26T00:10 + timestamp-from: "2024-03-05T00:00:00.000Z" + timestamp-to: "2024-03-05T00:10:00.000Z" duration: 60 components: - - cloud/instance-type: Standard_A1_v2 - - cloud/instance-type: Standard_A2_v2 + - cloud/instance-type: Standard_E64_v3 + - cloud/instance-type: Standard_E64_v4 generators: common: cloud/vendor: azure - cloud/region: uk-west - geolocation: 37.7749,-122.4194 + cloud/region: westus + cloud/region-wt-id: CAISO_NORTH randint: cpu/utilization: min: 1 @@ -33,10 +33,3 @@ tree: - watttime config: inputs: - - timestamp: "2024-02-26 00:00:00" - duration: 60 - cloud/instance-type: Standard_A1_v2 - cloud/vendor: azure - cloud/region: uk-west - geolocation: 37.7749,-122.4194 - cpu/utilization: 15 From 984c56d77f717ab6192863ca45e27b8125d45600 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 4 Apr 2024 14:23:15 +0400 Subject: [PATCH 61/65] fix(examples): rename cloud-metadata to cloud-metadata-teads --- examples/manifests/cloud-metadata-teads.yml | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 examples/manifests/cloud-metadata-teads.yml diff --git a/examples/manifests/cloud-metadata-teads.yml b/examples/manifests/cloud-metadata-teads.yml new file mode 100644 index 000000000..a4fb7e092 --- /dev/null +++ b/examples/manifests/cloud-metadata-teads.yml @@ -0,0 +1,41 @@ +name: cloud-metadata-demo +description: null +tags: null +initialize: + plugins: + teads-curve: + path: "@grnsft/if-unofficial-plugins" + method: TeadsCurve + global-config: + interpolation: spline + cloud-metadata: + method: CloudMetadata + path: "@grnsft/if-plugins" +tree: + children: + child-0: + defaults: + cpu/thermal-design-power: 100 + cloud/vendor: aws + cloud/instance-type: m5n.large + pipeline: + - teads-curve + - cloud-metadata + inputs: + - timestamp: 2023-07-06T00:00 + duration: 10 + cpu/utilization: 80 + - timestamp: 2023-07-06T00:01 + duration: 10 + cpu/utilization: 80 + outputs: + - timestamp: 2023-07-06T00:00 + duration: 10 + cpu/utilization: 80 + cpu/thermal-design-power: 100 + cpu/energy: 0.00025568089430894314 + - timestamp: 2023-07-06T00:01 + duration: 10 + cpu/utilization: 80 + cpu/thermal-design-power: 100 + cpu/energy: 0.00025568089430894314 From d46907854d50e0771bae3ab697b1182cc9f21de6 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 4 Apr 2024 14:24:45 +0400 Subject: [PATCH 62/65] fix(examples): bring back cim.yml manifest by renaming it to cloud-metadata.yml --- examples/manifests/cloud-metadata.yml | 41 +++++++-------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/examples/manifests/cloud-metadata.yml b/examples/manifests/cloud-metadata.yml index a4fb7e092..112ba2165 100644 --- a/examples/manifests/cloud-metadata.yml +++ b/examples/manifests/cloud-metadata.yml @@ -1,41 +1,20 @@ -name: cloud-metadata-demo -description: null -tags: null +name: cloud-metadata +description: example impl invoking Cloud Metadata plugin +tags: initialize: plugins: - teads-curve: - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve - global-config: - interpolation: spline cloud-metadata: method: CloudMetadata path: "@grnsft/if-plugins" tree: children: - child-0: - defaults: - cpu/thermal-design-power: 100 - cloud/vendor: aws - cloud/instance-type: m5n.large + child: pipeline: - - teads-curve - cloud-metadata + config: inputs: - - timestamp: 2023-07-06T00:00 - duration: 10 - cpu/utilization: 80 - - timestamp: 2023-07-06T00:01 - duration: 10 - cpu/utilization: 80 - outputs: - - timestamp: 2023-07-06T00:00 - duration: 10 - cpu/utilization: 80 - cpu/thermal-design-power: 100 - cpu/energy: 0.00025568089430894314 - - timestamp: 2023-07-06T00:01 - duration: 10 - cpu/utilization: 80 - cpu/thermal-design-power: 100 - cpu/energy: 0.00025568089430894314 + - timestamp: 2023-07-06T00:00 # [KEYWORD] [NO-SUBFIELDS] time when measurement occurred + cloud/vendor: aws + cloud/instance-type: m5n.large + duration: 100 + cpu/utilization: 10 From e941cc5aab4bd54a71b67abf58726ba09d1a4098 Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 5 Apr 2024 09:12:34 +0400 Subject: [PATCH 63/65] test(util): add full test coverage for args.ts --- src/__tests__/unit/util/args.test.ts | 95 ++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/__tests__/unit/util/args.test.ts b/src/__tests__/unit/util/args.test.ts index 38084adc4..b188adb2d 100644 --- a/src/__tests__/unit/util/args.test.ts +++ b/src/__tests__/unit/util/args.test.ts @@ -1,20 +1,36 @@ +const processRunningPath = process.cwd(); + jest.mock('ts-command-line-args', () => ({ __esModule: true, parse: () => { switch (process.env.result) { + case 'throw-error-object': + throw new CliInputError(MANIFEST_IS_MISSING); case 'error': + throw MANIFEST_IS_MISSING; + case 'manifest-is-missing': return {}; case 'manifest': return { manifest: 'manifest-mock.yml', }; + case 'absolute-path': + return { + manifest: path.normalize(`${processRunningPath}/manifest-mock.yml`), + }; case 'manifest-output': return { manifest: 'manifest-mock.yml', output: 'output-mock.yml', }; + case 'override-params': + return { + 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': @@ -35,28 +51,47 @@ import path = require('path'); import {parseArgs} from '../../../util/args'; import {ERRORS} from '../../../util/errors'; -import {STRINGS} from '../../../config'; +import {STRINGS, CONFIG} from '../../../config'; +const {impact} = CONFIG; +const {HELP} = impact; const {CliInputError} = ERRORS; const {MANIFEST_IS_MISSING, FILE_IS_NOT_YAML} = STRINGS; +const info = jest.spyOn(console, 'info').mockImplementation(() => {}); + describe('util/args: ', () => { const originalEnv = process.env; describe('parseArgs(): ', () => { it('throws error if there is no argument passed.', () => { - expect.assertions(2); + expect.assertions(5); process.env.result = 'error'; // used for mocking try { parseArgs(); } catch (error) { - if (error instanceof Error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error.message).toEqual(MANIFEST_IS_MISSING); - } + expect(error).toEqual(MANIFEST_IS_MISSING); + } + + process.env.result = 'throw-error-object'; + + try { + parseArgs(); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError(MANIFEST_IS_MISSING)); + } + + process.env.result = 'manifest-is-missing'; + + try { + parseArgs(); + } catch (error) { + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError(MANIFEST_IS_MISSING)); } }); @@ -66,8 +101,20 @@ describe('util/args: ', () => { process.env.result = 'manifest'; const result = parseArgs(); - const processRunningPath = process.cwd(); + const manifestPath = 'manifest-mock.yml'; + const expectedResult = { + inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), + }; + expect(result).toEqual(expectedResult); + }); + + it('returns manifest with absolute path.', () => { + expect.assertions(1); + + process.env.result = 'absolute-path'; + + const result = parseArgs(); const manifestPath = 'manifest-mock.yml'; const expectedResult = { inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), @@ -76,14 +123,38 @@ describe('util/args: ', () => { expect(result).toEqual(expectedResult); }); + it('returns manifest with `paramPath`.', () => { + expect.assertions(1); + + process.env.result = 'override-params'; + + const result = parseArgs(); + const manifestPath = 'manifest-mock.yml'; + const expectedResult = { + inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), + paramPath: 'override-params-mock.yml', + }; + + expect(result).toEqual(expectedResult); + }); + + it('returns manifest with help.', () => { + expect.assertions(2); + + process.env.result = 'help'; + + const result = parseArgs(); + + expect(info).toHaveBeenCalledWith(HELP); + expect(result).toEqual(undefined); + }); + it('returns manifest and output path.', () => { expect.assertions(1); process.env.result = 'manifest-output'; const result = parseArgs(); - const processRunningPath = process.cwd(); - const manifestPath = 'manifest-mock.yml'; const outputPath = 'output-mock.yml'; const expectedResult = { @@ -102,10 +173,8 @@ describe('util/args: ', () => { try { parseArgs(); } catch (error) { - if (error instanceof Error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error.message).toEqual(FILE_IS_NOT_YAML); - } + expect(error).toBeInstanceOf(CliInputError); + expect(error).toEqual(new CliInputError(FILE_IS_NOT_YAML)); } }); }); From df7feceedebf028684982dff54c35ba12c8e0076 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 8 Apr 2024 16:46:35 +0400 Subject: [PATCH 64/65] test(lib): fix mock error class --- src/__tests__/unit/util/args.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/util/args.test.ts b/src/__tests__/unit/util/args.test.ts index b188adb2d..16de52e26 100644 --- a/src/__tests__/unit/util/args.test.ts +++ b/src/__tests__/unit/util/args.test.ts @@ -5,7 +5,7 @@ jest.mock('ts-command-line-args', () => ({ parse: () => { switch (process.env.result) { case 'throw-error-object': - throw new CliInputError(MANIFEST_IS_MISSING); + throw new Error(MANIFEST_IS_MISSING); case 'error': throw MANIFEST_IS_MISSING; case 'manifest-is-missing': From 282a41d3003714d3e9949bb2753867e321381c71 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 9 Apr 2024 23:57:52 +0400 Subject: [PATCH 65/65] =?UTF-8?q?chore(package):=20release=20v0.3.2=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1947f04ea..695317ad7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@grnsft/if", - "version": "v0.3.1", + "version": "v0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@grnsft/if", - "version": "v0.3.1", + "version": "v0.3.2", "license": "MIT", "dependencies": { "@commitlint/cli": "^18.6.0", diff --git a/package.json b/package.json index 9535275f5..b15c1c9ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@grnsft/if", "description": "Impact Framework", - "version": "v0.3.1", + "version": "v0.3.2", "author": { "name": "Green Software Foundation", "email": "info@gsf.com"