Skip to content

Commit 272a7b4

Browse files
authored
Fix some unrelabile tests in non-CI environments. (#5538)
1 parent ff3082d commit 272a7b4

File tree

4 files changed

+136
-11
lines changed

4 files changed

+136
-11
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "",
5+
"type": "none",
6+
"packageName": "@microsoft/rush"
7+
}
8+
],
9+
"packageName": "@microsoft/rush",
10+
"email": "[email protected]"
11+
}

libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ jest.mock(`@rushstack/package-deps-hash`, () => {
2020
getGitHashForFiles(filePaths: Iterable<string>): ReadonlyMap<string, string> {
2121
return new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath]));
2222
},
23-
hashFilesAsync(rootDirectory: string, filePaths: Iterable<string>): ReadonlyMap<string, string> {
24-
return new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath]));
23+
hashFilesAsync(rootDirectory: string, filePaths: Iterable<string>): Promise<ReadonlyMap<string, string>> {
24+
return Promise.resolve(new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath])));
2525
}
2626
};
2727
});
@@ -33,8 +33,13 @@ import { FileSystem, JsonFile, Path } from '@rushstack/node-core-library';
3333
import type { IDetailedRepoState } from '@rushstack/package-deps-hash';
3434
import { Autoinstaller } from '../../logic/Autoinstaller';
3535
import type { ITelemetryData } from '../../logic/Telemetry';
36-
import { getCommandLineParserInstanceAsync, type SpawnMockArgs, type SpawnMockCall } from './TestUtils';
37-
import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration';
36+
import {
37+
getCommandLineParserInstanceAsync,
38+
type SpawnMockArgs,
39+
type SpawnMockCall,
40+
isolateEnvironmentConfigurationForTests,
41+
type IEnvironmentConfigIsolation
42+
} from './TestUtils';
3843
import { IS_WINDOWS } from '../../utilities/executionUtilities';
3944

