From b551ed67d778b3ea2d22ed59a574770f458f5d00 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:28:06 -0800 Subject: [PATCH] [ECO-2390] Fix flaky sdk e2e test due to period boundary cross miscalculation (#338) --- .../calculate-periodic-boundaries-crossed.ts | 55 ++++ src/typescript/sdk/src/utils/test/index.ts | 1 + .../tests/e2e/queries/client/submit.test.ts | 290 ++++++++++-------- .../sdk/tests/unit/period-boundaries.test.ts | 118 ++++++- src/typescript/turbo.json | 4 + 5 files changed, 344 insertions(+), 124 deletions(-) create mode 100644 src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts diff --git a/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts b/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts new file mode 100644 index 000000000..a96365989 --- /dev/null +++ b/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts @@ -0,0 +1,55 @@ +import { type Period, periodEnumToRawDuration, PERIODS } from "../../const"; +import { type AnyNumberString } from "../../types"; +import { getPeriodBoundary } from "../misc"; + +/** + * Calculates the number of period boundaries crossed between two times. Since this will always + * be in ascending order of period boundary size, we just return the number of boundaries, not + * which specific ones. + * + * For example, (assume both times have the same date), 01:01:13 and 01:01:59 will cross 0 period + * boundaries. + * + * But 01:01:13 and 01:02:00 will cross 1 period boundary (a 1-minute period). + * + * 01:01:13 01:05:00 will cross a 1-minute and 5-minute period boundary. + * + * 01-01-2000 11:59:59 and 01-02-2000 12:00:00 will cross all period boundaries: + * 1m, 5m, 15m, 30m, 1h, 4h, and 1d. + * + * @param startMicroseconds the number/bigint/string start time in microseconds. + * @param endMicroseconds the number/bigint/string end time in microseconds. + * @returns the number of period boundaries crossed. + * @throws if the end time is later than the start time. + */ +export const calculatePeriodBoundariesCrossed = ({ + startMicroseconds, + endMicroseconds, +}: { + startMicroseconds: AnyNumberString; + endMicroseconds: AnyNumberString; +}): number => { + const start = BigInt(startMicroseconds); + const end = BigInt(endMicroseconds); + if (start > end) { + throw new Error("End time cannot be later than start time."); + } + const periodsCrossed = PERIODS.reduce( + (acc, period) => { + // Get each period boundary of the start time; i.e., round it down to the nearest boundary. + const lowerPeriodBoundary = getPeriodBoundary(start, period); + // Add the time delta for one period boundary to the start time's lower period boundary to get + // the upper (next) period boundary. + const periodDuration = BigInt(periodEnumToRawDuration(period)); + const upperPeriodBoundary = lowerPeriodBoundary + periodDuration; + // If the end time is greater than or equal to the start time's upper period boundary, that + // period boundary has been crossed. + if (end >= upperPeriodBoundary) { + acc.add(period); + } + return acc; + }, + new Set([]) as Set + ); + return periodsCrossed.size; +}; diff --git a/src/typescript/sdk/src/utils/test/index.ts b/src/typescript/sdk/src/utils/test/index.ts index 9541e4c8b..f28febebe 100644 --- a/src/typescript/sdk/src/utils/test/index.ts +++ b/src/typescript/sdk/src/utils/test/index.ts @@ -2,3 +2,4 @@ export * from "../aptos-client"; export * from "./helpers"; export * from "./publish"; export * from "./load-priv-key"; +export * from "./calculate-periodic-boundaries-crossed"; diff --git a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts index bc20feed6..e5c0c12d7 100644 --- a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts @@ -21,6 +21,7 @@ import { } from "@aptos-labs/ts-sdk"; import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; import { getAptosNetwork } from "../../../../src/utils/aptos-client"; +import { calculatePeriodBoundariesCrossed } from "../../../../src/utils/test"; jest.setTimeout(15000); @@ -133,53 +134,65 @@ describe("all submission types for the emojicoin client", () => { }); it("swap buys", async () => { const [sender, emojis] = senderAndSymbols[1]; - await emojicoin.register(sender, emojis, gasOptions); const inputAmount = 7654321n; - await emojicoin.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.swap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(false); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(0); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: swap.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(0); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); }); }); it("swap sells", async () => { const [sender, emojis] = senderAndSymbols[2]; const inputAmount = 7654321n; await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.buy(sender, emojis, inputAmount); - await emojicoin.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.swap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(true); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(0); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + await emojicoin.buy(sender, emojis, inputAmount).then(({ swap: buy }) => { + emojicoin.sell(sender, emojis, inputAmount).then(({ response, events, swap: sell }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: buy.event.time, + endMicroseconds: sell.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(sell.event.inputAmount).toEqual(inputAmount); + expect(sell.event.isSell).toEqual(true); + expect(sell.event.swapper).toEqual(sender.accountAddress.toString()); + expect(sell.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(sell.event.integratorFeeRateBPs).toEqual(0); + expect(sell.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(sell.model.market.trigger).toEqual(Trigger.SwapSell); + }); }); }); @@ -209,59 +222,71 @@ describe("all submission types for the emojicoin client", () => { const [sender, emojis] = senderAndSymbols[3]; const [a, b] = emojis; const expectedMessage = [a, b, b, a].join(""); - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.chat(sender, emojis, [a, b, b, a]).then(({ response, events, chat }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.chat); - expect(events.chatEvents.length).toEqual(1); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(0); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(chat.event.message).toEqual(expectedMessage); - expect(chat.event.user).toEqual(sender.accountAddress.toString()); - expect(chat.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(chat.model.market.trigger).toEqual(Trigger.Chat); - }); - }); - it("provides liquidity", async () => { - const [sender, emojis] = senderAndSymbols[4]; - const inputAmount = 12386n; - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); - await emojicoin.liquidity - .provide(sender, emojis, inputAmount) - .then(({ response, events, liquidity }) => { + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.chat(sender, emojis, [a, b, b, a]).then(({ response, events, chat }) => { const { success } = response; const payload = response.payload as EntryFunctionPayloadResponse; expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.provideLiquidity); - expect(events.chatEvents.length).toEqual(0); + expect(payload.function).toEqual(functionNames.chat); + expect(events.chatEvents.length).toEqual(1); expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(1); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: chat.event.emitTime, + }) + ); expect(events.stateEvents.length).toEqual(1); expect(events.swapEvents.length).toEqual(0); expect(events.marketRegistrationEvents.length).toEqual(0); - expect(liquidity.event.quoteAmount).toEqual(inputAmount); - expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); - expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(liquidity.model.market.trigger).toEqual(Trigger.ProvideLiquidity); + expect(chat.event.message).toEqual(expectedMessage); + expect(chat.event.user).toEqual(sender.accountAddress.toString()); + expect(chat.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(chat.model.market.trigger).toEqual(Trigger.Chat); }); + }); + }); + it("provides liquidity", async () => { + const [sender, emojis] = senderAndSymbols[4]; + const inputAmount = 12386n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT).then(({ swap }) => { + emojicoin.liquidity + .provide(sender, emojis, inputAmount) + .then(({ response, events, liquidity }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.provideLiquidity); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(1); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: swap.event.time, + endMicroseconds: liquidity.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(liquidity.event.quoteAmount).toEqual(inputAmount); + expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); + expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(liquidity.model.market.trigger).toEqual(Trigger.ProvideLiquidity); + }); + }); }); it("removes liquidity", async () => { const [sender, emojis] = senderAndSymbols[5]; await emojicoin.register(sender, emojis, gasOptions); await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); - await emojicoin.liquidity.provide(sender, emojis, 59182n).then(({ liquidity }) => { - const lpCoinAmount = liquidity.event.lpCoinAmount; + await emojicoin.liquidity.provide(sender, emojis, 59182n).then(({ liquidity: provide }) => { + const lpCoinAmount = provide.event.lpCoinAmount; emojicoin.liquidity .remove(sender, emojis, lpCoinAmount) - .then(({ response, events, liquidity }) => { + .then(({ response, events, liquidity: remove }) => { const { success } = response; const payload = response.payload as EntryFunctionPayloadResponse; expect(success).toBe(true); @@ -269,14 +294,19 @@ describe("all submission types for the emojicoin client", () => { expect(events.chatEvents.length).toEqual(0); expect(events.globalStateEvents.length).toEqual(0); expect(events.liquidityEvents.length).toEqual(1); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: provide.event.time, + endMicroseconds: remove.event.time, + }) + ); expect(events.stateEvents.length).toEqual(1); expect(events.swapEvents.length).toEqual(0); expect(events.marketRegistrationEvents.length).toEqual(0); - expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); - expect(liquidity.event.lpCoinAmount).toEqual(lpCoinAmount); - expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(liquidity.model.market.trigger).toEqual(Trigger.RemoveLiquidity); + expect(remove.event.provider).toEqual(sender.accountAddress.toString()); + expect(remove.event.lpCoinAmount).toEqual(lpCoinAmount); + expect(remove.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(remove.model.market.trigger).toEqual(Trigger.RemoveLiquidity); }); }); }); @@ -284,52 +314,66 @@ describe("all submission types for the emojicoin client", () => { it("swap buys with the rewards contract", async () => { const [sender, emojis] = senderAndSymbols[6]; const inputAmount = 1234567n; - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.rewardsSwap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(false); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: swap.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); }); }); it("swap sells with the rewards contract", async () => { const [sender, emojis] = senderAndSymbols[7]; const inputAmount = 1234567n; await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.rewards.buy(sender, emojis, inputAmount); - await emojicoin.rewards.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.rewardsSwap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(true); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + await emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ swap: buy }) => { + emojicoin.rewards + .sell(sender, emojis, inputAmount) + .then(({ response, events, swap: sell }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: buy.event.time, + endMicroseconds: sell.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(sell.event.inputAmount).toEqual(inputAmount); + expect(sell.event.isSell).toEqual(true); + expect(sell.event.swapper).toEqual(sender.accountAddress.toString()); + expect(sell.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(sell.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(sell.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(sell.model.market.trigger).toEqual(Trigger.SwapSell); + }); }); }); diff --git a/src/typescript/sdk/tests/unit/period-boundaries.test.ts b/src/typescript/sdk/tests/unit/period-boundaries.test.ts index ce2b66689..2b0939302 100644 --- a/src/typescript/sdk/tests/unit/period-boundaries.test.ts +++ b/src/typescript/sdk/tests/unit/period-boundaries.test.ts @@ -1,4 +1,5 @@ -import { PeriodDuration, getPeriodStartTime } from "../../src"; +import { PERIODS, PeriodDuration, getPeriodStartTime } from "../../src"; +import { calculatePeriodBoundariesCrossed } from "../../src/utils/test"; import { SAMPLE_STATE_EVENT, SAMPLE_SWAP_EVENT } from "../../src/utils/test/sample-data"; const swap = SAMPLE_SWAP_EVENT; @@ -123,3 +124,118 @@ describe("tests period boundaries", () => { expect(getPeriodStartTime(state, PERIOD_30M) === 0n * BigInt(PERIOD_30M)).toEqual(true); }); }); + +describe("calculates period boundaries crossed", () => { + type TwoNumbers = `${number}${number}`; + type TimeFormat = `${TwoNumbers}:${TwoNumbers}:${TwoNumbers}`; + const defaultDate = "01-01-2000"; + const getUTCDateFromHMS = (time: TimeFormat) => new Date(`${defaultDate} ${time}Z`); + // Converts hours:minutes:seconds to the number of microseconds since the Unix epoch using a + // default date for the day, month and year. + const getMicrosecondsFromHMS = (time: TimeFormat) => + BigInt(getUTCDateFromHMS(time).getTime() * 1000); + + it("uses the same date by default with the utility function", () => { + const times: Array = ["00:00:00", "11:59:59", "12:00:00", "23:59:59"]; + times.forEach((time) => { + const utcDate = getUTCDateFromHMS(time); + expect(utcDate.getUTCDate()).toEqual(1); + expect(utcDate.getUTCMonth()).toEqual(0); // getUTCMonth() is offset by 0; December is 11. + expect(utcDate.getUTCFullYear()).toEqual(2000); + const micros = getMicrosecondsFromHMS(time); + expect( + calculatePeriodBoundariesCrossed({ startMicroseconds: micros, endMicroseconds: micros }) + ).toEqual(0); + const date = new Date(Number(micros / 1000n)); + expect(date.getUTCDate()).toEqual(1); + expect(date.getUTCMonth()).toEqual(0); // getUTCMonth() is offset by 0; December is 11. + expect(date.getUTCFullYear()).toEqual(2000); + }); + }); + + it("calculates that no period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const endMicroseconds = getMicrosecondsFromHMS("01:01:59"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(0); + }); + + it("calculates that a 1 minute period boundary is crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const endMicroseconds = getMicrosecondsFromHMS("01:02:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(1); + }); + + it("calculates that 1m and 5m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:04:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:05:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(2); + }); + + it("calculates that 1m, 5m, and 15m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:14:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:15:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(3); + }); + + it("calculates that 1m, 5m, 15m, and 30m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:29:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:30:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(4); + }); + + it("calculates that 1m, 5m, 15m, 30m, and 1h period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:59:59"); + const endMicroseconds = getMicrosecondsFromHMS("02:00:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(5); + }); + + it("calculates that 1m, 5m, 15m, 30m, 1h, and 4h period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("03:59:59"); + const endMicroseconds = getMicrosecondsFromHMS("04:00:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(6); + }); + + it("calculates that all period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("23:59:59"); + const oneDay = BigInt(PeriodDuration.PERIOD_1D); + const endMicroseconds = getMicrosecondsFromHMS("00:00:00") + oneDay; + const startDay = new Date(Number(startMicroseconds / 1000n)).getUTCDate(); + const endDay = new Date(Number(endMicroseconds / 1000n)).getUTCDate(); + expect(startDay + 1).toEqual(endDay); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(PERIODS.length); + }); + + it("calculates that exactly only 1 period boundary is crossed over a 4m59s time period", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:00:00"); + const endMicroseconds = getMicrosecondsFromHMS("01:04:59"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(1); + }); + + it("calculates that exactly 7 period boundaries are crossed over multiple days", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const threeDays = BigInt(PeriodDuration.PERIOD_1D) * 3n; + const endMicroseconds = getMicrosecondsFromHMS("01:02:00") + threeDays; + const startDay = new Date(Number(startMicroseconds / 1000n)).getUTCDate(); + const endDay = new Date(Number(endMicroseconds / 1000n)).getUTCDate(); + expect(startDay + 3).toEqual(endDay); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(7); + expect(numBoundaries).toEqual(PERIODS.length); + }); + + it("throws if the end time is later than the start time", () => { + const startMicroseconds = new Date("01-01-2000 00:00:01Z").getTime() * 1000; + const endMicroseconds = new Date("01-01-2000 00:00:00Z").getTime() * 1000; + const fn = () => calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(fn).toThrow(); + }); +}); diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index a03f5fa9f..74ce297cd 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -71,6 +71,10 @@ "test:sequential": { "cache": false, "outputs": [] + }, + "test:unit": { + "cache": false, + "outputs": [] } } }