Skip to content

Commit d12f69f

Browse files
authored
Introduce a seed for the PRNG (#70)
* Introduce a seed for the PRNG This will make it possible to repeat the randomly chosen LS operations, but will not prevent changes in the TS or the repos under test from causing the output to differ. Pipeline timeouts may also vary from run to run. * Use n/a as the sentinel value since empty string is not supported
1 parent 94e0174 commit d12f69f

9 files changed

+92
-17
lines changed

azure-pipelines-gitTests-template.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ parameters:
3333
- name: LANGUAGE
3434
displayName: Language of repos on GitHub (tsserver only)
3535
type: string
36+
- name: PRNG_SEED
37+
displayName: Pseudo-random number generator seed
38+
type: string
39+
default: 'n/a'
3640

3741
jobs:
3842
- job: ListRepos
@@ -69,7 +73,7 @@ jobs:
6973
npm run build
7074
npm install -g pnpm
7175
mkdir 'RepoResults$(System.JobPositionInPhase)'
72-
node dist/checkGithubRepos ${{ parameters.ENTRYPOINT }} ${{ parameters.OLD_VERSION }} ${{ parameters.NEW_VERSION }} '$(Pipeline.Workspace)/RepoList/repos.json' $(System.TotalJobsInPhase) $(System.JobPositionInPhase) 'RepoResults$(System.JobPositionInPhase)' ${{ parameters.DIAGNOSTIC_OUTPUT }}
76+
node dist/checkGithubRepos ${{ parameters.ENTRYPOINT }} ${{ parameters.OLD_VERSION }} ${{ parameters.NEW_VERSION }} '$(Pipeline.Workspace)/RepoList/repos.json' $(System.TotalJobsInPhase) $(System.JobPositionInPhase) 'RepoResults$(System.JobPositionInPhase)' ${{ parameters.DIAGNOSTIC_OUTPUT }} ${{ parameters.PRNG_SEED }}
7377
displayName: 'Run TypeScript on repos'
7478
continueOnError: true
7579
env:

azure-pipelines-gitTests.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ parameters:
4646
values:
4747
- TypeScript
4848
- JavaScript
49+
- name: PRNG_SEED
50+
displayName: Pseudo-random number generator seed
51+
type: string
52+
default: 'n/a'
4953

5054
schedules:
5155
- cron: "0 18 * * Sun" # time is in UTC
@@ -72,4 +76,5 @@ extends:
7276
NEW_VERSION: ${{ parameters.NEW_VERSION }}
7377
MACHINE_COUNT: ${{ parameters.MACHINE_COUNT }}
7478
ENTRYPOINT: ${{ parameters.ENTRYPOINT }}
75-
LANGUAGE: ${{ parameters.LANGUAGE }}
79+
LANGUAGE: ${{ parameters.LANGUAGE }}
80+
PRNG_SEED: ${{ parameters.PRNG_SEED }}

azure-pipelines-userTests.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ parameters:
4444
values:
4545
- tsc
4646
- tsserver
47+
- name: PRNG_SEED
48+
displayName: Pseudo-random number generator seed
49+
type: string
50+
default: 'n/a'
4751

4852
pr: none
4953
trigger: none
@@ -89,7 +93,7 @@ jobs:
8993
npm run build
9094
npm install -g pnpm
9195
mkdir 'RepoResults$(System.JobPositionInPhase)'
92-
node dist/checkUserTestRepos ${{ parameters.ENTRYPOINT }} ${{ parameters.OLD_TS_REPO_URL }} ${{ parameters.OLD_HEAD_REF }} ${{ parameters.SOURCE_ISSUE }} ${{ parameters.TOP_REPOS }} '$(Pipeline.Workspace)/RepoList/repos.json' $(System.TotalJobsInPhase) $(System.JobPositionInPhase) 'RepoResults$(System.JobPositionInPhase)' ${{ parameters.DIAGNOSTIC_OUTPUT }}
96+
node dist/checkUserTestRepos ${{ parameters.ENTRYPOINT }} ${{ parameters.OLD_TS_REPO_URL }} ${{ parameters.OLD_HEAD_REF }} ${{ parameters.SOURCE_ISSUE }} ${{ parameters.TOP_REPOS }} '$(Pipeline.Workspace)/RepoList/repos.json' $(System.TotalJobsInPhase) $(System.JobPositionInPhase) 'RepoResults$(System.JobPositionInPhase)' ${{ parameters.DIAGNOSTIC_OUTPUT }} ${{ parameters.PRNG_SEED }}
9397
displayName: 'Run user tests'
9498
env:
9599
GITHUB_PAT: $(GITHUB_PAT)

package-lock.json

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
"@typescript/server-replay": "^0.2.6",
1818
"glob": "^7.2.3",
1919
"json5": "^2.2.1",
20+
"random-seed": "^0.3.0",
2021
"simple-git": "^3.10.0"
2122
},
2223
"devDependencies": {
2324
"@types/glob": "^7.2.0",
2425
"@types/jest": "^27.5.2",
2526
"@types/node": "^14.18.21",
27+
"@types/random-seed": "^0.3.3",
2628
"jest": "^27.4.4",
2729
"ts-jest": "^27.1.5",
2830
"typescript": "^4.8.3"

src/checkGithubRepos.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { mainAsync, reportError, TsEntrypoint } from "./main";
33

44
const { argv } = process;
55

6-
if (argv.length !== 10) {
7-
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <ts_entrypoint> <old_ts_npm_version> <new_ts_npm_version> <repo_list_path> <worker_count> <worker_number> <result_dir_name> <diagnostic_output>`);
6+
if (argv.length !== 11) {
7+
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <ts_entrypoint> <old_ts_npm_version> <new_ts_npm_version> <repo_list_path> <worker_count> <worker_number> <result_dir_name> <diagnostic_output> <prng_seed>`);
88
process.exit(-1);
99
}
1010

