From f5a69e7d5758122bca257986b9cccbb0b15173b9 Mon Sep 17 00:00:00 2001 From: Nathan Klick Date: Fri, 6 Dec 2024 11:24:27 -0600 Subject: [PATCH] feat: add extended math utils and duration support (#923) Signed-off-by: Nathan Klick --- package-lock.json | 8 +- package.json | 1 + src/commands/node/tasks.ts | 10 +- src/core/constants.ts | 3 - src/core/helpers.ts | 5 +- src/core/k8.ts | 23 +- src/core/lease/lease.ts | 11 +- src/core/lease/lease_renewal.ts | 8 +- src/core/lease/types.ts | 3 +- src/core/time/duration.ts | 490 ++++++++++++++++++ src/core/time/time.ts | 54 ++ src/core/util/arithmetic_error.ts | 38 ++ src/core/util/math_ex.ts | 118 +++++ test/e2e/commands/account.test.ts | 29 +- test/e2e/commands/cluster.test.ts | 24 +- test/e2e/commands/mirror_node.test.ts | 28 +- test/e2e/commands/network.test.ts | 12 +- test/e2e/commands/node_add.test.ts | 4 +- test/e2e/commands/node_add_local.test.ts | 4 +- test/e2e/commands/node_delete.test.ts | 11 +- test/e2e/commands/node_local_hedera.test.ts | 11 +- test/e2e/commands/node_local_ptt.test.ts | 4 +- test/e2e/commands/node_update.test.ts | 13 +- test/e2e/commands/node_upgrade.test.ts | 12 +- test/e2e/commands/relay.test.ts | 8 +- test/e2e/commands/separate_node_add.test.ts | 12 +- .../e2e/commands/separate_node_delete.test.ts | 11 +- .../e2e/commands/separate_node_update.test.ts | 15 +- test/e2e/e2e_node_util.ts | 16 +- test/e2e/integration/commands/init.test.ts | 4 +- .../integration/core/account_manager.test.ts | 4 +- test/e2e/integration/core/k8_e2e.test.ts | 4 +- test/e2e/integration/core/lease.test.ts | 14 +- .../integration/core/lease_renewal.test.ts | 10 +- .../core/noop_lease_renewal_service.test.ts | 5 +- .../core/package_downloader_e2e.test.ts | 4 +- .../core/platform_installer_e2e.test.ts | 8 +- .../core/remote_config_manager.test.ts | 7 +- test/test_add.ts | 10 +- test/test_util.ts | 25 +- test/unit/core/k8.test.ts | 4 +- test/unit/core/key_manager.test.ts | 4 +- test/unit/core/shell_runner.test.ts | 4 +- test/unit/core/time/duration.test.ts | 103 ++++ test/unit/core/util/math_ex.test.ts | 187 +++++++ 45 files changed, 1197 insertions(+), 186 deletions(-) create mode 100644 src/core/time/duration.ts create mode 100644 src/core/time/time.ts create mode 100644 src/core/util/arithmetic_error.ts create mode 100644 src/core/util/math_ex.ts create mode 100644 test/unit/core/time/duration.test.ts create mode 100644 test/unit/core/util/math_ex.test.ts diff --git a/package-lock.json b/package-lock.json index 4d4c77bb2..1a1a56b52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2170,6 +2170,7 @@ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -5279,6 +5280,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5527,6 +5529,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" @@ -5859,7 +5862,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -10063,6 +10067,7 @@ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, + "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" }, @@ -11856,6 +11861,7 @@ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, + "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" diff --git a/package.json b/package.json index 54f24f4ec..783486df9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "scripts": { "test": "cross-env MOCHA_SUITE_NAME=\"Unit Tests\" c8 --report-dir='coverage/unit' mocha 'test/unit/**/*.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit.xml", + "test-mathex": "cross-env MOCHA_SUITE_NAME=\"MathEx Unit Tests\" c8 --report-dir='coverage/unit-mathex' mocha 'test/unit/**/math_ex*.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit.xml", "test-e2e-all": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E All Tests\" c8 --report-dir='coverage/e2e-all' mocha 'test/e2e/**/*.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-all.xml", "test-e2e-integration": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Integration Tests\" c8 --report-dir='coverage/e2e-integration' mocha 'test/e2e/integration/**/*.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-integration.xml", "test-e2e-leases": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Lease Tests\" c8 --report-dir='coverage/e2e-leases' mocha 'test/e2e/integration/core/lease*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-integration.xml", diff --git a/src/commands/node/tasks.ts b/src/commands/node/tasks.ts index 176966967..9adab17e4 100644 --- a/src/commands/node/tasks.ts +++ b/src/commands/node/tasks.ts @@ -32,7 +32,6 @@ import { HEDERA_NODE_DEFAULT_STAKE_AMOUNT, IGNORED_NODE_ACCOUNT_ID, LOCAL_HOST, - SECONDS, TREASURY_ACCOUNT_ID, } from '../../core/constants.js'; import { @@ -82,6 +81,7 @@ import type { } from './configs.js'; import {type Lease} from '../../core/lease/types.js'; import {ListrLease} from '../../core/lease/listr_lease.js'; +import {Duration} from '../../core/time/duration.js'; import {type BaseCommand} from '../base.js'; export class NodeCommandTasks { @@ -403,7 +403,7 @@ export class NodeCommandTasks { attempt++; clearTimeout(timeoutId); - await sleep(delay); + await sleep(Duration.ofMillis(delay)); } await this.k8.stopPortForward(srv); @@ -415,7 +415,7 @@ export class NodeCommandTasks { ); } - await sleep(1.5 * SECONDS); // delaying prevents - gRPC service error + await sleep(Duration.ofSeconds(2)); // delaying prevents - gRPC service error return podName; } @@ -990,7 +990,7 @@ export class NodeCommandTasks { self.logger.info( 'sleep 60 seconds for the handler to be able to trigger the network node stake weight recalculate', ); - await sleep(60 * SECONDS); + await sleep(Duration.ofSeconds(60)); const accountMap = getNodeAccountMap(config.allNodeAliases); switch (transactionType) { @@ -1490,7 +1490,7 @@ export class NodeCommandTasks { sleep(title: string, milliseconds: number) { return new Task(title, async (ctx: any, task: ListrTaskWrapper) => { - await sleep(milliseconds); + await sleep(Duration.ofMillis(milliseconds)); }); } diff --git a/src/core/constants.ts b/src/core/constants.ts index 7e25ab562..db5e71dd1 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -181,9 +181,6 @@ export const UPGRADE_FILE_CHUNK_SIZE = 1024 * 5; // 5Kb export const JVM_DEBUG_PORT = 5005; -export const SECONDS = 1000; -export const MINUTES = 60 * SECONDS; - export const PODS_RUNNING_MAX_ATTEMPTS = +process.env.PODS_RUNNING_MAX_ATTEMPTS || 60 * 15; export const PODS_RUNNING_DELAY = +process.env.PODS_RUNNING_DELAY || 1000; export const NETWORK_NODE_ACTIVE_MAX_ATTEMPTS = +process.env.NETWORK_NODE_ACTIVE_MAX_ATTEMPTS || 120; diff --git a/src/core/helpers.ts b/src/core/helpers.ts index 762840ad2..657d846a8 100644 --- a/src/core/helpers.ts +++ b/src/core/helpers.ts @@ -27,10 +27,11 @@ import {PrivateKey, ServiceEndpoint} from '@hashgraph/sdk'; import {type NodeAlias, type NodeAliases} from '../types/aliases.js'; import {type CommandFlag} from '../types/flag_types.js'; import {type SoloLogger} from './logging.js'; +import {type Duration} from './time/duration.js'; -export function sleep(ms: number) { +export function sleep(duration: Duration) { return new Promise(resolve => { - setTimeout(resolve, ms); + setTimeout(resolve, duration.toMillis()); }); } diff --git a/src/core/k8.ts b/src/core/k8.ts index ac7435e1a..e7b11d51a 100644 --- a/src/core/k8.ts +++ b/src/core/k8.ts @@ -34,7 +34,8 @@ import {type ConfigManager} from './config_manager.js'; import {type SoloLogger} from './logging.js'; import {type PodName, type TarCreateFilter} from '../types/aliases.js'; import type {ExtendedNetServer, LocalContextObject} from '../types/index.js'; -import {HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER, SOLO_LOGS_DIR} from './constants.js'; +import {HEDERA_HAPI_PATH, ROOT_CONTAINER, SOLO_LOGS_DIR} from './constants.js'; +import {Duration} from './time/duration.js'; interface TDirectoryData { directory: boolean; @@ -213,7 +214,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); return this.filterItem(resp.body.items, {name}); @@ -237,7 +238,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); return result.body.items; @@ -296,7 +297,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); return this.filterItem(resp.body.items, {name}); @@ -999,7 +1000,7 @@ export class K8 { } catch (e: Error | any) { return; } - await sleep(timeout); + await sleep(Duration.ofMillis(timeout)); } if (attempts >= maxAttempts) { throw new SoloError(`failed to stop port-forwarder [${server.info}]`); @@ -1036,7 +1037,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); this.logger.debug( @@ -1149,7 +1150,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); for (const item of resp.body.items) { @@ -1179,7 +1180,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); for (const item of resp.body.items) { @@ -1234,7 +1235,7 @@ export class K8 { undefined, undefined, undefined, - 5 * MINUTES, + Duration.ofMinutes(5).toMillis(), ); if (result.response.statusCode === 200 && result.body.items && result.body.items.length > 0) { @@ -1520,7 +1521,7 @@ export class K8 { if (!pod?.metadata?.deletionTimestamp) { podExists = false; } else { - await sleep(1000); + await sleep(Duration.ofSeconds(1)); } } } catch (e) { @@ -1562,7 +1563,7 @@ export class K8 { const scriptName = 'support-zip.sh'; const sourcePath = path.join(constants.RESOURCES_DIR, scriptName); // script source path await this.copyTo(podName, ROOT_CONTAINER, sourcePath, `${HEDERA_HAPI_PATH}`); - await sleep(1000); // wait for the script to sync to the file system + await sleep(Duration.ofSeconds(1)); // wait for the script to sync to the file system await this.execContainer(podName, ROOT_CONTAINER, [ 'bash', '-c', diff --git a/src/core/lease/lease.ts b/src/core/lease/lease.ts index c163c5f57..c675ac9df 100644 --- a/src/core/lease/lease.ts +++ b/src/core/lease/lease.ts @@ -17,11 +17,11 @@ import {MissingArgumentError, SoloError} from '../errors.js'; import {type V1Lease} from '@kubernetes/client-node'; import {type K8} from '../k8.js'; -import {SECONDS} from '../constants.js'; import {LeaseHolder} from './lease_holder.js'; import {LeaseAcquisitionError, LeaseRelinquishmentError} from './lease_errors.js'; import {sleep} from '../helpers.js'; -import {type Lease, type LeaseRenewalService} from './types.js'; +import {Duration} from '../time/duration.js'; +import type {Lease, LeaseRenewalService} from './types.js'; /** * Concrete implementation of a Kubernetes based time-based mutually exclusive lock via the Coordination API. @@ -382,10 +382,11 @@ export class IntervalLease implements Lease { * @returns true if the lease has expired; otherwise, false. */ private static checkExpiration(lease: V1Lease): boolean { - const now = Date.now(); + const now = Duration.ofMillis(Date.now()); const durationSec = lease.spec.leaseDurationSeconds || IntervalLease.DEFAULT_LEASE_DURATION; - const lastRenewal = lease.spec?.renewTime || lease.spec?.acquireTime; - const deltaSec = (now - new Date(lastRenewal).valueOf()) / SECONDS; + const lastRenewalTime = lease.spec?.renewTime || lease.spec?.acquireTime; + const lastRenewal = Duration.ofMillis(new Date(lastRenewalTime).valueOf()); + const deltaSec = now.minus(lastRenewal).seconds; return deltaSec > durationSec; } diff --git a/src/core/lease/lease_renewal.ts b/src/core/lease/lease_renewal.ts index 513d73ac6..4bf00e44d 100644 --- a/src/core/lease/lease_renewal.ts +++ b/src/core/lease/lease_renewal.ts @@ -15,8 +15,8 @@ * */ import {type Lease} from './types.js'; -import {SECONDS} from '../constants.js'; import {type LeaseRenewalService} from './types.js'; +import {Duration} from '../time/duration.js'; /** * Implements a lease renewal service which utilizes a setInterval() based approach to renew leases at regular intervals. @@ -53,7 +53,7 @@ export class IntervalLeaseRenewalService implements LeaseRenewalService { */ public async schedule(lease: Lease): Promise { const renewalDelay = this.calculateRenewalDelay(lease); - const timeout = setInterval(() => lease.tryRenew(), renewalDelay); + const timeout = setInterval(() => lease.tryRenew(), renewalDelay.toMillis()); const scheduleId = Number(timeout); this._scheduledLeases.set(scheduleId, lease); @@ -102,7 +102,7 @@ export class IntervalLeaseRenewalService implements LeaseRenewalService { * @param lease - the lease to be renewed. * @returns the delay in milliseconds. */ - public calculateRenewalDelay(lease: Lease): number { - return Math.round(lease.durationSeconds * 0.5) * SECONDS; + public calculateRenewalDelay(lease: Lease): Duration { + return Duration.ofSeconds(lease.durationSeconds * 0.5); } } diff --git a/src/core/lease/types.ts b/src/core/lease/types.ts index fa0b13d91..b76604b72 100644 --- a/src/core/lease/types.ts +++ b/src/core/lease/types.ts @@ -16,6 +16,7 @@ */ import type {K8} from '../k8.js'; import {type LeaseHolder} from './lease_holder.js'; +import {type Duration} from '../time/duration.js'; /** * Copyright (C) 2024 Hedera Hashgraph, LLC @@ -66,7 +67,7 @@ export interface LeaseRenewalService { * @param lease - the lease to be renewed. * @returns the delay in milliseconds. */ - calculateRenewalDelay(lease: Lease): number; + calculateRenewalDelay(lease: Lease): Duration; } export interface Lease { readonly client: K8; diff --git a/src/core/time/duration.ts b/src/core/time/duration.ts new file mode 100644 index 000000000..efe167b1a --- /dev/null +++ b/src/core/time/duration.ts @@ -0,0 +1,490 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import {IllegalArgumentError} from '../errors.js'; +import {MathEx} from '../util/math_ex.js'; +import {Time} from './time.js'; + +/** + * A time-based amount of time, such as '34.5 seconds'. + * + * This class models a quantity or amount of time in terms of seconds and nanoseconds. It can be accessed using other + * duration-based units, such as minutes and hours. In addition, the DAYS unit can be used and is treated as exactly + * equal to 24 hours, thus ignoring daylight savings effects. + * + * A physical duration could be of infinite length. The duration uses nanosecond resolution with a maximum value of the + * seconds that can be held in a long. This is greater than the current estimated age of the universe. + * + * The range of a duration requires the storage of a number larger than a long. To achieve this, the class stores a long + * representing seconds and an integer representing nanosecond-of-second, which will always be between 0 and 999,999,999. + * The model is of a directed duration, meaning that the duration may be negative. + * + * The duration is measured in "seconds", but these are not necessarily identical to the scientific "SI second" + * definition based on atomic clocks. This difference only impacts durations measured near a leap-second and should not + * affect most applications. + * + * This is a value-based class; use of identity-sensitive operations on instances of Duration may have unpredictable + * results and should be avoided. The equals method should be used for comparisons. + */ +export class Duration { + /** + * A constant for a duration of zero. + */ + public static readonly ZERO = new Duration(0, 0); + + /** + * A constant for a duration of forever. + */ + public static readonly FOREVER = new Duration(Number.MAX_SAFE_INTEGER, 999_999_999); + + /** + * Creates a new instance of Duration with the specified number of seconds and nanoseconds. + * This is a private constructor and not intended to be called directly. + * + * @param seconds - the number of seconds + * @param nanos - the number of nanoseconds + */ + private constructor( + public readonly seconds: number, + public readonly nanos: number, + ) { + Duration.checkValidNanos(nanos); + } + + /** + * Checks if this duration is zero. + * + * @returns true if this duration is zero; false otherwise. + */ + public isZero(): boolean { + return (this.seconds | this.nanos) === 0; + } + + /** + * Checks if this duration is negative. + * + * @returns true if this duration is negative; false otherwise. + */ + public isNegative(): boolean { + return this.seconds < 0; + } + + /** + * Creates a new Duration instance with the specified number of seconds. The current number of nanoseconds in this + * duration is preserved. + * + * @param seconds - the number of seconds for the new duration + * @returns a new Duration instance with the specified number of seconds. + */ + public withSeconds(seconds: number): Duration { + return Duration.create(seconds, this.nanos); + } + + /** + * Creates a new Duration instance with the specified number of nanoseconds. The current number of seconds in this + * duration is preserved. + * + * @param nanos - the number of nanoseconds for the new duration + * @returns a new Duration instance with the specified number of nanoseconds. + */ + public withNanos(nanos: number): Duration { + Duration.checkValidNanos(nanos); + return Duration.create(this.seconds, nanos); + } + + /** + * Creates a new Duration instance by adding the specified duration to this duration. + * + * @param other - the duration to add + * @returns a new Duration instance with the sum of this duration and the specified duration. + */ + public plus(other: Duration): Duration { + return this.plusExact(other.seconds, other.nanos); + } + + /** + * Creates a new Duration instance by adding the specified number of days to this duration. + * + * @param daysToAdd - the number of days to add + * @returns a new Duration instance with the sum of this duration and the specified number of days. + */ + public plusDays(daysToAdd: number): Duration { + return this.plusExact(MathEx.multiplyExact(daysToAdd, Time.SECONDS_PER_DAY), 0); + } + + /** + * Creates a new Duration instance by adding the specified number of hours to this duration. + * + * @param hoursToAdd - the number of hours to add + * @returns a new Duration instance with the sum of this duration and the specified number of hours. + */ + public plusHours(hoursToAdd: number): Duration { + return this.plusExact(MathEx.multiplyExact(hoursToAdd, Time.SECONDS_PER_HOUR), 0); + } + + /** + * Creates a new Duration instance by adding the specified number of minutes to this duration. + * + * @param minutesToAdd - the number of minutes to add + * @returns a new Duration instance with the sum of this duration and the specified number of minutes. + */ + public plusMinutes(minutesToAdd: number): Duration { + return this.plusExact(MathEx.multiplyExact(minutesToAdd, Time.SECONDS_PER_MINUTE), 0); + } + + /** + * Creates a new Duration instance by adding the specified number of seconds to this duration. + * + * @param secondsToAdd - the number of seconds to add + * @returns a new Duration instance with the sum of this duration and the specified number of seconds. + */ + public plusSeconds(secondsToAdd: number): Duration { + return this.plusExact(secondsToAdd, 0); + } + + /** + * Creates a new Duration instance by adding the specified number of milliseconds to this duration. + * + * @param millisToAdd - the number of milliseconds to add + * @returns a new Duration instance with the sum of this duration and the specified number of milliseconds. + */ + public plusMillis(millisToAdd: number): Duration { + return this.plusExact( + Math.trunc(millisToAdd / Time.MILLIS_PER_SECOND), + (millisToAdd % Time.MILLIS_PER_SECOND) * Time.NANOS_PER_MILLI, + ); + } + + /** + * Creates a new Duration instance by adding the specified number of nanoseconds to this duration. + * + * @param nanosToAdd - the number of nanoseconds to add + * @returns a new Duration instance with the sum of this duration and the specified number of nanoseconds. + */ + public plusNanos(nanosToAdd: number): Duration { + return this.plusExact(0, nanosToAdd); + } + + /** + * Creates a new Duration instance by subtracting the specified duration from this duration. + * + * @param other - the duration to subtract + * @returns a new Duration instance with the difference of this duration and the specified duration. + */ + public minus(other: Duration): Duration { + const secondsToSubtract: number = other.seconds; + const nanosToSubtract: number = other.nanos; + + if (secondsToSubtract === Number.MIN_SAFE_INTEGER) { + return this.plusExact(Number.MAX_SAFE_INTEGER, -nanosToSubtract).plusExact(1, 0); + } + + return this.plusExact(-secondsToSubtract, -nanosToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of days from this duration. + * + * @param daysToSubtract - the number of days to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of days. + */ + public minusDays(daysToSubtract: number): Duration { + return daysToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusDays(Number.MAX_SAFE_INTEGER).plusDays(1) + : this.plusDays(-daysToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of hours from this duration. + * + * @param hoursToSubtract - the number of hours to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of hours. + */ + public minusHours(hoursToSubtract: number): Duration { + return hoursToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusHours(Number.MAX_SAFE_INTEGER).plusHours(1) + : this.plusHours(-hoursToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of minutes from this duration. + * + * @param minutesToSubtract - the number of minutes to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of minutes. + */ + public minusMinutes(minutesToSubtract: number): Duration { + return minutesToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusMinutes(Number.MAX_SAFE_INTEGER).plusMinutes(1) + : this.plusMinutes(-minutesToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of seconds from this duration. + * + * @param secondsToSubtract - the number of seconds to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of seconds. + */ + public minusSeconds(secondsToSubtract: number): Duration { + return secondsToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusSeconds(Number.MAX_SAFE_INTEGER).plusSeconds(1) + : this.plusSeconds(-secondsToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of milliseconds from this duration. + * + * @param millisToSubtract - the number of milliseconds to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of milliseconds. + */ + public minusMillis(millisToSubtract: number): Duration { + return millisToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusMillis(Number.MAX_SAFE_INTEGER).plusMillis(1) + : this.plusMillis(-millisToSubtract); + } + + /** + * Creates a new Duration instance by subtracting the specified number of nanoseconds from this duration. + * + * @param nanosToSubtract - the number of nanoseconds to subtract + * @returns a new Duration instance with the difference of this duration and the specified number of nanoseconds. + */ + public minusNanos(nanosToSubtract: number): Duration { + return nanosToSubtract === Number.MIN_SAFE_INTEGER + ? this.plusNanos(Number.MAX_SAFE_INTEGER).plusNanos(1) + : this.plusNanos(-nanosToSubtract); + } + + /** + * Converts this duration to days. + * + * @returns the number of days in this duration. + */ + public toDays(): number { + return this.seconds / Time.SECONDS_PER_DAY; + } + + /** + * Converts this duration to hours. + * + * @returns the number of hours in this duration. + */ + public toHours(): number { + return this.seconds / Time.SECONDS_PER_HOUR; + } + + /** + * Converts this duration to minutes. + * + * @returns the number of minutes in this duration. + */ + public toMinutes(): number { + return this.seconds / Time.SECONDS_PER_MINUTE; + } + + /** + * Converts this duration to seconds. + * + * @returns the number of seconds in this duration. + */ + public toMillis(): number { + const millis = MathEx.multiplyExact(this.seconds, Time.MILLIS_PER_SECOND); + return MathEx.addExact(millis, Math.trunc(this.nanos / Time.NANOS_PER_MILLI)); + } + + /** + * Converts this duration to nanoseconds. + * + * @returns the number of nanoseconds in this duration. + */ + public toNanos(): number { + const totalNanos = MathEx.multiplyExact(this.seconds, Time.NANOS_PER_SECOND); + return MathEx.addExact(totalNanos, this.nanos); + } + + /** + * Compares this duration to the specified duration. + * + * @param other - the duration being compared to this duration + * @returns true if the two durations are equal; false otherwise. + */ + public equals(other: Duration): boolean { + return this.seconds === other.seconds && this.nanos === other.nanos; + } + + /** + * Compares this duration to the specified duration. + * + * @param other - the duration being compared to this duration + * @returns a negative value if this duration is less than the specified duration, a positive value if this duration + * is greater than the specified duration, or zero if the two durations are equal. + */ + public compareTo(other: Duration): number { + const cmp = this.seconds - other.seconds; + if (cmp !== 0) { + return cmp; + } + return this.nanos - other.nanos; + } + + /** + * Creates a new Duration instance representing the specified number of days. + * + * @param days - the number of days + * @returns a new Duration instance representing the specified number of days. + */ + public static ofDays(days: number): Duration { + return Duration.create(MathEx.multiplyExact(days, Time.SECONDS_PER_DAY), 0); + } + + /** + * Creates a new Duration instance representing the specified number of hours. + * + * @param hours - the number of hours + * @returns a new Duration instance representing the specified number of hours. + */ + public static ofHours(hours: number): Duration { + return Duration.create(MathEx.multiplyExact(hours, Time.SECONDS_PER_HOUR), 0); + } + + /** + * Creates a new Duration instance representing the specified number of minutes. + * + * @param minutes - the number of minutes + * @returns a new Duration instance representing the specified number of minutes. + */ + public static ofMinutes(minutes: number): Duration { + return Duration.create(MathEx.multiplyExact(minutes, Time.SECONDS_PER_MINUTE), 0); + } + + /** + * Creates a new Duration instance representing the specified number of seconds. + * + * @param seconds - the number of seconds + * @returns a new Duration instance representing the specified number of seconds. + */ + public static ofSeconds(seconds: number): Duration { + return Duration.create(seconds, 0); + } + + /** + * Creates a new Duration instance representing the specified number of seconds adjusted by a number of nanoseconds. + * + * @param seconds - the number of seconds + * @param nanoAdjustment - the number of nanoseconds by which to adjust the seconds + * @returns a new Duration instance representing the specified number of seconds adjusted by the specified number of nanoseconds. + */ + public static ofSecondsAdjusted(seconds: number, nanoAdjustment: number): Duration { + const secs: number = MathEx.addExact(seconds, MathEx.floorDiv(nanoAdjustment, Time.NANOS_PER_SECOND)); + const nos: number = MathEx.floorMod(nanoAdjustment, Time.NANOS_PER_SECOND); + return Duration.create(secs, nos); + } + + /** + * Creates a new Duration instance representing the specified number of milliseconds. + * + * @param millis - the number of milliseconds + * @returns a new Duration instance representing the specified number of milliseconds. + */ + public static ofMillis(millis: number): Duration { + let secs = millis / Time.MILLIS_PER_SECOND; + let mos = millis % Time.MILLIS_PER_SECOND; + + if (mos < 0) { + mos += Time.MILLIS_PER_SECOND; + secs--; + } + + return Duration.create(Math.trunc(secs), mos * Time.NANOS_PER_MILLI); + } + + /** + * Creates a new Duration instance representing the specified number of nanoseconds. + * + * @param nanos - the number of nanoseconds + * @returns a new Duration instance representing the specified number of nanoseconds. + */ + public static ofNanos(nanos: number): Duration { + let secs = nanos / Time.NANOS_PER_SECOND; + let nos = nanos % Time.NANOS_PER_SECOND; + + if (nos < 0) { + nos += Time.NANOS_PER_SECOND; + secs--; + } + + return Duration.create(Math.trunc(secs), nos); + } + + /** + * Private utility method to create a new Duration instance representing the specified number of seconds and + * nanoseconds. + * + * @param seconds - the number of seconds + * @param nanoAdjustment - the number of nanoseconds + * @returns a new Duration instance representing the specified number of seconds and nanoseconds. + */ + private static create(seconds: number, nanoAdjustment: number): Duration { + if ((seconds | nanoAdjustment) === 0) { + return Duration.ZERO; + } + + return new Duration(seconds, nanoAdjustment); + } + + /** + * Private utility method to create a new Duration instance by adding the specified number of seconds and nanoseconds. + * + * @param secondsToAdd - the number of seconds to add + * @param nanosToAdd - the number of nanoseconds to add + * @returns a new Duration instance with the sum of this duration and the specified number of seconds and nanoseconds. + */ + private plusExact(secondsToAdd: number, nanosToAdd: number): Duration { + if ((secondsToAdd | nanosToAdd) === 0) { + return this; + } + + let epochSec: number = MathEx.addExact(this.seconds, secondsToAdd); + epochSec = MathEx.addExact(epochSec, Math.trunc(nanosToAdd / Time.NANOS_PER_SECOND)); + nanosToAdd = nanosToAdd % Time.NANOS_PER_SECOND; + const nanoAdjustment: number = this.nanos + nanosToAdd; + + return Duration.ofSecondsAdjusted(epochSec, nanoAdjustment); + } + + /** + * Private utility method to validate the specified number of nanoseconds. + * + * @param nanos - the number of nanoseconds to validate + * @returns true if the specified number of nanoseconds is valid; false otherwise. + */ + private static isValidNanos(nanos: number): boolean { + return Number.isSafeInteger(nanos) && nanos >= 0 && nanos <= 999_999_999; + } + + /** + * Private utility method to validate the specified number of nanoseconds. + * + * @param nanos - the number of nanoseconds to validate + * @throws IllegalArgumentError if the specified number of nanoseconds is invalid. + */ + private static checkValidNanos(nanos: number): void { + if (!Duration.isValidNanos(nanos)) { + throw new IllegalArgumentError( + 'The nanoseconds value must be zero or greater and less than or equal to 999,999,999', + ); + } + } +} diff --git a/src/core/time/time.ts b/src/core/time/time.ts new file mode 100644 index 000000000..7cf6eb21b --- /dev/null +++ b/src/core/time/time.ts @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Provides constants for working with time. + */ +export class Time { + /** + * The number of hours in a day. + */ + public static readonly HOURS_PER_DAY = 24; + /** + * The number of minutes in an hour. + */ + public static readonly MINUTES_PER_HOUR = 60; + /** + * The number of seconds in a minute. + */ + public static readonly SECONDS_PER_MINUTE = 60; + /** + * The number of seconds in an hour. + */ + public static readonly SECONDS_PER_HOUR = Time.SECONDS_PER_MINUTE * Time.MINUTES_PER_HOUR; + /** + * The number of seconds in a day. + */ + public static readonly SECONDS_PER_DAY = Time.SECONDS_PER_HOUR * Time.HOURS_PER_DAY; + /** + * The number of milliseconds in a second. + */ + public static readonly MILLIS_PER_SECOND = 1000; + /** + * The number of nanoseconds in a second. + */ + public static readonly NANOS_PER_SECOND = 1_000_000_000; + /** + * The number of nanoseconds in a millisecond. + */ + public static readonly NANOS_PER_MILLI = 1_000_000; +} diff --git a/src/core/util/arithmetic_error.ts b/src/core/util/arithmetic_error.ts new file mode 100644 index 000000000..f748b2a7f --- /dev/null +++ b/src/core/util/arithmetic_error.ts @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Thrown when an arithmetic operation fails due to NaN, Infinity, Division by Zero, or other arithmetic errors. + */ +export class ArithmeticError extends Error { + /** + * Constructs a new `ArithmeticError` instance. + * + * @param message - The error message. + * @param cause - The nest error (if any). + */ + constructor(message: string, cause: Error | any = {}) { + super(message); + this.name = this.constructor.name; + + if (cause) { + this.cause = cause; + } + + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/core/util/math_ex.ts b/src/core/util/math_ex.ts new file mode 100644 index 000000000..2ba3508cb --- /dev/null +++ b/src/core/util/math_ex.ts @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {ArithmeticError} from './arithmetic_error.js'; + +/** + * Utility class for performing exact arithmetic operations with overflow checking. + */ +export class MathEx { + /** + * Prevents instantiation of this utility class. + */ + private constructor() { + throw new Error('This class cannot be instantiated'); + } + + /** + * Returns the sum of the two arguments; throwing an exception if the result overflows a long integer. + * + * @param x - The first value + * @param y - The second value + * @returns The result of adding the two values + * @throws ArithmeticError if the result overflows a long integer + */ + public static addExact(x: number, y: number): number { + const r = Math.trunc(x) + Math.trunc(y); + if (((x ^ r) & (y ^ r)) < 0 || !Number.isSafeInteger(r)) { + throw new ArithmeticError('Addition overflows a long integer'); + } + return r; + } + + /** + * Returns the difference of the two arguments; throwing an exception if the result overflows a long integer. + * + * @param x - The first value + * @param y - The second value + * @returns The result of subtracting the two values + * @throws ArithmeticError if the result overflows a long integer + */ + public static subtractExact(x: number, y: number): number { + const r = Math.trunc(x) - Math.trunc(y); + if (((x ^ y) & (x ^ r)) < 0 || !Number.isSafeInteger(r)) { + throw new ArithmeticError('Subtraction overflows a long integer'); + } + return r; + } + + /** + * Returns the product of the two arguments; throwing an exception if the result overflows a long integer. + * + * @param x - The first value + * @param y - The second value + * @returns The result of multiplying the two values + * @throws ArithmeticError if the result overflows a long integer + */ + public static multiplyExact(x: number, y: number): number { + const r = Math.trunc(x) * Math.trunc(y); + const ax = Math.abs(x); + const ay = Math.abs(y); + + if ((ax | ay) >>> 31 !== 0) { + if ((y !== 0 && r / y !== x) || !Number.isSafeInteger(r)) { + throw new ArithmeticError('Multiplication overflows a long integer'); + } + } + return r; + } + + /** + * Returns the floor division of the two arguments; throwing an exception if the result overflows a long integer. + * + * @param x - The dividend + * @param y - The divisor + * @returns The quotient of dividing the two values rounded towards positive infinity. + */ + public static floorDiv(x: number, y: number): number { + let r = Math.trunc(x) / Math.trunc(y); + + if (Number.isNaN(r)) { + throw new ArithmeticError('Division by zero'); + } + + if (!Number.isFinite(r)) { + throw new ArithmeticError('Division overflows a long integer'); + } + + r = Math.floor(r); + + return r; + } + + /** + * Returns the remainder of the floor division of the two arguments; throwing an exception if the result overflows a long integer. + * + * @param x - The dividend + * @param y - The divisor + * @returns The remainder of dividing the two values rounded towards positive infinity. + */ + public static floorMod(x: number, y: number): number { + const dy = Math.trunc(MathEx.floorDiv(x, y) * Math.trunc(y)); + return Math.trunc(x) - dy; + } +} diff --git a/test/e2e/commands/account.test.ts b/test/e2e/commands/account.test.ts index c4fcdd914..5abcad3b5 100644 --- a/test/e2e/commands/account.test.ts +++ b/test/e2e/commands/account.test.ts @@ -42,9 +42,9 @@ import { } from '../../test_util.js'; import {AccountCommand} from '../../../src/commands/account.js'; import {Flags as flags} from '../../../src/commands/flags.js'; -import {MINUTES, SECONDS} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; -const defaultTimeout = 20 * SECONDS; +const defaultTimeout = Duration.ofSeconds(20).toMillis(); const testName = 'account-cmd-e2e'; const namespace = testName; @@ -70,7 +70,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin const nodeCmd = bootstrapResp.cmd.nodeCmd; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -81,8 +81,13 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin describe('node proxies should be UP', () => { for (const nodeAlias of argv[flags.nodeAliasesUnparsed.name].split(',')) { it(`proxy should be UP: ${nodeAlias} `, async () => { - await k8.waitForPodReady([`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'], 1, 300, 2 * SECONDS); - }).timeout(30 * SECONDS); + await k8.waitForPodReady( + [`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'], + 1, + 300, + Duration.ofSeconds(2).toMillis(), + ); + }).timeout(Duration.ofSeconds(30).toMillis()); } }); @@ -90,7 +95,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(3 * MINUTES); + }).timeout(Duration.ofMinutes(3).toMillis()); describe('special accounts should have new keys', () => { const genesisKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY); @@ -98,12 +103,12 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin const shard = constants.HEDERA_NODE_ACCOUNT_ID_START.shard; before(async function () { - this.timeout(20 * SECONDS); + this.timeout(Duration.ofSeconds(20).toMillis()); await accountManager.loadNodeClient(namespace); }); after(async function () { - this.timeout(20 * SECONDS); + this.timeout(Duration.ofSeconds(20).toMillis()); await accountManager.close(); }); @@ -119,7 +124,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin expect(keys.length).not.to.equal(0); expect(keys[0].toString()).not.to.equal(genesisKey.toString()); - }).timeout(20 * SECONDS); + }).timeout(Duration.ofSeconds(20).toMillis()); } } }); @@ -148,7 +153,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin testLogger.showUserError(e); expect.fail(); } - }).timeout(40 * SECONDS); + }).timeout(Duration.ofSeconds(40).toMillis()); it('should create account with private key and hbar amount options', async () => { try { @@ -329,7 +334,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin } catch (e) { networkCmd.logger.showUserError(e); } - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); it('Create client from network config and submit topic/message should succeed', async () => { try { @@ -358,7 +363,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin } catch (e) { networkCmd.logger.showUserError(e); } - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); }); }); }); diff --git a/test/e2e/commands/cluster.test.ts b/test/e2e/commands/cluster.test.ts index c52c425e9..b734ab8d6 100644 --- a/test/e2e/commands/cluster.test.ts +++ b/test/e2e/commands/cluster.test.ts @@ -24,7 +24,7 @@ import * as constants from '../../../src/core/constants.js'; import * as logging from '../../../src/core/logging.js'; import {sleep} from '../../../src/core/helpers.js'; import * as version from '../../../version.js'; -import {MINUTES, SECONDS} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; describe('ClusterCommand', () => { // mock showUser and showJSON to silent logging during tests @@ -62,14 +62,14 @@ describe('ClusterCommand', () => { const clusterCmd = bootstrapResp.cmd.clusterCmd; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.deleteNamespace(namespace); argv[flags.clusterSetupNamespace.name] = constants.SOLO_SETUP_NAMESPACE; configManager.update(argv); await clusterCmd.setup(argv); // restore solo-cluster-setup for other e2e tests to leverage do { - await sleep(5 * SECONDS); + await sleep(Duration.ofSeconds(5)); } while ( !(await chartManager.isChartInstalled(constants.SOLO_SETUP_NAMESPACE, constants.SOLO_CLUSTER_SETUP_CHART)) ); @@ -81,38 +81,38 @@ describe('ClusterCommand', () => { }); // give a few ticks so that connections can close - afterEach(async () => await sleep(5)); + afterEach(async () => await sleep(Duration.ofMillis(5))); it('should cleanup existing deployment', async () => { if (await chartManager.isChartInstalled(constants.SOLO_SETUP_NAMESPACE, constants.SOLO_CLUSTER_SETUP_CHART)) { expect(await clusterCmd.reset(argv)).to.be.true; } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('solo cluster setup should fail with invalid cluster name', async () => { argv[flags.clusterSetupNamespace.name] = 'INVALID'; configManager.update(argv); await expect(clusterCmd.setup(argv)).to.be.rejectedWith('Error on cluster setup'); - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('solo cluster setup should work with valid args', async () => { argv[flags.clusterSetupNamespace.name] = namespace; configManager.update(argv); expect(await clusterCmd.setup(argv)).to.be.true; - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('function getClusterInfo should return true', () => { expect(clusterCmd.getClusterInfo()).to.be.ok; - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('function showClusterList should return right true', async () => { expect(clusterCmd.showClusterList()).to.be.ok; - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('function showInstalledChartList should return right true', async () => { // @ts-ignore await expect(clusterCmd.showInstalledChartList()).to.eventually.be.undefined; - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); // helm list would return an empty list if given invalid namespace it('solo cluster reset should fail with invalid cluster name', async () => { @@ -125,11 +125,11 @@ describe('ClusterCommand', () => { clusterCmd.logger.showUserError(e); expect.fail(); } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('solo cluster reset should work with valid args', async () => { argv[flags.clusterSetupNamespace.name] = namespace; configManager.update(argv); expect(await clusterCmd.reset(argv)).to.be.true; - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); }); diff --git a/test/e2e/commands/mirror_node.test.ts b/test/e2e/commands/mirror_node.test.ts index bca802c5f..897f469b6 100644 --- a/test/e2e/commands/mirror_node.test.ts +++ b/test/e2e/commands/mirror_node.test.ts @@ -31,9 +31,9 @@ import {sleep} from '../../../src/core/helpers.js'; import {MirrorNodeCommand} from '../../../src/commands/mirror_node.js'; import {Status, TopicCreateTransaction, TopicMessageSubmitTransaction} from '@hashgraph/sdk'; import * as http from 'http'; -import {MINUTES, SECONDS} from '../../../src/core/constants.js'; import type {PodName} from '../../../src/types/aliases.js'; import {PackageDownloader} from '../../../src/core/package_downloader.js'; +import {Duration} from '../../../src/core/time/duration.js'; const testName = 'mirror-cmd-e2e'; const namespace = testName; @@ -69,7 +69,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin }); after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -79,7 +79,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin }); // give a few ticks so that connections can close - afterEach(async () => await sleep(500)); + afterEach(async () => await sleep(Duration.ofMillis(500))); balanceQueryShouldSucceed(accountManager, mirrorNodeCmd, namespace); @@ -100,7 +100,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin flags.quiet.constName, flags.tlsClusterIssuerType.constName, ]); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); it('mirror node API should be running', async () => { await accountManager.loadNodeClient(namespace); @@ -110,30 +110,30 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin const explorerPod = pods[0]; portForwarder = await k8.portForward(explorerPod.metadata.name as PodName, 8_080, 8_080); - await sleep(2 * SECONDS); + await sleep(Duration.ofSeconds(2)); // check if mirror node api server is running const apiURL = 'http://127.0.0.1:8080/api/v1/transactions'; expect(await downloader.urlExists(apiURL)).to.be.true; - await sleep(2 * SECONDS); + await sleep(Duration.ofSeconds(2)); } catch (e) { mirrorNodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('Explorer GUI should be running', async () => { try { const guiURL = 'http://127.0.0.1:8080/localnet/dashboard'; expect(await downloader.urlExists(guiURL)).to.be.true; - await sleep(2 * SECONDS); + await sleep(Duration.ofSeconds(2)); mirrorNodeCmd.logger.debug('mirror node API and explorer GUI are running'); } catch (e) { mirrorNodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); it('Create topic and submit message should success', async () => { try { @@ -154,7 +154,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin mirrorNodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); // trigger some extra transactions to trigger MirrorNode to fetch the transactions accountCreationShouldSucceed(accountManager, mirrorNodeCmd, namespace); @@ -189,16 +189,16 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin mirrorNodeCmd.logger.debug(`problem with request: ${e.message}`); }); req.end(); // make the request - await sleep(2 * SECONDS); + await sleep(Duration.ofSeconds(2)); } - await sleep(SECONDS); + await sleep(Duration.ofSeconds(1)); expect(receivedMessage).to.equal(testMessage); await k8.stopPortForward(portForwarder); } catch (e) { mirrorNodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); it('mirror node destroy should success', async () => { try { @@ -207,6 +207,6 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin mirrorNodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); }); }); diff --git a/test/e2e/commands/network.test.ts b/test/e2e/commands/network.test.ts index 369392ae8..fdb4139ce 100644 --- a/test/e2e/commands/network.test.ts +++ b/test/e2e/commands/network.test.ts @@ -24,8 +24,8 @@ import {sleep} from '../../../src/core/helpers.js'; import path from 'path'; import fs from 'fs'; import {NetworkCommand} from '../../../src/commands/network.js'; -import {MINUTES, SECONDS} from '../../../src/core/constants.js'; import {Flags as flags} from '../../../src/commands/flags.js'; +import {Duration} from '../../../src/core/time/duration.js'; describe('NetworkCommand', () => { const testName = 'network-cmd-e2e'; @@ -58,7 +58,7 @@ describe('NetworkCommand', () => { const nodeCmd = bootstrapResp.cmd.nodeCmd; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -108,7 +108,7 @@ describe('NetworkCommand', () => { networkCmd.logger.showUserError(e); expect.fail(); } - }).timeout(4 * MINUTES); + }).timeout(Duration.ofMinutes(4).toMillis()); it('application env file contents should be in cached values file', () => { // @ts-ignore in order to access the private property @@ -131,12 +131,12 @@ describe('NetworkCommand', () => { while ((await k8.getPodsByLabel(['solo.hedera.com/type=network-node'])).length > 0) { networkCmd.logger.debug('Pods are still running. Waiting...'); - await sleep(3 * SECONDS); + await sleep(Duration.ofSeconds(3)); } while ((await k8.getPodsByLabel(['app=minio'])).length > 0) { networkCmd.logger.showUser('Waiting for minio container to be deleted...'); - await sleep(3 * SECONDS); + await sleep(Duration.ofSeconds(3)); } // check if chart is uninstalled @@ -155,5 +155,5 @@ describe('NetworkCommand', () => { networkCmd.logger.showUserError(e); expect.fail(); } - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); }); diff --git a/test/e2e/commands/node_add.test.ts b/test/e2e/commands/node_add.test.ts index acca816d9..ad21ab053 100644 --- a/test/e2e/commands/node_add.test.ts +++ b/test/e2e/commands/node_add.test.ts @@ -15,7 +15,7 @@ * */ import {testNodeAdd} from '../../test_add.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; const localBuildPath = ''; -testNodeAdd(localBuildPath, 'Node add with released hedera', 3 * MINUTES); +testNodeAdd(localBuildPath, 'Node add with released hedera', Duration.ofMinutes(3).toMillis()); diff --git a/test/e2e/commands/node_add_local.test.ts b/test/e2e/commands/node_add_local.test.ts index 3fb6fd349..847a44599 100644 --- a/test/e2e/commands/node_add_local.test.ts +++ b/test/e2e/commands/node_add_local.test.ts @@ -17,10 +17,10 @@ import {describe} from 'mocha'; import {testNodeAdd} from '../../test_add.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; describe('Node add with hedera local build', () => { const localBuildPath = 'node1=../hedera-services/hedera-node/data/,../hedera-services/hedera-node/data,node3=../hedera-services/hedera-node/data'; testNodeAdd(localBuildPath); -}).timeout(3 * MINUTES); +}).timeout(Duration.ofMinutes(3).toMillis()); diff --git a/test/e2e/commands/node_delete.test.ts b/test/e2e/commands/node_delete.test.ts index 01c197420..8e0e035f2 100644 --- a/test/e2e/commands/node_delete.test.ts +++ b/test/e2e/commands/node_delete.test.ts @@ -26,10 +26,11 @@ import { HEDERA_PLATFORM_VERSION_TAG, } from '../../test_util.js'; import {getTmpDir} from '../../../src/core/helpers.js'; -import {HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER} from '../../../src/core/constants.js'; +import {HEDERA_HAPI_PATH, ROOT_CONTAINER} from '../../../src/core/constants.js'; import fs from 'fs'; import type {PodName} from '../../../src/types/aliases.js'; import * as NodeCommandConfigs from '../../../src/commands/node/configs.js'; +import {Duration} from '../../../src/core/time/duration.js'; const namespace = 'node-delete'; const nodeAlias = 'node1'; @@ -52,7 +53,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi const k8 = bootstrapResp.opts.k8; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); }); @@ -60,7 +61,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should delete a node from the network successfully', async () => { await nodeCmd.handlers.delete(argv); @@ -71,7 +72,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); @@ -86,6 +87,6 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi const configTxt = fs.readFileSync(`${tmpDir}/config.txt`, 'utf8'); console.log('config.txt:', configTxt); expect(configTxt).not.to.contain(nodeAlias); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); }); }); diff --git a/test/e2e/commands/node_local_hedera.test.ts b/test/e2e/commands/node_local_hedera.test.ts index f918792c7..46b613f1d 100644 --- a/test/e2e/commands/node_local_hedera.test.ts +++ b/test/e2e/commands/node_local_hedera.test.ts @@ -19,11 +19,12 @@ import {describe} from 'mocha'; import {Flags as flags} from '../../../src/commands/flags.js'; import {e2eTestSuite, getDefaultArgv, TEST_CLUSTER} from '../../test_util.js'; import {sleep} from '../../../src/core/helpers.js'; -import {MINUTES, SOLO_LOGS_DIR} from '../../../src/core/constants.js'; +import {SOLO_LOGS_DIR} from '../../../src/core/constants.js'; import {type K8} from '../../../src/core/k8.js'; import path from 'path'; import {expect} from 'chai'; import {AccountBalanceQuery, AccountCreateTransaction, Hbar, HbarUnit, PrivateKey} from '@hashgraph/sdk'; +import {Duration} from '../../../src/core/time/duration.js'; const LOCAL_HEDERA = 'local-hedera-app'; const argv = getDefaultArgv(); @@ -79,9 +80,9 @@ e2eTestSuite( // create more transactions to save more round of states await accountCmd.create(argv); - await sleep(3); + await sleep(Duration.ofMillis(3)); await accountCmd.create(argv); - await sleep(3); + await sleep(Duration.ofMillis(3)); // stop network and save the state await nodeCmd.handlers.stop(argv); @@ -97,12 +98,12 @@ e2eTestSuite( .execute(accountManager._nodeClient); expect(balance.hbars).to.be.eql(Hbar.from(accountInfo.balance, HbarUnit.Hbar)); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); it('get the logs and delete the namespace', async () => { await hederaK8.getNodeLogs(LOCAL_HEDERA); await hederaK8.deleteNamespace(LOCAL_HEDERA); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); }); }, ); diff --git a/test/e2e/commands/node_local_ptt.test.ts b/test/e2e/commands/node_local_ptt.test.ts index c9ad17b5d..20d426306 100644 --- a/test/e2e/commands/node_local_ptt.test.ts +++ b/test/e2e/commands/node_local_ptt.test.ts @@ -18,7 +18,7 @@ import {describe} from 'mocha'; import {Flags as flags} from '../../../src/commands/flags.js'; import {e2eTestSuite, getDefaultArgv, TEST_CLUSTER} from '../../test_util.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; const LOCAL_PTT = 'local-ptt-app'; const argv = getDefaultArgv(); @@ -44,6 +44,6 @@ e2eTestSuite(LOCAL_PTT, argv, undefined, undefined, undefined, undefined, undefi it('get the logs and delete the namespace', async () => { await pttK8.getNodeLogs(LOCAL_PTT); await pttK8.deleteNamespace(LOCAL_PTT); - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); }); }); diff --git a/test/e2e/commands/node_update.test.ts b/test/e2e/commands/node_update.test.ts index a55ba91a3..c1c88c143 100644 --- a/test/e2e/commands/node_update.test.ts +++ b/test/e2e/commands/node_update.test.ts @@ -28,12 +28,13 @@ import { getTmpDir, HEDERA_PLATFORM_VERSION_TAG, } from '../../test_util.js'; -import {HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER} from '../../../src/core/constants.js'; +import {HEDERA_HAPI_PATH, ROOT_CONTAINER} from '../../../src/core/constants.js'; import fs from 'fs'; import type {PodName} from '../../../src/types/aliases.js'; import * as NodeCommandConfigs from '../../../src/commands/node/configs.js'; +import {Duration} from '../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); const namespace = 'node-update'; const updateNodeId = 'node2'; const newAccountId = '0.0.7'; @@ -63,7 +64,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi let existingNodeIdsPrivateKeysHash; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await nodeCmd.handlers.stop(argv); @@ -83,7 +84,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should update a new node property successfully', async () => { // generate gossip and tls keys for the updated node @@ -112,7 +113,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi flags.grpcEndpoints.constName, ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(30 * MINUTES); + }).timeout(Duration.ofMinutes(30).toMillis()); balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); @@ -156,6 +157,6 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi console.log('config.txt:', configTxt); expect(configTxt).to.contain(newAccountId); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); }); }); diff --git a/test/e2e/commands/node_upgrade.test.ts b/test/e2e/commands/node_upgrade.test.ts index ed9fd318b..11566f1cc 100644 --- a/test/e2e/commands/node_upgrade.test.ts +++ b/test/e2e/commands/node_upgrade.test.ts @@ -23,7 +23,7 @@ import { PREPARE_UPGRADE_CONFIGS_NAME, DOWNLOAD_GENERATED_FILES_CONFIGS_NAME, } from '../../../src/commands/node/configs.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; const namespace = 'node-upgrade'; const argv = getDefaultArgv(); @@ -45,7 +45,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi const k8 = bootstrapResp.opts.k8; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -54,12 +54,12 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should prepare network upgrade successfully', async () => { await nodeCmd.handlers.prepareUpgrade(upgradeArgv); expect(nodeCmd.getUnusedConfigs(PREPARE_UPGRADE_CONFIGS_NAME)).to.deep.equal([flags.devMode.constName]); - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); it('should download generated files successfully', async () => { await nodeCmd.handlers.downloadGeneratedFiles(upgradeArgv); @@ -67,13 +67,13 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi flags.devMode.constName, 'allNodeAliases', ]); - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); it('should upgrade all nodes on the network successfully', async () => { await nodeCmd.handlers.freezeUpgrade(upgradeArgv); expect(nodeCmd.getUnusedConfigs(PREPARE_UPGRADE_CONFIGS_NAME)).to.deep.equal([flags.devMode.constName]); await bootstrapResp.opts.accountManager.close(); - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); }); }); diff --git a/test/e2e/commands/relay.test.ts b/test/e2e/commands/relay.test.ts index 6345f88ae..760c7ab83 100644 --- a/test/e2e/commands/relay.test.ts +++ b/test/e2e/commands/relay.test.ts @@ -23,7 +23,7 @@ import {e2eTestSuite, getDefaultArgv, HEDERA_PLATFORM_VERSION_TAG, TEST_CLUSTER} import * as version from '../../../version.js'; import {sleep} from '../../../src/core/helpers.js'; import {RelayCommand} from '../../../src/commands/relay.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; const testName = 'relay-cmd-e2e'; const namespace = testName; @@ -50,10 +50,10 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin await k8.deleteNamespace(namespace); }); - afterEach(async () => await sleep(5)); + afterEach(async () => await sleep(Duration.ofMillis(5))); each(['node1', 'node1,node2']).it('relay deploy and destroy should work with $value', async function (relayNodes) { - this.timeout(5 * MINUTES); + this.timeout(Duration.ofMinutes(5).toMillis()); argv[flags.nodeAliasesUnparsed.name] = relayNodes; configManager.update(argv); @@ -70,7 +70,7 @@ e2eTestSuite(testName, argv, undefined, undefined, undefined, undefined, undefin flags.profileName.constName, flags.quiet.constName, ]); - await sleep(500); + await sleep(Duration.ofMillis(500)); // test relay destroy try { diff --git a/test/e2e/commands/separate_node_add.test.ts b/test/e2e/commands/separate_node_add.test.ts index 54b1da599..74b82dded 100644 --- a/test/e2e/commands/separate_node_add.test.ts +++ b/test/e2e/commands/separate_node_add.test.ts @@ -28,9 +28,9 @@ import { HEDERA_PLATFORM_VERSION_TAG, } from '../../test_util.js'; import * as NodeCommandConfigs from '../../../src/commands/node/configs.js'; -import {MINUTES} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); const namespace = 'node-add-separated'; const argv = getDefaultArgv(); argv[flags.nodeAliasesUnparsed.name] = 'node1,node2'; @@ -65,7 +65,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi let existingNodeIdsPrivateKeysHash; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); // @ts-ignore @@ -89,7 +89,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should add a new node to the network via the segregated commands successfully', async () => { await nodeCmd.handlers.addPrepare(argvPrepare); @@ -106,7 +106,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi 'freezeAdminPrivateKey', ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(12 * MINUTES); + }).timeout(Duration.ofMinutes(12).toMillis()); // @ts-ignore balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); @@ -132,5 +132,5 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi } } }).timeout(defaultTimeout); - }).timeout(3 * MINUTES); + }).timeout(Duration.ofMinutes(3).toMillis()); }); diff --git a/test/e2e/commands/separate_node_delete.test.ts b/test/e2e/commands/separate_node_delete.test.ts index 1c84cfd45..a577c59dc 100644 --- a/test/e2e/commands/separate_node_delete.test.ts +++ b/test/e2e/commands/separate_node_delete.test.ts @@ -26,10 +26,11 @@ import { HEDERA_PLATFORM_VERSION_TAG, } from '../../test_util.js'; import {getTmpDir} from '../../../src/core/helpers.js'; -import {HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER} from '../../../src/core/constants.js'; +import {HEDERA_HAPI_PATH, ROOT_CONTAINER} from '../../../src/core/constants.js'; import fs from 'fs'; import type {NodeAlias, PodName} from '../../../src/types/aliases.js'; import * as NodeCommandConfigs from '../../../src/commands/node/configs.js'; +import {Duration} from '../../../src/core/time/duration.js'; const namespace = 'node-delete-separate'; const nodeAlias = 'node1' as NodeAlias; @@ -59,7 +60,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi const k8 = bootstrapResp.opts.k8; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -68,7 +69,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should delete a node from the network successfully', async () => { await nodeCmd.handlers.deletePrepare(argvPrepare); @@ -83,7 +84,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); @@ -98,6 +99,6 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi const configTxt = fs.readFileSync(`${tmpDir}/config.txt`, 'utf8'); console.log('config.txt:', configTxt); expect(configTxt).not.to.contain(nodeAlias); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); }); }); diff --git a/test/e2e/commands/separate_node_update.test.ts b/test/e2e/commands/separate_node_update.test.ts index 237f4fe82..3440d9762 100644 --- a/test/e2e/commands/separate_node_update.test.ts +++ b/test/e2e/commands/separate_node_update.test.ts @@ -28,12 +28,13 @@ import { getTmpDir, HEDERA_PLATFORM_VERSION_TAG, } from '../../test_util.js'; -import {HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER} from '../../../src/core/constants.js'; +import {HEDERA_HAPI_PATH, ROOT_CONTAINER} from '../../../src/core/constants.js'; import fs from 'fs'; import type {PodName} from '../../../src/types/aliases.js'; import * as NodeCommandConfigs from '../../../src/commands/node/configs.js'; +import {Duration} from '../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); const namespace = 'node-update-separate'; const updateNodeId = 'node2'; const newAccountId = '0.0.7'; @@ -63,7 +64,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi let existingNodeIdsPrivateKeysHash; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await nodeCmd.handlers.stop(argv); @@ -78,12 +79,12 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi k8, getTmpDir(), ); - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should succeed with init command', async () => { const status = await accountCmd.init(argv); expect(status).to.be.ok; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should update a new node property successfully', async () => { // generate gossip and tls keys for the updated node @@ -123,7 +124,7 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi 'freezeAdminPrivateKey', ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(30 * MINUTES); + }).timeout(Duration.ofMinutes(30).toMillis()); balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); @@ -167,6 +168,6 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi console.log('config.txt:', configTxt); expect(configTxt).to.contain(newAccountId); - }).timeout(10 * MINUTES); + }).timeout(Duration.ofMinutes(10).toMillis()); }); }); diff --git a/test/e2e/e2e_node_util.ts b/test/e2e/e2e_node_util.ts index c64a73fef..47d8882de 100644 --- a/test/e2e/e2e_node_util.ts +++ b/test/e2e/e2e_node_util.ts @@ -29,12 +29,12 @@ import { } from '../test_util.js'; import {sleep} from '../../src/core/helpers.js'; import * as NodeCommandConfigs from '../../src/commands/node/configs.js'; -import {MINUTES, SECONDS} from '../../src/core/constants.js'; import type {NodeAlias} from '../../src/types/aliases.js'; import type {ListrTaskWrapper} from 'listr2'; import {ConfigManager} from '../../src/core/config_manager.js'; import {type K8} from '../../src/core/k8.js'; import {type NodeCommand} from '../../src/commands/node/index.js'; +import {Duration} from '../../src/core/time/duration.js'; export function e2eNodeKeyRefreshTest(testName: string, mode: string, releaseTag = HEDERA_PLATFORM_VERSION_TAG) { const namespace = testName; @@ -61,7 +61,7 @@ export function e2eNodeKeyRefreshTest(testName: string, mode: string, releaseTag undefined, true, bootstrapResp => { - const defaultTimeout = 2 * MINUTES; + const defaultTimeout = Duration.ofMinutes(2).toMillis(); describe(`NodeCommand [testName ${testName}, mode ${mode}, release ${releaseTag}]`, async () => { const accountManager = bootstrapResp.opts.accountManager; @@ -76,7 +76,7 @@ export function e2eNodeKeyRefreshTest(testName: string, mode: string, releaseTag }); after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await k8.deleteNamespace(namespace); @@ -107,16 +107,16 @@ export function e2eNodeKeyRefreshTest(testName: string, mode: string, releaseTag const nodeAlias = 'node1'; before(async function () { - this.timeout(2 * MINUTES); + this.timeout(Duration.ofMinutes(2).toMillis()); const podName = await nodeRefreshTestSetup(argv, testName, k8, nodeAlias); if (mode === 'kill') { const resp = await k8.kubeClient.deleteNamespacedPod(podName, namespace); expect(resp.response.statusCode).to.equal(200); - await sleep(20 * SECONDS); // sleep to wait for pod to finish terminating + await sleep(Duration.ofSeconds(20)); // sleep to wait for pod to finish terminating } else if (mode === 'stop') { expect(await nodeCmd.handlers.stop(argv)).to.be.true; - await sleep(20 * SECONDS); // give time for node to stop and update its logs + await sleep(Duration.ofSeconds(20)); // give time for node to stop and update its logs } else { throw new Error(`invalid mode: ${mode}`); } @@ -160,9 +160,9 @@ export function e2eNodeKeyRefreshTest(testName: string, mode: string, releaseTag expect.fail(); } finally { await nodeCmd.close(); - await sleep(10 * SECONDS); // sleep to wait for node to finish starting + await sleep(Duration.ofSeconds(10)); // sleep to wait for node to finish starting } - }).timeout(20 * MINUTES); + }).timeout(Duration.ofMinutes(20).toMillis()); } function nodeShouldNotBeActive(nodeCmd: NodeCommand, nodeAlias: NodeAlias) { diff --git a/test/e2e/integration/commands/init.test.ts b/test/e2e/integration/commands/init.test.ts index 856543f46..58c5c713c 100644 --- a/test/e2e/integration/commands/init.test.ts +++ b/test/e2e/integration/commands/init.test.ts @@ -31,11 +31,11 @@ import {LeaseManager} from '../../../../src/core/lease/lease_manager.js'; import {RemoteConfigManager} from '../../../../src/core/config/remote/remote_config_manager.js'; import * as logging from '../../../../src/core/logging.js'; import {PackageDownloader} from '../../../../src/core/package_downloader.js'; -import {SECONDS} from '../../../../src/core/constants.js'; import sinon from 'sinon'; import {IntervalLeaseRenewalService} from '../../../../src/core/lease/lease_renewal.js'; import path from 'path'; import {BASE_TEST_DIR} from '../../../test_util.js'; +import {Duration} from '../../../../src/core/time/duration.js'; const testLogger = logging.NewLogger('debug', true); describe('InitCommand', () => { @@ -89,7 +89,7 @@ describe('InitCommand', () => { describe('commands', () => { it('init execution should succeed', async () => { await expect(initCmd.init({})).to.eventually.equal(true); - }).timeout(60 * SECONDS); + }).timeout(Duration.ofSeconds(60).toMillis()); }); describe('methods', () => { diff --git a/test/e2e/integration/core/account_manager.test.ts b/test/e2e/integration/core/account_manager.test.ts index 7f85458a5..0adebe16e 100644 --- a/test/e2e/integration/core/account_manager.test.ts +++ b/test/e2e/integration/core/account_manager.test.ts @@ -20,8 +20,8 @@ import {expect} from 'chai'; import {Flags as flags} from '../../../../src/commands/flags.js'; import {e2eTestSuite, getDefaultArgv, TEST_CLUSTER} from '../../../test_util.js'; import * as version from '../../../../version.js'; -import {MINUTES} from '../../../../src/core/constants.js'; import type {PodName} from '../../../../src/types/aliases.js'; +import {Duration} from '../../../../src/core/time/duration.js'; const namespace = 'account-mngr-e2e'; const argv = getDefaultArgv(); @@ -51,7 +51,7 @@ e2eTestSuite( const configManager = bootstrapResp.opts.configManager; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.deleteNamespace(namespace); await accountManager.close(); diff --git a/test/e2e/integration/core/k8_e2e.test.ts b/test/e2e/integration/core/k8_e2e.test.ts index 075f5c758..f4642039a 100644 --- a/test/e2e/integration/core/k8_e2e.test.ts +++ b/test/e2e/integration/core/k8_e2e.test.ts @@ -45,10 +45,10 @@ import { V1VolumeResourceRequirements, } from '@kubernetes/client-node'; import crypto from 'crypto'; -import {MINUTES} from '../../../../src/core/constants.js'; import type {PodName} from '../../../../src/types/aliases.js'; +import {Duration} from '../../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); async function createPod( podName: PodName, diff --git a/test/e2e/integration/core/lease.test.ts b/test/e2e/integration/core/lease.test.ts index 493687234..86d9070da 100644 --- a/test/e2e/integration/core/lease.test.ts +++ b/test/e2e/integration/core/lease.test.ts @@ -18,15 +18,15 @@ import {it, describe, before, after} from 'mocha'; import {ConfigManager} from '../../../../src/core/config_manager.js'; import * as logging from '../../../../src/core/logging.js'; import {K8} from '../../../../src/core/k8.js'; -import {MINUTES, SECONDS} from '../../../../src/core/constants.js'; import {expect} from 'chai'; import {IntervalLease} from '../../../../src/core/lease/lease.js'; import {LeaseHolder} from '../../../../src/core/lease/lease_holder.js'; import {sleep} from '../../../../src/core/helpers.js'; import {LeaseRelinquishmentError} from '../../../../src/core/lease/lease_errors.js'; import {NoopLeaseRenewalService} from './noop_lease_renewal_service.test.js'; +import {Duration} from '../../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); const leaseDuration = 4; describe('Lease', async () => { @@ -40,7 +40,7 @@ describe('Lease', async () => { this.timeout(defaultTimeout); if (await k8.hasNamespace(testNamespace)) { await k8.deleteNamespace(testNamespace); - await sleep(5 * SECONDS); + await sleep(Duration.ofSeconds(5)); } await k8.createNamespace(testNamespace); @@ -103,7 +103,7 @@ describe('Lease', async () => { expect(await lease.isAcquired()).to.be.true; expect(await lease.isExpired()).to.be.false; - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds).plusSeconds(1)); expect(await lease.isAcquired()).to.be.false; expect(await lease.isExpired()).to.be.true; @@ -118,7 +118,7 @@ describe('Lease', async () => { await lease.acquire(); expect(await lease.isAcquired()).to.be.true; - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds).plusSeconds(1)); expect(await lease.isAcquired()).to.be.false; expect(await lease.isExpired()).to.be.true; @@ -182,7 +182,7 @@ describe('Lease', async () => { expect(await lease.isAcquired()).to.be.true; expect(await lease.isExpired()).to.be.false; - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds).plusSeconds(1)); expect(await lease.isAcquired()).to.be.false; expect(await lease.isExpired()).to.be.true; @@ -198,7 +198,7 @@ describe('Lease', async () => { expect(await lease.isAcquired()).to.be.true; expect(await lease.isExpired()).to.be.false; - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds).plusSeconds(1)); expect(await lease.isAcquired()).to.be.false; expect(await lease.isExpired()).to.be.true; diff --git a/test/e2e/integration/core/lease_renewal.test.ts b/test/e2e/integration/core/lease_renewal.test.ts index 2cb4db88e..55e9a07be 100644 --- a/test/e2e/integration/core/lease_renewal.test.ts +++ b/test/e2e/integration/core/lease_renewal.test.ts @@ -18,15 +18,15 @@ import {it, describe, before, after} from 'mocha'; import {ConfigManager} from '../../../../src/core/config_manager.js'; import * as logging from '../../../../src/core/logging.js'; import {K8} from '../../../../src/core/k8.js'; -import {MINUTES, SECONDS} from '../../../../src/core/constants.js'; import {expect} from 'chai'; import {IntervalLease} from '../../../../src/core/lease/lease.js'; import {LeaseHolder} from '../../../../src/core/lease/lease_holder.js'; import {sleep} from '../../../../src/core/helpers.js'; import {IntervalLeaseRenewalService} from '../../../../src/core/lease/lease_renewal.js'; import {type V1Lease} from '@kubernetes/client-node'; +import {Duration} from '../../../../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); const leaseDuration = 4; describe('LeaseRenewalService', async () => { @@ -40,7 +40,7 @@ describe('LeaseRenewalService', async () => { this.timeout(defaultTimeout); if (await k8.hasNamespace(testNamespace)) { await k8.deleteNamespace(testNamespace); - await sleep(5 * SECONDS); + await sleep(Duration.ofSeconds(5)); } await k8.createNamespace(testNamespace); @@ -80,7 +80,7 @@ describe('LeaseRenewalService', async () => { const acquireTime = new Date(remoteObject?.spec?.acquireTime).valueOf(); expect(acquireTime).to.be.greaterThan(0); - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds)); // @ts-ignore remoteObject = await lease.retrieveLease(); expect(remoteObject).to.not.be.null; @@ -116,7 +116,7 @@ describe('LeaseRenewalService', async () => { const acquireTime = new Date(remoteObject?.spec?.acquireTime).valueOf(); expect(acquireTime).to.be.greaterThan(0); - await sleep(lease.durationSeconds * SECONDS); + await sleep(Duration.ofSeconds(lease.durationSeconds)); // @ts-ignore remoteObject = await lease.retrieveLease(); expect(remoteObject).to.not.be.null; diff --git a/test/e2e/integration/core/noop_lease_renewal_service.test.ts b/test/e2e/integration/core/noop_lease_renewal_service.test.ts index 5c6d90047..e4e829871 100644 --- a/test/e2e/integration/core/noop_lease_renewal_service.test.ts +++ b/test/e2e/integration/core/noop_lease_renewal_service.test.ts @@ -15,6 +15,7 @@ * */ import type {Lease, LeaseRenewalService} from '../../../../src/core/lease/types.js'; +import {Duration} from '../../../../src/core/time/duration.js'; export class NoopLeaseRenewalService implements LeaseRenewalService { private readonly buffer: SharedArrayBuffer; @@ -42,7 +43,7 @@ export class NoopLeaseRenewalService implements LeaseRenewalService { return new Map(); } - public calculateRenewalDelay(lease: Lease): number { - return 10; + public calculateRenewalDelay(lease: Lease): Duration { + return Duration.ofSeconds(10); } } diff --git a/test/e2e/integration/core/package_downloader_e2e.test.ts b/test/e2e/integration/core/package_downloader_e2e.test.ts index 41f2ba8cf..5a8cb8019 100644 --- a/test/e2e/integration/core/package_downloader_e2e.test.ts +++ b/test/e2e/integration/core/package_downloader_e2e.test.ts @@ -23,7 +23,7 @@ import * as fs from 'fs'; import {PackageDownloader} from '../../../../src/core/package_downloader.js'; import {Templates} from '../../../../src/core/templates.js'; import * as logging from '../../../../src/core/logging.js'; -import {MINUTES} from '../../../../src/core/constants.js'; +import {Duration} from '../../../../src/core/time/duration.js'; describe('PackageDownloaderE2E', () => { const testLogger = logging.NewLogger('debug', true); @@ -39,5 +39,5 @@ describe('PackageDownloaderE2E', () => { await expect(downloader.fetchPlatform(tag, testCacheDir)).to.eventually.be.equal(destPath); expect(fs.existsSync(destPath)).to.be.ok; testLogger.showUser(destPath); - }).timeout(3 * MINUTES); + }).timeout(Duration.ofMinutes(3).toMillis()); }); diff --git a/test/e2e/integration/core/platform_installer_e2e.test.ts b/test/e2e/integration/core/platform_installer_e2e.test.ts index 788c1f1fa..c7cf04720 100644 --- a/test/e2e/integration/core/platform_installer_e2e.test.ts +++ b/test/e2e/integration/core/platform_installer_e2e.test.ts @@ -23,9 +23,9 @@ import * as fs from 'fs'; import {e2eTestSuite, getDefaultArgv, getTestCacheDir, TEST_CLUSTER, testLogger} from '../../../test_util.js'; import {Flags as flags} from '../../../../src/commands/flags.js'; import * as version from '../../../../version.js'; -import {MINUTES, SECONDS} from '../../../../src/core/constants.js'; +import {Duration} from '../../../../src/core/time/duration.js'; -const defaultTimeout = 20 * SECONDS; +const defaultTimeout = Duration.ofSeconds(20).toMillis(); const namespace = 'pkg-installer-e2e'; const argv = getDefaultArgv(); @@ -59,7 +59,7 @@ e2eTestSuite( const packageVersion = 'v0.42.5'; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.deleteNamespace(namespace); await accountManager.close(); @@ -109,7 +109,7 @@ e2eTestSuite( `ls -la ${constants.HEDERA_HAPI_PATH}`, ); testLogger.showUser(outputs); - }).timeout(MINUTES); + }).timeout(Duration.ofMinutes(1).toMillis()); }); }); }, diff --git a/test/e2e/integration/core/remote_config_manager.test.ts b/test/e2e/integration/core/remote_config_manager.test.ts index d6be5ba2b..8098bb2c1 100644 --- a/test/e2e/integration/core/remote_config_manager.test.ts +++ b/test/e2e/integration/core/remote_config_manager.test.ts @@ -24,12 +24,13 @@ import {RemoteConfigManager} from '../../../../src/core/config/remote/remote_con import {e2eTestSuite, getDefaultArgv, getTestCacheDir, TEST_CLUSTER} from '../../../test_util.js'; import {Flags as flags} from '../../../../src/commands/flags.js'; import * as version from '../../../../version.js'; -import {MINUTES, SECONDS} from '../../../../src/core/constants.js'; + import path from 'path'; import {SoloError} from '../../../../src/core/errors.js'; import {RemoteConfigDataWrapper} from '../../../../src/core/config/remote/remote_config_data_wrapper.js'; +import {Duration} from '../../../../src/core/time/duration.js'; -const defaultTimeout = 20 * SECONDS; +const defaultTimeout = Duration.ofSeconds(20).toMillis(); const namespace = 'remote-config-manager-e2e'; const argv = getDefaultArgv(); @@ -71,7 +72,7 @@ e2eTestSuite( localConfig.currentDeploymentName = namespace; after(async function () { - this.timeout(3 * MINUTES); + this.timeout(Duration.ofMinutes(3).toMillis()); await k8.deleteNamespace(namespace); }); diff --git a/test/test_add.ts b/test/test_add.ts index 5dca1bc03..f6ca4af32 100644 --- a/test/test_add.ts +++ b/test/test_add.ts @@ -28,11 +28,11 @@ import { HEDERA_PLATFORM_VERSION_TAG, } from './test_util.js'; import * as NodeCommandConfigs from '../src/commands/node/configs.js'; -import {MINUTES} from '../src/core/constants.js'; import type {NodeAlias} from '../src/types/aliases.js'; import type {NetworkNodeServices} from '../src/core/network_node_services.js'; +import {Duration} from '../src/core/time/duration.js'; -const defaultTimeout = 2 * MINUTES; +const defaultTimeout = Duration.ofMinutes(2).toMillis(); export function testNodeAdd( localBuildPath: string, @@ -75,7 +75,7 @@ export function testNodeAdd( let existingNodeIdsPrivateKeysHash: Map>; after(async function () { - this.timeout(10 * MINUTES); + this.timeout(Duration.ofMinutes(10).toMillis()); await k8.getNodeLogs(namespace); await bootstrapResp.opts.accountManager.close(); @@ -96,7 +96,7 @@ export function testNodeAdd( it('should succeed with init command', async () => { expect(await accountCmd.init(argv)).to.be.true; - }).timeout(8 * MINUTES); + }).timeout(Duration.ofMinutes(8).toMillis()); it('should add a new node to the network successfully', async () => { await nodeCmd.handlers.add(argv); @@ -108,7 +108,7 @@ export function testNodeAdd( 'chartPath', ]); await bootstrapResp.opts.accountManager.close(); - }).timeout(12 * MINUTES); + }).timeout(Duration.ofMinutes(12).toMillis()); balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace); diff --git a/test/test_util.ts b/test/test_util.ts index 5a4d8c9e6..a4e67924b 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -30,7 +30,7 @@ import {NodeCommand} from '../src/commands/node/index.js'; import {DependencyManager, HelmDependencyManager} from '../src/core/dependency_managers/index.js'; import {sleep} from '../src/core/helpers.js'; import {AccountBalanceQuery, AccountCreateTransaction, Hbar, HbarUnit, PrivateKey} from '@hashgraph/sdk'; -import {MINUTES, NODE_LOG_FAILURE_MSG, ROOT_CONTAINER, SECONDS, SOLO_LOGS_DIR} from '../src/core/constants.js'; +import {NODE_LOG_FAILURE_MSG, ROOT_CONTAINER, SOLO_LOGS_DIR} from '../src/core/constants.js'; import crypto from 'crypto'; import {AccountCommand} from '../src/commands/account.js'; import {SoloError} from '../src/core/errors.js'; @@ -59,6 +59,7 @@ import {KeyManager} from '../src/core/key_manager.js'; import {Zippy} from '../src/core/zippy.js'; import {HEDERA_PLATFORM_VERSION} from '../version.js'; import {IntervalLeaseRenewalService} from '../src/core/lease/lease_renewal.js'; +import {Duration} from '../src/core/time/duration.js'; export const testLogger = logging.NewLogger('debug', true); export const TEST_CLUSTER = 'solo-e2e'; @@ -237,7 +238,7 @@ export function e2eTestSuite( }); after(async function () { - this.timeout(5 * MINUTES); + this.timeout(Duration.ofMinutes(5).toMillis()); await k8.getNodeLogs(namespace); bootstrapResp.opts.logger.showUser( `------------------------- END: bootstrap (${testName}) ----------------------------`, @@ -252,7 +253,7 @@ export function e2eTestSuite( while (await k8.hasNamespace(namespace)) { testLogger.debug(`Namespace ${namespace} still exist. Waiting...`); - await sleep(1.5 * SECONDS); + await sleep(Duration.ofSeconds(2)); } } @@ -261,7 +262,7 @@ export function e2eTestSuite( ) { await clusterCmd.setup(argv); } - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); it('generate key files', async () => { expect(await nodeCmd.handlers.keys(argv)).to.be.true; @@ -269,7 +270,7 @@ export function e2eTestSuite( flags.devMode.constName, flags.quiet.constName, ]); - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); it('should succeed with network deploy', async () => { await networkCmd.deploy(argv); @@ -289,7 +290,7 @@ export function e2eTestSuite( flags.grpcWebTlsKeyPath.constName, 'chartPath', ]); - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); if (startNodes) { it('should succeed with node setup command', async () => { @@ -303,7 +304,7 @@ export function e2eTestSuite( nodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(4 * MINUTES); + }).timeout(Duration.ofMinutes(4).toMillis()); it('should succeed with node start command', async () => { try { @@ -312,7 +313,7 @@ export function e2eTestSuite( nodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(30 * MINUTES); + }).timeout(Duration.ofMinutes(30).toMillis()); it('node log command should work', async () => { expect(await nodeCmd.handlers.logs(argv)).to.be.true; @@ -321,7 +322,7 @@ export function e2eTestSuite( const soloLog = fs.readFileSync(soloLogPath, 'utf8'); expect(soloLog).to.not.have.string(NODE_LOG_FAILURE_MSG); - }).timeout(5 * MINUTES); + }).timeout(Duration.ofMinutes(5).toMillis()); } }); @@ -347,8 +348,8 @@ export function balanceQueryShouldSucceed(accountManager: AccountManager, cmd: B cmd.logger.showUserError(e); expect.fail(); } - await sleep(SECONDS); - }).timeout(2 * MINUTES); + await sleep(Duration.ofSeconds(1)); + }).timeout(Duration.ofMinutes(2).toMillis()); } export function accountCreationShouldSucceed(accountManager: AccountManager, nodeCmd: BaseCommand, namespace: string) { @@ -379,7 +380,7 @@ export function accountCreationShouldSucceed(accountManager: AccountManager, nod nodeCmd.logger.showUserError(e); expect.fail(); } - }).timeout(2 * MINUTES); + }).timeout(Duration.ofMinutes(2).toMillis()); } export async function getNodeAliasesPrivateKeysHash( diff --git a/test/unit/core/k8.test.ts b/test/unit/core/k8.test.ts index ed349aa9d..bef75fb7f 100644 --- a/test/unit/core/k8.test.ts +++ b/test/unit/core/k8.test.ts @@ -23,7 +23,7 @@ import {K8} from '../../../src/core/k8.js'; import {ConfigManager} from '../../../src/core/config_manager.js'; import {testLogger} from '../../test_util.js'; import {Flags as flags} from '../../../src/commands/flags.js'; -import {SECONDS} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; function listNamespacedPodMockSetup(k8: K8, numOfFailures: number, result: any) { for (let i = 0; i < numOfFailures - 1; i++) { @@ -45,7 +45,7 @@ function listNamespacedPodMockSetup(k8: K8, numOfFailures: number, result: any) ); } -const defaultTimeout = 20 * SECONDS; +const defaultTimeout = Duration.ofSeconds(20).toMillis(); describe('K8 Unit Tests', function () { this.timeout(defaultTimeout); diff --git a/test/unit/core/key_manager.test.ts b/test/unit/core/key_manager.test.ts index 3f3798152..9662f43f3 100644 --- a/test/unit/core/key_manager.test.ts +++ b/test/unit/core/key_manager.test.ts @@ -23,8 +23,8 @@ import path from 'path'; import {KeyManager} from '../../../src/core/key_manager.js'; import * as constants from '../../../src/core/constants.js'; import * as logging from '../../../src/core/logging.js'; -import {SECONDS} from '../../../src/core/constants.js'; import type {NodeAlias} from '../../../src/types/aliases.js'; +import {Duration} from '../../../src/core/time/duration.js'; describe('KeyManager', () => { const logger = logging.NewLogger('debug', true); @@ -84,5 +84,5 @@ describe('KeyManager', () => { ).to.be.true; fs.rmSync(tmpDir, {recursive: true}); - }).timeout(20 * SECONDS); + }).timeout(Duration.ofSeconds(20).toMillis()); }); diff --git a/test/unit/core/shell_runner.test.ts b/test/unit/core/shell_runner.test.ts index c30f899b2..886fef943 100644 --- a/test/unit/core/shell_runner.test.ts +++ b/test/unit/core/shell_runner.test.ts @@ -24,7 +24,7 @@ import {ShellRunner} from '../../../src/core/shell_runner.js'; import {NewLogger, SoloLogger} from '../../../src/core/logging.js'; import {ChildProcess} from 'child_process'; import {Readable} from 'stream'; -import {SECONDS} from '../../../src/core/constants.js'; +import {Duration} from '../../../src/core/time/duration.js'; describe('ShellRunner', () => { let logger: SoloLogger, @@ -55,5 +55,5 @@ describe('ShellRunner', () => { expect(readableSpy).to.have.been.calledWith('data', sinon.match.any); expect(childProcessSpy).to.have.been.calledWith('exit', sinon.match.any); - }).timeout(10 * SECONDS); + }).timeout(Duration.ofSeconds(10).toMillis()); }); diff --git a/test/unit/core/time/duration.test.ts b/test/unit/core/time/duration.test.ts new file mode 100644 index 000000000..35a5101a6 --- /dev/null +++ b/test/unit/core/time/duration.test.ts @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {Duration} from '../../../../src/core/time/duration.js'; + +describe('Duration', () => { + describe('Java TCK', () => { + it('plus zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.plus(Duration.ZERO)).to.equal(t); + }); + + it('plus zero singleton', () => { + const t = Duration.ofSeconds(-1); + expect(t.plus(Duration.ofSeconds(1))).to.equal(Duration.ZERO); + }); + + it('plusSeconds zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.plusSeconds(0)).to.equal(t); + }); + + it('plusSeconds zero singleton', () => { + const t = Duration.ofSeconds(-1); + expect(t.plusSeconds(1)).to.equal(Duration.ZERO); + }); + + it('plusMillis zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.plusMillis(0)).to.equal(t); + }); + + it('plusMillis zero singleton', () => { + const t = Duration.ofMillis(-1); + expect(t.plusMillis(1)).to.equal(Duration.ZERO); + }); + + it('plusNanos zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.plusNanos(0)).to.equal(t); + }); + + it('plusNanos zero singleton', () => { + const t = Duration.ofNanos(-1); + expect(t.plusNanos(1)).to.equal(Duration.ZERO); + }); + + it('minus zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.minus(Duration.ZERO)).to.equal(t); + }); + + it('minus zero singleton', () => { + const t = Duration.ofSeconds(1); + expect(t.minus(Duration.ofSeconds(1))).to.equal(Duration.ZERO); + }); + + it('minusSeconds zero returns this', () => { + const t = Duration.ofSeconds(-1); + expect(t.minusSeconds(0)).to.equal(t); + }); + + it('minusSeconds zero singleton', () => { + const t = Duration.ofSeconds(1); + expect(t.minusSeconds(1)).to.equal(Duration.ZERO); + }); + + it('minusMillis zero returns this', () => { + const t = Duration.ofSecondsAdjusted(1, 2_000_000); + expect(t.minusMillis(0)).to.equal(t); + }); + + it('minusMillis zero singleton', () => { + const t = Duration.ofSecondsAdjusted(1, 2_000_000); + expect(t.minusMillis(1002)).to.equal(Duration.ZERO); + }); + + it('minusNanos zero returns this', () => { + const t = Duration.ofSecondsAdjusted(1, 2_000_000); + expect(t.minusNanos(0)).to.equal(t); + }); + + it('minusNanos zero singleton', () => { + const t = Duration.ofSecondsAdjusted(1, 2_000_000); + expect(t.minusNanos(1_002_000_000)).to.equal(Duration.ZERO); + }); + }); +}); diff --git a/test/unit/core/util/math_ex.test.ts b/test/unit/core/util/math_ex.test.ts new file mode 100644 index 000000000..e697d86af --- /dev/null +++ b/test/unit/core/util/math_ex.test.ts @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {MathEx} from '../../../../src/core/util/math_ex.js'; +import {ArithmeticError} from '../../../../src/core/util/arithmetic_error.js'; + +describe('MathEx', () => { + it('testLongExact', () => { + testLongExactTwice(0, 0); + testLongExactTwice(1, 1); + testLongExactTwice(1, -1); + testLongExactTwice(1000, 2000); + + testLongExactTwice(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER); + testLongExactTwice(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + testLongExactTwice(Number.MIN_SAFE_INTEGER, 1); + testLongExactTwice(Number.MAX_SAFE_INTEGER, 1); + testLongExactTwice(Number.MIN_SAFE_INTEGER, 2); + testLongExactTwice(Number.MAX_SAFE_INTEGER, 2); + testLongExactTwice(Number.MIN_SAFE_INTEGER, -1); + testLongExactTwice(Number.MAX_SAFE_INTEGER, -1); + testLongExactTwice(Number.MIN_SAFE_INTEGER, -2); + testLongExactTwice(Number.MAX_SAFE_INTEGER, -2); + testLongExactTwice(Math.trunc(Number.MIN_SAFE_INTEGER / 2), 2); + testLongExactTwice(Math.trunc(Number.MAX_SAFE_INTEGER / 2), 2); + }); + + it('testLongFloorDivMod', () => { + testLongFloorDivMod(4, 0, 0, 0, true, true); + testLongFloorDivMod(4, 3, 1, 1, false, false); + testLongFloorDivMod(3, 3, 1, 0, false, false); + testLongFloorDivMod(2, 3, 0, 2, false, false); + testLongFloorDivMod(1, 3, 0, 1, false, false); + testLongFloorDivMod(0, 3, 0, 0, false, false); + testLongFloorDivMod(4, -3, -2, -2, false, false); + testLongFloorDivMod(3, -3, -1, 0, false, false); + testLongFloorDivMod(2, -3, -1, -1, false, false); + testLongFloorDivMod(1, -3, -1, -2, false, false); + testLongFloorDivMod(0, -3, 0, 0, false, false); + testLongFloorDivMod(-1, 3, -1, 2, false, false); + testLongFloorDivMod(-2, 3, -1, 1, false, false); + testLongFloorDivMod(-3, 3, -1, 0, false, false); + testLongFloorDivMod(-4, 3, -2, 2, false, false); + testLongFloorDivMod(-1, -3, 0, -1, false, false); + testLongFloorDivMod(-2, -3, 0, -2, false, false); + testLongFloorDivMod(-3, -3, 1, 0, false, false); + testLongFloorDivMod(-4, -3, 1, -1, false, false); + + testLongFloorDivMod(Number.MAX_SAFE_INTEGER, 1, Number.MAX_SAFE_INTEGER, 0, false, false); + testLongFloorDivMod(Number.MAX_SAFE_INTEGER, -1, -Number.MAX_SAFE_INTEGER, 0, false, false); + testLongFloorDivMod(Number.MAX_SAFE_INTEGER, 3, Math.floor(Number.MAX_SAFE_INTEGER / 3), 1, false, false); + testLongFloorDivMod(Number.MAX_SAFE_INTEGER - 1, 3, Math.floor(Number.MAX_SAFE_INTEGER - 1) / 3, 0, false, false); + testLongFloorDivMod(Number.MIN_SAFE_INTEGER, 3, Math.floor(Number.MIN_SAFE_INTEGER / 3), 1, false, false); + testLongFloorDivMod(Number.MIN_SAFE_INTEGER + 1, 3, Math.floor(Number.MIN_SAFE_INTEGER / 3) + 1, 0, false, false); + testLongFloorDivMod(Number.MIN_SAFE_INTEGER + 1, -1, Number.MAX_SAFE_INTEGER - 1, 0, false, false); + // Special case of integer overflow + testLongFloorDivMod(Number.MIN_SAFE_INTEGER, -1, Number.MAX_SAFE_INTEGER, 0, false, false); + }); +}); + +function testLongExact(x: number, y: number) { + let resultBig: bigint; + try { + resultBig = BigInt(x) + BigInt(y); + const sum = MathEx.addExact(x, y); + + checkResult('addExact', x, y, sum, resultBig); + } catch (e: ArithmeticError | any) { + checkError('addExact', x, y, resultBig, e); + } + + try { + resultBig = BigInt(x) - BigInt(y); + const diff = MathEx.subtractExact(x, y); + + checkResult('subtractExact', x, y, diff, resultBig); + } catch (e: ArithmeticError | any) { + checkError('subtractExact', x, y, resultBig, e); + } + + try { + resultBig = BigInt(x) * BigInt(y); + const product = MathEx.multiplyExact(x, y); + + checkResult('multiplyExact', x, y, product, resultBig); + } catch (e: ArithmeticError | any) { + checkError('multiplyExact', x, y, resultBig, e); + } +} + +function testLongExactTwice(x: number, y: number) { + testLongExact(x, y); + testLongExact(y, x); +} + +function checkError(message: string, x: number, y: number, resultBig: bigint, e: Error) { + if (!(e instanceof ArithmeticError)) { + throw e; + } + + if (Number.isSafeInteger(Number(resultBig))) { + expect.fail(`${message}(${x}, ${y}); Unexpected exception: ${e}`); + } +} + +function checkResult(message: string, x: number, y: number, result: number, expected: bigint) { + const resultBig = BigInt(result); + const finalMessage = `${message}(${x}, ${y}) = ${result}`; + if (!Number.isSafeInteger(Number(resultBig))) { + expect.fail(`${finalMessage}; expected an arithmetic error`); + } else if (resultBig !== expected) { + expect.fail(`${finalMessage}; expected ${expected}`); + } +} + +function testLongFloorDivMod( + x: number, + y: number, + divExpected: number, + modExpected: number, + divThrows: boolean, + modThrows: boolean, +) { + testLongFloorDiv(x, y, divExpected, divThrows); + testLongFloorMod(x, y, modExpected, modThrows); +} + +function testLongFloorDiv(x: number, y: number, expected: number, shouldThrow: boolean) { + try { + const result = MathEx.floorDiv(x, y); + if (result !== expected) { + expect.fail(`floorDiv(${x}, ${y}) = ${result}; expected ${expected}`); + } + + if (shouldThrow) { + expect.fail(`floorDiv(${x}, ${y}); expected an arithmetic error`); + } + } catch (e: ArithmeticError | any) { + if (!(e instanceof ArithmeticError)) { + throw e; + } + + if (!shouldThrow) { + expect.fail(`floorDiv(${x}, ${y}); Unexpected exception: ${e}`); + } + } +} + +function testLongFloorMod(x: number, y: number, expected: number, shouldThrow: boolean) { + try { + const result = MathEx.floorMod(x, y); + if (result !== expected) { + expect.fail(`floorMod(${x}, ${y}) = ${result}; expected ${expected}`); + } + + if (shouldThrow) { + expect.fail(`floorMod(${x}, ${y}); expected an arithmetic error`); + } + } catch (e: ArithmeticError | any) { + if (!(e instanceof ArithmeticError)) { + throw e; + } + + if (e instanceof ArithmeticError && y !== 0) { + expect.fail(`floorMod(${x}, ${y}); Unexpected arithmetic error: ${e}`); + } + + if (!shouldThrow) { + expect.fail(`floorMod(${x}, ${y}); Unexpected exception: ${e}`); + } + } +}