diff --git a/manifests/examples/builtins/interpolation/interpolation.yml b/manifests/examples/builtins/interpolation/interpolation.yml deleted file mode 100644 index 8f4fc946e..000000000 --- a/manifests/examples/builtins/interpolation/interpolation.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: interpolation-demo -description: simple demo of interpolation plugin -tags: -initialize: - plugins: - interpolation: - method: Interpolation - path: "builtin" - config: - method: linear - x: [0, 10, 50, 100] - y: [0.12, 0.32, 0.75, 1.02] - input-parameter: "cpu/utilization" - output-parameter: "result" -tree: - children: - child: - pipeline: - compute: - - interpolation - inputs: - - timestamp: 2023-07-06T00:00 - duration: 3600 - cpu/utilization: 45 diff --git a/src/__tests__/common/util/helpers.test.ts b/src/__tests__/common/util/helpers.test.ts index 92b4f9dab..b73e638ef 100644 --- a/src/__tests__/common/util/helpers.test.ts +++ b/src/__tests__/common/util/helpers.test.ts @@ -2,7 +2,11 @@ jest.mock('node:readline/promises', () => require('../../../__mocks__/readline') ); -import {parseManifestFromStdin} from '../../../common/util/helpers'; +import { + parseManifestFromStdin, + mapInputIfNeeded, + mapConfigIfNeeded, +} from '../../../common/util/helpers'; describe('common/util/helpers: ', () => { describe('parseManifestFromStdin(): ', () => { @@ -37,8 +41,103 @@ describe('common/util/helpers: ', () => { const response = await parseManifestFromStdin(); const expectedMessage = '\nname: mock-name\ndescription: mock-description\n'; - + expect.assertions(1); expect(response).toEqual(expectedMessage); }); }); + + describe('mapInputIfNeeded(): ', () => { + it('returns a new object with no changes when mapping is empty.', () => { + const input = { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/carbon-footprint': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }; + const mapping = {}; + + const result = mapInputIfNeeded(input, mapping); + + expect(result).toEqual(input); + }); + + it('returns a new object with keys remapped according to the mapping.', () => { + const input = { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/carbon-footprint': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }; + const mapping = {'device/emissions-embodied': 'device/carbon-footprint'}; + + const expectedOutput = { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/emissions-embodied': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }; + + const result = mapInputIfNeeded(input, mapping); + + expect(result).toEqual(expectedOutput); + expect(result).not.toHaveProperty('device/carbon-footprint'); + }); + }); + + describe('mapConfigIfNeeded', () => { + it('returns the config as is if no mapping is provided.', () => { + const config = { + filepath: './file.csv', + query: { + 'cpu-cores-available': 'cpu/available', + 'cpu-cores-utilized': 'cpu/utilized', + 'cpu-manufacturer': 'cpu/manufacturer', + }, + output: ['cpu-tdp', 'tdp'], + }; + + const nullMapping = null; + expect(mapConfigIfNeeded(config, nullMapping!)).toEqual(config); + + const undefinedMapping = undefined; + expect(mapConfigIfNeeded(config, undefinedMapping!)).toEqual(config); + }); + + it('recursively maps config keys and values according to the mapping.', () => { + const config = { + filepath: './file.csv', + query: { + 'cpu-cores-available': 'cpu/available', + 'cpu-cores-utilized': 'cpu/utilized', + 'cpu-manufacturer': 'cpu/manufacturer', + }, + output: ['cpu-tdp', 'tdp'], + }; + const mapping = { + 'cpu/utilized': 'cpu/util', + }; + + const expected = { + filepath: './file.csv', + query: { + 'cpu-cores-available': 'cpu/available', + 'cpu-cores-utilized': 'cpu/util', + 'cpu-manufacturer': 'cpu/manufacturer', + }, + output: ['cpu-tdp', 'tdp'], + }; + expect(mapConfigIfNeeded(config, mapping)).toEqual(expected); + }); + + it('returns an empty object or array when config is an empty object or array.', () => { + expect(mapConfigIfNeeded({}, {})).toEqual({}); + expect(mapConfigIfNeeded([], {})).toEqual([]); + }); + }); }); diff --git a/src/__tests__/if-run/builtins/coefficient.test.ts b/src/__tests__/if-run/builtins/coefficient.test.ts index b715a8b61..9c8a912d0 100644 --- a/src/__tests__/if-run/builtins/coefficient.test.ts +++ b/src/__tests__/if-run/builtins/coefficient.test.ts @@ -18,7 +18,7 @@ describe('builtins/coefficient: ', () => { inputs: {}, outputs: {}, }; - const coefficient = Coefficient(config, parametersMetadata); + const coefficient = Coefficient(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -53,9 +53,39 @@ describe('builtins/coefficient: ', () => { expect(result).toStrictEqual(expectedResult); }); - it('throws an error when config is not provided.', () => { + it('succcessfully executes when the mapping has data.', () => { + const mapping = { + carbon: 'carbon-for-production', + }; + + const coefficient = Coefficient(config, parametersMetadata, mapping); + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + 'carbon-for-production': 3, + 'carbon-product': 9, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = coefficient.execute([ + { + duration: 3600, + 'carbon-for-production': 3, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error when global config is not provided.', () => { const config = undefined; - const coefficient = Coefficient(config!, parametersMetadata); + const coefficient = Coefficient(config!, parametersMetadata, {}); expect.assertions(1); @@ -78,7 +108,7 @@ describe('builtins/coefficient: ', () => { coefficient: 3, 'output-parameter': 'carbon-product', }; - const coefficient = Coefficient(invalidConfig, parametersMetadata); + const coefficient = Coefficient(invalidConfig, parametersMetadata, {}); const expectedMessage = '"input-parameter" parameter is string must contain at least 1 character(s). Error code: too_small.'; @@ -105,7 +135,7 @@ describe('builtins/coefficient: ', () => { coefficient: 10, 'output-parameter': '', }; - const coefficient = Coefficient(invalidConfig, parametersMetadata); + const coefficient = Coefficient(invalidConfig, parametersMetadata, {}); const expectedMessage = '"output-parameter" parameter is string must contain at least 1 character(s). Error code: too_small.'; diff --git a/src/__tests__/if-run/builtins/copy-param.test.ts b/src/__tests__/if-run/builtins/copy-param.test.ts index 4c1b2d682..33c079fd9 100644 --- a/src/__tests__/if-run/builtins/copy-param.test.ts +++ b/src/__tests__/if-run/builtins/copy-param.test.ts @@ -18,7 +18,7 @@ describe('builtins/copy: ', () => { inputs: {}, outputs: {}, }; - const copy = Copy(config, parametersMetadata); + const copy = Copy(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -51,9 +51,37 @@ describe('builtins/copy: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executed when `mapping` has valid data.', () => { + expect.assertions(1); + + const mapping = { + original: 'from', + }; + + const copy = Copy(config, parametersMetadata, mapping); + const expectedResult = [ + { + duration: 3600, + from: 'hello', + copy: 'hello', + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = copy.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + from: 'hello', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error when config is not provided.', () => { const config = undefined; - const copy = Copy(config!, parametersMetadata); + const copy = Copy(config!, parametersMetadata, {}); expect.assertions(1); @@ -76,7 +104,7 @@ describe('builtins/copy: ', () => { from: 'original', to: 'copy', }; - const copy = Copy(config, parametersMetadata); + const copy = Copy(config, parametersMetadata, {}); expect.assertions(1); try { @@ -101,8 +129,8 @@ describe('builtins/copy: ', () => { from: 'original', to: 'copy', }; - const copy = Copy(config, parametersMetadata); + const copy = Copy(config, parametersMetadata, {}); const expectedResult = [ { duration: 3600, diff --git a/src/__tests__/if-run/builtins/csv-lookup.test.ts b/src/__tests__/if-run/builtins/csv-lookup.test.ts index 50d1b1ee0..1248878b5 100644 --- a/src/__tests__/if-run/builtins/csv-lookup.test.ts +++ b/src/__tests__/if-run/builtins/csv-lookup.test.ts @@ -19,13 +19,13 @@ const { const {MISSING_CONFIG, MISSING_CSV_COLUMN, NO_QUERY_DATA} = STRINGS; describe('builtins/CSVLookup: ', () => { - const parametersMetadata = { - inputs: {}, - outputs: {}, - }; const mock = new AxiosMockAdapter(axios); describe('CSVLookup: ', () => { + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; afterEach(() => { mock.reset(); }); @@ -39,7 +39,9 @@ describe('builtins/CSVLookup: ', () => { }, output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + + const csvLookup = CSVLookup(config, parametersMetadata, {}); + expect(csvLookup).toHaveProperty('metadata'); expect(csvLookup).toHaveProperty('execute'); }); @@ -58,8 +60,8 @@ describe('builtins/CSVLookup: ', () => { }, output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const responseData = `cpu-cores-available,cpu-cores-utilized,cpu-manufacturer,cpu-model-name,cpu-tdp,gpu-count,gpu-model-name,Hardware Information on AWS Documentation & Comments,instance-class,instance-storage,memory-available,platform-memory,release-date,storage-drives 16,8,AWS,AWS Graviton,150.00,N/A,N/A,AWS Graviton (ARM),a1.2xlarge,EBS-Only,16,32,November 2018,0 16,16,AWS,AWS Graviton,150.00,N/A,N/A,AWS Graviton (ARM),a1.4xlarge,EBS-Only,32,32,November 2018,0`; @@ -97,8 +99,8 @@ describe('builtins/CSVLookup: ', () => { }, output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -120,6 +122,44 @@ describe('builtins/CSVLookup: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` has valid data.', async () => { + expect.assertions(1); + const globalConfig = { + filepath: './file.csv', + query: { + 'cpu-cores-available': 'cpu/available', + 'cpu-cores-utilized': 'cpu/utilized', + 'cpu-manufacturer': 'cpu/manufacturer', + }, + output: ['cpu-tdp', 'tdp'], + }; + const parameterMetadata = {inputs: {}, outputs: {}}; + const mapping = { + 'cpu/utilized': 'cpu/util', + }; + const csvLookup = CSVLookup(globalConfig, parameterMetadata, mapping); + + const result = await csvLookup.execute([ + { + timestamp: '2024-03-01', + 'cpu/available': 16, + 'cpu/util': 16, + 'cpu/manufacturer': 'AWS', + }, + ]); + const expectedResult = [ + { + timestamp: '2024-03-01', + 'cpu/available': 16, + 'cpu/util': 16, + 'cpu/manufacturer': 'AWS', + tdp: 150, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); + it('rejects with file not found error.', async () => { const config = { filepath: './file-fail.csv', @@ -130,7 +170,8 @@ describe('builtins/CSVLookup: ', () => { }, output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + + const csvLookup = CSVLookup(config, parametersMetadata, {}); const input = [ { timestamp: '2024-03-01', @@ -159,7 +200,7 @@ describe('builtins/CSVLookup: ', () => { }, output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const input = [ { timestamp: '2024-03-01', @@ -191,7 +232,7 @@ describe('builtins/CSVLookup: ', () => { }; mock.onGet(config.filepath).reply(404); - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const input = [ { timestamp: '2024-03-01', @@ -221,8 +262,7 @@ describe('builtins/CSVLookup: ', () => { }, output: '*', }; - const csvLookup = CSVLookup(config, parametersMetadata); - + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -269,8 +309,8 @@ describe('builtins/CSVLookup: ', () => { ['gpu-model-name', 'gpumodel'], ], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -304,8 +344,8 @@ describe('builtins/CSVLookup: ', () => { }, output: 'gpu-count', }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -339,7 +379,7 @@ describe('builtins/CSVLookup: ', () => { output: ['cpu-tdp', 'tdp'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const input = [ { timestamp: '2024-03-01', @@ -396,7 +436,7 @@ describe('builtins/CSVLookup: ', () => { }, output: 'mock', }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const input = [ { timestamp: '2024-03-01', @@ -427,8 +467,8 @@ describe('builtins/CSVLookup: ', () => { }, output: ['gpu-count'], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -461,8 +501,8 @@ describe('builtins/CSVLookup: ', () => { }, output: [['gpu-count']], }; - const csvLookup = CSVLookup(config, parametersMetadata); + const csvLookup = CSVLookup(config, parametersMetadata, {}); const result = await csvLookup.execute([ { timestamp: '2024-03-01', @@ -497,7 +537,8 @@ describe('builtins/CSVLookup: ', () => { }, output: [['gpu-count']], }; - const csvLookup = CSVLookup(config, parametersMetadata); + + const csvLookup = CSVLookup(config, parametersMetadata, {}); try { await csvLookup.execute([ diff --git a/src/__tests__/if-run/builtins/divide.test.ts b/src/__tests__/if-run/builtins/divide.test.ts index 5802b0d40..ff5e912e7 100644 --- a/src/__tests__/if-run/builtins/divide.test.ts +++ b/src/__tests__/if-run/builtins/divide.test.ts @@ -18,7 +18,7 @@ describe('builtins/divide: ', () => { inputs: {}, outputs: {}, }; - const divide = Divide(config, parametersMetadata); + const divide = Divide(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -51,6 +51,34 @@ describe('builtins/divide: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` has valid data.', async () => { + expect.assertions(1); + const mapping = { + 'vcpus-allocated': 'vcpus-distributed', + }; + + const divide = Divide(config, parametersMetadata, mapping); + + const expectedResult = [ + { + duration: 3600, + 'vcpus-distributed': 24, + 'cpu/number-cores': 12, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = await divide.execute([ + { + duration: 3600, + 'vcpus-distributed': 24, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('returns a result when `denominator` is provded in input.', async () => { expect.assertions(1); const config = { @@ -58,8 +86,8 @@ describe('builtins/divide: ', () => { denominator: 'duration', output: 'vcpus-allocated-per-second', }; - const divide = Divide(config, parametersMetadata); + const divide = Divide(config, parametersMetadata, {}); const input = [ { timestamp: '2021-01-01T00:00:00Z', @@ -90,7 +118,8 @@ describe('builtins/divide: ', () => { denominator: 3600, output: 'vcpus-allocated-per-second', }; - const divide = Divide(config, parametersMetadata); + + const divide = Divide(config, parametersMetadata, {}); expect.assertions(1); @@ -111,7 +140,7 @@ describe('builtins/divide: ', () => { it('throws an error on missing config.', async () => { const config = undefined; - const divide = Divide(config!, parametersMetadata); + const divide = Divide(config!, parametersMetadata, {}); expect.assertions(1); @@ -133,7 +162,7 @@ describe('builtins/divide: ', () => { denominator: 0, output: 'vcpus-allocated-per-second', }; - const divide = Divide(config, parametersMetadata); + const divide = Divide(config, parametersMetadata, {}); expect.assertions(1); @@ -161,7 +190,8 @@ describe('builtins/divide: ', () => { denominator: '10', output: 'vcpus-allocated-per-second', }; - const divide = Divide(config, parametersMetadata); + + const divide = Divide(config, parametersMetadata, {}); expect.assertions(1); diff --git a/src/__tests__/if-run/builtins/exponent.test.ts b/src/__tests__/if-run/builtins/exponent.test.ts index 2c828bd5f..e7b3fa9b8 100644 --- a/src/__tests__/if-run/builtins/exponent.test.ts +++ b/src/__tests__/if-run/builtins/exponent.test.ts @@ -15,7 +15,8 @@ describe('builtins/exponent: ', () => { inputs: {}, outputs: {}, }; - const exponent = Exponent(config, parametersMetadata); + + const exponent = Exponent(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -48,6 +49,37 @@ describe('builtins/exponent: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` has valid data.', async () => { + expect.assertions(1); + const mapping = { + 'energy/base': 'energy/main', + }; + const globalConfig = { + 'input-parameter': 'energy/base', + exponent: 3, + 'output-parameter': 'energy', + }; + const exponent = Exponent(globalConfig, parametersMetadata, mapping); + const expectedResult = [ + { + duration: 3600, + 'energy/main': 2, + energy: 8, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = await exponent.execute([ + { + duration: 3600, + 'energy/main': 2, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error on missing params in input.', async () => { expect.assertions(1); @@ -95,7 +127,7 @@ describe('builtins/exponent: ', () => { exponent: 4, 'output-parameter': 'carbon', }; - const exponent = Exponent(newConfig, parametersMetadata); + const exponent = Exponent(newConfig, parametersMetadata, {}); const data = [ { diff --git a/src/__tests__/if-run/builtins/interpolation.test.ts b/src/__tests__/if-run/builtins/interpolation.test.ts index 904111e8f..a2dad9a12 100644 --- a/src/__tests__/if-run/builtins/interpolation.test.ts +++ b/src/__tests__/if-run/builtins/interpolation.test.ts @@ -29,7 +29,8 @@ describe('builtins/interpolation: ', () => { 'cpu/utilization': 45, }, ]; - const plugin = Interpolation(config, parametersMetadata); + + const plugin = Interpolation(config, parametersMetadata, {}); describe('init Interpolation: ', () => { it('initalizes object with properties.', async () => { @@ -52,6 +53,38 @@ describe('builtins/interpolation: ', () => { expect(plugin.execute(inputs)).toEqual(outputs); }); + it('returns result when `mapping` has valid data.', () => { + const mapping = { + 'cpu/utilization': 'cpu/util', + 'interpolation-result': 'result', + }; + const config = { + method: Method.LINEAR, + x: [0, 10, 50, 100], + y: [0.12, 0.32, 0.75, 1.02], + 'input-parameter': 'cpu/utilization', + 'output-parameter': 'interpolation-result', + }; + const inputs = [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/util': 45, + }, + ]; + const plugin = Interpolation(config, parametersMetadata, mapping); + const outputs = [ + { + timestamp: '2023-07-06T00:00', + duration: 3600, + 'cpu/util': 45, + result: 0.69625, + }, + ]; + + expect(plugin.execute(inputs)).toEqual(outputs); + }); + it('returns result when the `method` is not provided in the config.', () => { const config = { x: [0, 10, 50, 100], @@ -59,8 +92,8 @@ describe('builtins/interpolation: ', () => { 'input-parameter': 'cpu/utilization', 'output-parameter': 'interpolation-result', }; - const plugin = Interpolation(config, parametersMetadata); + const plugin = Interpolation(config, parametersMetadata, {}); const outputs = [ { timestamp: '2023-07-06T00:00', @@ -75,7 +108,7 @@ describe('builtins/interpolation: ', () => { it('returns result when the `method` is `spline`.', () => { const newConfig = Object.assign({}, config, {method: Method.SPLINE}); - const plugin = Interpolation(newConfig, parametersMetadata); + const plugin = Interpolation(newConfig, parametersMetadata, {}); const outputs = [ { @@ -93,7 +126,7 @@ describe('builtins/interpolation: ', () => { const newConfig = Object.assign({}, config, { method: Method.POLYNOMIAL, }); - const plugin = Interpolation(newConfig, parametersMetadata); + const plugin = Interpolation(newConfig, parametersMetadata, {}); const outputs = [ { @@ -111,8 +144,7 @@ describe('builtins/interpolation: ', () => { const newConfig = Object.assign({}, config, { x: [0, 10, 100, 50], }); - const plugin = Interpolation(newConfig, parametersMetadata); - + const plugin = Interpolation(newConfig, parametersMetadata, {}); const outputs = [ { timestamp: '2023-07-06T00:00', @@ -147,7 +179,7 @@ describe('builtins/interpolation: ', () => { it('throws an when the config is not provided.', () => { const config = undefined; - const plugin = Interpolation(config!, parametersMetadata); + const plugin = Interpolation(config!, parametersMetadata, {}); expect.assertions(2); try { @@ -162,8 +194,7 @@ describe('builtins/interpolation: ', () => { const newConfig = Object.assign({}, config, { x: [0, 10, 100], }); - - const plugin = Interpolation(newConfig, parametersMetadata); + const plugin = Interpolation(newConfig, parametersMetadata, {}); expect.assertions(2); try { @@ -197,8 +228,9 @@ describe('builtins/interpolation: ', () => { 'input-parameter': 'cpu/utilization', 'output-parameter': 'interpolation-result', }; + const config = Object.assign({}, basicConfig, {method: Method.SPLINE}); - const plugin = Interpolation(config, parametersMetadata); + const plugin = Interpolation(config, parametersMetadata, {}); const inputs = [ { timestamp: '2023-07-06T00:00', diff --git a/src/__tests__/if-run/builtins/mock-observations.test.ts b/src/__tests__/if-run/builtins/mock-observations.test.ts index af5acbf6c..a3ad83fc7 100644 --- a/src/__tests__/if-run/builtins/mock-observations.test.ts +++ b/src/__tests__/if-run/builtins/mock-observations.test.ts @@ -12,27 +12,28 @@ describe('builtins/mock-observations: ', () => { inputs: {}, outputs: {}, }; - describe('init: ', () => { it('successfully initalized.', () => { - const mockObservations = MockObservations( - { - 'timestamp-from': '2023-07-06T00:00', - 'timestamp-to': '2023-07-06T00:01', - duration: 5, - components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], - generators: { - common: { - region: 'uk-west', - 'common-key': 'common-val', - }, - randint: { - 'cpu/utilization': {min: 10, max: 95}, - 'memory/utilization': {min: 10, max: 85}, - }, + const globalConfig = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, }, }, - parametersMetadata + }; + const mockObservations = MockObservations( + globalConfig, + parametersMetadata, + {} ); expect(mockObservations).toHaveProperty('metadata'); @@ -57,7 +58,7 @@ describe('builtins/mock-observations: ', () => { }, }, }; - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations(config, parametersMetadata, {}); const result = await mockObservations.execute([]); expect.assertions(1); @@ -98,6 +99,70 @@ describe('builtins/mock-observations: ', () => { ]); }); + it('executes successfully when `mapping` is provided.', 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: { + 'cpu/util': {min: 10, max: 11}, + }, + }, + }; + const mapping = { + 'cpu/utilization': 'cpu/util', + }; + const mockObservations = MockObservations( + config, + parametersMetadata, + mapping + ); + const result = await mockObservations.execute([]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2023-07-06T00:00:00.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'A1', + region: 'uk-west', + 'cpu/util': 10, + }, + { + timestamp: '2023-07-06T00:00:30.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'A1', + region: 'uk-west', + 'cpu/util': 10, + }, + { + timestamp: '2023-07-06T00:00:00.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'B1', + region: 'uk-west', + 'cpu/util': 10, + }, + { + timestamp: '2023-07-06T00:00:30.000Z', + duration: 30, + 'common-key': 'common-val', + 'instance-type': 'B1', + region: 'uk-west', + 'cpu/util': 10, + }, + ]); + }); + it('throws an error when the `min` is greater then `max` of `randint` config.', async () => { const config = { 'timestamp-from': '2023-07-06T00:00', @@ -117,7 +182,7 @@ describe('builtins/mock-observations: ', () => { expect.assertions(2); - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations(config, parametersMetadata, {}); try { await mockObservations.execute([]); } catch (error) { @@ -135,11 +200,14 @@ describe('builtins/mock-observations: ', () => { duration: 5, components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], }; - expect.assertions(2); try { - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations( + config, + parametersMetadata, + {} + ); await mockObservations.execute([]); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); @@ -169,11 +237,14 @@ describe('builtins/mock-observations: ', () => { }, }, }; - expect.assertions(2); try { - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations( + config, + parametersMetadata, + {} + ); await mockObservations.execute([]); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); @@ -185,23 +256,25 @@ describe('builtins/mock-observations: ', () => { expect.assertions(2); try { - const mockObservations = MockObservations( - { - 'timestamp-from': '2023-07-06T00:00', - 'timestamp-to': '2023-07-06T00:01', - components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], - generators: { - common: { - region: 'uk-west', - 'common-key': 'common-val', - }, - randint: { - 'cpu/utilization': {min: 10, max: 95}, - 'memory/utilization': {min: 10, max: 85}, - }, + const globalConfig = { + 'timestamp-from': '2023-07-06T00:00', + 'timestamp-to': '2023-07-06T00:01', + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, }, }, - parametersMetadata + }; + const mockObservations = MockObservations( + globalConfig, + parametersMetadata, + {} ); await mockObservations.execute([]); } catch (error) { @@ -218,25 +291,26 @@ describe('builtins/mock-observations: ', () => { expect.assertions(2); try { - const mockObservations = MockObservations( - { - 'timestamp-from': '2023-07-06T00:00', - duration: 5, - components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], - generators: { - common: { - region: 'uk-west', - 'common-key': 'common-val', - }, - randint: { - 'cpu/utilization': {min: 10, max: 95}, - 'memory/utilization': {min: 10, max: 85}, - }, + const globalConfig = { + 'timestamp-from': '2023-07-06T00:00', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, }, }, - parametersMetadata + }; + const mockObservations = MockObservations( + globalConfig, + parametersMetadata, + {} ); - await mockObservations.execute([]); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); @@ -252,25 +326,26 @@ describe('builtins/mock-observations: ', () => { expect.assertions(2); try { - const mockObservations = MockObservations( - { - 'timestamp-to': '2023-07-06T00:01', - duration: 5, - components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], - generators: { - common: { - region: 'uk-west', - 'common-key': 'common-val', - }, - randint: { - 'cpu/utilization': {min: 10, max: 95}, - 'memory/utilization': {min: 10, max: 85}, - }, + const globalConfig = { + 'timestamp-to': '2023-07-06T00:01', + duration: 5, + components: [{'instance-type': 'A1'}, {'instance-type': 'B1'}], + generators: { + common: { + region: 'uk-west', + 'common-key': 'common-val', + }, + randint: { + 'cpu/utilization': {min: 10, max: 95}, + 'memory/utilization': {min: 10, max: 85}, }, }, - parametersMetadata + }; + const mockObservations = MockObservations( + globalConfig, + parametersMetadata, + {} ); - await mockObservations.execute([]); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); @@ -296,7 +371,7 @@ describe('builtins/mock-observations: ', () => { randint: null, }, }; - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations(config, parametersMetadata, {}); expect.assertions(2); @@ -326,7 +401,7 @@ describe('builtins/mock-observations: ', () => { }, }, }; - const mockObservations = MockObservations(config, parametersMetadata); + const mockObservations = MockObservations(config, parametersMetadata, {}); expect.assertions(2); diff --git a/src/__tests__/if-run/builtins/multiply.test.ts b/src/__tests__/if-run/builtins/multiply.test.ts index 5deee49c0..7588d7689 100644 --- a/src/__tests__/if-run/builtins/multiply.test.ts +++ b/src/__tests__/if-run/builtins/multiply.test.ts @@ -14,7 +14,8 @@ describe('builtins/multiply: ', () => { inputs: {}, outputs: {}, }; - const multiply = Multiply(config, parametersMetadata); + + const multiply = Multiply(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -51,6 +52,43 @@ describe('builtins/multiply: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` is provided.', async () => { + expect.assertions(1); + const mapping = { + 'cpu/energy': 'energy-from-cpu', + 'network/energy': 'energy-from-network', + 'memory/energy': 'energy-from-memory', + }; + const globalConfig = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': 'energy', + }; + const multiply = Multiply(globalConfig, parametersMetadata, mapping); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-from-cpu': 2, + 'energy-from-network': 2, + 'energy-from-memory': 2, + energy: 8, + }, + ]; + + const result = await multiply.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-from-cpu': 2, + 'energy-from-network': 2, + 'energy-from-memory': 2, + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error on missing params in input.', async () => { expect.assertions(1); @@ -76,7 +114,7 @@ describe('builtins/multiply: ', () => { 'input-parameters': ['carbon', 'other-carbon'], 'output-parameter': 'carbon-product', }; - const multiply = Multiply(newConfig, parametersMetadata); + const multiply = Multiply(newConfig, parametersMetadata, {}); const data = [ { diff --git a/src/__tests__/if-run/builtins/regex.test.ts b/src/__tests__/if-run/builtins/regex.test.ts index 9265043be..864cde571 100644 --- a/src/__tests__/if-run/builtins/regex.test.ts +++ b/src/__tests__/if-run/builtins/regex.test.ts @@ -18,7 +18,7 @@ describe('builtins/regex: ', () => { inputs: {}, outputs: {}, }; - const regex = Regex(config, parametersMetadata); + const regex = Regex(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -59,8 +59,7 @@ describe('builtins/regex: ', () => { match: '/(?<=_)[^_]+?(?=_|$)/g', output: 'cloud/instance-type', }; - const regex = Regex(config, parametersMetadata); - + const regex = Regex(config, parametersMetadata, {}); const expectedResult = [ { timestamp: '2023-08-06T00:00', @@ -80,6 +79,37 @@ describe('builtins/regex: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully applies regex when `mapping` has valid data.', async () => { + const globalConfig = { + parameter: 'cloud/instance-type', + match: '/(?<=_)[^_]+?(?=_|$)/g', + output: 'cloud/instance-type', + }; + + const mapping = { + 'cloud/instance-type': 'instance-type', + }; + const regex = Regex(globalConfig, parametersMetadata, mapping); + + const expectedResult = [ + { + timestamp: '2023-08-06T00:00', + duration: 3600, + 'instance-type': 'DS1 v2', + }, + ]; + + const result = await regex.execute([ + { + timestamp: '2023-08-06T00:00', + duration: 3600, + 'instance-type': 'Standard_DS1_v2', + }, + ]); + + 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'; @@ -90,8 +120,7 @@ describe('builtins/regex: ', () => { match: '[^,]+/', output: 'cpu/name', }; - const regex = Regex(config, parametersMetadata); - + const regex = Regex(config, parametersMetadata, {}); const expectedResult = [ { timestamp: '2021-01-01T00:00:00Z', @@ -121,7 +150,8 @@ describe('builtins/regex: ', () => { match: '^(^:)+', output: 'cpu/name', }; - const regex = Regex(config, parametersMetadata); + + const regex = Regex(config, parametersMetadata, {}); expect.assertions(1); @@ -144,7 +174,7 @@ describe('builtins/regex: ', () => { it('throws an error on missing config.', async () => { const config = undefined; - const regex = Regex(config!, parametersMetadata); + const regex = Regex(config!, parametersMetadata, {}); expect.assertions(1); diff --git a/src/__tests__/if-run/builtins/sci-embodied.test.ts b/src/__tests__/if-run/builtins/sci-embodied.test.ts index 0e2c234f7..bf3ebfb1e 100644 --- a/src/__tests__/if-run/builtins/sci-embodied.test.ts +++ b/src/__tests__/if-run/builtins/sci-embodied.test.ts @@ -13,7 +13,7 @@ describe('builtins/sci-embodied:', () => { inputs: {}, outputs: {}, }; - const sciEmbodied = SciEmbodied(parametersMetadata); + const sciEmbodied = SciEmbodied(undefined, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -69,6 +69,56 @@ describe('builtins/sci-embodied:', () => { ]); }); + it('executes when `mapping` has valid data.', async () => { + const mapping = { + 'device/emissions-embodied': 'device/carbon-footprint', + }; + const sciEmbodied = SciEmbodied(undefined, parametersMetadata, mapping); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/carbon-footprint': 200, + '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/carbon-footprint': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + }, + ]; + + const result = await sciEmbodied.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30, + 'device/carbon-footprint': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + 'carbon-embodied': 4.10958904109589, + }, + { + timestamp: '2021-01-01T00:00:00Z', + duration: 60 * 60 * 24 * 30 * 2, + 'device/carbon-footprint': 200, + 'device/expected-lifespan': 60 * 60 * 24 * 365 * 4, + 'resources-reserved': 1, + 'resources-total': 1, + 'carbon-embodied': 4.10958904109589 * 2, + }, + ]); + }); + it('returns a result when `vcpus-allocated` and `vcpus-total` are in the input.', async () => { const inputs = [ { diff --git a/src/__tests__/if-run/builtins/sci.test.ts b/src/__tests__/if-run/builtins/sci.test.ts index 0360149a9..143d34757 100644 --- a/src/__tests__/if-run/builtins/sci.test.ts +++ b/src/__tests__/if-run/builtins/sci.test.ts @@ -6,11 +6,9 @@ const {MissingInputDataError} = ERRORS; describe('builtins/sci:', () => { describe('Sci: ', () => { - const parametersMetadata = { - inputs: {}, - outputs: {}, - }; - const sci = Sci({'functional-unit': 'users'}, parametersMetadata); + const config = {'functional-unit': 'users'}; + const parametersMetadata = {inputs: {}, outputs: {}}; + const sci = Sci(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -21,12 +19,6 @@ describe('builtins/sci:', () => { describe('execute():', () => { it('returns a result with valid inputs.', async () => { - const sci = Sci( - { - 'functional-unit': 'users', - }, - parametersMetadata - ); const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -54,13 +46,41 @@ describe('builtins/sci:', () => { ]); }); - it('returns the same result regardless of input duration.', async () => { - const sci = Sci( + it('successfully executes when `mapping` has valid data.', async () => { + const mapping = { + 'carbon-footprint': 'carbon-embodied', + }; + const sci = Sci(config, parametersMetadata, mapping); + const inputs = [ { - 'functional-unit': 'requests', + timestamp: '2021-01-01T00:00:00Z', + duration: 1, + 'carbon-operational': 0.02, + 'carbon-embodied': 5, + carbon: 5.02, + users: 100, }, - parametersMetadata - ); + ]; + const result = await sci.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.02, + 'carbon-embodied': 5, + carbon: 5.02, + users: 100, + duration: 1, + sci: 0.050199999999999995, + }, + ]); + }); + + it('returns the same result regardless of input duration.', async () => { + const config = {'functional-unit': 'requests'}; + const sci = Sci(config, parametersMetadata, {}); const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -106,12 +126,8 @@ describe('builtins/sci:', () => { }); it('throws exception on invalid functional unit data.', async () => { - const sci = Sci( - { - 'functional-unit': 'requests', - }, - parametersMetadata - ); + const config = {'functional-unit': 'requests'}; + const sci = Sci(config, parametersMetadata, {}); const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -131,12 +147,8 @@ describe('builtins/sci:', () => { }); it('throws exception if functional unit value is not positive integer.', async () => { - const sci = Sci( - { - 'functional-unit': 'requests', - }, - parametersMetadata - ); + const config = {'functional-unit': 'requests'}; + const sci = Sci(config, parametersMetadata, {}); const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -158,12 +170,8 @@ describe('builtins/sci:', () => { }); it('fallbacks to carbon value, if functional unit is 0.', async () => { - const sci = Sci( - { - 'functional-unit': 'requests', - }, - parametersMetadata - ); + const config = {'functional-unit': 'requests'}; + const sci = Sci(config, parametersMetadata, {}); const inputs = [ { timestamp: '2021-01-01T00:00:00Z', diff --git a/src/__tests__/if-run/builtins/shell.test.ts b/src/__tests__/if-run/builtins/shell.test.ts index 20b6a7e1c..125595ced 100644 --- a/src/__tests__/if-run/builtins/shell.test.ts +++ b/src/__tests__/if-run/builtins/shell.test.ts @@ -10,12 +10,13 @@ jest.mock('child_process'); jest.mock('js-yaml'); describe('builtins/shell', () => { - const parametersMetadata = { - inputs: {}, - outputs: {}, - }; describe('Shell', () => { - const shell = Shell({}, parametersMetadata); + const globalConfig = {command: 'python3 /path/to/script.py'}; + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + const shell = Shell(globalConfig, parametersMetadata); describe('init: ', () => { it('successfully initalized.', () => { @@ -25,11 +26,7 @@ describe('builtins/shell', () => { }); describe('execute(): ', () => { - it('execute with valid inputs and command', async () => { - const shell = Shell( - {command: 'python3 /path/to/script.py'}, - parametersMetadata - ); + it('executes with valid inputs and command.', async () => { const mockSpawnSync = spawnSync as jest.MockedFunction< typeof spawnSync >; @@ -59,11 +56,13 @@ describe('builtins/shell', () => { expect(mockLoadAll).toHaveBeenCalledWith('mocked stdout'); }); - it('throw an error if validation fails', async () => { + it('throws an error if validation fails.', async () => { + const shell = Shell({}, parametersMetadata); const invalidInputs = [ {duration: 3600, timestamp: '2022-01-01T00:00:00Z', command: 123}, ]; + expect.assertions(2); try { await shell.execute(invalidInputs); } catch (error) { @@ -76,11 +75,8 @@ describe('builtins/shell', () => { } }); - it('throw an error when shell could not run command.', async () => { - const shell = Shell( - {command: 'python3 /path/to/script.py'}, - parametersMetadata - ); + it('throws an error when shell could not run command.', async () => { + const shell = Shell(globalConfig, parametersMetadata); (spawnSync as jest.Mock).mockImplementation(() => { throw new InputValidationError('Could not run the command'); }); diff --git a/src/__tests__/if-run/builtins/subtract.test.ts b/src/__tests__/if-run/builtins/subtract.test.ts index 1f2189fee..7831f308d 100644 --- a/src/__tests__/if-run/builtins/subtract.test.ts +++ b/src/__tests__/if-run/builtins/subtract.test.ts @@ -14,7 +14,7 @@ describe('builtins/subtract: ', () => { inputs: {}, outputs: {}, }; - const subtract = Subtract(config, parametersMetadata); + const subtract = Subtract(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -51,6 +51,41 @@ describe('builtins/subtract: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` is provided.', async () => { + const mapping = { + 'cpu/energy': 'energy-for-cpu', + }; + const globalConfig = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': 'energy/diff', + }; + const subtract = Subtract(globalConfig, parametersMetadata, mapping); + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + 'energy-for-cpu': 4, + 'network/energy': 2, + 'memory/energy': 1, + 'energy/diff': 1, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = await subtract.execute([ + { + duration: 3600, + 'energy-for-cpu': 4, + 'network/energy': 2, + 'memory/energy': 1, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error on missing params in input.', async () => { expect.assertions(1); @@ -76,7 +111,7 @@ describe('builtins/subtract: ', () => { 'input-parameters': ['carbon', 'other-carbon'], 'output-parameter': 'carbon-diff', }; - const subtract = Subtract(newConfig, parametersMetadata); + const subtract = Subtract(newConfig, parametersMetadata, {}); const data = [ { diff --git a/src/__tests__/if-run/builtins/sum.test.ts b/src/__tests__/if-run/builtins/sum.test.ts index 071e075d5..e742a2762 100644 --- a/src/__tests__/if-run/builtins/sum.test.ts +++ b/src/__tests__/if-run/builtins/sum.test.ts @@ -17,7 +17,7 @@ describe('builtins/sum: ', () => { inputs: {}, outputs: {}, }; - const sum = Sum(config, parametersMetadata); + const sum = Sum(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -54,9 +54,47 @@ describe('builtins/sum: ', () => { expect(result).toStrictEqual(expectedResult); }); + it('successfully executes when `mapping` has valid data.', () => { + expect.assertions(1); + + const mapping = { + 'cpu/energy': 'energy-from-cpu', + 'network/energy': 'energy-from-network', + }; + const config = { + 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], + 'output-parameter': 'energy', + }; + + const sum = Sum(config, parametersMetadata, mapping); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-from-cpu': 1, + 'energy-from-network': 1, + 'memory/energy': 1, + energy: 3, + }, + ]; + + const result = sum.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'energy-from-cpu': 1, + 'energy-from-network': 1, + 'memory/energy': 1, + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + it('throws an error when config is not provided.', () => { const config = undefined; - const sum = Sum(config!, parametersMetadata); + const sum = Sum(config!, parametersMetadata, {}); expect.assertions(1); @@ -100,7 +138,7 @@ describe('builtins/sum: ', () => { 'input-parameters': ['carbon', 'other-carbon'], 'output-parameter': 'carbon-sum', }; - const sum = Sum(newConfig, parametersMetadata); + const sum = Sum(newConfig, parametersMetadata, {}); const data = [ { diff --git a/src/__tests__/if-run/builtins/time-converter.test.ts b/src/__tests__/if-run/builtins/time-converter.test.ts index 72a71cf5d..4791c6640 100644 --- a/src/__tests__/if-run/builtins/time-converter.test.ts +++ b/src/__tests__/if-run/builtins/time-converter.test.ts @@ -19,7 +19,7 @@ describe('builtins/time-converter: ', () => { inputs: {}, outputs: {}, }; - const timeConverter = TimeConverter(config, parametersMetadata); + const timeConverter = TimeConverter(config, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -54,7 +54,7 @@ describe('builtins/time-converter: ', () => { it('throws an error when config is not provided.', () => { const config = undefined; - const timeConverter = TimeConverter(config!, parametersMetadata); + const timeConverter = TimeConverter(config!, parametersMetadata, {}); expect.assertions(1); @@ -98,7 +98,7 @@ describe('builtins/time-converter: ', () => { 'new-time-unit': 'month', 'output-parameter': 'energy-per-duration', }; - const timeConverter = TimeConverter(newConfig, parametersMetadata); + const timeConverter = TimeConverter(newConfig, parametersMetadata, {}); const data = [ { diff --git a/src/__tests__/if-run/builtins/time-sync.test.ts b/src/__tests__/if-run/builtins/time-sync.test.ts index 76fc4aeb3..b3350fbe0 100644 --- a/src/__tests__/if-run/builtins/time-sync.test.ts +++ b/src/__tests__/if-run/builtins/time-sync.test.ts @@ -72,10 +72,6 @@ describe('builtins/time-sync:', () => { }); describe('time-sync: ', () => { - const parametersMetadata = { - inputs: {}, - outputs: {}, - }; const basicConfig = { 'start-time': '2023-12-12T00:01:00.000Z', 'end-time': '2023-12-12T00:01:00.000Z', @@ -83,7 +79,11 @@ describe('builtins/time-sync:', () => { 'allow-padding': true, }; - const timeSync = TimeSync(basicConfig, parametersMetadata); + const parametersMetadata = { + inputs: {}, + outputs: {}, + }; + const timeSync = TimeSync(basicConfig, parametersMetadata, {}); describe('init: ', () => { it('successfully initalized.', () => { @@ -91,717 +91,775 @@ describe('builtins/time-sync:', () => { expect(timeSync).toHaveProperty('execute'); }); }); - }); -}); - -describe('execute(): ', () => { - const parametersMetadata = { - inputs: {}, - outputs: {}, - }; - - it('throws error if `start-time` is missing.', async () => { - const invalidStartTimeConfig = { - 'start-time': '', - 'end-time': '2023-12-12T00:01:00.000Z', - interval: 5, - 'allow-padding': true, - }; - - const timeModel = TimeSync(invalidStartTimeConfig, parametersMetadata); - - expect.assertions(1); - - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - { - timestamp: '2023-12-12T00:00:10.000Z', - duration: 30, - 'cpu/utilization': 20, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InputValidationError( - '"start-time" parameter is invalid datetime. Error code: invalid_string.' - ) - ); - } - }); - - it('throws error if `end-time` is missing.', async () => { - const errorMessage = - '"end-time" parameter is invalid datetime. Error code: invalid_string.,`start-time` should be lower than `end-time`'; - const invalidEndTimeConfig = { - 'start-time': '2023-12-12T00:01:00.000Z', - 'end-time': '', - interval: 5, - 'allow-padding': true, - }; - const timeModel = TimeSync(invalidEndTimeConfig, parametersMetadata); - - expect.assertions(1); - - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - { - timestamp: '2023-12-12T00:00:10.000Z', - duration: 30, - 'cpu/utilization': 20, - }, - ]); - } catch (error) { - expect(error).toStrictEqual(new InputValidationError(errorMessage)); - } - }); - - it('fails if `start-time` is not a valid ISO date.', async () => { - const invalidStartTimeConfig = { - 'start-time': '0023-X', - 'end-time': '2023-12-12T00:01:00.000Z', - interval: 5, - 'allow-padding': true, - }; - const timeModel = TimeSync(invalidStartTimeConfig, parametersMetadata); - expect.assertions(1); - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InputValidationError( - '"start-time" parameter is invalid datetime. Error code: invalid_string.' - ) - ); - } - }); - it('fails if `end-time` is not a valid ISO date.', async () => { - const invalidEndTimeConfig = { - 'start-time': '2023-12-12T00:01:00.000Z', - 'end-time': '20XX', - interval: 5, - 'allow-padding': true, - }; - const timeModel = TimeSync(invalidEndTimeConfig, parametersMetadata); - - expect.assertions(1); - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InputValidationError( - '"end-time" parameter is invalid datetime. Error code: invalid_string.' - ) - ); - } - }); - - it('throws error on missing config.', async () => { - const config = undefined; - const timeModel = TimeSync(config!, parametersMetadata); - - 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 ConfigError(INVALID_TIME_NORMALIZATION)); - } - }); - - it('throws error if interval is invalid.', async () => { - const invalidIntervalConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:01:00.000Z', - interval: 0, - 'allow-padding': true, - }; - - const timeModel = TimeSync(invalidIntervalConfig, parametersMetadata); - - 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 InvalidInputError(INVALID_OBSERVATION_OVERLAP) - ); - } - }); - - it('throws error if timestamps overlap.', 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, parametersMetadata); - - 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 InvalidInputError(INVALID_OBSERVATION_OVERLAP) - ); - } - }); - - 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, parametersMetadata); - - 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 required in input[0]. 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, - }; + describe('execute(): ', () => { + it('throws error if `start-time` is missing.', async () => { + const invalidStartTimeConfig = { + 'start-time': '', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync( + invalidStartTimeConfig, + parametersMetadata, + {} + ); + + expect.assertions(1); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '"start-time" parameter is invalid datetime. Error code: invalid_string.' + ) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - 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 datetime in input[0]. Error code: invalid_string.' - ) - ); - } - }); + it('throws error if `end-time` is missing.', async () => { + const errorMessage = + '"end-time" parameter is invalid datetime. Error code: invalid_string.,`start-time` should be lower than `end-time`'; + const invalidEndTimeConfig = { + 'start-time': '2023-12-12T00:01:00.000Z', + 'end-time': '', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync( + invalidEndTimeConfig, + parametersMetadata, + {} + ); + + expect.assertions(1); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new InputValidationError(errorMessage)); + } + }); - 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 data = [ - { - timestamp: 45, - duration: 10, - 'cpu/utilization': 10, - }, - ]; - - const timeModel = TimeSync(basicConfig, parametersMetadata); - expect.assertions(2); - - try { - await timeModel.execute(data); - } catch (error) { - expect(error).toBeInstanceOf(InvalidDateInInputError); - expect(error).toStrictEqual( - new InvalidDateInInputError(INVALID_DATE_TYPE(data[0].timestamp)) - ); - } - }); + it('fails if `start-time` is not a valid ISO date.', async () => { + const invalidStartTimeConfig = { + 'start-time': '0023-X', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync( + invalidStartTimeConfig, + parametersMetadata, + {} + ); + expect.assertions(1); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '"start-time" parameter is invalid datetime. Error code: invalid_string.' + ) + ); + } + }); - it('throws error if end is before start in config.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:10.000Z', - 'end-time': '2023-12-12T00:00:00.000Z', - interval: 5, - 'allow-padding': true, - }; + it('fails if `end-time` is not a valid ISO date.', async () => { + const invalidEndTimeConfig = { + 'start-time': '2023-12-12T00:01:00.000Z', + 'end-time': '20XX', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync( + invalidEndTimeConfig, + parametersMetadata, + {} + ); + + expect.assertions(1); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '"end-time" parameter is invalid datetime. Error code: invalid_string.' + ) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - 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('`start-time` should be lower than `end-time`') - ); - } - }); + it('throws error on missing config.', async () => { + const config = undefined; + const timeModel = TimeSync(config!, parametersMetadata, {}); + + 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 ConfigError(INVALID_TIME_NORMALIZATION) + ); + } + }); - it('converts Date objects to string outputs.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:00:01.000Z', - interval: 1, - 'allow-padding': false, - }; + it('throws error if interval is invalid.', async () => { + const invalidIntervalConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:01:00.000Z', + interval: 0, + 'allow-padding': true, + }; + const timeModel = TimeSync( + invalidIntervalConfig, + parametersMetadata, + {} + ); + + 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 InvalidInputError(INVALID_OBSERVATION_OVERLAP) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - const result = await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 1, - 'cpu/utilization': 10, - }, - { - timestamp: new Date('2023-12-12T00:00:01.000Z'), - duration: 1, - 'cpu/utilization': 10, - }, - ]); - - const expectedResult = [ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 1, - }, - { - timestamp: '2023-12-12T00:00:01.000Z', - duration: 1, - }, - ]; - - expect(result).toStrictEqual(expectedResult); - }); + it('throws error if timestamps overlap.', 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, parametersMetadata, {}); + + 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 InvalidInputError(INVALID_OBSERVATION_OVERLAP) + ); + } + }); - it('checks that metric (carbon) with aggregation-method == sum is properly spread over interpolated time points.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:00:10.000Z', - interval: 1, - 'allow-padding': true, - }; - storeAggregationMetrics({carbon: 'sum'}); - - const timeModel = TimeSync(basicConfig, parametersMetadata); - - const result = await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - carbon: 10, - }, - ]); - - const expectedResult = [ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:01.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:02.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:03.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:04.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:05.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:06.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:07.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:08.000Z', - duration: 1, - carbon: 1, - }, - { - timestamp: '2023-12-12T00:00:09.000Z', - duration: 1, - carbon: 1, - }, - ]; - - expect(result).toStrictEqual(expectedResult); - }); + 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, parametersMetadata, {}); + + 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 required in input[0]. Error code: invalid_union.' + ) + ); + } + }); - it('checks that constants are copied to results unchanged.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:00:09.000Z', - interval: 5, - 'allow-padding': true, - }; + 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, parametersMetadata, {}); + + 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 datetime in input[0]. Error code: invalid_string.' + ) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - const result = await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 3, - 'resources-total': 10, - }, - { - timestamp: '2023-12-12T00:00:05.000Z', - duration: 3, - 'resources-total': 10, - }, - ]); - - const expectedResult = [ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 5, - }, - { - timestamp: '2023-12-12T00:00:05.000Z', - duration: 5, - }, - ]; - - expect(result).toStrictEqual(expectedResult); - }); + 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 data = [ + { + timestamp: 45, + duration: 10, + 'cpu/utilization': 10, + }, + ]; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + expect.assertions(2); + + try { + await timeModel.execute(data); + } catch (error) { + expect(error).toBeInstanceOf(InvalidDateInInputError); + expect(error).toStrictEqual( + new InvalidDateInInputError(INVALID_DATE_TYPE(data[0].timestamp)) + ); + } + }); - 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, - }; - storeAggregationMetrics({'time-reserved': 'avg'}); - storeAggregationMetrics({'resources-total': 'sum'}); - - const timeModel = TimeSync(basicConfig, parametersMetadata); - - 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('throws error if end is before start in global config.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:10.000Z', + 'end-time': '2023-12-12T00:00:00.000Z', + interval: 5, + 'allow-padding': true, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + 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( + '`start-time` should be lower than `end-time`' + ) + ); + } + }); - 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, - }; + it('converts Date objects to string outputs.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:01.000Z', + interval: 1, + 'allow-padding': false, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + 'cpu/utilization': 10, + }, + { + timestamp: new Date('2023-12-12T00:00:01.000Z'), + duration: 1, + 'cpu/utilization': 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + }, + { + timestamp: '2023-12-12T00:00:01.000Z', + duration: 1, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - 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( - '"start-time" parameter is invalid datetime. Error code: invalid_string.' - ) - ); - } - }); + it('checks that metric (carbon) with aggregation-method == sum is properly spread over interpolated time points.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:10.000Z', + interval: 1, + 'allow-padding': true, + }; + + storeAggregationMetrics({carbon: 'sum'}); + + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + carbon: 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:01.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:02.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:03.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:04.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:06.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:07.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:08.000Z', + duration: 1, + carbon: 1, + }, + { + timestamp: '2023-12-12T00:00:09.000Z', + duration: 1, + carbon: 1, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); - 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', - interval: 5, - 'allow-padding': true, - }; - storeAggregationMetrics({'resources-total': 'none'}); - - const timeModel = TimeSync(basicConfig, parametersMetadata); - - 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('checks that constants are copied to results unchanged.', 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, parametersMetadata, {}); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3, + 'resources-total': 10, + }, + ]); + + /**In each 5 second interval, 60% of the time cpu/utilization = 10, 40% of the time it is 0, so cpu/utilization in the averaged result be 6 */ + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + }, + ]; + + 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', - 'end-time': '2023-12-12T00:00:10.000Z', - interval: 5, - 'allow-padding': false, - }; + 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, + }; + + storeAggregationMetrics({'time-reserved': 'avg'}); + storeAggregationMetrics({'resources-total': 'sum'}); + + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + 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); + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:02.000Z', - duration: 15, - 'cpu/utilization': 10, - }, - { - timestamp: '2023-12-12T00:00:10.000Z', - duration: 30, - 'cpu/utilization': 20, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, false)) - ); - } - }); + it('returns a result when `mapping` has valid data.', 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 mapping = { + 'time-reserved': 'time-allocated', + }; + + storeAggregationMetrics({'time-allocated': 'avg'}); + storeAggregationMetrics({'resources-total': 'sum'}); + + const timeModel = TimeSync(basicConfig, parametersMetadata, mapping); + + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3, + 'time-allocated': 5, + 'resources-total': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3, + 'time-allocated': 5, + 'resources-total': 10, + }, + ]); + + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + 'resources-total': 10, + 'time-allocated': 3.2, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + 'resources-total': 10, + 'time-allocated': 3.2, + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); - it('throws error if padding is required at end while allow-padding = false.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:00:10.000Z', - interval: 5, - 'allow-padding': false, - }; + 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, parametersMetadata, {}); + + 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( + '"start-time" parameter is invalid datetime. Error code: invalid_string.' + ) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - { - timestamp: '2023-12-12T00:00:10.000Z', - duration: 30, - 'cpu/utilization': 20, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InputValidationError('Avoiding padding at end') - ); - } - }); + 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', + interval: 5, + 'allow-padding': true, + }; + + storeAggregationMetrics({'resources-total': 'none'}); + + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + 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 and end while allow-padding = false.', async () => { - const basicConfig = { - 'start-time': '2023-12-12T00:00:00.000Z', - 'end-time': '2023-12-12T00:00:10.000Z', - interval: 5, - 'allow-padding': false, - }; + it('throws error if padding is required at start while allow-padding = false.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:10.000Z', + interval: 5, + 'allow-padding': false, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:02.000Z', + duration: 15, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, false)) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - - try { - await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:02.000Z', - duration: 10, - 'cpu/utilization': 10, - }, - { - timestamp: '2023-12-12T00:00:08.000Z', - duration: 1, - 'cpu/utilization': 20, - }, - ]); - } catch (error) { - expect(error).toStrictEqual( - new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, true)) - ); - } - }); + it('throws error if padding is required at end while allow-padding = false.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:10.000Z', + interval: 5, + 'allow-padding': false, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError('Avoiding padding at end') + ); + } + }); - 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', - interval: 1, - 'allow-padding': true, - }; + it('throws error if padding is required at start and end while allow-padding = false.', async () => { + const basicConfig = { + 'start-time': '2023-12-12T00:00:00.000Z', + 'end-time': '2023-12-12T00:00:10.000Z', + interval: 5, + 'allow-padding': false, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:02.000Z', + duration: 10, + 'cpu/utilization': 10, + }, + { + timestamp: '2023-12-12T00:00:08.000Z', + duration: 1, + 'cpu/utilization': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, true)) + ); + } + }); - const timeModel = TimeSync(basicConfig, parametersMetadata); - const result = await timeModel.execute([ - { - timestamp: '2023-12-12T00:00:00.000Z', - duration: 1, - carbon: 1, - }, - ]); - expect( - DateTime.fromISO(result[0].timestamp).zone.valueOf() === - 'FixedOffsetZone { fixed: 0 }' - ); - expect(DateTime.fromISO(result[0].timestamp).offset === 0); + 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', + interval: 1, + 'allow-padding': true, + }; + const timeModel = TimeSync(basicConfig, parametersMetadata, {}); + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + carbon: 1, + }, + ]); + expect( + DateTime.fromISO(result[0].timestamp).zone.valueOf() === + 'FixedOffsetZone { fixed: 0 }' + ); + expect(DateTime.fromISO(result[0].timestamp).offset === 0); + }); + }); }); }); diff --git a/src/common/util/helpers.ts b/src/common/util/helpers.ts index 01b87c79d..f9cc69db4 100644 --- a/src/common/util/helpers.ts +++ b/src/common/util/helpers.ts @@ -2,6 +2,7 @@ import {createInterface} from 'node:readline/promises'; import {exec} from 'child_process'; import * as path from 'path'; import {promisify} from 'util'; +import {MappingParams, PluginParams} from '@grnsft/if-core/types'; /** * Promise version of Node's `exec` from `child-process`. @@ -63,3 +64,51 @@ export const parseManifestFromStdin = async () => { return match![1]; }; + +/** + * Maps input data if the mapping has valid data. + */ +export const mapInputIfNeeded = ( + input: PluginParams, + mapping: MappingParams +) => { + const newInput = Object.assign({}, input); + + Object.entries(mapping || {}).forEach(([key, value]) => { + if (value in newInput) { + const mappedKey = input[value]; + newInput[key] = mappedKey; + delete newInput[value]; + } + }); + + return newInput; +}; + +/** + * Maps config data if the mapping hass valid data. + */ +export const mapConfigIfNeeded = (config: any, mapping: MappingParams) => { + if (!mapping) { + return config; + } + + if (typeof config !== 'object' || config === null) { + return config; + } + + const result: Record = Array.isArray(config) ? [] : {}; + + Object.entries(config).forEach(([key, value]) => { + const mappedKey = mapping[key] || key; + + if (typeof value === 'object' && value !== null) { + result[mappedKey] = mapConfigIfNeeded(value, mapping); + } else { + result[mappedKey] = + typeof value === 'string' && value in mapping ? mapping[value] : value; + } + }); + + return result; +}; diff --git a/src/common/util/validations.ts b/src/common/util/validations.ts index 20d1f3952..646aee3fc 100644 --- a/src/common/util/validations.ts +++ b/src/common/util/validations.ts @@ -81,6 +81,7 @@ export const manifestSchema = z.object({ .object({ path: z.string(), method: z.string(), + mapping: z.record(z.string(), z.string()).optional(), config: z.record(z.string(), z.any()).optional(), 'parameter-metadata': parameterMetadataSchema, }) diff --git a/src/if-run/builtins/coefficient/README.md b/src/if-run/builtins/coefficient/README.md index 4ea300abf..218936041 100644 --- a/src/if-run/builtins/coefficient/README.md +++ b/src/if-run/builtins/coefficient/README.md @@ -32,6 +32,18 @@ of the parameters of the inputs and outputs - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +coefficient: + method: Coefficient + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs All of `input-parameters` must be available in the input array. @@ -51,13 +63,15 @@ output = input * coefficient To run the plugin from a Typescript app, you must first create an instance of `Coefficient`. Then, you can call `execute()`. ```typescript -const config = { +const globalConfig = { 'input-parameter': 'carbon', coefficient: 10, 'output-parameter': 'carbon-product', }; +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; -const coeff = Coefficient(config); +const coeff = Coefficient(globalConfig, parametersMetadata, mapping); const result = coeff.execute([ { duration: 3600, @@ -84,15 +98,17 @@ initialize: input-parameter: 'carbon' coefficient: 3 output-parameter: 'carbon-product' - parameter-metadata: + parameter-metadata: inputs: carbon: - description: "an amount of carbon emitted into the atmosphere" - unit: "gCO2e" + description: 'an amount of carbon emitted into the atmosphere' + unit: 'gCO2e' + aggregation-method: sum outputs: carbon-product: - description: "a product of cabon property and the coefficient" - unit: "gCO2e" + description: 'a product of cabon property and the coefficient' + unit: 'gCO2e' + aggregation-method: sum tree: children: child: diff --git a/src/if-run/builtins/coefficient/index.ts b/src/if-run/builtins/coefficient/index.ts index 8f15dbee1..4e2b29bbc 100644 --- a/src/if-run/builtins/coefficient/index.ts +++ b/src/if-run/builtins/coefficient/index.ts @@ -3,11 +3,13 @@ import {ERRORS} from '@grnsft/if-core/utils'; import { CoefficientConfig, ExecutePlugin, + MappingParams, PluginParametersMetadata, PluginParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG} = STRINGS; export const Coefficient = ( config: CoefficientConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -73,13 +76,15 @@ export const Coefficient = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const configSchema = z.object({ coefficient: z.number(), 'input-parameter': z.string().min(1), 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; return { diff --git a/src/if-run/builtins/copy-param/README.md b/src/if-run/builtins/copy-param/README.md index abe33d221..997540c76 100644 --- a/src/if-run/builtins/copy-param/README.md +++ b/src/if-run/builtins/copy-param/README.md @@ -54,6 +54,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +copy-param: + path: builtin + method: Copy + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs As with all plugins, `timestamp` and `duration` are required. The key passed to `from` must exist in the `input` data. @@ -69,11 +81,15 @@ To run the plugin, you must first create an instance of `Copy`. Then, you can ca ```typescript import {Copy} from '.'; -const plugin = Copy({ +const globalConfig = { 'keep-existing': true, from: 'from-param', to: 'to-param', -}); +}; +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; + +const plugin = Copy(globalConfig, parametersMetadata, mapping); const result = plugin.execute([ { diff --git a/src/if-run/builtins/copy-param/index.ts b/src/if-run/builtins/copy-param/index.ts index b4a5dbce1..cbee3252a 100644 --- a/src/if-run/builtins/copy-param/index.ts +++ b/src/if-run/builtins/copy-param/index.ts @@ -1,7 +1,9 @@ import {z} from 'zod'; import {ERRORS} from '@grnsft/if-core/utils'; import { + ConfigParams, ExecutePlugin, + MappingParams, PluginParametersMetadata, PluginParams, } from '@grnsft/if-core/types'; @@ -9,16 +11,21 @@ import { import {validate} from '../../../common/util/validations'; import {STRINGS} from '../../config'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; const {MISSING_CONFIG} = STRINGS; const {ConfigError} = ERRORS; -// keep-existing: true/false (whether to remove the parameter you are copying from) -// from-param: the parameter you are copying from (e.g. cpu/name) -// to-field: the parameter you are copying to (e.g. cpu/processor-name) + +/** + * keep-existing: true/false (whether to remove the parameter you are copying from) + * from-param: the parameter you are copying from (e.g. cpu/name) + * to-field: the parameter you are copying to (e.g. cpu/processor-name) + */ export const Copy = ( - config: Record, - parametersMetadata: PluginParametersMetadata + config: ConfigParams, + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -34,13 +41,15 @@ export const Copy = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const configSchema = z.object({ 'keep-existing': z.boolean(), from: z.string().min(1).or(z.number()), to: z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; /** diff --git a/src/if-run/builtins/csv-lookup/README.md b/src/if-run/builtins/csv-lookup/README.md index 30f1788e6..e5059c5bb 100644 --- a/src/if-run/builtins/csv-lookup/README.md +++ b/src/if-run/builtins/csv-lookup/README.md @@ -68,6 +68,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +cloud-metadata: + method: CSVLookup + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs There are no strict requirements on input for this plugin because they depend upon the contents of the target CSV and your input data at the time the CSV lookup is invoked. Please make sure you are requesting data from columns that exist in the target csv file and that your query values are available in your `input` data. @@ -99,7 +111,9 @@ const config = { }, output: ['cpu-tdp', 'tdp'], }; -const csvLookup = CSVLookup(config); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const csvLookup = CSVLookup(config, parametersMetadata, mapping); const input = [ { @@ -131,6 +145,8 @@ initialize: cloud-provider: 'cloud/provider' cloud-region: 'cloud/region' output: '*' + mapping: + cloud/region: cloud/area tree: children: child: @@ -141,7 +157,7 @@ tree: - timestamp: 2023-08-06T00:00 duration: 3600 cloud/provider: Google Cloud - cloud/region: europe-north1 + cloud/area: europe-north1 ``` You can run this example by saving it as `./examples/manifests/csv-lookup.yml` and executing the following command from the project root: diff --git a/src/if-run/builtins/csv-lookup/index.ts b/src/if-run/builtins/csv-lookup/index.ts index e1960e14d..38b7b112f 100644 --- a/src/if-run/builtins/csv-lookup/index.ts +++ b/src/if-run/builtins/csv-lookup/index.ts @@ -7,6 +7,7 @@ import {parse} from 'csv-parse/sync'; import {ERRORS} from '@grnsft/if-core/utils'; import { ExecutePlugin, + MappingParams, PluginParametersMetadata, PluginParams, } from '@grnsft/if-core/types'; @@ -14,6 +15,7 @@ import { import {validate} from '../../../common/util/validations'; import {STRINGS} from '../../config'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; const { FILE_FETCH_FAILED, @@ -34,7 +36,8 @@ const { export const CSVLookup = ( config: any, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -199,7 +202,6 @@ export const CSVLookup = ( const execute = async (inputs: PluginParams[]) => { const safeGlobalConfig = validateConfig(); const {filepath, query, output} = safeGlobalConfig; - const file = await retrieveFile(filepath); const parsedCSV = parseCSVFile(file); @@ -233,6 +235,7 @@ export const CSVLookup = ( if (!config) { throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); const configSchema = z.object({ filepath: z.string(), @@ -243,7 +246,7 @@ export const CSVLookup = ( .or(z.array(z.array(z.string()))), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; return { diff --git a/src/if-run/builtins/divide/README.md b/src/if-run/builtins/divide/README.md index be0d0ec50..b7b296cda 100644 --- a/src/if-run/builtins/divide/README.md +++ b/src/if-run/builtins/divide/README.md @@ -22,11 +22,23 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) -- `outputs`: describe the parameter of the `denominator` of the config. The parameter has the following attributes: +- `outputs`: describe the parameter of the `denominator` of the global config. The parameter has the following attributes: - `description`: description of the parameter - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +divide: + method: Divide + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs - `numerator` - as input parameter, must be available in the input array @@ -57,7 +69,11 @@ const config = { denominator: 2, output: 'cpu/number-cores', }; -const divide = Divide(config, parametersMetadata); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = { + 'vcpus-allocated': 'vcpus-distributed', +}; +const divide = Divide(config, parametersMetadata, mapping); const input = [ { @@ -85,6 +101,8 @@ initialize: numerator: vcpus-allocated denominator: 2 output: cpu/number-cores + mapping: + vcpus-allocated: vcpus-distributed tree: children: child: @@ -94,7 +112,7 @@ tree: inputs: - timestamp: 2023-08-06T00:00 duration: 3600 - vcpus-allocated: 24 + vcpus-distributed: 24 ``` You can run this example by saving it as `./examples/manifests/divide.yml` and executing the following command from the project root: diff --git a/src/if-run/builtins/divide/index.ts b/src/if-run/builtins/divide/index.ts index 5b39512e7..66ab8faab 100644 --- a/src/if-run/builtins/divide/index.ts +++ b/src/if-run/builtins/divide/index.ts @@ -5,18 +5,21 @@ import { PluginParams, ConfigParams, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; import {STRINGS} from '../../config'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; const {ConfigError, MissingInputDataError} = ERRORS; const {MISSING_CONFIG, MISSING_INPUT_DATA, ZERO_DIVISION} = STRINGS; export const Divide = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -53,13 +56,14 @@ export const Divide = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); const schema = z.object({ numerator: z.string().min(1), denominator: z.string().or(z.number()), output: z.string(), }); - return validate>(schema, config); + return validate>(schema, mappedConfig); }; /** diff --git a/src/if-run/builtins/exponent/README.md b/src/if-run/builtins/exponent/README.md index 8e837b10d..a743fcedb 100644 --- a/src/if-run/builtins/exponent/README.md +++ b/src/if-run/builtins/exponent/README.md @@ -31,6 +31,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +exponent: + method: Exponent + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs `input-parameter` and `exponent` must be available in the input array. @@ -52,13 +64,15 @@ To run the plugin, you must first create an instance of `Exponent`. Then, you ca ```typescript import {Exponent} from 'builtins'; -const config = { - inputParameter: ['cpu/energy'], - exponent: 2 - outputParameter: 'energy', +const globalConfig = { + inputParameter: ['cpu/energy'], + exponent: 2 + outputParameter: 'energy', }; +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; -const exponent = Exponent(config); +const exponent = Exponent(globalConfig, parametersMetadata, mapping); const result = await exponent.execute([ { duration: 3600, diff --git a/src/if-run/builtins/exponent/index.ts b/src/if-run/builtins/exponent/index.ts index 096128fa0..79a0d7ce3 100644 --- a/src/if-run/builtins/exponent/index.ts +++ b/src/if-run/builtins/exponent/index.ts @@ -4,10 +4,12 @@ import { PluginParams, ExponentConfig, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {ERRORS} from '@grnsft/if-core/utils'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG} = STRINGS; export const Exponent = ( config: ExponentConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -32,13 +35,14 @@ export const Exponent = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); const configSchema = z.object({ 'input-parameter': z.string().min(1), exponent: z.number(), 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; /** diff --git a/src/if-run/builtins/interpolation/README.md b/src/if-run/builtins/interpolation/README.md index dc710aa74..6a9097c74 100644 --- a/src/if-run/builtins/interpolation/README.md +++ b/src/if-run/builtins/interpolation/README.md @@ -40,6 +40,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +interpolation: + method: Interpolation + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ## Input Parameters The plugin expects the following input parameters: @@ -88,18 +100,17 @@ const config = { method: 'linear', x: [0, 10, 50, 100], y: [0.12, 0.32, 0.75, 1.02], - 'input-parameter': 'cpu/utilization' - 'output-parameter': 'cpu/energy' - + 'input-parameter': 'cpu/utilization', + 'output-parameter': 'cpu/energy', }; - -const interpolationPlugin = Interpolation(config); +const parametersMetadata = {inputs: {}, outputs: {}}; +const interpolationPlugin = Interpolation(config, parametersMetadata, {}); const inputs = [ { timestamp: '2024-04-16T12:00:00Z', duration: 3600, - 'cpu/utilization': 45 + 'cpu/utilization': 45, }, ]; diff --git a/src/if-run/builtins/interpolation/index.ts b/src/if-run/builtins/interpolation/index.ts index 4a292285c..6c1965f0e 100644 --- a/src/if-run/builtins/interpolation/index.ts +++ b/src/if-run/builtins/interpolation/index.ts @@ -7,9 +7,11 @@ import { ConfigParams, Method, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -19,7 +21,8 @@ const {MISSING_CONFIG, X_Y_EQUAL, ARRAY_LENGTH_NON_EMPTY, WITHIN_THE_RANGE} = export const Interpolation = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -137,6 +140,8 @@ export const Interpolation = ( throw new ConfigError(MISSING_CONFIG); } + config = mapConfigIfNeeded(config, mapping); + const schema = z .object({ method: z.nativeEnum(Method), diff --git a/src/if-run/builtins/mock-observations/README.md b/src/if-run/builtins/mock-observations/README.md index 82f965006..1a69d99a4 100644 --- a/src/if-run/builtins/mock-observations/README.md +++ b/src/if-run/builtins/mock-observations/README.md @@ -32,6 +32,19 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +mock-observations: + kind: plugin + method: MockObservations + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Authentication N/A @@ -44,7 +57,7 @@ The plugin's `config` section in the manifest file determines its behaviour. ### Typescript Usage ```typescript -const mockObservations = MockObservations({ +const globalConfig = { 'timestamp-from': '2023-07-06T00:00', 'timestamp-to': '2023-07-06T00:10', duration: 60, @@ -56,7 +69,14 @@ const mockObservations = MockObservations({ region: 'uk-west', }, }, -}); +}; +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const mockObservations = MockObservations( + globalConfig, + parametersMetadata, + mapping +); const result = await mockObservations.execute([]); ``` diff --git a/src/if-run/builtins/mock-observations/index.ts b/src/if-run/builtins/mock-observations/index.ts index 4bc421601..6a110f004 100644 --- a/src/if-run/builtins/mock-observations/index.ts +++ b/src/if-run/builtins/mock-observations/index.ts @@ -6,10 +6,12 @@ import { ConfigParams, ObservationParams, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {ERRORS} from '@grnsft/if-core/utils'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -23,7 +25,8 @@ const {MISSING_CONFIG} = STRINGS; export const MockObservations = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -68,6 +71,8 @@ export const MockObservations = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const schema = z.object({ 'timestamp-from': z.string(), 'timestamp-to': z.string(), @@ -79,7 +84,7 @@ export const MockObservations = ( }), }); - return validate>(schema, config); + return validate>(schema, mappedConfig); }; /** @@ -93,6 +98,7 @@ export const MockObservations = ( generators, components, } = validateConfig(); + const convertedTimestampFrom = DateTime.fromISO(timestampFrom, { zone: 'UTC', }); diff --git a/src/if-run/builtins/multiply/README.md b/src/if-run/builtins/multiply/README.md index 426a8ba45..a16872ef6 100644 --- a/src/if-run/builtins/multiply/README.md +++ b/src/if-run/builtins/multiply/README.md @@ -30,6 +30,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +multiply: + method: Multiply + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs All of `input-parameters` must be available in the input array. @@ -51,12 +63,14 @@ To run the plugin, you must first create an instance of `Multiply`. Then, you ca ```typescript import {Multiply} from 'builtins'; -const config = { +const globalConfig = { inputParameters: ['cpu/energy', 'network/energy'], outputParameter: 'energy-product', }; -const multiply = Multiply(config, parametersMetadata); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const multiply = Multiply(globalConfig, parametersMetadata, mapping); const result = await multiply.execute([ { duration: 3600, @@ -69,7 +83,7 @@ const result = await multiply.execute([ ## Example manifest -IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by `ie` and does not have to be done explicitly by the user. The following is an example manifest that calls `multiply`: +IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by `if-run` and does not have to be done explicitly by the user. The following is an example manifest that calls `multiply`: ```yaml name: multiply-demo diff --git a/src/if-run/builtins/multiply/index.ts b/src/if-run/builtins/multiply/index.ts index 834bdacaa..65a75259f 100644 --- a/src/if-run/builtins/multiply/index.ts +++ b/src/if-run/builtins/multiply/index.ts @@ -4,10 +4,12 @@ import { PluginParams, MultiplyConfig, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {ERRORS} from '@grnsft/if-core/utils'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG} = STRINGS; export const Multiply = ( config: MultiplyConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -32,12 +35,14 @@ export const Multiply = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const configSchema = z.object({ 'input-parameters': z.array(z.string()), 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; /** diff --git a/src/if-run/builtins/regex/README.md b/src/if-run/builtins/regex/README.md index a4fe4f244..5aff8fb4e 100644 --- a/src/if-run/builtins/regex/README.md +++ b/src/if-run/builtins/regex/README.md @@ -31,6 +31,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +regex: + method: Regex + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs - `parameter` - as input parameter, must be available in the input array @@ -49,7 +61,9 @@ const config = { match: '^[^,]+', output: 'cpu/name', }; -const regex = Regex(config); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const regex = Regex(config, parametersMetadata, mapping); const input = [ { diff --git a/src/if-run/builtins/regex/index.ts b/src/if-run/builtins/regex/index.ts index f405afc76..266363d30 100644 --- a/src/if-run/builtins/regex/index.ts +++ b/src/if-run/builtins/regex/index.ts @@ -5,9 +5,11 @@ import { PluginParams, ConfigParams, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG, MISSING_INPUT_DATA, REGEX_MISMATCH} = STRINGS; export const Regex = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -32,13 +35,15 @@ export const Regex = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const schema = z.object({ parameter: z.string().min(1), match: z.string().min(1), output: z.string(), }); - return validate>(schema, config); + return validate>(schema, mappedConfig); }; /** diff --git a/src/if-run/builtins/sci-embodied/README.md b/src/if-run/builtins/sci-embodied/README.md index a4c304c6d..dd75dea6c 100644 --- a/src/if-run/builtins/sci-embodied/README.md +++ b/src/if-run/builtins/sci-embodied/README.md @@ -25,6 +25,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +sci-embodied: + method: SciEmbodied + path: 'builtins' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs - `device/emissions-embodied`: the sum of Life Cycle Assessment (LCA) emissions for the component @@ -73,7 +85,9 @@ The following snippet demonstrates how to call the `sci-embodied` plugin from Ty ```typescript import {SciEmbodied} from 'builtins'; -const sciEmbodied = SciEmbodied(); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const sciEmbodied = SciEmbodied(undefined, parametersMetadata, mapping); const results = await sciEmbodied.execute([ { 'device/emissions-embodied': 200, // in gCO2e for total resource units @@ -87,7 +101,7 @@ const results = await sciEmbodied.execute([ ## Example manifest -IF users will typically call the plugin as part of a pipeline defined in a `manifest` file. In this case, instantiating the plugin is handled by `ie` and does not have to be done explicitly by the user. The following is an example `manifest` that calls `sci-embodied`: +IF users will typically call the plugin as part of a pipeline defined in a `manifest` file. In this case, instantiating the plugin is handled by `if-run` and does not have to be done explicitly by the user. The following is an example `manifest` that calls `sci-embodied`: ```yaml name: sci-embodied @@ -98,6 +112,8 @@ initialize: sci-embodied: method: SciEmbodied path: 'builtins' + mapping: + device/emissions-embodied: device/carbon-footprint tree: children: child: @@ -105,7 +121,7 @@ tree: compute: - sci-embodied # duration & config -> embodied defaults: - device/emissions-embodied: 1533.120 # gCO2eq + device/carbon-footprint: 1533.120 # gCO2eq device/expected-lifespan: 3 # 3 years in seconds resources-reserved: 1 resources-total: 8 @@ -133,4 +149,4 @@ This error class is used to describe a problem with one of the input values to ` You will receive a specific error message explaining which parameter is problematic, and you can check and replace where appropriate. -For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors) diff --git a/src/if-run/builtins/sci-embodied/index.ts b/src/if-run/builtins/sci-embodied/index.ts index b304d4ae1..b18410454 100644 --- a/src/if-run/builtins/sci-embodied/index.ts +++ b/src/if-run/builtins/sci-embodied/index.ts @@ -2,6 +2,7 @@ import {z} from 'zod'; import { ExecutePlugin, ParameterMetadata, + MappingParams, PluginParametersMetadata, PluginParams, } from '@grnsft/if-core/types'; @@ -9,11 +10,14 @@ import { import {validate, allDefined} from '../../../common/util/validations'; import {STRINGS} from '../../config'; +import {mapInputIfNeeded} from '../../../common/util/helpers'; const {SCI_EMBODIED_ERROR} = STRINGS; export const SciEmbodied = ( - parametersMetadata: PluginParametersMetadata + _config: undefined, + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -76,7 +80,8 @@ export const SciEmbodied = ( */ const execute = (inputs: PluginParams[]) => inputs.map(input => { - const safeInput = validateInput(input); + const mappedInput = mapInputIfNeeded(input, mapping); + const safeInput = validateInput(mappedInput); return { ...input, diff --git a/src/if-run/builtins/sci/README.md b/src/if-run/builtins/sci/README.md index 0211f6a32..c9a11f217 100644 --- a/src/if-run/builtins/sci/README.md +++ b/src/if-run/builtins/sci/README.md @@ -19,10 +19,23 @@ The `parameter-metadata` section contains information about `description`, `unit - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) - `outputs`: describe the `sci` parameter which has the following attributes: + - `description`: description of the parameter - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +sci: + method: Sci + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs - `carbon`: total carbon in gCO2eq (required) @@ -48,12 +61,14 @@ To run the plugin, you must first create an instance of `Sci`. Then, you can cal ```typescript import {Sci} from 'builtins'; - -const sci = Sci({'functional-unit': 'requests'}); +const globalConfig = {'functional-unit': 'requests'} +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const sci = Sci(globalConfig, parametersMetadata, mapping); const results = await sci.execute( [ { - 'carbon': 5' + 'carbon': 5 duration: 1, requests: 100, }, @@ -63,7 +78,7 @@ const results = await sci.execute( ## Example manifest -IF users will typically call the plugin as part of a pipeline defined in a `manifest` file. In this case, instantiating the plugin is handled by `ie` and does not have to be done explicitly by the user. +IF users will typically call the plugin as part of a pipeline defined in a `manifest` file. In this case, instantiating the plugin is handled by `if-run` and does not have to be done explicitly by the user. The following is an example `manifest` that calls `sci`: diff --git a/src/if-run/builtins/sci/index.ts b/src/if-run/builtins/sci/index.ts index 138fe961d..ea97be941 100644 --- a/src/if-run/builtins/sci/index.ts +++ b/src/if-run/builtins/sci/index.ts @@ -6,9 +6,11 @@ import { ConfigParams, PluginParametersMetadata, ParameterMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate, allDefined} from '../../../common/util/validations'; +import {mapInputIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -25,7 +27,8 @@ const { export const Sci = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -76,9 +79,10 @@ export const Sci = ( /** * Calculate the total emissions for a list of inputs. */ - const execute = (inputs: PluginParams[]): PluginParams[] => - inputs.map((input, index) => { - const safeInput = validateInput(input); + const execute = (inputs: PluginParams[]): PluginParams[] => { + return inputs.map((input, index) => { + const mappedInput = mapInputIfNeeded(input, mapping); + const safeInput = validateInput(mappedInput); const functionalUnit = input[config['functional-unit']]; if (functionalUnit === 0) { @@ -95,7 +99,7 @@ export const Sci = ( sci: safeInput['carbon'] / functionalUnit, }; }); - + }; /** * Checks for fields in input. */ diff --git a/src/if-run/builtins/shell/README.md b/src/if-run/builtins/shell/README.md index f21da8a4b..08c9f70f2 100644 --- a/src/if-run/builtins/shell/README.md +++ b/src/if-run/builtins/shell/README.md @@ -53,7 +53,11 @@ The specific return types depend on the plugin being invoked. Typically, we woul To run the plugin, you must first create an instance of `Shell` and call its `execute()` to run the external plugin. ```typescript -const output = Shell({command: '/usr/local/bin/sampler'}); +const globalConfig = { + command: '/usr/local/bin/sampler', +}; +const parametersMetadata = {inputs: {}, outputs: {}}; +const output = Shell(globalConfig, parametersMetadata); const result = await output.execute([ { timestamp: '2021-01-01T00:00:00Z', diff --git a/src/if-run/builtins/shell/index.ts b/src/if-run/builtins/shell/index.ts index 9b81bf16d..7095b69da 100644 --- a/src/if-run/builtins/shell/index.ts +++ b/src/if-run/builtins/shell/index.ts @@ -36,7 +36,7 @@ export const Shell = ( const inputAsString: string = dump(inputs, {indent: 2}); const results = runModelInShell(inputAsString, command); - return results?.outputs?.flat(); + return results?.outputs?.flat() as PluginParams[]; }; /** diff --git a/src/if-run/builtins/subtract/README.md b/src/if-run/builtins/subtract/README.md index e97a9bcf6..fc0fee1dc 100644 --- a/src/if-run/builtins/subtract/README.md +++ b/src/if-run/builtins/subtract/README.md @@ -30,6 +30,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +subtract: + method: Subtract + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs All of `input-parameters` must be available in the input array. @@ -51,12 +63,13 @@ To run the plugin, you must first create an instance of `Subtract`. Then, you ca ```typescript import {Subtract} from 'builtins'; -const config = { +const globalConfig = { inputParameters: ['cpu/energy', 'network/energy'], outputParameter: 'offset/energy', }; - -const subtract = Subtract(config); +const parametersMetadata = {inputs: {}, outputs: {}}; +const mapping = {}; +const subtract = Subtract(globalConfig, parametersMetadata, mapping); const result = subtract subtract.execute([ { duration: 3600, @@ -113,4 +126,4 @@ The results will be saved to a new `yaml` file in `manifests/outputs`. This error arises when an invalid value is passed to `Subtract`. Typically, this can occur when a non-numeric value (such as a string made of alphabetic characters) is passed where a number or numeric string is expected. Please check that the types are correct for all the relevant fields in your `inputs` array. -For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors) diff --git a/src/if-run/builtins/subtract/index.ts b/src/if-run/builtins/subtract/index.ts index 11dd344c1..5d468ade9 100644 --- a/src/if-run/builtins/subtract/index.ts +++ b/src/if-run/builtins/subtract/index.ts @@ -1,6 +1,7 @@ import {z} from 'zod'; import { ExecutePlugin, + MappingParams, PluginParametersMetadata, PluginParams, SubtractConfig, @@ -8,6 +9,7 @@ import { import {ERRORS} from '@grnsft/if-core/utils'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG} = STRINGS; export const Subtract = ( config: SubtractConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -32,12 +35,14 @@ export const Subtract = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const configSchema = z.object({ 'input-parameters': z.array(z.string()), 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; /** diff --git a/src/if-run/builtins/sum/README.md b/src/if-run/builtins/sum/README.md index b11484494..eda6b0612 100644 --- a/src/if-run/builtins/sum/README.md +++ b/src/if-run/builtins/sum/README.md @@ -30,6 +30,18 @@ The `parameter-metadata` section contains information about `description`, `unit - `unit`: unit of the parameter - `aggregation-method`: aggregation method of the parameter (it can be `sum`, `avg` or `none`) +### Mapping + +The `mapping` block is an optional block. It is added in the plugin section and allows the plugin to receive a parameter from the input with a different name than the one the plugin uses for data manipulation. The parameter with the mapped name will not appear in the outputs. The structure of the `mapping` block is: + +```yaml +sum: + method: Sum + path: 'builtin' + mapping: + 'parameter-name-in-the-plugin': 'parameter-name-in-the-input' +``` + ### Inputs All of `input-parameters` must be available in the input array. @@ -49,12 +61,17 @@ output = input0 + input1 + input2 ... inputN To run the plugin, you must first create an instance of `Sum`. Then, you can call `execute()`. ```typescript -const config = { +const globalConfig = { inputParameters: ['cpu/energy', 'network/energy'], outputParameter: 'energy', }; +const parametersMetadata = {inputs: {}, outputs: {}}; +const = mapping { + 'cpu/energy': 'energy-from-cpu', + 'network/energy': 'energy-from-network', +}; -const sum = Sum(config, parametersMetadata); +const sum = Sum(globalConfig, parametersMetadata, mapping); const result = sum.execute([ { timestamp: '2021-01-01T00:00:00Z', @@ -87,10 +104,12 @@ initialize: description: energy consumed by the cpu unit: kWh aggregation-method: sum + aggregation-method: sum network/energy: description: energy consumed by data ingress and egress unit: kWh aggregation-method: sum + aggregation-method: sum outputs: energy: description: sum of energy components diff --git a/src/if-run/builtins/sum/index.ts b/src/if-run/builtins/sum/index.ts index 03d93af0f..3bacdae11 100644 --- a/src/if-run/builtins/sum/index.ts +++ b/src/if-run/builtins/sum/index.ts @@ -5,9 +5,11 @@ import { PluginParams, SumConfig, PluginParametersMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; import {STRINGS} from '../../config'; @@ -16,7 +18,8 @@ const {MISSING_CONFIG} = STRINGS; export const Sum = ( config: SumConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -50,12 +53,14 @@ export const Sum = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); + const configSchema = z.object({ 'input-parameters': z.array(z.string()), 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; /** diff --git a/src/if-run/builtins/time-converter/index.ts b/src/if-run/builtins/time-converter/index.ts index 0b583dbd5..66c35843e 100644 --- a/src/if-run/builtins/time-converter/index.ts +++ b/src/if-run/builtins/time-converter/index.ts @@ -5,6 +5,7 @@ import { PluginParams, PluginParametersMetadata, ConfigParams, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; @@ -12,13 +13,15 @@ import {validate} from '../../../common/util/validations'; import {STRINGS} from '../../config'; import {TIME_UNITS_IN_SECONDS} from './config'; +import {mapConfigIfNeeded} from '../../../common/util/helpers'; const {ConfigError} = ERRORS; const {MISSING_CONFIG} = STRINGS; export const TimeConverter = ( config: ConfigParams, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -77,6 +80,7 @@ export const TimeConverter = ( throw new ConfigError(MISSING_CONFIG); } + const mappedConfig = mapConfigIfNeeded(config, mapping); const timeUnitsValues = Object.keys(TIME_UNITS_IN_SECONDS); const originalTimeUnitValuesWithDuration = [ 'duration', @@ -91,7 +95,7 @@ export const TimeConverter = ( 'output-parameter': z.string().min(1), }); - return validate>(configSchema, config); + return validate>(configSchema, mappedConfig); }; return { metadata, diff --git a/src/if-run/builtins/time-sync/index.ts b/src/if-run/builtins/time-sync/index.ts index 814c27b90..39015b161 100644 --- a/src/if-run/builtins/time-sync/index.ts +++ b/src/if-run/builtins/time-sync/index.ts @@ -11,12 +11,14 @@ import { TimeParams, PluginParametersMetadata, ParameterMetadata, + MappingParams, } from '@grnsft/if-core/types'; import {validate} from '../../../common/util/validations'; import {STRINGS} from '../../config'; import {getAggregationMethod} from '../../lib/aggregate'; +import {mapInputIfNeeded} from '../../../common/util/helpers'; Settings.defaultZone = 'utc'; @@ -54,7 +56,8 @@ const { */ export const TimeSync = ( config: TimeNormalizerConfig, - parametersMetadata: PluginParametersMetadata + parametersMetadata: PluginParametersMetadata, + mapping: MappingParams ): ExecutePlugin => { const metadata = { kind: 'execute', @@ -95,7 +98,12 @@ export const TimeSync = ( const flattenInputs = paddedInputs.reduce( (acc: PluginParams[], input, index) => { - const safeInput = Object.assign({}, input, validateInput(input, index)); + const mappedInput = mapInputIfNeeded(input, mapping); + const safeInput = Object.assign( + {}, + mappedInput, + validateInput(mappedInput, index) + ); const currentMoment = parseDate(safeInput.timestamp); /** Checks if not the first input, then check consistency with previous ones. */ @@ -126,14 +134,14 @@ export const TimeSync = ( ...getZeroishInputPerSecondBetweenRange( compareableTime, currentMoment, - safeInput + input ) ); } } /** Break down current observation. */ - for (let i = 0; i < safeInput.duration; i++) { - const normalizedInput = breakDownInput(safeInput, i); + for (let i = 0; i < input.duration; i++) { + const normalizedInput = breakDownInput(input, i); acc.push(normalizedInput); } @@ -285,7 +293,12 @@ export const TimeSync = ( return acc; } - if (metric === 'time-reserved') { + if ( + metric === 'time-reserved' || + (mapping && + mapping['time-reserved'] && + metric === mapping['time-reserved']) + ) { acc[metric] = acc['duration']; return acc; diff --git a/src/if-run/lib/initialize.ts b/src/if-run/lib/initialize.ts index 6c404e227..c8e18f563 100644 --- a/src/if-run/lib/initialize.ts +++ b/src/if-run/lib/initialize.ts @@ -83,7 +83,8 @@ const initPlugin = async ( const { method, path, - config: config, + mapping, + config, 'parameter-metadata': parameterMetadata, } = initPluginParams!; @@ -99,7 +100,7 @@ const initPlugin = async ( const plugin = await handModule(method, path); - return plugin(config, parameterMetadata); + return plugin(config, parameterMetadata, mapping); }; /**