From 79bfda85f451a13b3b68d2ea9caa00308cb3736d Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 18:25:25 +0400 Subject: [PATCH 01/21] test(lib): cover cloud-metedata test --- .../unit/lib/cloud-metadata/index.test.ts | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/__tests__/unit/lib/cloud-metadata/index.test.ts b/src/__tests__/unit/lib/cloud-metadata/index.test.ts index 7de45aa..2a044fd 100644 --- a/src/__tests__/unit/lib/cloud-metadata/index.test.ts +++ b/src/__tests__/unit/lib/cloud-metadata/index.test.ts @@ -73,6 +73,134 @@ describe('lib/cloud-metadata:', () => { ]); }); + it('returns a result when azure instance type do not have size number.', async () => { + const inputs = [ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_B1ms', + 'cloud/vendor': 'azure', + }, + ]; + + const result = await cloudMetadata.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_B1ms', + 'cloud/vendor': 'azure', + 'cpu/thermal-design-power': 270, + 'physical-processor': + 'Intel® Xeon® Platinum 8370C,Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz', + 'vcpus-allocated': 1, + 'vcpus-total': 64, + 'memory-available': 2, + }, + ]); + }); + + it('returns a result with configured outputs.', async () => { + const inputs = [ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_A1_v2', + 'cloud/vendor': 'azure', + 'cloud/region': 'francesouth', + }, + ]; + const config = { + fields: [ + 'cloud/vendor', + 'cloud/region-wt-id', + 'cloud/instance-type', + 'physical-processor', + 'cpu/thermal-design-power', + ], + }; + const result = await cloudMetadata.execute(inputs, config); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_A1_v2', + 'cloud/region': 'francesouth', + 'cloud/region-wt-id': 'FR', + 'cloud/vendor': 'azure', + 'cpu/thermal-design-power': 205, + 'physical-processor': + 'Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz', + }, + ]); + }); + + it('returns a result when provided a `cloud/region` in the input.', async () => { + const inputs = [ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_A1_v2', + 'cloud/vendor': 'azure', + 'cloud/region': 'francesouth', + }, + ]; + + const result = await cloudMetadata.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_A1_v2', + 'cloud/region': 'francesouth', + 'cloud/region-cfe': 'France', + 'cloud/region-em-zone-id': 'FR', + 'cloud/region-geolocation': '48.8567,2.3522', + 'cloud/region-location': 'Paris', + 'cloud/region-wt-id': 'FR', + 'cloud/vendor': 'azure', + 'cpu/thermal-design-power': 205, + 'memory-available': 2, + 'physical-processor': + 'Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz', + 'vcpus-allocated': 1, + 'vcpus-total': 52, + }, + ]); + }); + + it('throws an error when provided a wrong `cloud/region` for vendor in the input.', async () => { + const errorMessage = + "CloudMetadata: 'uk-west' region is not supported in 'azure' cloud vendor."; + const inputs = [ + { + timestamp: '', + duration: 5, + 'cloud/instance-type': 'Standard_A1_v2', + 'cloud/vendor': 'azure', + 'cloud/region': 'uk-west', + }, + ]; + + expect.assertions(2); + + try { + await cloudMetadata.execute(inputs); + } catch (error) { + expect(error).toStrictEqual(new UnsupportedValueError(errorMessage)); + expect(error).toBeInstanceOf(UnsupportedValueError); + } + }); + it('throws on `cloud/instance-type` when `cloud/vendor` is aws.', async () => { const errorMessage = "CloudMetadata(cloud/instance-type): 't2.micro2' instance type is not supported in 'aws' cloud vendor."; From 53ce07b91d323d55209867563d4a24bb51f5e577 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 18:37:18 +0400 Subject: [PATCH 02/21] fix(lib): remove unnecessary check in cloud-metadata --- src/lib/cloud-metadata/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/cloud-metadata/index.ts b/src/lib/cloud-metadata/index.ts index e518ad9..6aa7f01 100644 --- a/src/lib/cloud-metadata/index.ts +++ b/src/lib/cloud-metadata/index.ts @@ -163,10 +163,7 @@ export const CloudMetadata = (): PluginInterface => { if (instanceType.includes('-')) { const [instanceFamily, instanceSize] = instanceType.split('-'); const sizeNumberIndex = instanceSize.search(/\D/); - const instanceSizeNumber = - sizeNumberIndex !== -1 - ? instanceSize.slice(sizeNumberIndex) - : instanceSize; + const instanceSizeNumber = instanceSize.slice(sizeNumberIndex); instanceType = `${instanceFamily}${instanceSizeNumber}`; } From 1040acae1f23e4007062aa39fba354ec9031ab82 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 18:44:38 +0400 Subject: [PATCH 03/21] test(lib): cover csv-export plugin tests --- .../unit/lib/csv-export/index.test.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/csv-export/index.test.ts b/src/__tests__/unit/lib/csv-export/index.test.ts index 5b452f0..889a358 100644 --- a/src/__tests__/unit/lib/csv-export/index.test.ts +++ b/src/__tests__/unit/lib/csv-export/index.test.ts @@ -5,7 +5,7 @@ import {CsvExport} from '../../../../lib/csv-export'; import {ERRORS} from '../../../../util/errors'; -const {MakeDirectoryError, WriteFileError} = ERRORS; +const {MakeDirectoryError, WriteFileError, InputValidationError} = ERRORS; jest.mock('fs/promises', () => ({ mkdir: jest.fn<() => Promise>().mockResolvedValue(), @@ -126,6 +126,36 @@ describe('lib/csv-export: ', () => { expect(result).toStrictEqual(input); }); + it('throws an error when node config is not provided.', async () => { + const csvExport = CsvExport(); + + const input = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + energy: 10, + carbon: 2, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + energy: 20, + carbon: 5, + }, + ]; + + expect.assertions(2); + + try { + await csvExport.execute(input); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError(`CsvExport: Configuration data is missing.`) + ); + } + }); + it('throws an error when a file writing fails.', async () => { (fs.writeFile as jest.Mock).mockImplementation(() => { throw new Error('Permission denied'); From c07a8c70ee5505016ad06aa08ff69bba41551aa2 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 19:00:10 +0400 Subject: [PATCH 04/21] test(lib): cover divide plugin test --- src/__tests__/unit/lib/divide/index.test.ts | 46 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/divide/index.test.ts b/src/__tests__/unit/lib/divide/index.test.ts index e2842bc..71aff39 100644 --- a/src/__tests__/unit/lib/divide/index.test.ts +++ b/src/__tests__/unit/lib/divide/index.test.ts @@ -2,7 +2,7 @@ import {Divide} from '../../../../lib'; import {ERRORS} from '../../../../util/errors'; -const {InputValidationError} = ERRORS; +const {InputValidationError, ConfigValidationError} = ERRORS; describe('lib/divide: ', () => { describe('Divide: ', () => { @@ -102,6 +102,25 @@ describe('lib/divide: ', () => { }); }); + it('throws an error on missing global config.', async () => { + const expectedMessage = 'Divide: Configuration data is missing.'; + const config = undefined; + const divide = Divide(config!); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigValidationError(expectedMessage)); + } + }); + it('throws an error when `denominator` is 0.', async () => { const expectedMessage = '"denominator" parameter is number must be greater than 0. Error code: too_small.'; @@ -127,5 +146,30 @@ describe('lib/divide: ', () => { expect(error).toStrictEqual(new InputValidationError(expectedMessage)); } }); + + it('throws an error when `denominator` is string.', async () => { + const expectedMessage = 'Divide: `10` is missing from the input.'; + + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: '10', + output: 'vcpus-allocated-per-second', + }; + const divide = Divide(globalConfig); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new InputValidationError(expectedMessage)); + } + }); }); }); From 8cf6ab8fd1a7e10ccee16a61f0fa7380a0a87cce Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 19:31:55 +0400 Subject: [PATCH 05/21] test(lib): add coverage for regex plugin --- src/__tests__/unit/lib/regex/index.test.ts | 56 +++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/regex/index.test.ts b/src/__tests__/unit/lib/regex/index.test.ts index ade7e9f..15864d3 100644 --- a/src/__tests__/unit/lib/regex/index.test.ts +++ b/src/__tests__/unit/lib/regex/index.test.ts @@ -2,7 +2,7 @@ import {Regex} from '../../../../lib'; import {ERRORS} from '../../../../util/errors'; -const {InputValidationError} = ERRORS; +const {InputValidationError, ConfigValidationError} = ERRORS; describe('lib/regex: ', () => { describe('Regex: ', () => { @@ -46,6 +46,38 @@ describe('lib/regex: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('returns a result when regex is not started and ended with ``.', async () => { + const physicalProcessor = + 'Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz'; + expect.assertions(1); + + const globalConfig = { + parameter: 'physical-processor', + match: '[^,]+/', + output: 'cpu/name', + }; + const regex = Regex(globalConfig); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'physical-processor': physicalProcessor, + 'cpu/name': 'Intel® Xeon® Platinum 8272CL', + }, + ]; + + const result = await regex.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'physical-processor': physicalProcessor, + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error when `parameter` does not match to `match`.', async () => { const physicalProcessor = 'Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz'; @@ -75,6 +107,28 @@ describe('lib/regex: ', () => { } }); + it('throws an error on missing global config.', async () => { + const expectedMessage = 'Regex: Configuration data is missing.'; + + const config = undefined; + const regex = Regex(config!); + + expect.assertions(1); + + try { + await regex.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new ConfigValidationError(expectedMessage) + ); + } + }); + it('throws an error on missing params in input.', async () => { const expectedMessage = 'Regex: `physical-processor` is missing from the input.'; From 1bbfa03e24fa8b2a125575af13316e5b4d325b05 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 19:33:06 +0400 Subject: [PATCH 06/21] fix(lib): fix regex validation check --- src/lib/regex/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/regex/index.ts b/src/lib/regex/index.ts index 767a5ed..d7cc445 100644 --- a/src/lib/regex/index.ts +++ b/src/lib/regex/index.ts @@ -77,15 +77,12 @@ export const Regex = (globalConfig: ConfigParams): PluginInterface => { parameter: string, match: string ) => { - if ( - !match.startsWith('/') || - (match.endsWith('/g') && match.lastIndexOf('/') === 0) - ) { + if (!match.startsWith('/')) { match = '/' + match; + } - if (!match.endsWith('/g') || !match.endsWith('/')) { - match += '/'; - } + if (!match.endsWith('/g') && !match.endsWith('/')) { + match += '/'; } const regex = eval(match); From 91317cc21c7fb60688a9de0fb72462b6f5e24764 Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 19:39:07 +0400 Subject: [PATCH 07/21] test(lib): add coverage for sci plugin --- src/__tests__/unit/lib/sci/index.test.ts | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/__tests__/unit/lib/sci/index.test.ts b/src/__tests__/unit/lib/sci/index.test.ts index 188f3ea..c3212f7 100644 --- a/src/__tests__/unit/lib/sci/index.test.ts +++ b/src/__tests__/unit/lib/sci/index.test.ts @@ -90,6 +90,90 @@ describe('lib/sci:', () => { ]); }); + it('returns a result when `functional-unit-time` is not provided.', async () => { + const sci = Sci({ + 'functional-unit': 'requests', + }); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.2, + 'carbon-embodied': 0.05, + duration: 100, + }, + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.002, + 'carbon-embodied': 0.0005, + duration: 2, + }, + ]; + const result = await sci.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.2, + 'carbon-embodied': 0.05, + carbon: 0.0025, + duration: 100, + sci: 0.0025, + }, + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.002, + 'carbon-embodied': 0.0005, + carbon: 0.00125, + duration: 2, + sci: 0.00125, + }, + ]); + }); + + it('returns a result when `functional-unit` is not provided.', async () => { + const sci = Sci({ + 'functional-unit-time': '1 day', + }); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.2, + 'carbon-embodied': 0.05, + duration: 100, + }, + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.002, + 'carbon-embodied': 0.0005, + duration: 2, + }, + ]; + const result = await sci.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.2, + 'carbon-embodied': 0.05, + carbon: 0.0025, + duration: 100, + sci: 216, + }, + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.002, + 'carbon-embodied': 0.0005, + carbon: 0.00125, + duration: 2, + sci: 108, + }, + ]); + }); + it('throws exception on invalid functional unit data.', async () => { const sci = Sci({ 'functional-unit': 'requests', From 5531acf26b83cff607adff8ddbcbf9b53f30e61f Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 20:54:29 +0400 Subject: [PATCH 08/21] test(lib): add test coverage for sci-m plugin --- src/__tests__/unit/lib/sci-m/index.test.ts | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/sci-m/index.test.ts b/src/__tests__/unit/lib/sci-m/index.test.ts index 7e10582..cad559c 100644 --- a/src/__tests__/unit/lib/sci-m/index.test.ts +++ b/src/__tests__/unit/lib/sci-m/index.test.ts @@ -191,6 +191,37 @@ describe('lib/sci-m:', () => { ]); }); + it('throws an error when `device/emissions-embodied` is string.', async () => { + const errorMessage = + '"device/emissions-embodied" parameter is not a valid number in input. please provide it as `gco2e`. Error code: invalid_union.'; + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/emissions-embodied': '10,00', + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }, + { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30 * 2, + 'device/emissions-embodied': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }, + ]; + + expect.assertions(2); + try { + await sciM.execute(inputs); + } catch (error) { + expect(error).toStrictEqual(new InputValidationError(errorMessage)); + expect(error).toBeInstanceOf(InputValidationError); + } + }); + it('throws an exception on missing `device/emissions-embodied`.', async () => { const errorMessage = '"device/emissions-embodied" parameter is required. Error code: invalid_union.'; @@ -239,7 +270,7 @@ describe('lib/sci-m:', () => { it('throws an exception on invalid values.', async () => { const errorMessage = - '"device/emissions-embodied" parameter is expected number, received string. Error code: invalid_union.'; + '"device/emissions-embodied" parameter is not a valid number in input. please provide it as `gco2e`. Error code: invalid_union.'; const inputs = [ { timestamp: '2021-01-01T00:00:00Z', From be6b515551b80cb6f54788976f65a4df5a03e92c Mon Sep 17 00:00:00 2001 From: manushak Date: Thu, 14 Mar 2024 20:56:25 +0400 Subject: [PATCH 09/21] fix(lib): remove unnecessary function in sci-m --- src/lib/sci-m/index.ts | 110 +++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/src/lib/sci-m/index.ts b/src/lib/sci-m/index.ts index ee0cbea..84be886 100644 --- a/src/lib/sci-m/index.ts +++ b/src/lib/sci-m/index.ts @@ -4,13 +4,8 @@ import {PluginInterface} from '../../interfaces'; import {PluginParams} from '../../types/common'; import {validate, allDefined} from '../../util/validations'; -import {buildErrorMessage} from '../../util/helpers'; -import {ERRORS} from '../../util/errors'; - -const {InputValidationError} = ERRORS; export const SciM = (): PluginInterface => { - const errorBuilder = buildErrorMessage(SciM.name); const metadata = { kind: 'execute', }; @@ -42,23 +37,12 @@ export const SciM = (): PluginInterface => { * M = totalEmissions * (duration/ExpectedLifespan) * (resourcesReserved/totalResources) */ const calculateEmbodiedCarbon = (input: PluginParams) => { - const totalEmissions = parseNumberInput( - input['device/emissions-embodied'], - 'gCO2e' - ); - const duration = parseNumberInput(input['duration'], 'seconds'); - const expectedLifespan = parseNumberInput( - input['device/expected-lifespan'], - 'seconds' - ); - const resourcesReserved = parseNumberInput( - input['vcpus-allocated'] || input['resources-reserved'], - 'count' - ); - const totalResources = parseNumberInput( - input['vcpus-total'] || input['resources-total'], - 'count' - ); + const totalEmissions = input['device/emissions-embodied']; + const duration = input['duration']; + const expectedLifespan = input['device/expected-lifespan']; + const resourcesReserved = + input['vcpus-allocated'] || input['resources-reserved']; + const totalResources = input['vcpus-total'] || input['resources-total']; return ( totalEmissions * @@ -68,41 +52,69 @@ export const SciM = (): PluginInterface => { }; /** - * Parses the input value, ensuring it is a valid number, and returns the parsed number. - * Throws an InputValidationError if the value is not a valid number. + * Checks for required fields in input. */ - const parseNumberInput = (value: any, unit: string): number => { - const parsedValue = typeof value === 'string' ? parseFloat(value) : value; + const validateInput = (input: PluginParams) => { + const errorMessage = (unit: string) => + `not a valid number in input. Please provide it as \`${unit}\``; - if (typeof parsedValue !== 'number' || isNaN(parsedValue)) { - throw new InputValidationError( - errorBuilder({ - message: `'${value}' is not a valid number in input. Please provide it as ${unit}.`, + const commonSchemaPart = (errorMessage: (unit: string) => string) => ({ + 'device/emissions-embodied': z + .number({ + invalid_type_error: errorMessage('gCO2e'), + }) + .gte(0) + .min(0), + 'device/expected-lifespan': z + .number({ + invalid_type_error: errorMessage('gCO2e'), }) - ); - } + .gte(0) + .min(0), + duration: z + .number({ + invalid_type_error: errorMessage('seconds'), + }) + .gte(1), + }); - return parsedValue; - }; + const vcpusSchemaPart = { + 'vcpus-allocated': z + .number({ + invalid_type_error: errorMessage('count'), + }) + .gte(0) + .min(0), + 'vcpus-total': z + .number({ + invalid_type_error: errorMessage('count'), + }) + .gte(0) + .min(0), + }; + + const resourcesSchemaPart = { + 'resources-reserved': z + .number({ + invalid_type_error: errorMessage('count'), + }) + .gte(0) + .min(0), + 'resources-total': z + .number({ + invalid_type_error: errorMessage('count'), + }) + .gte(0) + .min(0), + }; - /** - * Checks for required fields in input. - */ - const validateInput = (input: PluginParams) => { const schemaWithVcpus = z.object({ - 'device/emissions-embodied': z.number().gte(0).min(0), - 'device/expected-lifespan': z.number().gte(0).min(0), - 'vcpus-allocated': z.number().gte(0).min(0), - 'vcpus-total': z.number().gte(0).min(0), - duration: z.number().gte(1), + ...commonSchemaPart(errorMessage), + ...vcpusSchemaPart, }); - const schemaWithResources = z.object({ - 'device/emissions-embodied': z.number().gte(0).min(0), - 'device/expected-lifespan': z.number().gte(0).min(0), - 'resources-reserved': z.number().gte(0).min(0), - 'resources-total': z.number().gte(0).min(0), - duration: z.number().gte(1), + ...commonSchemaPart(errorMessage), + ...resourcesSchemaPart, }); const schema = schemaWithVcpus.or(schemaWithResources).refine(allDefined, { From 631e1995a4a1ebf4191e0a66f52ef1ce006fb0dd Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 15 Mar 2024 16:29:16 +0400 Subject: [PATCH 10/21] test(lib): add coverage for tdp-finder plugin --- .../unit/lib/tdp-finder/index.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/tdp-finder/index.test.ts b/src/__tests__/unit/lib/tdp-finder/index.test.ts index 0c4412c..a16201e 100644 --- a/src/__tests__/unit/lib/tdp-finder/index.test.ts +++ b/src/__tests__/unit/lib/tdp-finder/index.test.ts @@ -1,8 +1,9 @@ +import * as fs from 'fs'; import {TdpFinder} from '../../../../lib'; import {ERRORS} from '../../../../util/errors'; -const {InputValidationError, UnsupportedValueError} = ERRORS; +const {InputValidationError, UnsupportedValueError, ReadFileError} = ERRORS; describe('lib/tdp-finder:', () => { describe('TdpFinder', () => { @@ -101,6 +102,28 @@ describe('lib/tdp-finder:', () => { expect(error).toBeInstanceOf(UnsupportedValueError); } }); + + it('throws an error when the file cannot be read/', async () => { + jest.spyOn(fs.promises, 'readFile').mockRejectedValueOnce('data.csv'); + const inputs = [ + { + timestamp: '2023-11-02T10:35:31.820Z', + duration: 3600, + 'physical-processor': 'Intel Xeon Platinum 8175M,AMD A8-9600f', + }, + ]; + + expect.assertions(2); + + try { + await tdpFinder.execute(inputs); + } catch (error) { + expect(error).toStrictEqual( + new ReadFileError('Error reading file data.csv: data.csv') + ); + expect(error).toBeInstanceOf(ReadFileError); + } + }); }); }); }); From fed861f7de7642c672705571ef6bd6182815de7f Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 15 Mar 2024 17:05:01 +0400 Subject: [PATCH 11/21] test(lib): add test coverage for shell plugin --- src/__tests__/unit/lib/shell/index.test.ts | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/__tests__/unit/lib/shell/index.test.ts b/src/__tests__/unit/lib/shell/index.test.ts index e0d018b..1a71609 100644 --- a/src/__tests__/unit/lib/shell/index.test.ts +++ b/src/__tests__/unit/lib/shell/index.test.ts @@ -3,6 +3,10 @@ import {loadAll} from 'js-yaml'; import {Shell} from '../../../../lib'; +import {ERRORS} from '../../../../util/errors'; + +const {InputValidationError} = ERRORS; + jest.mock('child_process'); jest.mock('js-yaml'); @@ -57,6 +61,30 @@ describe('lib/shell', () => { await expect(shell.execute(invalidInputs)).rejects.toThrow(); }); + + it('throw an error when shell could not run command.', async () => { + const shell = Shell({command: 'python3 /path/to/script.py'}); + (spawnSync as jest.Mock).mockImplementation(() => { + throw new InputValidationError('Could not run the command'); + }); + + const inputs = [ + { + duration: 3600, + timestamp: '2022-01-01T00:00:00Z', + }, + ]; + expect.assertions(2); + + try { + await shell.execute(inputs); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError('Could not run the command') + ); + } + }); }); }); }); From 29732b7fb4c7a08437447eb126db4db68773f64f Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 15 Mar 2024 17:49:55 +0400 Subject: [PATCH 12/21] test(lib): add test coverage for commom-generator of mock-observations plugin --- .../unit/lib/mock-observations/CommonGenerator.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts index 0622b1c..bae8e2c 100644 --- a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts +++ b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts @@ -8,9 +8,13 @@ const {InputValidationError} = ERRORS; describe('lib/mock-observations/CommonGenerator: ', () => { describe('initialize: ', () => { - it('initialize with an empty config.', async () => { + it('throws an error when config is not empty object.', async () => { + const commonGenerator = CommonGenerator({}); + + expect.assertions(1); + try { - CommonGenerator({}); + expect(commonGenerator.next([])); } catch (error) { expect(error).toEqual( new InputValidationError( From 1958a5210ffe6dd40002d9b32dd7501f7b5871c8 Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 15 Mar 2024 17:52:34 +0400 Subject: [PATCH 13/21] test(lib): add assertion in common-generartor test --- .../unit/lib/mock-observations/CommonGenerator.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts index bae8e2c..c3295ff 100644 --- a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts +++ b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts @@ -33,6 +33,8 @@ describe('lib/mock-observations/CommonGenerator: ', () => { }; const commonGenerator = CommonGenerator(config); + expect.assertions(1); + expect(commonGenerator.next([])).toStrictEqual({ key1: 'value1', key2: 'value2', From 566909ad696434f2e64d50e9a6591744dccda389 Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 15 Mar 2024 18:39:45 +0400 Subject: [PATCH 14/21] test(lib): add test coverage for RandIntGenerator of mock-observations plugin --- .../RandIntGenerator.test.ts | 24 +++++++++++++++++-- .../helpers/rand-int-generator.ts | 8 +++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/__tests__/unit/lib/mock-observations/RandIntGenerator.test.ts b/src/__tests__/unit/lib/mock-observations/RandIntGenerator.test.ts index 1840b67..b69ae59 100644 --- a/src/__tests__/unit/lib/mock-observations/RandIntGenerator.test.ts +++ b/src/__tests__/unit/lib/mock-observations/RandIntGenerator.test.ts @@ -8,7 +8,8 @@ const {InputValidationError} = ERRORS; describe('lib/mock-observations/RandIntGenerator: ', () => { describe('initialize', () => { - it('initialize with an empty name', async () => { + it('throws an error when the generator name is empty string.', async () => { + expect.assertions(1); try { RandIntGenerator('', {}); } catch (error) { @@ -20,7 +21,8 @@ describe('lib/mock-observations/RandIntGenerator: ', () => { } }); - it('initialize with an empty config', async () => { + it('throws an error when config is empty object.', async () => { + expect.assertions(1); try { RandIntGenerator('generator-name', {}); } catch (error) { @@ -31,6 +33,22 @@ describe('lib/mock-observations/RandIntGenerator: ', () => { ); } }); + + it('throws an error `min` is missing from the config.', async () => { + const config = {max: 90}; + + expect.assertions(1); + + try { + RandIntGenerator('random', config); + } catch (error) { + expect(error).toEqual( + new InputValidationError( + 'RandIntGenerator: Config is missing min or max.' + ) + ); + } + }); }); describe('next(): ', () => { @@ -42,6 +60,8 @@ describe('lib/mock-observations/RandIntGenerator: ', () => { const randIntGenerator = RandIntGenerator('random', config); const result = randIntGenerator.next([]) as {random: number}; + expect.assertions(4); + expect(result).toBeInstanceOf(Object); expect(result).toHaveProperty('random'); expect(result.random).toBeGreaterThanOrEqual(10); diff --git a/src/lib/mock-observations/helpers/rand-int-generator.ts b/src/lib/mock-observations/helpers/rand-int-generator.ts index 0615b91..9f55864 100644 --- a/src/lib/mock-observations/helpers/rand-int-generator.ts +++ b/src/lib/mock-observations/helpers/rand-int-generator.ts @@ -13,11 +13,9 @@ export const RandIntGenerator = ( ): Generator => { const errorBuilder = buildErrorMessage(RandIntGenerator.name); - const next = (_historical: Object[] | undefined): Object => - (validatedName && { - [validatedName]: generateRandInt(getFieldToPopulate()), - }) || - {}; + const next = (_historical: Object[] | undefined) => ({ + [validatedName]: generateRandInt(getFieldToPopulate()), + }); const validateName = (name: string | null): string => { if (!name || name.trim() === '') { From 3bb7983d5d70e45f741b4097032ccf01ac201c3e Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 16 Mar 2024 17:58:06 +0400 Subject: [PATCH 15/21] fix(lib): add validation for global config in mock-obervations --- src/lib/mock-observations/index.ts | 85 ++++++++++++++---------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/lib/mock-observations/index.ts b/src/lib/mock-observations/index.ts index a0f2b93..4013a25 100644 --- a/src/lib/mock-observations/index.ts +++ b/src/lib/mock-observations/index.ts @@ -1,13 +1,13 @@ import * as dayjs from 'dayjs'; +import {z} from 'zod'; import * as utc from 'dayjs/plugin/utc'; import * as timezone from 'dayjs/plugin/timezone'; -import {buildErrorMessage} from '../../util/helpers'; -import {ERRORS} from '../../util/errors'; - import {PluginInterface} from '../../interfaces'; import {ConfigParams, KeyValuePair, PluginParams} from '../../types/common'; +import {validate} from '../../util/validations'; + import {CommonGenerator} from './helpers/common-generator'; import {RandIntGenerator} from './helpers/rand-int-generator'; import {Generator} from './interfaces/index'; @@ -16,12 +16,9 @@ import {ObservationParams} from './types'; dayjs.extend(utc); dayjs.extend(timezone); -const {InputValidationError} = ERRORS; - export const MockObservations = ( globalConfig: ConfigParams ): PluginInterface => { - const errorBuilder = buildErrorMessage('MockObservations'); const metadata = { kind: 'execute', }; @@ -57,47 +54,50 @@ export const MockObservations = ( ); }; + /** + * Validates global config parameters. + */ + const validateGlobalConfig = () => { + const schema = z.object({ + 'timestamp-from': z.string(), + 'timestamp-to': z.string(), + duration: z.number(), + components: z.array(z.record(z.string())), + generators: z.object({ + common: z.record(z.string().or(z.number())), + randint: z.record(z.object({min: z.number(), max: z.number()})), + }), + }); + + return validate>(schema, globalConfig); + }; + /** * Configures the MockObservations Plugin for IF */ const generateParamsFromConfig = async () => { - const timestampFrom = dayjs.tz( - getValidatedParam('timestamp-from', globalConfig), - 'UTC' - ); - const timestampTo = dayjs.tz( - getValidatedParam('timestamp-to', globalConfig), - 'UTC' - ); - const duration = getValidatedParam('duration', globalConfig); + const { + 'timestamp-from': timestampFrom, + 'timestamp-to': timestampTo, + duration, + generators, + components, + } = validateGlobalConfig(); + const convertedTimestampFrom = dayjs.tz(timestampFrom, 'UTC'); + const convertedTimestampTo = dayjs.tz(timestampTo, 'UTC'); return { duration, - timeBuckets: createTimeBuckets(timestampFrom, timestampTo, duration), - components: getValidatedParam('components', globalConfig) as KeyValuePair, - generators: createGenerators( - getValidatedParam('generators', globalConfig) + timeBuckets: createTimeBuckets( + convertedTimestampFrom, + convertedTimestampTo, + duration ), + generators: createGenerators(generators), + components, }; }; - /* - * validate a parameter is included in a given parameters map. - * return the validated param value, otherwise throw an InputValidationError. - */ - const getValidatedParam = ( - paramName: string, - params: {[key: string]: any} - ): T => { - if (!(paramName in params)) { - throw new InputValidationError( - errorBuilder({message: `${paramName} missing from global config`}) - ); - } - - return params[paramName]; - }; - /* * create time buckets based on start time, end time and duration of each bucket. */ @@ -135,14 +135,11 @@ export const MockObservations = ( ); }; - return Object.entries(generatorsConfig).flatMap(([key, value]) => { - if (key === 'common') { - return createCommonGenerator(value); - } else if (key === 'randint') { - return createRandIntGenerators(value).flat(); - } - return []; - }); + return Object.entries(generatorsConfig).flatMap(([key, value]) => + key === 'randint' + ? createRandIntGenerators(value).flat() + : createCommonGenerator(value) + ); }; /* From 2385594cc5f1e02431783d2f8a68d87eb04a97e3 Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 16 Mar 2024 18:01:59 +0400 Subject: [PATCH 16/21] test(lib): add test coverage for mock-observations plugin --- .../unit/lib/mock-observations/index.test.ts | 99 +++++++++++++++---- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/src/__tests__/unit/lib/mock-observations/index.test.ts b/src/__tests__/unit/lib/mock-observations/index.test.ts index 953e23e..2907d29 100644 --- a/src/__tests__/unit/lib/mock-observations/index.test.ts +++ b/src/__tests__/unit/lib/mock-observations/index.test.ts @@ -41,6 +41,9 @@ describe('lib/mock-observations: ', () => { region: 'uk-west', 'common-key': 'common-val', }, + randint: { + 'cpu/utilization': {min: 10, max: 10}, + }, }, }; const mockObservations = MockObservations(config); @@ -50,32 +53,36 @@ describe('lib/mock-observations: ', () => { expect(result).toStrictEqual([ { - 'common-key': 'common-val', + timestamp: '2023-07-06T00:00:00.000Z', duration: 30, + 'common-key': 'common-val', 'instance-type': 'A1', region: 'uk-west', - timestamp: '2023-07-06T00:00:00.000Z', + 'cpu/utilization': 10, }, { - 'common-key': 'common-val', + timestamp: '2023-07-06T00:00:30.000Z', duration: 30, + 'common-key': 'common-val', 'instance-type': 'A1', region: 'uk-west', - timestamp: '2023-07-06T00:00:30.000Z', + 'cpu/utilization': 10, }, { - 'common-key': 'common-val', + timestamp: '2023-07-06T00:00:00.000Z', duration: 30, + 'common-key': 'common-val', 'instance-type': 'B1', region: 'uk-west', - timestamp: '2023-07-06T00:00:00.000Z', + 'cpu/utilization': 10, }, { - 'common-key': 'common-val', + timestamp: '2023-07-06T00:00:30.000Z', duration: 30, + 'common-key': 'common-val', 'instance-type': 'B1', region: 'uk-west', - timestamp: '2023-07-06T00:00:30.000Z', + 'cpu/utilization': 10, }, ]); }); @@ -97,13 +104,15 @@ describe('lib/mock-observations: ', () => { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( new InputValidationError( - 'MockObservations: generators missing from global config.' + '"generators" parameter is required. Error code: invalid_type.' ) ); } }); it('throws when `components` are not provided.', async () => { + const errorMessage = + '"components" parameter is required. Error code: invalid_type.'; const config = { 'timestamp-from': '2023-07-06T00:00', 'timestamp-to': '2023-07-06T00:01', @@ -127,11 +136,7 @@ describe('lib/mock-observations: ', () => { await mockObservations.execute([]); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual( - new InputValidationError( - 'MockObservations: components missing from global config.' - ) - ); + expect(error).toEqual(new InputValidationError(errorMessage)); } }); @@ -159,7 +164,7 @@ describe('lib/mock-observations: ', () => { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( new InputValidationError( - 'MockObservations: duration missing from global config.' + '"duration" parameter is required. Error code: invalid_type.' ) ); } @@ -189,7 +194,7 @@ describe('lib/mock-observations: ', () => { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( new InputValidationError( - 'MockObservations: timestamp-to missing from global config.' + '"timestamp-to" parameter is required. Error code: invalid_type.' ) ); } @@ -219,7 +224,67 @@ describe('lib/mock-observations: ', () => { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( new InputValidationError( - 'MockObservations: timestamp-from missing from global config.' + '"timestamp-from" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when `randInt` is not valid.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: null, + }, + }; + const mockObservations = MockObservations(config); + + expect.assertions(2); + + try { + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"generators.randint" parameter is expected object, received null. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when `common` is not valid.', async () => { + const config = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 30, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: null, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, + }, + }, + }; + const mockObservations = MockObservations(config); + + expect.assertions(2); + + try { + await mockObservations.execute([]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + '"generators.common" parameter is expected object, received null. Error code: invalid_type.' ) ); } From 573f25b845de37924d5f4b9ef7983a3016ca0321 Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 16 Mar 2024 19:16:51 +0400 Subject: [PATCH 17/21] test(lib): add tests for validations.ts --- src/__tests__/unit/util/validations.test.ts | 76 +++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/__tests__/unit/util/validations.test.ts diff --git a/src/__tests__/unit/util/validations.test.ts b/src/__tests__/unit/util/validations.test.ts new file mode 100644 index 0000000..1954c23 --- /dev/null +++ b/src/__tests__/unit/util/validations.test.ts @@ -0,0 +1,76 @@ +import {z} from 'zod'; + +import {validate} from '../../../util/validations'; +import {ERRORS} from '../../../util/errors'; + +const {InputValidationError} = ERRORS; + +describe('Validations: ', () => { + it('returns validated data if input is valid according to schema.', () => { + const schema = z.object({ + timestamp: z.string(), + duration: z.number(), + energy: z.number(), + }); + + const input = { + timestamp: '2023-12-12T00:00:00.000Z', + energy: 10, + duration: 60, + }; + + expect(validate(schema, input)).toEqual(input); + }); + + it('throws an error and prettify error message.', () => { + const schema = z.object({ + energy: z.number(), + }); + + const invalidInput = { + energy: 'invalidEnergy', + }; + + expect.assertions(2); + + try { + validate(schema, invalidInput); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + '"energy" parameter is expected number, received string. Error code: invalid_type.' + ) + ); + } + }); + + it('throws an error when not valid union is provided.', () => { + const schema1 = z.object({ + energy: z.number(), + 7: z.string(), + }); + + const schema2 = z.object({ + timestamp: z.string(), + }); + const invalidInput = { + carbon: 'invalidEnergy', + 4: '2', + }; + + const schema = schema1.or(schema2); + + expect.assertions(2); + try { + validate(schema, invalidInput); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + '"7" parameter is required. Error code: invalid_union.' + ) + ); + } + }); +}); From bbe2a3a0b78d49aac7a67f4011ee8e1770efbd0a Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 16 Mar 2024 19:30:37 +0400 Subject: [PATCH 18/21] fix(lib): update file name in cloud-metadata --- src/lib/cloud-metadata/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/cloud-metadata/index.ts b/src/lib/cloud-metadata/index.ts index 6aa7f01..a6b606f 100644 --- a/src/lib/cloud-metadata/index.ts +++ b/src/lib/cloud-metadata/index.ts @@ -15,7 +15,7 @@ import {AWS_HEADERS, AZURE_HEADERS, GSF_HEADERS} from './config'; const AWS_INSTANCES = path.resolve(__dirname, './aws-instances.csv'); const AZURE_INSTANCES = path.resolve(__dirname, './azure-instances.csv'); -const GSF_DATA = path.resolve(__dirname, './GSF-data.csv'); +const GSF_DATA = path.resolve(__dirname, './gsf-data.csv'); const {UnsupportedValueError} = ERRORS; From 7380697184edc6d4556272a05ce2e30c5240088b Mon Sep 17 00:00:00 2001 From: manushak Date: Sat, 16 Mar 2024 19:32:13 +0400 Subject: [PATCH 19/21] feat(package): update package version --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f065ff4..22387c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@grnsft/if-plugins", - "version": "v0.2.0", + "version": "v0.3.0", "license": "MIT", "dependencies": { "@azure/arm-compute": "^21.2.0", From e12d9f495536590ed3f15e7c3db51a727ab3291e Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 18 Mar 2024 12:25:42 +0400 Subject: [PATCH 20/21] test(lib): fix ' instead of ` Signed-off-by: Narek Hovhannisyan --- src/__tests__/unit/lib/csv-export/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/csv-export/index.test.ts b/src/__tests__/unit/lib/csv-export/index.test.ts index 889a358..d744738 100644 --- a/src/__tests__/unit/lib/csv-export/index.test.ts +++ b/src/__tests__/unit/lib/csv-export/index.test.ts @@ -151,7 +151,7 @@ describe('lib/csv-export: ', () => { } catch (error) { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( - new InputValidationError(`CsvExport: Configuration data is missing.`) + new InputValidationError('CsvExport: Configuration data is missing.') ); } }); From 658aa8132d7ada65eea32cb75b2c3a1303defae3 Mon Sep 17 00:00:00 2001 From: manushak Date: Mon, 18 Mar 2024 13:06:58 +0400 Subject: [PATCH 21/21] test(lib): remove unneccesary expect function --- .../unit/lib/mock-observations/CommonGenerator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts index c3295ff..539f5bb 100644 --- a/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts +++ b/src/__tests__/unit/lib/mock-observations/CommonGenerator.test.ts @@ -14,7 +14,7 @@ describe('lib/mock-observations/CommonGenerator: ', () => { expect.assertions(1); try { - expect(commonGenerator.next([])); + commonGenerator.next([]); } catch (error) { expect(error).toEqual( new InputValidationError(