From 9726388604cd8335e54b57203a7662e1e0dad808 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 1 Aug 2024 12:07:15 -0600 Subject: [PATCH 01/11] fix: wait to read until file is unlocked --- src/config/configFile.ts | 5 ++-- src/util/fileLocking.ts | 57 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/config/configFile.ts b/src/config/configFile.ts index 32c1302bbf..3b7e94c544 100644 --- a/src/config/configFile.ts +++ b/src/config/configFile.ts @@ -14,7 +14,7 @@ import { Global } from '../global'; import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { resolveProjectPath, resolveProjectPathSync } from '../util/internal'; -import { lockInit, lockInitSync } from '../util/fileLocking'; +import { lockInit, lockInitSync, pollUntilUnlock, pollUntilUnlockSync } from '../util/fileLocking'; import { BaseConfigStore } from './configStore'; import { ConfigContents } from './configStackTypes'; import { stateFromContents } from './lwwMap'; @@ -167,7 +167,7 @@ export class ConfigFile< !this.hasRead ? 'hasRead is false' : 'force parameter is true' }` ); - + await pollUntilUnlock(this.getPath()); const obj = parseJsonMap

(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath()); this.setContentsFromFileContents(obj, (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs); } @@ -203,6 +203,7 @@ export class ConfigFile< // Only need to read config files once. They are kept up to date // internally and updated persistently via write(). if (!this.hasRead || force) { + pollUntilUnlockSync(this.getPath()); this.logger.debug(`Reading config file: ${this.getPath()}`); const obj = parseJsonMap

(fs.readFileSync(this.getPath(), 'utf8')); this.setContentsFromFileContents(obj, fs.statSync(this.getPath(), { bigint: true }).mtimeNs); diff --git a/src/util/fileLocking.ts b/src/util/fileLocking.ts index db955221b0..76bab60057 100644 --- a/src/util/fileLocking.ts +++ b/src/util/fileLocking.ts @@ -6,9 +6,12 @@ */ import * as fs from 'node:fs'; import { dirname } from 'node:path'; -import { lock, lockSync } from 'proper-lockfile'; +import { lock, lockSync, check, checkSync } from 'proper-lockfile'; +import { Duration } from '@salesforce/kit'; import { SfError } from '../sfError'; import { Logger } from '../logger/logger'; +import { PollingClient } from '../status/pollingClient'; +import { StatusResult } from '../status/types'; import { lockOptions, lockRetryOptions } from './lockRetryOptions'; type LockInitResponse = { writeAndUnlock: (data: string) => Promise; unlock: () => Promise }; @@ -95,3 +98,55 @@ export const lockInitSync = (filePath: string): LockInitSyncResponse => { unlock, }; }; + +/** + * Poll until the file is unlocked. + * + * @param filePath file path to check + */ +export const pollUntilUnlock = async (filePath: string): Promise => { + const options: PollingClient.Options = { + async poll(): Promise { + try { + const locked = await check(filePath, lockRetryOptions); + return { completed: !locked, payload: 'File unlocked' }; + } catch (e) { + if (e instanceof SfError) { + return { completed: true, payload: e.toObject() }; + } + if (e instanceof Error) { + return { + completed: true, + payload: { + name: e.name, + message: e.message, + stack: e.stack, + }, + }; + } + + return { completed: true, payload: 'Error occurred' }; + } + }, + frequency: Duration.milliseconds(10), + timeout: Duration.minutes(1), + }; + + const client = await PollingClient.create(options); + await client.subscribe(); +}; + +export const pollUntilUnlockSync = (filePath: string): void => { + // Set a counter to ensure that the while loop does not run indefinitely + let counter = 0; + let locked = true; + while (locked && counter < 100) { + try { + locked = checkSync(filePath, lockOptions); + counter++; + } catch { + // Likely a file not found error, which means the file is not locked + locked = false; + } + } +}; From b8015d3e80bd593fc7abf99723bdf320a1c6ab21 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 6 Aug 2024 18:07:14 -0500 Subject: [PATCH 02/11] test: failing ut --- src/util/fileLocking.ts | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/util/fileLocking.ts b/src/util/fileLocking.ts index 76bab60057..6f9eca396b 100644 --- a/src/util/fileLocking.ts +++ b/src/util/fileLocking.ts @@ -8,10 +8,9 @@ import * as fs from 'node:fs'; import { dirname } from 'node:path'; import { lock, lockSync, check, checkSync } from 'proper-lockfile'; import { Duration } from '@salesforce/kit'; +import { retryDecorator } from 'ts-retry-promise'; import { SfError } from '../sfError'; import { Logger } from '../logger/logger'; -import { PollingClient } from '../status/pollingClient'; -import { StatusResult } from '../status/types'; import { lockOptions, lockRetryOptions } from './lockRetryOptions'; type LockInitResponse = { writeAndUnlock: (data: string) => Promise; unlock: () => Promise }; @@ -105,35 +104,7 @@ export const lockInitSync = (filePath: string): LockInitSyncResponse => { * @param filePath file path to check */ export const pollUntilUnlock = async (filePath: string): Promise => { - const options: PollingClient.Options = { - async poll(): Promise { - try { - const locked = await check(filePath, lockRetryOptions); - return { completed: !locked, payload: 'File unlocked' }; - } catch (e) { - if (e instanceof SfError) { - return { completed: true, payload: e.toObject() }; - } - if (e instanceof Error) { - return { - completed: true, - payload: { - name: e.name, - message: e.message, - stack: e.stack, - }, - }; - } - - return { completed: true, payload: 'Error occurred' }; - } - }, - frequency: Duration.milliseconds(10), - timeout: Duration.minutes(1), - }; - - const client = await PollingClient.create(options); - await client.subscribe(); + await retryDecorator(check, { timeout: Duration.minutes(1).milliseconds, delay: 10 })(filePath, lockRetryOptions); }; export const pollUntilUnlockSync = (filePath: string): void => { From c10653bc67ecb819e72bab8c03a496197f1a2179 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 6 Aug 2024 18:07:35 -0500 Subject: [PATCH 03/11] test: nut for concurrency --- .github/workflows/test.yml | 4 ++ package.json | 3 +- src/util/fileLocking.ts | 8 +++- src/util/lockRetryOptions.ts | 2 +- test/nut/concurrencyConfig.ts | 31 +++++++++++++++ test/nut/concurrencyReadWrite.ts | 22 +++++++++++ .../configFileConcurrency.nut.ts} | 39 ++++++++----------- test/tsconfig.json | 2 +- 8 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 test/nut/concurrencyConfig.ts create mode 100644 test/nut/concurrencyReadWrite.ts rename test/{unit/config/configFileConcurrency.test.ts => nut/configFileConcurrency.nut.ts} (91%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97716a67d1..9d619dbd80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,10 @@ jobs: windows-unit-tests: needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main + nuts: + needs: linux-unit-tests + uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main + xNuts: needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/externalNut.yml@main diff --git a/package.json b/package.json index be01dd1e67..0ffeb1b207 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "prepack": "sf-prepack", "prepare": "sf-install", "test": "wireit", + "test:nuts": "mocha \"test/**/*.nut.ts\" --timeout 500000", "test:only": "wireit", "test:perf": "ts-node test/perf/logger/main.test.ts" }, @@ -181,4 +182,4 @@ "command": "tsc -p typedocExamples" } } -} +} \ No newline at end of file diff --git a/src/util/fileLocking.ts b/src/util/fileLocking.ts index 6f9eca396b..2e2aff3115 100644 --- a/src/util/fileLocking.ts +++ b/src/util/fileLocking.ts @@ -104,7 +104,13 @@ export const lockInitSync = (filePath: string): LockInitSyncResponse => { * @param filePath file path to check */ export const pollUntilUnlock = async (filePath: string): Promise => { - await retryDecorator(check, { timeout: Duration.minutes(1).milliseconds, delay: 10 })(filePath, lockRetryOptions); + await retryDecorator(check, { + timeout: Duration.minutes(1).milliseconds, + delay: 10, + until: (locked) => locked === false, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + retryIf: () => false, + })(filePath, lockRetryOptions); }; export const pollUntilUnlockSync = (filePath: string): void => { diff --git a/src/util/lockRetryOptions.ts b/src/util/lockRetryOptions.ts index 20e836c667..509d4870c5 100644 --- a/src/util/lockRetryOptions.ts +++ b/src/util/lockRetryOptions.ts @@ -10,5 +10,5 @@ export const lockOptions = { stale: 10_000 }; export const lockRetryOptions = { ...lockOptions, - retries: { retries: 10, maxTimeout: 1000, factor: 2 }, + retries: { retries: 50, maxTimeout: 1000, factor: 2 }, }; diff --git a/test/nut/concurrencyConfig.ts b/test/nut/concurrencyConfig.ts new file mode 100644 index 0000000000..8d79b39002 --- /dev/null +++ b/test/nut/concurrencyConfig.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { tmpdir } from 'node:os'; +import { ConfigFile } from '../../src'; + +export const FILENAME = 'concurrency.json'; + +export class TestConfig extends ConfigFile { + public static getOptions( + filename: string, + isGlobal: boolean, + isState?: boolean, + filePath?: string + ): ConfigFile.Options { + return { + rootFolder: tmpdir(), + filename, + isGlobal, + isState, + filePath, + }; + } + + public static getFileName() { + return FILENAME; + } +} diff --git a/test/nut/concurrencyReadWrite.ts b/test/nut/concurrencyReadWrite.ts new file mode 100644 index 0000000000..82d773ca49 --- /dev/null +++ b/test/nut/concurrencyReadWrite.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { join } from 'node:path'; +import { expect } from 'chai'; // Add this line to import the expect function +import { TestConfig } from './concurrencyConfig'; + +const sharedLocation = join('sfdx-core-ut', 'test', 'configFile'); + +/** ex: `yarn ts-node test/nut/concurrencyReadWrite.ts 1` */ +(async function (i: number = parseInt(process.argv[2], 10)) { + const config = new TestConfig(TestConfig.getOptions('test', true, true, sharedLocation)); + config.set('x', i); + await config.write(); + const readConfig = await config.read(true, true); + expect(readConfig.x).to.be.a('number'); +})().catch((err) => { + throw err; +}); diff --git a/test/unit/config/configFileConcurrency.test.ts b/test/nut/configFileConcurrency.nut.ts similarity index 91% rename from test/unit/config/configFileConcurrency.test.ts rename to test/nut/configFileConcurrency.nut.ts index 8314b8a879..4c6dd26638 100644 --- a/test/unit/config/configFileConcurrency.test.ts +++ b/test/nut/configFileConcurrency.nut.ts @@ -7,33 +7,15 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { rm } from 'node:fs/promises'; +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; import { expect } from 'chai'; import { sleep } from '@salesforce/kit'; -import { ConfigFile } from '../../../src'; +import { TestConfig, FILENAME } from './concurrencyConfig'; -const FILENAME = 'concurrency.json'; -const sharedLocation = join('sfdx-core-ut', 'test', 'configFile'); +const execProm = promisify(exec); -class TestConfig extends ConfigFile { - public static getOptions( - filename: string, - isGlobal: boolean, - isState?: boolean, - filePath?: string - ): ConfigFile.Options { - return { - rootFolder: tmpdir(), - filename, - isGlobal, - isState, - filePath, - }; - } - - public static getFileName() { - return FILENAME; - } -} +const sharedLocation = join('sfdx-core-ut', 'test', 'configFile'); /* file and node - clock timestamps aren't precise enough to run in a UT. * the goal of this and the `sleep` is to put a bit of space between operations @@ -190,4 +172,15 @@ describe('concurrency', () => { expect(config4.get('x')).to.be.greaterThanOrEqual(7).and.lessThanOrEqual(9); } }); + + it.only('safe reads on parallel writes', async () => { + const configOriginal = new TestConfig(TestConfig.getOptions('test', true, true, sharedLocation)); + configOriginal.set('x', 0); + await configOriginal.write(); + await sleep(SLEEP_FUDGE_MS); + + await Promise.all( + Array.from({ length: 50 }).map((_, i) => execProm(`yarn ts-node test/nut/concurrencyReadWrite.ts ${i}`)) + ); + }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index d1a7d129dd..248ac75904 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@salesforce/dev-config/tsconfig-test-strict", - "include": ["unit/**/*.ts", "perf/**/*.ts"], + "include": ["nut/**/*.ts", "unit/**/*.ts", "perf/**/*.ts", "nut/configFileConcurrency.nut.ts"], "compilerOptions": { "noEmit": true, "skipLibCheck": true, From d084ea58788c4eef0ca489949bdac67c439bff89 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 6 Aug 2024 18:23:30 -0500 Subject: [PATCH 04/11] refactor: retry decorator for polling --- src/util/fileLocking.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/util/fileLocking.ts b/src/util/fileLocking.ts index 2e2aff3115..ee90d3f74f 100644 --- a/src/util/fileLocking.ts +++ b/src/util/fileLocking.ts @@ -104,13 +104,17 @@ export const lockInitSync = (filePath: string): LockInitSyncResponse => { * @param filePath file path to check */ export const pollUntilUnlock = async (filePath: string): Promise => { - await retryDecorator(check, { - timeout: Duration.minutes(1).milliseconds, - delay: 10, - until: (locked) => locked === false, - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - retryIf: () => false, - })(filePath, lockRetryOptions); + try { + await retryDecorator(check, { + timeout: Duration.minutes(1).milliseconds, + delay: 10, + until: (locked) => locked === false, + // don't retry errors (typically enoent or access on the lockfile, therefore not locked) + retryIf: () => false, + })(filePath, lockRetryOptions); + } catch (e) { + // intentionally swallow the error, same reason as above + } }; export const pollUntilUnlockSync = (filePath: string): void => { From 70f9b3565b385d4b0aaea44079ee129cba0f9dce Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 6 Aug 2024 18:24:39 -0500 Subject: [PATCH 05/11] test: run entire nut --- test/nut/configFileConcurrency.nut.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nut/configFileConcurrency.nut.ts b/test/nut/configFileConcurrency.nut.ts index 4c6dd26638..5b6c7af78f 100644 --- a/test/nut/configFileConcurrency.nut.ts +++ b/test/nut/configFileConcurrency.nut.ts @@ -173,7 +173,7 @@ describe('concurrency', () => { } }); - it.only('safe reads on parallel writes', async () => { + it('safe reads on parallel writes', async () => { const configOriginal = new TestConfig(TestConfig.getOptions('test', true, true, sharedLocation)); configOriginal.set('x', 0); await configOriginal.write(); From 659e4909c58eaeb6be9caf685251aa5d7566b20e Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 6 Aug 2024 18:29:32 -0500 Subject: [PATCH 06/11] refactor: back to original retries --- src/util/lockRetryOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/lockRetryOptions.ts b/src/util/lockRetryOptions.ts index 509d4870c5..20e836c667 100644 --- a/src/util/lockRetryOptions.ts +++ b/src/util/lockRetryOptions.ts @@ -10,5 +10,5 @@ export const lockOptions = { stale: 10_000 }; export const lockRetryOptions = { ...lockOptions, - retries: { retries: 50, maxTimeout: 1000, factor: 2 }, + retries: { retries: 10, maxTimeout: 1000, factor: 2 }, }; From 0245156a57212d97d10474b79703a24da6d1a7c6 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 7 Aug 2024 09:49:16 -0500 Subject: [PATCH 07/11] test: use normal naming convention --- .sfdevrc.json | 2 +- package.json | 4 ++-- test/unit/config/{configTest.ts => config.test.ts} | 0 .../{configAggregatorTest.ts => configAggregator.test.ts} | 0 test/unit/config/{configFileTest.ts => configFile.test.ts} | 0 .../unit/config/{configStoreTest.ts => configStore.test.ts} | 0 test/unit/config/{envVarsTest.ts => envVars.test.ts} | 0 test/unit/config/{lwwMapTest.ts => lwwMap.test.ts} | 0 test/unit/config/{ttlConfigTest.ts => ttlConfig.test.ts} | 4 ++-- .../{cryptoKeyFailuresTest.ts => cryptoKeyFailures.test.ts} | 0 test/unit/crypto/{cryptoTest.ts => cryptoTest.test.ts} | 0 test/unit/crypto/{keyChainTest.ts => keyChain.test.ts} | 0 .../crypto/{keyChainImplTest.ts => keyChainImpl.test.ts} | 0 .../crypto/{secureStringTest.ts => secureString.test.ts} | 0 ...deviceOauthServiceTest.ts => deviceOauthService.test.ts} | 0 test/unit/{globalTest.ts => global.test.ts} | 0 .../{lifecycleEventsTest.ts => lifecycleEvents.test.ts} | 0 test/unit/logger/{cleanupTest.ts => cleanup.test.ts} | 0 test/unit/logger/{filterTest.ts => filter.test.ts} | 0 test/unit/{loggerTest.ts => logger/logger.test.ts} | 4 ++-- test/unit/{messagesTest.ts => messages.test.ts} | 0 test/unit/org/{authInfoTest.ts => authInfo.test.ts} | 0 test/unit/org/{authRemoverTest.ts => authRemover.test.ts} | 0 test/unit/org/{connectionTest.ts => connection.test.ts} | 0 test/unit/org/{orgTest.ts => org.test.ts} | 0 ...SetAssignmentTest.ts => permissionSetAssignment.test.ts} | 0 .../{scratchOrgCreateTest.ts => scratchOrgCreate.test.ts} | 0 ...tchOrgErrorCodesTest.ts => scratchOrgErrorCodes.test.ts} | 0 ...recationTest.ts => scratchOrgFeatureDeprecation.test.ts} | 0 .../{scratchOrgInfoApiTest.ts => scratchOrgInfoApi.test.ts} | 0 ...InfoGeneratorTest.ts => scratchOrgInfoGenerator.test.ts} | 0 ...GeneratorTest.ts => scratchOrgSettingsGenerator.test.ts} | 0 test/unit/org/{userTest.ts => user.test.ts} | 0 test/unit/{projectTest.ts => project.test.ts} | 0 test/unit/schema/{validatorTest.ts => validator.test.ts} | 0 test/unit/{sfErrorTest.ts => sfError.test.ts} | 6 +++--- .../{aliasAccessorTest.ts => aliasAccessor.test.ts} | 0 .../accessors/{orgAccessorTest.ts => orgAccessor.test.ts} | 0 .../{sandboxAccessorTest.ts => sandboxAccessor.test.ts} | 0 .../{myDomainResolverTest.ts => myDomainResolver.test.ts} | 0 .../status/{pollingClientTest.ts => pollingClient.test.ts} | 0 .../{streamingClientTest.ts => streamingClient.test.ts} | 0 test/unit/{testSetupTest.ts => testSetup.test.ts} | 0 test/unit/util/{cacheTest.ts => cache.test.ts} | 0 .../{directoryWriterTest.ts => directoryWriter.test.ts} | 0 test/unit/util/{fileLockingTest.ts => fileLocking.test.ts} | 0 .../{findUppercaseKeysTest.ts => findUppercaseKeys.test.ts} | 0 .../{getJwtAudienceUrlTest.ts => getJwtAudienceUrl.test.ts} | 0 test/unit/util/{internalTest.ts => internal.test.ts} | 0 test/unit/util/{mapKeysTest.ts => mapKeys.test.ts} | 0 test/unit/util/{sfdcTest.ts => sfdc.test.ts} | 0 test/unit/util/{sfdcUrlTest.ts => sfdcUrl.test.ts} | 0 test/unit/util/{unwrapArrayTest.ts => unwrapArray.test.ts} | 0 test/unit/util/{zipWriterTest.ts => zipWriter.test.ts} | 0 test/unit/{webOauthServerTest.ts => webOauthServer.test.ts} | 0 55 files changed, 10 insertions(+), 10 deletions(-) rename test/unit/config/{configTest.ts => config.test.ts} (100%) rename test/unit/config/{configAggregatorTest.ts => configAggregator.test.ts} (100%) rename test/unit/config/{configFileTest.ts => configFile.test.ts} (100%) rename test/unit/config/{configStoreTest.ts => configStore.test.ts} (100%) rename test/unit/config/{envVarsTest.ts => envVars.test.ts} (100%) rename test/unit/config/{lwwMapTest.ts => lwwMap.test.ts} (100%) rename test/unit/config/{ttlConfigTest.ts => ttlConfig.test.ts} (98%) rename test/unit/crypto/{cryptoKeyFailuresTest.ts => cryptoKeyFailures.test.ts} (100%) rename test/unit/crypto/{cryptoTest.ts => cryptoTest.test.ts} (100%) rename test/unit/crypto/{keyChainTest.ts => keyChain.test.ts} (100%) rename test/unit/crypto/{keyChainImplTest.ts => keyChainImpl.test.ts} (100%) rename test/unit/crypto/{secureStringTest.ts => secureString.test.ts} (100%) rename test/unit/{deviceOauthServiceTest.ts => deviceOauthService.test.ts} (100%) rename test/unit/{globalTest.ts => global.test.ts} (100%) rename test/unit/{lifecycleEventsTest.ts => lifecycleEvents.test.ts} (100%) rename test/unit/logger/{cleanupTest.ts => cleanup.test.ts} (100%) rename test/unit/logger/{filterTest.ts => filter.test.ts} (100%) rename test/unit/{loggerTest.ts => logger/logger.test.ts} (98%) rename test/unit/{messagesTest.ts => messages.test.ts} (100%) rename test/unit/org/{authInfoTest.ts => authInfo.test.ts} (100%) rename test/unit/org/{authRemoverTest.ts => authRemover.test.ts} (100%) rename test/unit/org/{connectionTest.ts => connection.test.ts} (100%) rename test/unit/org/{orgTest.ts => org.test.ts} (100%) rename test/unit/org/{permissionSetAssignmentTest.ts => permissionSetAssignment.test.ts} (100%) rename test/unit/org/{scratchOrgCreateTest.ts => scratchOrgCreate.test.ts} (100%) rename test/unit/org/{scratchOrgErrorCodesTest.ts => scratchOrgErrorCodes.test.ts} (100%) rename test/unit/org/{scratchOrgFeatureDeprecationTest.ts => scratchOrgFeatureDeprecation.test.ts} (100%) rename test/unit/org/{scratchOrgInfoApiTest.ts => scratchOrgInfoApi.test.ts} (100%) rename test/unit/org/{scratchOrgInfoGeneratorTest.ts => scratchOrgInfoGenerator.test.ts} (100%) rename test/unit/org/{scratchOrgSettingsGeneratorTest.ts => scratchOrgSettingsGenerator.test.ts} (100%) rename test/unit/org/{userTest.ts => user.test.ts} (100%) rename test/unit/{projectTest.ts => project.test.ts} (100%) rename test/unit/schema/{validatorTest.ts => validator.test.ts} (100%) rename test/unit/{sfErrorTest.ts => sfError.test.ts} (98%) rename test/unit/stateAggregator/accessors/{aliasAccessorTest.ts => aliasAccessor.test.ts} (100%) rename test/unit/stateAggregator/accessors/{orgAccessorTest.ts => orgAccessor.test.ts} (100%) rename test/unit/stateAggregator/accessors/{sandboxAccessorTest.ts => sandboxAccessor.test.ts} (100%) rename test/unit/status/{myDomainResolverTest.ts => myDomainResolver.test.ts} (100%) rename test/unit/status/{pollingClientTest.ts => pollingClient.test.ts} (100%) rename test/unit/status/{streamingClientTest.ts => streamingClient.test.ts} (100%) rename test/unit/{testSetupTest.ts => testSetup.test.ts} (100%) rename test/unit/util/{cacheTest.ts => cache.test.ts} (100%) rename test/unit/util/{directoryWriterTest.ts => directoryWriter.test.ts} (100%) rename test/unit/util/{fileLockingTest.ts => fileLocking.test.ts} (100%) rename test/unit/util/{findUppercaseKeysTest.ts => findUppercaseKeys.test.ts} (100%) rename test/unit/util/{getJwtAudienceUrlTest.ts => getJwtAudienceUrl.test.ts} (100%) rename test/unit/util/{internalTest.ts => internal.test.ts} (100%) rename test/unit/util/{mapKeysTest.ts => mapKeys.test.ts} (100%) rename test/unit/util/{sfdcTest.ts => sfdc.test.ts} (100%) rename test/unit/util/{sfdcUrlTest.ts => sfdcUrl.test.ts} (100%) rename test/unit/util/{unwrapArrayTest.ts => unwrapArray.test.ts} (100%) rename test/unit/util/{zipWriterTest.ts => zipWriter.test.ts} (100%) rename test/unit/{webOauthServerTest.ts => webOauthServer.test.ts} (100%) diff --git a/.sfdevrc.json b/.sfdevrc.json index 2fba7b90c3..ca3c6b73aa 100644 --- a/.sfdevrc.json +++ b/.sfdevrc.json @@ -1,6 +1,6 @@ { "test": { - "testsPath": "test/**/*Test.ts" + "testsPath": "test/unit/**/*.test.ts" }, "wireit": { "compile": { diff --git a/package.json b/package.json index 0ffeb1b207..bc000c78a3 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "output": [] }, "test:only": { - "command": "nyc mocha \"test/**/*Test.ts\"", + "command": "nyc mocha \"test/unit/**/*.test.ts\"", "env": { "FORCE_COLOR": "2" }, @@ -182,4 +182,4 @@ "command": "tsc -p typedocExamples" } } -} \ No newline at end of file +} diff --git a/test/unit/config/configTest.ts b/test/unit/config/config.test.ts similarity index 100% rename from test/unit/config/configTest.ts rename to test/unit/config/config.test.ts diff --git a/test/unit/config/configAggregatorTest.ts b/test/unit/config/configAggregator.test.ts similarity index 100% rename from test/unit/config/configAggregatorTest.ts rename to test/unit/config/configAggregator.test.ts diff --git a/test/unit/config/configFileTest.ts b/test/unit/config/configFile.test.ts similarity index 100% rename from test/unit/config/configFileTest.ts rename to test/unit/config/configFile.test.ts diff --git a/test/unit/config/configStoreTest.ts b/test/unit/config/configStore.test.ts similarity index 100% rename from test/unit/config/configStoreTest.ts rename to test/unit/config/configStore.test.ts diff --git a/test/unit/config/envVarsTest.ts b/test/unit/config/envVars.test.ts similarity index 100% rename from test/unit/config/envVarsTest.ts rename to test/unit/config/envVars.test.ts diff --git a/test/unit/config/lwwMapTest.ts b/test/unit/config/lwwMap.test.ts similarity index 100% rename from test/unit/config/lwwMapTest.ts rename to test/unit/config/lwwMap.test.ts diff --git a/test/unit/config/ttlConfigTest.ts b/test/unit/config/ttlConfig.test.ts similarity index 98% rename from test/unit/config/ttlConfigTest.ts rename to test/unit/config/ttlConfig.test.ts index 59bf3823e9..ccfa870f17 100644 --- a/test/unit/config/ttlConfigTest.ts +++ b/test/unit/config/ttlConfig.test.ts @@ -51,7 +51,7 @@ describe('TTLConfig', () => { it('should return the latest entry', async () => { const config = await TestConfig.create(); config.set('1', { one: 'one' }); - await sleep(1000); + await sleep(200); config.set('2', { two: 'two' }); const latest = config.getLatestEntry(); expect(latest).to.deep.equal(['2', config.get('2')]); @@ -68,7 +68,7 @@ describe('TTLConfig', () => { it('should return the key of the latest entry', async () => { const config = await TestConfig.create(); config.set('1', { one: 'one' }); - await sleep(1000); + await sleep(200); config.set('2', { two: 'two' }); const latest = config.getLatestKey(); expect(latest).to.equal('2'); diff --git a/test/unit/crypto/cryptoKeyFailuresTest.ts b/test/unit/crypto/cryptoKeyFailures.test.ts similarity index 100% rename from test/unit/crypto/cryptoKeyFailuresTest.ts rename to test/unit/crypto/cryptoKeyFailures.test.ts diff --git a/test/unit/crypto/cryptoTest.ts b/test/unit/crypto/cryptoTest.test.ts similarity index 100% rename from test/unit/crypto/cryptoTest.ts rename to test/unit/crypto/cryptoTest.test.ts diff --git a/test/unit/crypto/keyChainTest.ts b/test/unit/crypto/keyChain.test.ts similarity index 100% rename from test/unit/crypto/keyChainTest.ts rename to test/unit/crypto/keyChain.test.ts diff --git a/test/unit/crypto/keyChainImplTest.ts b/test/unit/crypto/keyChainImpl.test.ts similarity index 100% rename from test/unit/crypto/keyChainImplTest.ts rename to test/unit/crypto/keyChainImpl.test.ts diff --git a/test/unit/crypto/secureStringTest.ts b/test/unit/crypto/secureString.test.ts similarity index 100% rename from test/unit/crypto/secureStringTest.ts rename to test/unit/crypto/secureString.test.ts diff --git a/test/unit/deviceOauthServiceTest.ts b/test/unit/deviceOauthService.test.ts similarity index 100% rename from test/unit/deviceOauthServiceTest.ts rename to test/unit/deviceOauthService.test.ts diff --git a/test/unit/globalTest.ts b/test/unit/global.test.ts similarity index 100% rename from test/unit/globalTest.ts rename to test/unit/global.test.ts diff --git a/test/unit/lifecycleEventsTest.ts b/test/unit/lifecycleEvents.test.ts similarity index 100% rename from test/unit/lifecycleEventsTest.ts rename to test/unit/lifecycleEvents.test.ts diff --git a/test/unit/logger/cleanupTest.ts b/test/unit/logger/cleanup.test.ts similarity index 100% rename from test/unit/logger/cleanupTest.ts rename to test/unit/logger/cleanup.test.ts diff --git a/test/unit/logger/filterTest.ts b/test/unit/logger/filter.test.ts similarity index 100% rename from test/unit/logger/filterTest.ts rename to test/unit/logger/filter.test.ts diff --git a/test/unit/loggerTest.ts b/test/unit/logger/logger.test.ts similarity index 98% rename from test/unit/loggerTest.ts rename to test/unit/logger/logger.test.ts index cfac7013cd..b231539fac 100644 --- a/test/unit/loggerTest.ts +++ b/test/unit/logger/logger.test.ts @@ -10,8 +10,8 @@ import { expect, config as chaiConfig } from 'chai'; import { isString } from '@salesforce/ts-types'; -import { Logger, LoggerLevel, computeLevel } from '../../src/logger/logger'; -import { shouldThrowSync, TestContext } from '../../src/testSetup'; +import { Logger, LoggerLevel, computeLevel } from '../../../src/logger/logger'; +import { shouldThrowSync, TestContext } from '../../../src/testSetup'; // NOTE: These tests still use 'await' which is how it use to work and were left to make // sure we didn't regress the way they were used. diff --git a/test/unit/messagesTest.ts b/test/unit/messages.test.ts similarity index 100% rename from test/unit/messagesTest.ts rename to test/unit/messages.test.ts diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfo.test.ts similarity index 100% rename from test/unit/org/authInfoTest.ts rename to test/unit/org/authInfo.test.ts diff --git a/test/unit/org/authRemoverTest.ts b/test/unit/org/authRemover.test.ts similarity index 100% rename from test/unit/org/authRemoverTest.ts rename to test/unit/org/authRemover.test.ts diff --git a/test/unit/org/connectionTest.ts b/test/unit/org/connection.test.ts similarity index 100% rename from test/unit/org/connectionTest.ts rename to test/unit/org/connection.test.ts diff --git a/test/unit/org/orgTest.ts b/test/unit/org/org.test.ts similarity index 100% rename from test/unit/org/orgTest.ts rename to test/unit/org/org.test.ts diff --git a/test/unit/org/permissionSetAssignmentTest.ts b/test/unit/org/permissionSetAssignment.test.ts similarity index 100% rename from test/unit/org/permissionSetAssignmentTest.ts rename to test/unit/org/permissionSetAssignment.test.ts diff --git a/test/unit/org/scratchOrgCreateTest.ts b/test/unit/org/scratchOrgCreate.test.ts similarity index 100% rename from test/unit/org/scratchOrgCreateTest.ts rename to test/unit/org/scratchOrgCreate.test.ts diff --git a/test/unit/org/scratchOrgErrorCodesTest.ts b/test/unit/org/scratchOrgErrorCodes.test.ts similarity index 100% rename from test/unit/org/scratchOrgErrorCodesTest.ts rename to test/unit/org/scratchOrgErrorCodes.test.ts diff --git a/test/unit/org/scratchOrgFeatureDeprecationTest.ts b/test/unit/org/scratchOrgFeatureDeprecation.test.ts similarity index 100% rename from test/unit/org/scratchOrgFeatureDeprecationTest.ts rename to test/unit/org/scratchOrgFeatureDeprecation.test.ts diff --git a/test/unit/org/scratchOrgInfoApiTest.ts b/test/unit/org/scratchOrgInfoApi.test.ts similarity index 100% rename from test/unit/org/scratchOrgInfoApiTest.ts rename to test/unit/org/scratchOrgInfoApi.test.ts diff --git a/test/unit/org/scratchOrgInfoGeneratorTest.ts b/test/unit/org/scratchOrgInfoGenerator.test.ts similarity index 100% rename from test/unit/org/scratchOrgInfoGeneratorTest.ts rename to test/unit/org/scratchOrgInfoGenerator.test.ts diff --git a/test/unit/org/scratchOrgSettingsGeneratorTest.ts b/test/unit/org/scratchOrgSettingsGenerator.test.ts similarity index 100% rename from test/unit/org/scratchOrgSettingsGeneratorTest.ts rename to test/unit/org/scratchOrgSettingsGenerator.test.ts diff --git a/test/unit/org/userTest.ts b/test/unit/org/user.test.ts similarity index 100% rename from test/unit/org/userTest.ts rename to test/unit/org/user.test.ts diff --git a/test/unit/projectTest.ts b/test/unit/project.test.ts similarity index 100% rename from test/unit/projectTest.ts rename to test/unit/project.test.ts diff --git a/test/unit/schema/validatorTest.ts b/test/unit/schema/validator.test.ts similarity index 100% rename from test/unit/schema/validatorTest.ts rename to test/unit/schema/validator.test.ts diff --git a/test/unit/sfErrorTest.ts b/test/unit/sfError.test.ts similarity index 98% rename from test/unit/sfErrorTest.ts rename to test/unit/sfError.test.ts index ae0576d5e8..082badf242 100644 --- a/test/unit/sfErrorTest.ts +++ b/test/unit/sfError.test.ts @@ -79,7 +79,7 @@ describe('SfError', () => { it('returned `name:message` when no cause', () => { const err = new SfError('test'); expect(inspect(err)).to.include('SfError: test'); - expect(inspect(err)).to.include('sfErrorTest.ts'); + expect(inspect(err)).to.include('sfError.test.ts'); // there's always 1 cause from the `cause:` property, even if undefined expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); }); @@ -87,7 +87,7 @@ describe('SfError', () => { const nestedError = new Error('nested'); const err = new SfError('test', undefined, undefined, nestedError); expect(inspect(err)).to.include('SfError: test'); - expect(inspect(err)).to.include('sfErrorTest.ts'); + expect(inspect(err)).to.include('sfError.test.ts'); expect(inspect(err)).to.include('nested'); expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); expect(inspect(err)?.match(nestedCauseRegex)).to.be.null; @@ -97,7 +97,7 @@ describe('SfError', () => { const nestedError2 = new Error('nested2', { cause: nestedError }); const err = new SfError('test', undefined, undefined, nestedError2); expect(inspect(err)).to.include('SfError: test'); - expect(inspect(err)).to.include('sfErrorTest.ts'); + expect(inspect(err)).to.include('sfError.test.ts'); expect(inspect(err)).to.include('nested'); expect(inspect(err)).to.include('nested2'); expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); diff --git a/test/unit/stateAggregator/accessors/aliasAccessorTest.ts b/test/unit/stateAggregator/accessors/aliasAccessor.test.ts similarity index 100% rename from test/unit/stateAggregator/accessors/aliasAccessorTest.ts rename to test/unit/stateAggregator/accessors/aliasAccessor.test.ts diff --git a/test/unit/stateAggregator/accessors/orgAccessorTest.ts b/test/unit/stateAggregator/accessors/orgAccessor.test.ts similarity index 100% rename from test/unit/stateAggregator/accessors/orgAccessorTest.ts rename to test/unit/stateAggregator/accessors/orgAccessor.test.ts diff --git a/test/unit/stateAggregator/accessors/sandboxAccessorTest.ts b/test/unit/stateAggregator/accessors/sandboxAccessor.test.ts similarity index 100% rename from test/unit/stateAggregator/accessors/sandboxAccessorTest.ts rename to test/unit/stateAggregator/accessors/sandboxAccessor.test.ts diff --git a/test/unit/status/myDomainResolverTest.ts b/test/unit/status/myDomainResolver.test.ts similarity index 100% rename from test/unit/status/myDomainResolverTest.ts rename to test/unit/status/myDomainResolver.test.ts diff --git a/test/unit/status/pollingClientTest.ts b/test/unit/status/pollingClient.test.ts similarity index 100% rename from test/unit/status/pollingClientTest.ts rename to test/unit/status/pollingClient.test.ts diff --git a/test/unit/status/streamingClientTest.ts b/test/unit/status/streamingClient.test.ts similarity index 100% rename from test/unit/status/streamingClientTest.ts rename to test/unit/status/streamingClient.test.ts diff --git a/test/unit/testSetupTest.ts b/test/unit/testSetup.test.ts similarity index 100% rename from test/unit/testSetupTest.ts rename to test/unit/testSetup.test.ts diff --git a/test/unit/util/cacheTest.ts b/test/unit/util/cache.test.ts similarity index 100% rename from test/unit/util/cacheTest.ts rename to test/unit/util/cache.test.ts diff --git a/test/unit/util/directoryWriterTest.ts b/test/unit/util/directoryWriter.test.ts similarity index 100% rename from test/unit/util/directoryWriterTest.ts rename to test/unit/util/directoryWriter.test.ts diff --git a/test/unit/util/fileLockingTest.ts b/test/unit/util/fileLocking.test.ts similarity index 100% rename from test/unit/util/fileLockingTest.ts rename to test/unit/util/fileLocking.test.ts diff --git a/test/unit/util/findUppercaseKeysTest.ts b/test/unit/util/findUppercaseKeys.test.ts similarity index 100% rename from test/unit/util/findUppercaseKeysTest.ts rename to test/unit/util/findUppercaseKeys.test.ts diff --git a/test/unit/util/getJwtAudienceUrlTest.ts b/test/unit/util/getJwtAudienceUrl.test.ts similarity index 100% rename from test/unit/util/getJwtAudienceUrlTest.ts rename to test/unit/util/getJwtAudienceUrl.test.ts diff --git a/test/unit/util/internalTest.ts b/test/unit/util/internal.test.ts similarity index 100% rename from test/unit/util/internalTest.ts rename to test/unit/util/internal.test.ts diff --git a/test/unit/util/mapKeysTest.ts b/test/unit/util/mapKeys.test.ts similarity index 100% rename from test/unit/util/mapKeysTest.ts rename to test/unit/util/mapKeys.test.ts diff --git a/test/unit/util/sfdcTest.ts b/test/unit/util/sfdc.test.ts similarity index 100% rename from test/unit/util/sfdcTest.ts rename to test/unit/util/sfdc.test.ts diff --git a/test/unit/util/sfdcUrlTest.ts b/test/unit/util/sfdcUrl.test.ts similarity index 100% rename from test/unit/util/sfdcUrlTest.ts rename to test/unit/util/sfdcUrl.test.ts diff --git a/test/unit/util/unwrapArrayTest.ts b/test/unit/util/unwrapArray.test.ts similarity index 100% rename from test/unit/util/unwrapArrayTest.ts rename to test/unit/util/unwrapArray.test.ts diff --git a/test/unit/util/zipWriterTest.ts b/test/unit/util/zipWriter.test.ts similarity index 100% rename from test/unit/util/zipWriterTest.ts rename to test/unit/util/zipWriter.test.ts diff --git a/test/unit/webOauthServerTest.ts b/test/unit/webOauthServer.test.ts similarity index 100% rename from test/unit/webOauthServerTest.ts rename to test/unit/webOauthServer.test.ts From 78cc9c5df9a2a828af23c78a485c7d1fd9ad0a26 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 7 Aug 2024 09:51:03 -0500 Subject: [PATCH 08/11] test: remove ut init hook --- .mocharc.json | 2 +- test/init.js | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 test/init.js diff --git a/.mocharc.json b/.mocharc.json index 8bb562fc13..5adf3d90c9 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,5 +1,5 @@ { - "require": "test/init.js, ts-node/register, source-map-support/register", + "require": "ts-node/register, source-map-support/register", "watch-extensions": "ts", "watch-files": ["src", "test"], "recursive": true, diff --git a/test/init.js b/test/init.js deleted file mode 100644 index d314a8fd4f..0000000000 --- a/test/init.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright (c) 2018, salesforce.com, inc. - * All rights reserved. - * SPDX-License-Identifier: BSD-3-Clause - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -const path = require('path'); -process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json'); From d1f78e76a8928784c4f8105785721ddbbd79fa50 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 7 Aug 2024 10:25:59 -0500 Subject: [PATCH 09/11] test: include for nut --- test/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tsconfig.json b/test/tsconfig.json index 248ac75904..ad28147f51 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@salesforce/dev-config/tsconfig-test-strict", - "include": ["nut/**/*.ts", "unit/**/*.ts", "perf/**/*.ts", "nut/configFileConcurrency.nut.ts"], + "include": ["nut/**/*.ts", "unit/**/*.ts", "perf/**/*.ts"], "compilerOptions": { "noEmit": true, "skipLibCheck": true, From db7424399e79a35814258b1b21ed669300669bfd Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 7 Aug 2024 13:07:33 -0500 Subject: [PATCH 10/11] ci: remove test folder to avoid a plugin running sfdx-core's NUTs --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d619dbd80..8851d1dddf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,6 +44,8 @@ jobs: command: 'yarn test:nuts' os: ${{ matrix.os }} useCache: false + # remove the test folder for sfdx-core + preBuildCommands: 'rm -rf test' preSwapCommands: 'yarn upgrade @jsforce/jsforce-node@latest; npx yarn-deduplicate; yarn install' preExternalBuildCommands: 'shx rm -rf node_modules/@salesforce/core/node_modules/@jsforce/jsforce-node shx rm -rf node_modules/@salesforce/sf-plugins-core/node_modules/@salesforce/core node_modules/@salesforce/cli-plugins-testkit/node_modules/@salesforce/core node_modules/@salesforce/source-tracking/node_modules/@salesforce/core node_modules/@salesforce/source-deploy-retrieve/node_modules/@salesforce/core node_modules/@salesforce/apex-node/node_modules/@salesforce/core' secrets: inherit From 00155a3d2b2981e5b47b05f446dc30efadf7ac60 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 7 Aug 2024 13:15:58 -0500 Subject: [PATCH 11/11] ci: fix on windows, too --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8851d1dddf..ab45858f88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: os: ${{ matrix.os }} useCache: false # remove the test folder for sfdx-core - preBuildCommands: 'rm -rf test' + preBuildCommands: 'shx rm -rf test' preSwapCommands: 'yarn upgrade @jsforce/jsforce-node@latest; npx yarn-deduplicate; yarn install' preExternalBuildCommands: 'shx rm -rf node_modules/@salesforce/core/node_modules/@jsforce/jsforce-node shx rm -rf node_modules/@salesforce/sf-plugins-core/node_modules/@salesforce/core node_modules/@salesforce/cli-plugins-testkit/node_modules/@salesforce/core node_modules/@salesforce/source-tracking/node_modules/@salesforce/core node_modules/@salesforce/source-deploy-retrieve/node_modules/@salesforce/core node_modules/@salesforce/apex-node/node_modules/@salesforce/core' secrets: inherit