diff --git a/src/__tests__/if-run/lib/explain.test.ts b/src/__tests__/if-run/lib/explain.test.ts index 506c62669..8178bf07d 100644 --- a/src/__tests__/if-run/lib/explain.test.ts +++ b/src/__tests__/if-run/lib/explain.test.ts @@ -1,8 +1,15 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import {ERRORS} from '@grnsft/if-core/utils'; + +import {STRINGS} from '../../../common/config'; + import {explain, addExplainData} from '../../../if-run/lib/explain'; +const {ManifestValidationError} = ERRORS; +const {AGGREGATION_UNITS_NOT_MATCH, AGGREGATION_METHODS_NOT_MATCH} = STRINGS; + describe('lib/explain: ', () => { - it('successfully adds explain data if `inputs` and `outputs` of `metadata` are `undefined`.', () => { + it('missing explain data if `inputs` and `outputs` of `metadata` are `undefined`.', () => { const mockData = { pluginName: 'divide', metadata: {kind: 'execute', inputs: undefined, outputs: undefined}, @@ -11,19 +18,11 @@ describe('lib/explain: ', () => { method: 'Divide', }, }; - const expectedResult = { - divide: { - method: 'Divide', - path: 'builtin', - inputs: 'undefined', - outputs: 'undefined', - }, - }; addExplainData(mockData); const result = explain(); expect.assertions(1); - expect(result).toEqual(expectedResult); + expect(result).toEqual({}); }); it('successfully adds explain data if `inputs` and `outputs` of `metadata` are valid data.', () => { @@ -56,36 +55,99 @@ describe('lib/explain: ', () => { method: 'Sum', }, }; + const expectedResult = { - divide: { - method: 'Divide', - path: 'builtin', - inputs: 'undefined', - outputs: 'undefined', + 'cpu/energy': { + plugins: ['sum'], + unit: 'kWh', + description: 'energy consumed by the cpu', + 'aggregation-method': 'sum', }, - sum: { - method: 'Sum', - path: 'builtin', + 'network/energy': { + plugins: ['sum'], + unit: 'kWh', + description: 'energy consumed by data ingress and egress', + 'aggregation-method': 'sum', + }, + 'energy-sum': { + plugins: ['sum'], + unit: 'kWh', + description: 'sum of energy components', + 'aggregation-method': 'sum', + }, + }; + + // @ts-ignore + addExplainData(mockData); + + const result = explain(); + + expect.assertions(1); + expect(result).toEqual(expectedResult); + }); + + it('successfully adds explain data if the parameter is using more than one plugin.', () => { + const mockData = { + pluginName: 'sum-energy', + metadata: { + kind: 'execute', inputs: { 'cpu/energy': { unit: 'kWh', description: 'energy consumed by the cpu', 'aggregation-method': 'sum', }, - 'network/energy': { + 'memory/energy': { unit: 'kWh', - description: 'energy consumed by data ingress and egress', + description: 'energy consumed by data from memory', 'aggregation-method': 'sum', }, }, outputs: { - 'energy-sum': { + 'total/energy': { unit: 'kWh', description: 'sum of energy components', 'aggregation-method': 'sum', }, }, }, + pluginData: { + path: 'builtin', + method: 'Sum', + }, + }; + + const expectedResult = { + 'cpu/energy': { + plugins: ['sum', 'sum-energy'], + unit: 'kWh', + description: 'energy consumed by the cpu', + 'aggregation-method': 'sum', + }, + 'network/energy': { + plugins: ['sum'], + unit: 'kWh', + description: 'energy consumed by data ingress and egress', + 'aggregation-method': 'sum', + }, + 'energy-sum': { + plugins: ['sum'], + unit: 'kWh', + description: 'sum of energy components', + 'aggregation-method': 'sum', + }, + 'memory/energy': { + plugins: ['sum-energy'], + unit: 'kWh', + description: 'energy consumed by data from memory', + 'aggregation-method': 'sum', + }, + 'total/energy': { + plugins: ['sum-energy'], + unit: 'kWh', + description: 'sum of energy components', + 'aggregation-method': 'sum', + }, }; // @ts-ignore @@ -96,4 +158,96 @@ describe('lib/explain: ', () => { expect.assertions(1); expect(result).toEqual(expectedResult); }); + + it('throws an error if `unit` of the parameter is not matched.', () => { + const mockData = { + pluginName: 'sum-of-energy', + metadata: { + kind: 'execute', + inputs: { + 'cpu/energy': { + unit: 'co2q', + description: 'energy consumed by the cpu', + 'aggregation-method': 'sum', + }, + 'memory/energy': { + unit: 'kWh', + description: 'energy consumed by data from memory', + 'aggregation-method': 'sum', + }, + }, + outputs: { + 'total/energy': { + unit: 'kWh', + description: 'sum of energy components', + 'aggregation-method': 'sum', + }, + }, + }, + pluginData: { + path: 'builtin', + method: 'Sum', + }, + }; + + expect.assertions(2); + try { + // @ts-ignore + addExplainData(mockData); + explain(); + } catch (error) { + if (error instanceof Error) { + expect(error).toBeInstanceOf(ManifestValidationError); + expect(error.message).toEqual( + AGGREGATION_UNITS_NOT_MATCH('cpu/energy') + ); + } + } + }); + + it('throws an error if `aggregation-method` of the parameter is not matched.', () => { + const mockData = { + pluginName: 'sum-of-energy', + metadata: { + kind: 'execute', + inputs: { + 'cpu/energy': { + unit: 'kWh', + description: 'energy consumed by the cpu', + 'aggregation-method': 'avg', + }, + 'memory/energy': { + unit: 'kWh', + description: 'energy consumed by data from memory', + 'aggregation-method': 'sum', + }, + }, + outputs: { + 'total/energy': { + unit: 'kWh', + description: 'sum of energy components', + 'aggregation-method': 'sum', + }, + }, + }, + pluginData: { + path: 'builtin', + method: 'Sum', + }, + }; + + expect.assertions(2); + try { + // @ts-ignore + addExplainData(mockData); + explain(); + } catch (error) { + if (error instanceof Error) { + expect(error).toBeInstanceOf(ManifestValidationError); + expect(error.message).toEqual( + AGGREGATION_METHODS_NOT_MATCH('cpu/energy') + ); + } + } + }); }); diff --git a/src/common/config/strings.ts b/src/common/config/strings.ts index 82592463f..e5884852b 100644 --- a/src/common/config/strings.ts +++ b/src/common/config/strings.ts @@ -10,4 +10,8 @@ Incubation projects are experimental, offer no support guarantee, have minimal g SUCCESS_MESSAGE: 'The environment is successfully setup!', MANIFEST_IS_MISSING: 'Manifest is missing.', DIRECTORY_NOT_FOUND: 'Directory not found.', + AGGREGATION_UNITS_NOT_MATCH: (param: string) => + `Your manifest uses two instances of ${param} with different units. Please check that you are using consistent units for ${param} throughout your manifest.`, + AGGREGATION_METHODS_NOT_MATCH: (param: string) => + `Your manifest uses two instances of ${param} with different 'aggregation-method'. Please check that you are using right 'aggregation-method' for ${param} throughout your manifest.`, }; diff --git a/src/if-run/lib/explain.ts b/src/if-run/lib/explain.ts index dce49b75a..eb42cfd49 100644 --- a/src/if-run/lib/explain.ts +++ b/src/if-run/lib/explain.ts @@ -1,26 +1,33 @@ -import {ExplainParams} from '../types/explain'; +import {ERRORS} from '@grnsft/if-core/utils'; + +import {STRINGS} from '../../common/config'; + +import {ExplainParams, ExplainStorageType} from '../types/explain'; + +const {ManifestValidationError} = ERRORS; +const {AGGREGATION_UNITS_NOT_MATCH, AGGREGATION_METHODS_NOT_MATCH} = STRINGS; /** * Retrieves stored explain data. */ -export const explain = () => storeExplainData.plugins; +export const explain = () => storeExplainData.parameters; /** * Manages the storage of explain data. */ const storeExplainData = (() => { - let plugin = {}; + let parameter: ExplainStorageType = {}; - const pluginManager = { - get plugins() { - return plugin; + const parameterManager = { + get parameters() { + return parameter; }, - set plugins(value: object) { - plugin = value; + set parameters(value: ExplainStorageType) { + parameter = value; }, }; - return pluginManager; + return parameterManager; })(); /** @@ -28,17 +35,45 @@ const storeExplainData = (() => { */ export const addExplainData = (params: ExplainParams) => { const {pluginName, pluginData, metadata} = params; - const plugin = { - [pluginName]: { - method: pluginData!.method, - path: pluginData!.path, - inputs: metadata?.inputs || 'undefined', - outputs: metadata?.outputs || 'undefined', - }, + const parameterMetadata = pluginData?.['parameter-metadata'] || metadata; + const parameters = storeExplainData.parameters; + const allParameters = { + ...parameterMetadata?.inputs, + ...parameterMetadata?.outputs, }; - storeExplainData.plugins = { - ...storeExplainData.plugins, - ...plugin, + Object.entries(allParameters).forEach(([name, meta]) => { + const existingParameter = parameters[name]; + + if (parameters[name]?.plugins?.includes(pluginName)) { + return; + } + + if (existingParameter) { + if (meta.unit !== existingParameter.unit) { + throw new ManifestValidationError(AGGREGATION_UNITS_NOT_MATCH(name)); + } + + if ( + meta['aggregation-method'] !== existingParameter['aggregation-method'] + ) { + throw new ManifestValidationError(AGGREGATION_METHODS_NOT_MATCH(name)); + } + + existingParameter.plugins.push(pluginName); + existingParameter.description = + meta.description || existingParameter.description; + } else { + parameters[name] = { + plugins: [pluginName], + unit: meta.unit, + description: meta.description, + 'aggregation-method': meta['aggregation-method'], + }; + } + }); + + storeExplainData.parameters = { + ...parameters, }; }; diff --git a/src/if-run/types/explain.ts b/src/if-run/types/explain.ts index c7f1a6d38..c32f91325 100644 --- a/src/if-run/types/explain.ts +++ b/src/if-run/types/explain.ts @@ -7,3 +7,13 @@ export type ExplainParams = { pluginData: PluginOptions; metadata: {inputs?: ParameterMetadata; outputs?: ParameterMetadata}; }; + +export type ExplainStorageType = Record< + string, + { + plugins: string[]; + unit: string; + description: string; + 'aggregation-method': string; + } +>;