4045
// Ordinals into the `mock.calls` array referencing each of the arguments to `spawn`. Note that
@@ -73,13 +78,15 @@ function expectSpawnToMatchRegexp(spawnCall: SpawnMockCall, expectedRegexp: RegE
7378

7479
describe('RushCommandLineParser', () => {
7580
describe('execute', () => {
81+
let _envIsolation: IEnvironmentConfigIsolation;
82+
83+
beforeEach(() => {
84+
_envIsolation = isolateEnvironmentConfigurationForTests();
85+
});
86+
7687
afterEach(() => {
7788
jest.clearAllMocks();
78-
EnvironmentConfiguration.reset();
79-
jest
80-
.spyOn(EnvironmentConfiguration, 'buildCacheOverrideJsonFilePath', 'get')
81-
.mockReturnValue(undefined);
82-
jest.spyOn(EnvironmentConfiguration, 'buildCacheOverrideJson', 'get').mockReturnValue(undefined);
89+
_envIsolation.restore();
8390
});
8491

8592
describe('in basic repo', () => {

libraries/rush-lib/src/cli/test/RushCommandLineParserFailureCases.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ jest.mock(`@rushstack/package-deps-hash`, () => {
1313
return {
1414
hasSubmodules: false,
1515
hasUncommittedChanges: false,
16-
files: new Map(),
16+
files: new Map([['common/config/rush/npm-shrinkwrap.json', 'hash']]),
1717
symlinks: new Map()
1818
};
1919
},
2020
getRepoChangesAsync(): ReadonlyMap<string, string> {
2121
return new Map();
22+
},
23+
hashFilesAsync(rootDirectory: string, filePaths: Iterable<string>): Promise<ReadonlyMap<string, string>> {
24+
return Promise.resolve(new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath])));
2225
}
2326
};
2427
});
@@ -28,11 +31,19 @@ import type { IDetailedRepoState } from '@rushstack/package-deps-hash';
2831
import { Autoinstaller } from '../../logic/Autoinstaller';
2932
import type { ITelemetryData } from '../../logic/Telemetry';
3033
import { getCommandLineParserInstanceAsync, setSpawnMock } from './TestUtils';
34+
import { isolateEnvironmentConfigurationForTests, type IEnvironmentConfigIsolation } from './TestUtils';
3135

3236
describe('RushCommandLineParserFailureCases', () => {
3337
describe('execute', () => {
38+
let _envIsolation: IEnvironmentConfigIsolation;
39+
40+
beforeEach(() => {
41+
_envIsolation = isolateEnvironmentConfigurationForTests({ silenceStderrWrite: true });
42+
});
43+
3444
afterEach(() => {
35-
jest.clearAllMocks();
45+
_envIsolation.restore();
46+
jest.restoreAllMocks();
3647
});
3748

3849
describe('in repo plugin custom flushTelemetry', () => {

libraries/rush-lib/src/cli/test/TestUtils.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AlreadyExistsBehavior, FileSystem, PackageJsonLookup } from '@rushstack
66
import type { RushCommandLineParser as RushCommandLineParserType } from '../RushCommandLineParser';
77
import { FlagFile } from '../../api/FlagFile';
88
import { RushConstants } from '../../logic/RushConstants';
9+
import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration';
910

1011
export type SpawnMockArgs = Parameters<typeof import('node:child_process').spawn>;
1112
export type SpawnMock = jest.Mock<ReturnType<typeof import('node:child_process').spawn>, SpawnMockArgs>;
@@ -37,6 +38,23 @@ export interface IChildProcessModuleMock {
3738
spawn: jest.Mock;
3839
}
3940

41+
const DEFAULT_RUSH_ENV_VARS_TO_CLEAR: ReadonlyArray<string> = [
42+
'RUSH_BUILD_CACHE_OVERRIDE_JSON',
43+
'RUSH_BUILD_CACHE_OVERRIDE_JSON_FILE_PATH',
44+
'RUSH_BUILD_CACHE_CREDENTIAL',
45+
'RUSH_BUILD_CACHE_ENABLED',
46+
'RUSH_BUILD_CACHE_WRITE_ALLOWED'
47+
];
48+
49+
export interface IWithEnvironmentConfigIsolationOptions {
50+
envVarNamesToClear?: ReadonlyArray<string>;
51+
silenceStderrWrite?: boolean;
52+
}
53+
54+
export interface IEnvironmentConfigIsolation {
55+
restore(): void;
56+
}
57+
4058
/**
4159
* Configure the `child_process` `spawn` mock for these tests. This relies on the mock implementation
4260
* in `mock_child_process`.
@@ -102,3 +120,81 @@ export async function getCommandLineParserInstanceAsync(
102120
repoPath
103121
};
104122
}
123+
124+
/**
125+
* Clears Rush-related environment variables and resets EnvironmentConfiguration for deterministic tests.
126+
*
127+
* Notes:
128+
* - EnvironmentConfiguration caches some values, so we also stub the build-cache override getters.
129+
* - Rush treats any stderr output during `rush test` as a warning, which fails the command; some
130+
* tests intentionally simulate failures and may need stderr silenced.
131+
*/
132+
export function isolateEnvironmentConfigurationForTests(
133+
options: IWithEnvironmentConfigIsolationOptions = {}
134+
): IEnvironmentConfigIsolation {
135+
const envVarNamesToClear: ReadonlyArray<string> =
136+
options.envVarNamesToClear ?? DEFAULT_RUSH_ENV_VARS_TO_CLEAR;
137+
138+
const savedProcessEnv: Record<string, string | undefined> = {};
139+
for (const envVarName of envVarNamesToClear) {
140+
savedProcessEnv[envVarName] = process.env[envVarName];
141+
delete process.env[envVarName];
142+
}
143+
144+
EnvironmentConfiguration.reset();
145+
146+
const restoreFns: Array<() => void> = [];
147+
148+
restoreFns.push(() => {
149+
for (const envVarName of envVarNamesToClear) {
150+
const oldValue: string | undefined = savedProcessEnv[envVarName];
151+
if (oldValue === undefined) {
152+
delete process.env[envVarName];
153+
} else {
154+
process.env[envVarName] = oldValue;
155+
}
156+
}
157+
});
158+
159+
if (options.silenceStderrWrite) {
160+
type StderrWrite = typeof process.stderr.write;
161+
const silentWrite: unknown = (
162+
chunk: string | Uint8Array,
163+
encoding?: BufferEncoding | ((err?: Error | null) => void),
164+
cb?: (err?: Error | null) => void
165+
): boolean => {
166+
if (typeof encoding === 'function') {
167+
encoding(null);
168+
} else {
169+
cb?.(null);
170+
}
171+
return true;
172+
};
173+
174+
const writeSpy: jest.SpyInstance<ReturnType<StderrWrite>, Parameters<StderrWrite>> = jest
175+
.spyOn(process.stderr, 'write')
176+
.mockImplementation(silentWrite as StderrWrite);
177+
178+
restoreFns.push(() => writeSpy.mockRestore());
179+
}
180+
181+
// EnvironmentConfiguration.reset() does not clear cached values for these fields.
182+
const overrideJsonFilePathSpy: jest.SpyInstance<string | undefined, []> = jest
183+
.spyOn(EnvironmentConfiguration, 'buildCacheOverrideJsonFilePath', 'get')
184+
.mockReturnValue(undefined);
185+
const overrideJsonSpy: jest.SpyInstance<string | undefined, []> = jest
186+
.spyOn(EnvironmentConfiguration, 'buildCacheOverrideJson', 'get')
187+
.mockReturnValue(undefined);
188+
189+
restoreFns.push(() => overrideJsonFilePathSpy.mockRestore());
190+
restoreFns.push(() => overrideJsonSpy.mockRestore());
191+
restoreFns.push(() => EnvironmentConfiguration.reset());
192+
193+
return {
194+
restore: () => {
195+
for (let i: number = restoreFns.length - 1; i >= 0; i--) {
196+
restoreFns[i]();
197+
}
198+
}
199+
};
200+
}

0 commit comments

Comments
 (0)