11-
const [,, entrypoint, oldTsNpmVersion, newTsNpmVersion, repoListPath, workerCount, workerNumber, resultDirName, diagnosticOutput] = argv;
11+
const [,, entrypoint, oldTsNpmVersion, newTsNpmVersion, repoListPath, workerCount, workerNumber, resultDirName, diagnosticOutput, prngSeed] = argv;
1212

1313
mainAsync({
1414
testType: "github",
@@ -22,6 +22,7 @@ mainAsync({
2222
oldTsNpmVersion,
2323
newTsNpmVersion,
2424
resultDirName,
25+
prngSeed: prngSeed.toLowerCase() === "n/a" ? undefined : prngSeed,
2526
}).catch(err => {
2627
reportError(err, "Unhandled exception");
2728
process.exit(1);

src/checkUserTestRepos.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { mainAsync, reportError, TsEntrypoint } from "./main";
33

44
const { argv } = process;
55

6-
if (argv.length !== 12) {
7-
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <ts_entrypoint> <old_ts_repo_url> <old_head_ref> <pr_number> <is_top_repos> <repo_list_path> <worker_count> <worker_number> <result_dir_name> <diagnostic_output>`);
6+
if (argv.length !== 13) {
7+
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <ts_entrypoint> <old_ts_repo_url> <old_head_ref> <pr_number> <is_top_repos> <repo_list_path> <worker_count> <worker_number> <result_dir_name> <diagnostic_output> <prng_seed>`);
88
process.exit(-1);
99
}
1010

11-
const [,, entrypoint, oldTsRepoUrl, oldHeadRef, prNumber, buildWithNewWhenOldFails, repoListPath, workerCount, workerNumber, resultDirName, diagnosticOutput] = argv;
11+
const [,, entrypoint, oldTsRepoUrl, oldHeadRef, prNumber, buildWithNewWhenOldFails, repoListPath, workerCount, workerNumber, resultDirName, diagnosticOutput, prngSeed] = argv;
1212

1313
mainAsync({
1414
testType: "user",
@@ -23,6 +23,7 @@ mainAsync({
2323
workerNumber: +workerNumber,
2424
resultDirName,
2525
diagnosticOutput: diagnosticOutput.toLowerCase() === "true",
26+
prngSeed: prngSeed.toLowerCase() === "n/a" ? undefined : prngSeed,
2627
}).catch(err => {
2728
reportError(err, "Unhandled exception");
2829
process.exit(1);

src/main.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ip = require("./utils/installPackages");
77
import ut = require("./utils/userTestUtils");
88
import fs = require("fs");
99
import path = require("path");
10+
import randomSeed = require("random-seed");
1011

1112
interface Params {
1213
/**
@@ -45,6 +46,11 @@ interface Params {
4546
* Which TypeScript entrypoint (tsc or tsserver) to test.
4647
*/
4748
entrypoint: TsEntrypoint;
49+
/**
50+
* Used to make runs repeatable (e.g. when confirming that a PR no longer introduces failures).
51+
* Pass undefined to have a seed generated.
52+
*/
53+
prngSeed: string | undefined;
4854
}
4955
export interface GitParams extends Params {
5056
testType: "github";
@@ -64,6 +70,8 @@ const processCwd = process.cwd();
6470
const packageTimeout = 10 * 60 * 1000;
6571
const executionTimeout = 10 * 60 * 1000;
6672

73+
const prng = randomSeed.create();
74+
6775
export type RepoStatus =
6876
| "Unknown failure"
6977
| "Git clone failed"
@@ -174,7 +182,7 @@ async function getTsServerRepoResult(
174182
const repoDir = path.join(downloadDir, repo.name);
175183

176184
// Presumably, people occasionally browse repos without installing the packages first
177-
const installCommands = (Math.random() > 0.2)
185+
const installCommands = (prng.random() > 0.2)
178186
? (await installPackagesAndGetCommands(repo, downloadDir, repoDir, /*cleanOnFailure*/ true, diagnosticOutput))!
179187
: [];
180188

@@ -186,7 +194,7 @@ async function getTsServerRepoResult(
186194
const lsStart = performance.now();
187195
try {
188196
console.log(`Testing with ${newTsServerPath} (new)`);
189-
const newSpawnResult = await spawnWithTimeoutAsync(repoDir, process.argv[0], [path.join(__dirname, "utils", "exerciseServer.js"), repoDir, replayScriptPath, newTsServerPath, diagnosticOutput.toString()], executionTimeout);
197+
const newSpawnResult = await spawnWithTimeoutAsync(repoDir, process.argv[0], [path.join(__dirname, "utils", "exerciseServer.js"), repoDir, replayScriptPath, newTsServerPath, diagnosticOutput.toString(), prng.string(10)], executionTimeout);
190198
if (!newSpawnResult) {
191199
// CONSIDER: It might be interesting to treat timeouts as failures, but they'd be harder to baseline and more likely to have flaky repros
192200
console.log(`New server timed out after ${executionTimeout} ms`);
@@ -548,6 +556,10 @@ function getWorkerRepos(allRepos: readonly git.Repo[], workerCount: number, work
548556
}
549557

550558
export async function mainAsync(params: GitParams | UserParams): Promise<void> {
559+
if (params.prngSeed) {
560+
prng.seed(params.prngSeed);
561+
}
562+
551563
const downloadDir = params.tmpfs ? "/mnt/ts_downloads" : path.join(processCwd, "ts_downloads");
552564
// TODO: check first whether the directory exists and skip downloading if possible
553565
// TODO: Seems like this should come after the typescript download

src/utils/exerciseServer.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import process = require("process");
66
import path = require("path");
77
import glob = require("glob");
88
import { performance } from "perf_hooks";
9+
import randomSeed = require("random-seed");
910
import { EXIT_BAD_ARGS, EXIT_UNHANDLED_EXCEPTION, EXIT_SERVER_EXIT_FAILED, EXIT_SERVER_CRASH, EXIT_SERVER_ERROR, EXIT_LANGUAGE_SERVICE_DISABLED } from "./exerciseServerConstants";
1011

1112
const testDirPlaceholder = "@PROJECT_ROOT@";
@@ -14,15 +15,16 @@ const exitTimeoutMs = 5000;
1415

1516
const argv = process.argv;
1617

17-
if (argv.length !== 6) {
18-
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <project_dir> <requests_path> <server_path> <diagnostic_output>`);
18+
if (argv.length !== 7) {
19+
console.error(`Usage: ${path.basename(argv[0])} ${path.basename(argv[1])} <project_dir> <requests_path> <server_path> <diagnostic_output> <prng_seed>`);
1920
process.exit(EXIT_BAD_ARGS);
2021
}
2122

2223
// CONVENTION: stderr is for output to the log; stdout is for output to the user
2324

24-
const [, , testDir, replayScriptPath, tsserverPath, diag] = argv;
25+
const [, , testDir, replayScriptPath, tsserverPath, diag, seed] = argv;
2526
const diagnosticOutput = diag.toLocaleLowerCase() === "true";
27+
const prng = randomSeed.create(seed);
2628

2729
exerciseServer(testDir, replayScriptPath, tsserverPath).catch(e => {
2830
console.error(e);
@@ -138,7 +140,7 @@ async function exerciseServerWorker(testDir: string, tsserverPath: string, repla
138140
// NB: greater than 1 behaves the same as 1
139141
const skipFileProb = 1000 / files.length;
140142
for (const openFileRelativePath of files) {
141-
if (Math.random() > skipFileProb) continue;
143+
if (prng.random() > skipFileProb) continue;
142144

143145
const openFileAbsolutePath = path.join(testDirPlaceholder, openFileRelativePath).replace(/\\/g, "/");
144146

@@ -271,7 +273,7 @@ async function exerciseServerWorker(testDir: string, tsserverPath: string, repla
271273
"file": openFileAbsolutePath,
272274
"line": line,
273275
"offset": column,
274-
"includeExternalModuleExports": Math.random() < 0.01, // auto-imports are too slow to test everywhere
276+
"includeExternalModuleExports": prng.random() < 0.01, // auto-imports are too slow to test everywhere
275277
"includeInsertTextCompletions": true,
276278
"triggerKind": 1,
277279
}
@@ -369,7 +371,7 @@ async function exerciseServerWorker(testDir: string, tsserverPath: string, repla
369371
}
370372

371373
async function message(request: any, prob = 1) {
372-
if (Math.random() > prob) return undefined;
374+
if (prng.random() > prob) return undefined;
373375

374376
request = {
375377
"seq": seq++,

0 commit comments

Comments
 (0)