From e799b4f5bc149aa8510159f09656ddd5c358153c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 23 Oct 2025 23:57:38 +0200 Subject: [PATCH] fix: avoid circular module dependency MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- pnpm-lock.yaml | 21 +--- src/utils/Configuration.ts | 2 +- src/utils/ConfigurationUtils.ts | 9 -- src/worker/WorkerUtils.ts | 10 ++ src/worker/index.ts | 1 + tests/utils/ConfigurationUtils.test.ts | 99 ++++++++++++++----- tests/worker/WorkerUtils.test.ts | 130 +++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 tests/worker/WorkerUtils.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 052d632c..742144bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5047,11 +5047,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -8115,7 +8110,7 @@ snapshots: brfs@2.0.2: dependencies: quote-stream: 1.0.2 - resolve: 1.22.10 + resolve: 1.22.11 static-module: 3.0.4 through2: 2.0.5 @@ -8134,7 +8129,7 @@ snapshots: browser-resolve@2.0.0: dependencies: - resolve: 1.22.10 + resolve: 1.22.11 browserify-aes@1.2.0: dependencies: @@ -8215,7 +8210,7 @@ snapshots: querystring-es3: 0.2.1 read-only-stream: 2.0.0 readable-stream: 2.3.8 - resolve: 1.22.10 + resolve: 1.22.11 shasum-object: 1.0.1 shell-quote: 1.8.3 stream-browserify: 3.0.0 @@ -10992,7 +10987,7 @@ snapshots: inherits: 2.0.4 parents: 1.0.1 readable-stream: 2.3.8 - resolve: 1.22.10 + resolve: 1.22.11 stream-combiner2: 1.1.1 subarg: 1.0.0 through2: 2.0.5 @@ -11483,7 +11478,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.10 + resolve: 1.22.11 postcss-selector-parser@6.1.2: dependencies: @@ -11746,12 +11741,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.11: dependencies: is-core-module: 2.16.1 diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 1ddb2d2e..2fe28900 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -19,6 +19,7 @@ import { type WorkerConfiguration, } from '../types/index.js' import { + checkWorkerProcessType, DEFAULT_ELEMENT_ADD_DELAY, DEFAULT_POOL_MAX_SIZE, DEFAULT_POOL_MIN_SIZE, @@ -28,7 +29,6 @@ import { import { buildPerformanceUriFilePath, checkWorkerElementsPerWorker, - checkWorkerProcessType, getDefaultPerformanceStorageUri, handleFileException, logPrefix, diff --git a/src/utils/ConfigurationUtils.ts b/src/utils/ConfigurationUtils.ts index 034de130..f1f5c0cc 100644 --- a/src/utils/ConfigurationUtils.ts +++ b/src/utils/ConfigurationUtils.ts @@ -3,7 +3,6 @@ import { dirname, join, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { type ElementsPerWorkerType, type FileType, StorageType } from '../types/index.js' -import { WorkerProcessType } from '../worker/index.js' import { Constants } from './Constants.js' import { isNotEmptyString, logPrefix as utilsLogPrefix } from './Utils.js' @@ -76,14 +75,6 @@ export const handleFileException = ( throw error } -export const checkWorkerProcessType = (workerProcessType: WorkerProcessType): void => { - if (!Object.values(WorkerProcessType).includes(workerProcessType)) { - throw new SyntaxError( - `Invalid worker process type '${workerProcessType}' defined in configuration` - ) - } -} - export const checkWorkerElementsPerWorker = ( elementsPerWorker: ElementsPerWorkerType | undefined ): void => { diff --git a/src/worker/WorkerUtils.ts b/src/worker/WorkerUtils.ts index b982cf05..a5a06908 100644 --- a/src/worker/WorkerUtils.ts +++ b/src/worker/WorkerUtils.ts @@ -1,6 +1,8 @@ import chalk from 'chalk' import { getRandomValues } from 'node:crypto' +import { WorkerProcessType } from './WorkerTypes.js' + export const sleep = async (milliSeconds: number): Promise => { return await new Promise(resolve => { const timeout = setTimeout(() => { @@ -30,6 +32,14 @@ export const randomizeDelay = (delay: number): number => { return delay + sign * randomSum } +export const checkWorkerProcessType = (workerProcessType: WorkerProcessType): void => { + if (!Object.values(WorkerProcessType).includes(workerProcessType)) { + throw new SyntaxError( + `Invalid worker process type '${workerProcessType}' defined in configuration` + ) + } +} + /** * Generates a cryptographically secure random number in the [0,1[ range * @returns A number in the [0,1[ range diff --git a/src/worker/index.ts b/src/worker/index.ts index 32368203..00308170 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -15,3 +15,4 @@ export { WorkerMessageEvents, WorkerProcessType, } from './WorkerTypes.js' +export { checkWorkerProcessType } from './WorkerUtils.js' diff --git a/tests/utils/ConfigurationUtils.test.ts b/tests/utils/ConfigurationUtils.test.ts index f90945fd..73a4d087 100644 --- a/tests/utils/ConfigurationUtils.test.ts +++ b/tests/utils/ConfigurationUtils.test.ts @@ -1,22 +1,77 @@ -// /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -// import { expect } from '@std/expect' -// import { describe, it } from 'node:test' - -// import { FileType } from '../../src/types/index.js' -// import { handleFileException, logPrefix } from '../../src/utils/ConfigurationUtils.js' - -// await describe('ConfigurationUtils test suite', async () => { -// await it('Verify logPrefix()', () => { -// expect(logPrefix()).toContain(' Simulator configuration |') -// }) - -// await it('Verify handleFileException()', t => { -// t.mock.method(console, 'error') -// const error = new Error() -// error.code = 'ENOENT' -// expect(() => { -// handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |') -// }).toThrow(error) -// expect(console.error.mock.calls.length).toBe(1) -// }) -// }) +import { expect } from '@std/expect' +import { describe, it } from 'node:test' + +import { FileType, StorageType } from '../../src/types/index.js' +import { + buildPerformanceUriFilePath, + checkWorkerElementsPerWorker, + getDefaultPerformanceStorageUri, + handleFileException, + logPrefix, +} from '../../src/utils/ConfigurationUtils.js' + +await describe('ConfigurationUtils test suite', async () => { + await it('Verify logPrefix()', () => { + expect(logPrefix()).toContain(' Simulator configuration |') + }) + + await it('Verify buildPerformanceUriFilePath()', () => { + const result = buildPerformanceUriFilePath('test.json') + expect(result).toContain('test.json') + expect(result).toMatch(/^file:\/\/.*test\.json$/) + }) + + await it('Verify getDefaultPerformanceStorageUri()', () => { + // Test JSON_FILE storage type + const jsonUri = getDefaultPerformanceStorageUri(StorageType.JSON_FILE) + expect(jsonUri).toMatch(/^file:\/\/.*\.json$/) + expect(jsonUri).toContain('performanceRecords.json') + + // Test SQLITE storage type + const sqliteUri = getDefaultPerformanceStorageUri(StorageType.SQLITE) + expect(sqliteUri).toMatch(/^file:\/\/.*\.db$/) + expect(sqliteUri).toContain('charging-stations-simulator.db') + + // Test unsupported storage type + expect(() => { + getDefaultPerformanceStorageUri('unsupported' as StorageType) + }).toThrow(Error) + }) + + await it('Verify handleFileException()', t => { + const mockConsoleError = t.mock.method(console, 'error') + const error = new Error() as NodeJS.ErrnoException + error.code = 'ENOENT' + expect(() => { + handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |') + }).toThrow(error) + expect(mockConsoleError.mock.calls.length).toBe(1) + }) + + await it('Verify checkWorkerElementsPerWorker()', () => { + // These calls should not throw exceptions + expect(() => { + checkWorkerElementsPerWorker(undefined) + }).not.toThrow() + expect(() => { + checkWorkerElementsPerWorker('auto') + }).not.toThrow() + expect(() => { + checkWorkerElementsPerWorker('all') + }).not.toThrow() + expect(() => { + checkWorkerElementsPerWorker(4) + }).not.toThrow() + + // These calls should throw exceptions + expect(() => { + checkWorkerElementsPerWorker(0) + }).toThrow(RangeError) + expect(() => { + checkWorkerElementsPerWorker(-1) + }).toThrow(RangeError) + expect(() => { + checkWorkerElementsPerWorker(1.5) + }).toThrow(SyntaxError) + }) +}) diff --git a/tests/worker/WorkerUtils.test.ts b/tests/worker/WorkerUtils.test.ts new file mode 100644 index 00000000..28166ad0 --- /dev/null +++ b/tests/worker/WorkerUtils.test.ts @@ -0,0 +1,130 @@ +import { expect } from '@std/expect' +import { describe, it } from 'node:test' + +import { WorkerProcessType } from '../../src/worker/WorkerTypes.js' +import { + checkWorkerProcessType, + defaultErrorHandler, + defaultExitHandler, + randomizeDelay, + sleep, +} from '../../src/worker/WorkerUtils.js' + +await describe('WorkerUtils test suite', async () => { + await it('Verify checkWorkerProcessType()', () => { + // Valid worker process types should not throw + expect(() => { + checkWorkerProcessType(WorkerProcessType.dynamicPool) + }).not.toThrow() + expect(() => { + checkWorkerProcessType(WorkerProcessType.fixedPool) + }).not.toThrow() + expect(() => { + checkWorkerProcessType(WorkerProcessType.workerSet) + }).not.toThrow() + + // Invalid worker process type should throw + expect(() => { + checkWorkerProcessType('invalidType' as WorkerProcessType) + }).toThrow(SyntaxError) + }) + + await it('Verify sleep()', async () => { + const startTime = Date.now() + const delay = 10 // 10ms for fast test execution + + const timeout = await sleep(delay) + const endTime = Date.now() + const actualDelay = endTime - startTime + + // Verify timeout object is returned + expect(timeout).toBeDefined() + expect(typeof timeout).toBe('object') + + // Verify actual delay is approximately correct (within reasonable tolerance) + expect(actualDelay).toBeGreaterThanOrEqual(delay) + expect(actualDelay).toBeLessThan(delay + 50) // Allow 50ms tolerance for system variance + + // Clean up timeout + clearTimeout(timeout) + }) + + await it('Verify defaultExitHandler()', t => { + const mockConsoleInfo = t.mock.method(console, 'info') + const mockConsoleError = t.mock.method(console, 'error') + + // Test successful exit (code 0) + defaultExitHandler(0) + expect(mockConsoleInfo.mock.calls.length).toBe(1) + expect(mockConsoleError.mock.calls.length).toBe(0) + + // Reset mocks + mockConsoleInfo.mock.resetCalls() + mockConsoleError.mock.resetCalls() + + // Test terminated successfully (code 1) + defaultExitHandler(1) + expect(mockConsoleInfo.mock.calls.length).toBe(1) + expect(mockConsoleError.mock.calls.length).toBe(0) + + // Reset mocks + mockConsoleInfo.mock.resetCalls() + mockConsoleError.mock.resetCalls() + + // Test error exit (code > 1) + defaultExitHandler(2) + expect(mockConsoleInfo.mock.calls.length).toBe(0) + expect(mockConsoleError.mock.calls.length).toBe(1) + + // Test another error code + mockConsoleError.mock.resetCalls() + defaultExitHandler(5) + expect(mockConsoleError.mock.calls.length).toBe(1) + }) + + await it('Verify defaultErrorHandler()', t => { + const mockConsoleError = t.mock.method(console, 'error') + const testError = new Error('Test error message') + + defaultErrorHandler(testError) + + expect(mockConsoleError.mock.calls.length).toBe(1) + + // Test with different error types + const syntaxError = new SyntaxError('Syntax error') + defaultErrorHandler(syntaxError) + expect(mockConsoleError.mock.calls.length).toBe(2) + }) + + await it('Verify randomizeDelay()', () => { + const baseDelay = 1000 + const tolerance = baseDelay * 0.2 // 20% tolerance as per implementation + + // Test multiple random variations to verify range + const results: number[] = [] + for (let i = 0; i < 100; i++) { + const randomized = randomizeDelay(baseDelay) + results.push(randomized) + + // Each result should be within ±20% of base delay + expect(randomized).toBeGreaterThanOrEqual(baseDelay - tolerance) + expect(randomized).toBeLessThanOrEqual(baseDelay + tolerance) + } + + // Verify we get some variation (not all values identical) + const uniqueValues = new Set(results) + expect(uniqueValues.size).toBeGreaterThan(1) + + // Test with zero delay + const zeroResult = randomizeDelay(0) + expect(zeroResult).toBeGreaterThanOrEqual(-0) + expect(zeroResult).toBeLessThanOrEqual(0) + + // Test with negative delay (edge case) + const negativeDelay = -100 + const negativeResult = randomizeDelay(negativeDelay) + const negativeTolerance = Math.abs(negativeDelay) * 0.2 + expect(negativeResult).toBeGreaterThanOrEqual(negativeDelay - negativeTolerance) + expect(negativeResult).toBeLessThanOrEqual(negativeDelay + negativeTolerance) + }) +}) -- 2.43.0