From cda7845a8ef140355430046046ff83037579602c Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 1 Mar 2024 21:05:36 +0400 Subject: [PATCH 1/2] feat(util): add validate function - update `validateManifest` function --- src/util/validations.ts | 52 ++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/util/validations.ts b/src/util/validations.ts index 5100318fa..33dc7a512 100644 --- a/src/util/validations.ts +++ b/src/util/validations.ts @@ -1,4 +1,4 @@ -import {ZodIssue, z} from 'zod'; +import {ZodIssue, ZodSchema, z} from 'zod'; import {ERRORS} from './errors'; @@ -6,7 +6,7 @@ import {AGGREGATION_METHODS} from '../types/aggregation'; import {AGGREGATION_TYPES} from '../types/parameters'; import {Manifest} from '../types/manifest'; -const {ManifestValidationError} = ERRORS; +const {ManifestValidationError, InputValidationError} = ERRORS; /** * Validation schema for manifests. @@ -57,27 +57,41 @@ const manifestValidation = z.object({ * Validates given `manifest` object to match pattern. */ export const validateManifest = (manifest: Manifest) => { - const safeManifest = manifestValidation.safeParse(manifest); + return validate(manifestValidation, manifest, ManifestValidationError); +}; + +/** + * Validates given `object` with given `schema`. + */ +export const validate = ( + schema: ZodSchema, + object: any, + errorConstructor: ErrorConstructor = InputValidationError +) => { + const validationResult = schema.safeParse(object); - if (!safeManifest.success) { - const prettifyErrorMessage = (issues: string) => { - const issuesArray = JSON.parse(issues); + if (!validationResult.success) { + throw new errorConstructor( + prettifyErrorMessage(validationResult.error.message) + ); + } - return issuesArray.map((issue: ZodIssue) => { - const {code, path, message} = issue; - const flattenPath = path.map(part => - typeof part === 'number' ? `[${part}]` : part - ); - const fullPath = flattenPath.join('.'); + return validationResult.data; +}; - return `"${fullPath}" parameter is ${message.toLowerCase()}. Error code: ${code}.`; - }); - }; +const prettifyErrorMessage = (issues: string) => { + const issuesArray = JSON.parse(issues); - throw new ManifestValidationError( - prettifyErrorMessage(safeManifest.error.message) + return issuesArray.map((issue: ZodIssue) => { + const {code, path, message} = issue; + const flattenPath = path.map(part => + typeof part === 'number' ? `[${part}]` : part ); - } + const fullPath = flattenPath.join('.'); - return safeManifest.data; + if (code === 'custom') { + return `${message.toLowerCase()}. Error code: ${code}.`; + } + return `"${fullPath}" parameter is ${message.toLowerCase()}. Error code: ${code}.`; + }); }; From 86ab98e7ab606cefa5da94b14be238bb98387182 Mon Sep 17 00:00:00 2001 From: manushak Date: Fri, 1 Mar 2024 21:09:27 +0400 Subject: [PATCH 2/2] fix(src): validate global config parameters, timestamp and duration input parameters - update test according to plugin --- src/__tests__/unit/models/time-sync.test.ts | 1393 ++++--------------- src/models/time-sync.ts | 92 +- 2 files changed, 341 insertions(+), 1144 deletions(-) diff --git a/src/__tests__/unit/models/time-sync.test.ts b/src/__tests__/unit/models/time-sync.test.ts index f0b3e92db..00a344885 100644 --- a/src/__tests__/unit/models/time-sync.test.ts +++ b/src/__tests__/unit/models/time-sync.test.ts @@ -5,7 +5,7 @@ import {STRINGS} from '../../../config'; const {InputValidationError} = ERRORS; -const {INVALID_TIME_NORMALIZATION} = STRINGS; +const {INVALID_OBSERVATION_OVERLAP} = STRINGS; describe('lib/time-sync:', () => { describe('time-sync: ', () => { @@ -45,22 +45,26 @@ describe('execute(): ', () => { { timestamp: '2023-12-12T00:00:00.000Z', duration: 10, - 'cpu-util': 10, + 'cpu/utilizaition': 10, }, { timestamp: '2023-12-12T00:00:10.000Z', duration: 30, - 'cpu-util': 20, + 'cpu/utilizaition': 20, }, ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(INVALID_TIME_NORMALIZATION) + 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`. Error code: custom.'; const invalidEndTimeConfig = { 'start-time': '2023-12-12T00:01:00.000Z', 'end-time': '', @@ -76,943 +80,204 @@ describe('execute(): ', () => { { timestamp: '2023-12-12T00:00:00.000Z', duration: 10, - 'cpu-util': 10, + 'cpu/utilizaition': 10, }, { timestamp: '2023-12-12T00:00:10.000Z', duration: 30, - 'cpu-util': 20, + 'cpu/utilizaition': 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); + 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(INVALID_TIME_NORMALIZATION) + new InputValidationError( + '"start-time" parameter is invalid datetime. Error code: invalid_string.' + ) ); } }); - // 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 = await new TimeSyncModel().configure( - // invalidStartTimeConfig - // ); - // expect.assertions(1); - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 10, - // 'cpu-util': 10, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError(INVALID_TIME_NORMALIZATION) - // ); - // } - // }); - - // 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 = await new TimeSyncModel().configure(invalidEndTimeConfig); - - // expect.assertions(1); - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 10, - // 'cpu-util': 10, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError(INVALID_TIME_NORMALIZATION) - // ); - // } - // }); - - // it('silently fails and drops records if `timestamp` is not a valid ISO date.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:04.000Z', - // interval: 1, - // 'allow-padding': true, - // }; - // const timeModel = await new TimeSyncModel().configure(basicConfig); - - // expect.assertions(1); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:01.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-13x', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:04.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // ]); - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // 'cpu-util': 0, - // duration: 1, - // }, - // { - // timestamp: '2023-12-12T00:00:01.000Z', - // 'cpu-util': 10, - // duration: 1, - // }, - // { - // timestamp: '2023-12-12T00:00:04.000Z', - // 'cpu-util': 10, - // duration: 1, - // }, - // ]; - - // expect(result).toStrictEqual(expectedResult); - // }); - - // 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 = await new TimeSyncModel().configure( - // invalidIntervalConfig - // ); - - // expect.assertions(1); - - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 15, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError(INVALID_TIME_INTERVAL) - // ); - // } - // }); - - // 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 = await new TimeSyncModel().configure(basicConfig); - - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 15, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError(INVALID_OBSERVATION_OVERLAP) - // ); - // } - // }); - - // 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 = await new TimeSyncModel().configure(basicConfig); - - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 15, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError('Start time or end time is missing.') - // ); - // } - // }); - - // it('throws error if end is before start in observation timestamps.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:10.000Z', - // interval: 5, - // 'allow-padding': true, - // }; - - // const timeModel = await new TimeSyncModel().configure(basicConfig); - - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError(INVALID_OBSERVATION_OVERLAP) - // ); - // } - // }); - - // it('converts non-UTC inputs to UTC.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000+01:00', - // 'end-time': '2023-12-12T00:00:10.000+01:00', - // interval: 1, - // 'allow-padding': true, - // }; - - // const timeModel = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000+01:00', - // duration: 2, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000+01:00', - // duration: 2, - // 'cpu-util': 10, - // }, - // ]); - - // const expectedResult = [ - // { - // timestamp: '2023-12-11T23:00:00.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-11T23:00:01.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-11T23:00:02.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:03.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:04.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:05.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-11T23:00:06.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-11T23:00:07.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:08.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:09.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // { - // timestamp: '2023-12-11T23:00:10.000Z', - // duration: 1, - // 'cpu-util': 0, - // }, - // ]; - - // expect(result).toStrictEqual(expectedResult); - // }); - - // 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 = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: new Date('2023-12-12T00:00:01.000Z'), - // duration: 1, - // 'cpu-util': 10, - // }, - // ]); - - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:01.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // ]; - - // expect(result).toStrictEqual(expectedResult); - // }); - - // it('checks breaking down observations case, if padding and zeroish objects are not needed.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:10.000Z', - // interval: 1, - // 'allow-padding': true, - // }; - - // const timeModel = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 5, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'cpu-util': 10, - // }, - // ]); - - // const expectedResult = [ - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:00.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:01.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:02.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:03.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:04.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:05.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:06.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:07.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:08.000Z', - // }, - // { - // 'cpu-util': 10, - // duration: 1, - // timestamp: '2023-12-12T00:00:09.000Z', - // }, - // ]; - - // expect(result).toStrictEqual(expectedResult); - // }); - - // it('checks if padding done if global time frame is bigger than observations frame.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:20.000Z', - // interval: 1, - // 'allow-padding': true, - // }; - - // const timeModel = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 5, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // }, - // ]); - - // const expectedResult = [ - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:00.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:01.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:02.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:03.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:04.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:05.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:06.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:07.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:08.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:09.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:10.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:11.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:12.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:13.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 10, - // carbon: 4, - // duration: 1, - // timestamp: '2023-12-12T00:00:14.000Z', - // 'time-reserved': 10, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:15.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:16.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:17.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:18.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:19.000Z', - // 'time-reserved': 1, - // }, - // { - // 'cpu-util': 0, - // carbon: 0, - // duration: 1, - // timestamp: '2023-12-12T00:00:20.000Z', - // 'time-reserved': 1, - // }, - // ]; + 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); - // expect(result).toStrictEqual(expectedResult); - // }); + expect.assertions(1); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilizaition': 10, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '"end-time" parameter is invalid datetime. Error code: invalid_string.' + ) + ); + } + }); - // /** - // * Checks also while resampling inputs, is average calculated for time frame. - // */ - // it('checks if padding done with interval higher than `1`, if global time frame is bigger than observations frame.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:20.000Z', - // interval: 5, - // 'allow-padding': true, - // }; + 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 = await new TimeSyncModel().configure(basicConfig); + const timeModel = TimeSync(invalidIntervalConfig); - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 5, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // }, - // ]); + expect.assertions(1); - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 5, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 0.8, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'cpu-util': 8, - // carbon: 20, - // 'time-reserved': 8, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 5, - // 'cpu-util': 8, - // carbon: 20, - // 'time-reserved': 8, - // }, - // { - // timestamp: '2023-12-12T00:00:15.000Z', - // duration: 5, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 0.8, - // }, - // { - // timestamp: '2023-12-12T00:00:20.000Z', - // duration: 1, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 1, - // }, - // ]; + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 15, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(INVALID_OBSERVATION_OVERLAP) + ); + } + }); - // 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, + }; - // it('checks if 0ish inputs are applied if there is a gap in time frame.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:00.000Z', - // 'end-time': '2023-12-12T00:00:10.000Z', - // interval: 1, - // 'allow-padding': true, - // }; + const timeModel = TimeSync(basicConfig); - // const timeModel = await new TimeSyncModel().configure(basicConfig); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 15, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(INVALID_OBSERVATION_OVERLAP) + ); + } + }); - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 6, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // ]); + 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 expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:01.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:02.000Z', - // duration: 1, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 1, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:03.000Z', - // duration: 1, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 1, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:04.000Z', - // duration: 1, - // 'cpu-util': 0, - // carbon: 0, - // 'time-reserved': 1, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:06.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:07.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:08.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:09.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 3.3333333333333335, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // ]; + const timeModel = TimeSync(basicConfig); - // expect(result).toStrictEqual(expectedResult); - // }); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 15, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '`start-time` should be lower than `end-time`. Error code: custom.' + ) + ); + } + }); - // it('checks if time series is trimmed when global timeframe is smaller than observed timeframe.', async () => { - // const basicConfig = { - // 'start-time': '2023-12-12T00:00:05.000Z', - // 'end-time': '2023-12-12T00:00:10.000Z', - // interval: 1, - // '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 = await new TimeSyncModel().configure(basicConfig); + const timeModel = TimeSync(basicConfig); - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:02.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:04.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:06.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:08.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:12.000Z', - // duration: 2, - // 'cpu-util': 10, - // carbon: 20, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // ]); + const result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + 'cpu/utilizaition': 10, + }, + { + timestamp: new Date('2023-12-12T00:00:01.000Z'), + duration: 1, + 'cpu/utilizaition': 10, + }, + ]); - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:06.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:07.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:08.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:09.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 1, - // 'cpu-util': 10, - // carbon: 10, - // 'time-reserved': 10, - // 'total-resources': 4, - // }, - // ]; + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 1, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:01.000Z', + duration: 1, + 'cpu/utilizaition': 10, + }, + ]; - // expect(result).toStrictEqual(expectedResult); - // }); + expect(result).toStrictEqual(expectedResult); + }); it('checks that metric (carbon) with aggregation-method == sum is properly spread over interpolated time points.', async () => { const basicConfig = { @@ -1088,203 +353,133 @@ describe('execute(): ', () => { expect(result).toStrictEqual(expectedResult); }); - // it('checks that metric (cpu-util) with aggregation-method == avg is properly spread over interpolated time points.', 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 = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 3, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 3, - // 'cpu-util': 10, - // }, - // ]); - - // /**In each 5 second interval, 60% of the time cpu-util = 10, 40% of the time it is 0, so cpu-util in the averaged result be 6 */ - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 5, - // 'cpu-util': 6, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'cpu-util': 6, - // }, - // ]; - - // 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 = await new TimeSyncModel().configure(basicConfig); - - // const result = await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 3, - // 'total-resources': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 3, - // 'total-resources': 10, - // }, - // ]); - - // /**In each 5 second interval, 60% of the time cpu-util = 10, 40% of the time it is 0, so cpu-util in the averaged result be 6 */ - // const expectedResult = [ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 5, - // 'total-resources': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:05.000Z', - // duration: 5, - // 'total-resources': 10, - // }, - // ]; + 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, + }; - // expect(result).toStrictEqual(expectedResult); - // }); + const timeModel = TimeSync(basicConfig); - // 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 result = await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 3, + 'total-resources': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 3, + 'total-resources': 10, + }, + ]); - // const timeModel = await new TimeSyncModel().configure(basicConfig); + /**In each 5 second interval, 60% of the time cpu/utilizaition = 10, 40% of the time it is 0, so cpu/utilizaition in the averaged result be 6 */ + const expectedResult = [ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 5, + 'total-resources': 10, + }, + { + timestamp: '2023-12-12T00:00:05.000Z', + duration: 5, + 'total-resources': 10, + }, + ]; - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:02.000Z', - // duration: 15, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError('Avoiding padding at start') - // ); - // } - // }); + 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 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 = await new TimeSyncModel().configure(basicConfig); + const timeModel = TimeSync(basicConfig); - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 10, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError('Avoiding padding at end') - // ); - // } - // }); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:02.000Z', + duration: 15, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError('Avoiding padding at start') + ); + } + }); - // 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 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 = await new TimeSyncModel().configure(basicConfig); + const timeModel = TimeSync(basicConfig); - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:02.000Z', - // duration: 10, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:08.000Z', - // duration: 1, - // 'cpu-util': 20, - // }, - // ]); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError('Avoiding padding at start and end') - // ); - // } - // }); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:00.000Z', + duration: 10, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:10.000Z', + duration: 30, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError('Avoiding padding at end') + ); + } + }); - // it('throws error if padding is required on timeline gap 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 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 = await new TimeSync; + const timeModel = TimeSync(basicConfig); - // try { - // await timeModel.execute([ - // { - // timestamp: '2023-12-12T00:00:00.000Z', - // duration: 1, - // 'cpu-util': 10, - // }, - // { - // timestamp: '2023-12-12T00:00:10.000Z', - // duration: 30, - // 'cpu-util': 20, - // }, - // ], basicConfig); - // } catch (error) { - // expect(error).toStrictEqual( - // new InputValidationError('Avoiding padding at timeline gap') - // ); - // } - // }); + try { + await timeModel.execute([ + { + timestamp: '2023-12-12T00:00:02.000Z', + duration: 10, + 'cpu/utilizaition': 10, + }, + { + timestamp: '2023-12-12T00:00:08.000Z', + duration: 1, + 'cpu/utilizaition': 20, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError('Avoiding padding at start and end') + ); + } + }); }); diff --git a/src/models/time-sync.ts b/src/models/time-sync.ts index fdf14cd55..9c11a5e7f 100644 --- a/src/models/time-sync.ts +++ b/src/models/time-sync.ts @@ -1,5 +1,6 @@ import {isDate} from 'node:util/types'; import {DateTime, DateTimeMaybeValid, Interval} from 'luxon'; +import {z} from 'zod'; import {parameterize} from '../lib/parameterize'; @@ -14,15 +15,14 @@ import { TimeParams, } from '../types/time-sync'; import {PluginInterface} from '../types/interface'; +import {validate} from '../util/validations'; const {InputValidationError} = ERRORS; const { INVALID_TIME_NORMALIZATION, - INVALID_TIME_INTERVAL, INVALID_OBSERVATION_OVERLAP, AVOIDING_PADDING_BY_EDGES, - UNEXPECTED_TIME_CONFIG, } = STRINGS; export const TimeSync = ( @@ -32,32 +32,17 @@ export const TimeSync = ( kind: 'execute', }; - const configure = (globalConfig: TimeNormalizerConfig): TimeParams => { - const startTime = parseDate(globalConfig['start-time']); - const endTime = parseDate(globalConfig['end-time']); - const interval = globalConfig.interval; - const allowPadding = globalConfig['allow-padding']; - - return {startTime, endTime, interval, allowPadding}; - }; - /** * Take input array and return time-synchronized input array. */ - const execute = ( - inputs: PluginParams[], - config?: Record - ): PluginParams[] => { - if (globalConfig === undefined) { - throw new InputValidationError(INVALID_TIME_NORMALIZATION); - } - - if (config) { - throw new InputValidationError(UNEXPECTED_TIME_CONFIG); - } - - const timeParams = configure(globalConfig); - validateParams(timeParams); + const execute = (inputs: PluginParams[]): PluginParams[] => { + const validatedConfig = validateGlobalConfig(); + const timeParams = { + startTime: DateTime.fromISO(validatedConfig['start-time']), + endTime: DateTime.fromISO(validatedConfig['end-time']), + interval: validatedConfig.interval, + allowPadding: validatedConfig['allow-padding'], + }; const pad = checkForPadding(inputs, timeParams); validatePadding(pad, timeParams); @@ -66,7 +51,8 @@ export const TimeSync = ( const flattenInputs = paddedInputs.reduce( (acc: PluginParams[], input, index) => { - const currentMoment = parseDate(input.timestamp); + const safeInput = Object.assign({}, input, validateInput(input, index)); + const currentMoment = parseDate(safeInput.timestamp); /** Checks if not the first input, then check consistency with previous ones. */ if (index > 0) { @@ -96,14 +82,14 @@ export const TimeSync = ( ...getZeroishInputPerSecondBetweenRange( compareableTime, currentMoment, - input + safeInput ) ); } } /** Break down current observation. */ - for (let i = 0; i < input.duration; i++) { - const normalizedInput = breakDownInput(input, i); + for (let i = 0; i < safeInput.duration; i++) { + const normalizedInput = breakDownInput(safeInput, i); acc.push(normalizedInput); } @@ -137,28 +123,44 @@ export const TimeSync = ( }; /** - * Validates `startTime`, `endTime` and `interval` params. + * Validates input parameters. */ - const validateParams = (params: TimeParams) => { - if (!params.startTime || !params.endTime) { - throw new InputValidationError(INVALID_TIME_NORMALIZATION); - } - - if (!params.startTime.isValid) { - throw new InputValidationError(INVALID_TIME_NORMALIZATION); - } + const validateInput = (input: PluginParams, index: number) => { + const schema = z.object({ + timestamp: z + .string({ + required_error: `required in input[${index}]`, + }) + .datetime({ + message: `invalid datetime in input[${index}]`, + }) + .or(z.date()), + duration: z.number(), + }); + + return validate>(schema, input); + }; - if (!params.endTime.isValid) { + /** + * Validates global config parameters. + */ + const validateGlobalConfig = () => { + if (globalConfig === undefined) { throw new InputValidationError(INVALID_TIME_NORMALIZATION); } - if (params.startTime > params.endTime) { - throw new InputValidationError(INVALID_TIME_NORMALIZATION); - } + const schema = z + .object({ + 'start-time': z.string().datetime(), + 'end-time': z.string().datetime(), + interval: z.number(), + 'allow-padding': z.boolean(), + }) + .refine(data => data['start-time'] < data['end-time'], { + message: '`start-time` should be lower than `end-time`', + }); - if (!params.interval) { - throw new InputValidationError(INVALID_TIME_INTERVAL); - } + return validate>(schema, globalConfig); }; /**