Skip to content

Commit 2619522

Browse files
authored
Merge pull request microsoft#31945 from microsoft/skip-costly-tests
Skip costly tests
2 parents 105f763 + 56370e8 commit 2619522

File tree

8 files changed

+148
-6
lines changed

8 files changed

+148
-6
lines changed

Gulpfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ task("runtests-parallel").flags = {
429429
" --workers=<number>": "The number of parallel workers to use.",
430430
" --timeout=<ms>": "Overrides the default test timeout.",
431431
" --built": "Compile using the built version of the compiler.",
432+
" --skipPercent=<number>": "Skip expensive tests with <percent> chance to miss an edit. Default 5%.",
432433
};
433434

434435
task("diff", () => exec(getDiffTool(), [refBaseline, localBaseline], { ignoreExitCode: true }));

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"prex": "^0.4.3",
8383
"q": "latest",
8484
"remove-internal": "^2.9.2",
85+
"simple-git": "^1.113.0",
8586
"source-map-support": "latest",
8687
"through2": "latest",
8788
"travis-fold": "latest",
@@ -102,7 +103,8 @@
102103
"gulp": "gulp",
103104
"jake": "gulp",
104105
"lint": "gulp lint",
105-
"setup-hooks": "node scripts/link-hooks.js"
106+
"setup-hooks": "node scripts/link-hooks.js",
107+
"update-costly-tests": "node scripts/costly-tests.js"
106108
},
107109
"browser": {
108110
"fs": false,

scripts/build/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = minimist(process.argv.slice(2), {
1414
"ru": "runners", "runner": "runners",
1515
"r": "reporter",
1616
"c": "colors", "color": "colors",
17+
"skip-percent": "skipPercent",
1718
"w": "workers",
1819
"f": "fix"
1920
},
@@ -69,4 +70,4 @@ if (module.exports.built) {
6970
*
7071
* @typedef {import("minimist").ParsedArgs & TypedOptions} CommandLineOptions
7172
*/
72-
void 0;
73+
void 0;

scripts/build/tests.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ async function runConsoleTests(runJs, defaultReporter, runInParallel, watchMode,
3131
const inspect = cmdLineOptions.inspect;
3232
const runners = cmdLineOptions.runners;
3333
const light = cmdLineOptions.light;
34+
const skipPercent = process.env.CI === "true" ? 0 : cmdLineOptions.skipPercent;
3435
const stackTraceLimit = cmdLineOptions.stackTraceLimit;
3536
const testConfigFile = "test.config";
3637
const failed = cmdLineOptions.failed;
@@ -62,8 +63,8 @@ async function runConsoleTests(runJs, defaultReporter, runInParallel, watchMode,
6263
testTimeout = 400000;
6364
}
6465

65-
if (tests || runners || light || testTimeout || taskConfigsFolder || keepFailed) {
66-
writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, testTimeout, keepFailed);
66+
if (tests || runners || light || testTimeout || taskConfigsFolder || keepFailed || skipPercent !== undefined) {
67+
writeTestConfigFile(tests, runners, light, skipPercent, taskConfigsFolder, workerCount, stackTraceLimit, testTimeout, keepFailed);
6768
}
6869

6970
const colors = cmdLineOptions.colors;
@@ -158,17 +159,19 @@ exports.cleanTestDirs = cleanTestDirs;
158159
* @param {string} tests
159160
* @param {string} runners
160161
* @param {boolean} light
162+
* @param {string} skipPercent
161163
* @param {string} [taskConfigsFolder]
162164
* @param {string | number} [workerCount]
163165
* @param {string} [stackTraceLimit]
164166
* @param {string | number} [timeout]
165167
* @param {boolean} [keepFailed]
166168
*/
167-
function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, timeout, keepFailed) {
169+
function writeTestConfigFile(tests, runners, light, skipPercent, taskConfigsFolder, workerCount, stackTraceLimit, timeout, keepFailed) {
168170
const testConfigContents = JSON.stringify({
169171
test: tests ? [tests] : undefined,
170172
runners: runners ? runners.split(",") : undefined,
171173
light,
174+
skipPercent,
172175
workerCount,
173176
stackTraceLimit,
174177
taskConfigsFolder,

scripts/costly-tests.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// @ts-check
2+
const fs = require("fs");
3+
const git = require('simple-git/promise')('.')
4+
const readline = require('readline')
5+
6+
/** @typedef {{ [s: string]: number}} Histogram */
7+
8+
async function main() {
9+
/** @type {Histogram} */
10+
const edits = Object.create(null)
11+
/** @type {Histogram} */
12+
const perf = JSON.parse(fs.readFileSync('.parallelperf.json', 'utf8'))
13+
14+
await collectCommits(git, "release-2.3", "master", /*author*/ undefined, files => fillMap(files, edits))
15+
16+
const totalTime = Object.values(perf).reduce((n,m) => n + m, 0)
17+
const untouched = Object.values(perf).length - Object.values(edits).length
18+
const totalEdits = Object.values(edits).reduce((n,m) => n + m, 0) + untouched + Object.values(edits).length
19+
20+
let i = 0
21+
/** @type {{ name: string, time: number, edits: number, cost: number }[]} */
22+
let data = []
23+
for (const k in perf) {
24+
const otherk = k.replace(/tsrunner-[a-z-]+?:\/\//, '')
25+
const percentTime = perf[k] / totalTime
26+
const percentHits = (1 + (edits[otherk] || 0)) / totalEdits
27+
const cost = 5 + Math.log(percentTime / percentHits)
28+
data.push({ name: otherk, time: perf[k], edits: 1 + (edits[otherk] || 0), cost})
29+
if (edits[otherk])
30+
i++
31+
}
32+
const output = {
33+
totalTime,
34+
totalEdits,
35+
data: data.sort((x,y) => y.cost - x.cost).map(x => ({ ...x, cost: x.cost.toFixed(2) }))
36+
}
37+
38+
fs.writeFileSync('tests/.test-cost.json', JSON.stringify(output), 'utf8')
39+
}
40+
41+
main().catch(e => {
42+
console.log(e);
43+
process.exit(1);
44+
})
45+
46+
/**
47+
* @param {string[]} files
48+
* @param {Histogram} histogram
49+
*/
50+
function fillMap(files, histogram) {
51+
// keep edits to test cases (but not /users), and not file moves
52+
const tests = files.filter(f => f.startsWith('tests/cases/') && !f.startsWith('tests/cases/user') && !/=>/.test(f))
53+
for (const test of tests) {
54+
histogram[test] = (histogram[test] || 0) + 1
55+
}
56+
}
57+
58+
/**
59+
* @param {string} s
60+
*/
61+
function isSquashMergeMessage(s) {
62+
return /\(#[0-9]+\)$/.test(s)
63+
}
64+
65+
/**
66+
* @param {string} s
67+
*/
68+
function isMergeCommit(s) {
69+
return /Merge pull request #[0-9]+/.test(s)
70+
}
71+
72+
/**
73+
* @param {string} s
74+
*/
75+
function parseFiles(s) {
76+
const lines = s.split('\n')
77+
// Note that slice(2) only works for merge commits, which have an empty newline after the title
78+
return lines.slice(2, lines.length - 2).map(line => line.split("|")[0].trim())
79+
}
80+
81+
/**
82+
* @param {import('simple-git/promise').SimpleGit} git
83+
* @param {string} from
84+
* @param {string} to
85+
* @param {string | undefined} author - only include commits from this author
86+
* @param {(files: string[]) => void} update
87+
*/
88+
async function collectCommits(git, from, to, author, update) {
89+
let i = 0
90+
for (const commit of (await git.log({ from, to })).all) {
91+
i++
92+
if ((!author || commit.author_name === author) && isMergeCommit(commit.message) || isSquashMergeMessage(commit.message)) {
93+
readline.clearLine(process.stdout, /*left*/ -1)
94+
readline.cursorTo(process.stdout, 0)
95+
process.stdout.write(i + ": " + commit.date)
96+
const files = parseFiles(await git.show([commit.hash, "--stat=1000,960,40", "--pretty=oneline"]))
97+
update(files)
98+
}
99+
}
100+
}
101+
102+
103+

src/testRunner/parallel/host.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Harness.Parallel.Host {
1414
const isatty = tty.isatty(1) && tty.isatty(2);
1515
const path = require("path") as typeof import("path");
1616
const { fork } = require("child_process") as typeof import("child_process");
17-
const { statSync } = require("fs") as typeof import("fs");
17+
const { statSync, readFileSync } = require("fs") as typeof import("fs");
1818

1919
// NOTE: paths for module and types for FailedTestReporter _do not_ line up due to our use of --outFile for run.js
2020
// tslint:disable-next-line:variable-name
@@ -192,6 +192,31 @@ namespace Harness.Parallel.Host {
192192
return `tsrunner-${runner}://${test}`;
193193
}
194194

195+
function skipCostlyTests(tasks: Task[]) {
196+
if (statSync("tests/.test-cost.json")) {
197+
const costs = JSON.parse(readFileSync("tests/.test-cost.json", "utf8")) as {
198+
totalTime: number,
199+
totalEdits: number,
200+
data: { name: string, time: number, edits: number, costs: number }[]
201+
};
202+
let skippedEdits = 0;
203+
let skippedTime = 0;
204+
const skippedTests = new Set<string>();
205+
let i = 0;
206+
for (; i < costs.data.length && (skippedEdits / costs.totalEdits) < (skipPercent / 100); i++) {
207+
skippedEdits += costs.data[i].edits;
208+
skippedTime += costs.data[i].time;
209+
skippedTests.add(costs.data[i].name);
210+
}
211+
console.log(`Skipped ${i} expensive tests; estimated time savings of ${(skippedTime / costs.totalTime * 100).toFixed(2)}% with --skipPercent=${skipPercent.toFixed(2)} chance of missing a test.`);
212+
return tasks.filter(t => !skippedTests.has(t.file));
213+
}
214+
else {
215+
console.log("No cost analysis discovered.");
216+
return tasks;
217+
}
218+
}
219+
195220
function startDelayed(perfData: { [testHash: string]: number } | undefined, totalCost: number) {
196221
console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : "."));
197222
console.log("Discovering runner-based tests...");
@@ -231,6 +256,7 @@ namespace Harness.Parallel.Host {
231256
}
232257
tasks.sort((a, b) => a.size - b.size);
233258
tasks = tasks.concat(newTasks);
259+
tasks = skipCostlyTests(tasks);
234260
const batchCount = workerCount;
235261
const packfraction = 0.9;
236262
const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test

src/testRunner/runner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ let runUnitTests: boolean | undefined;
6565
let stackTraceLimit: number | "full" | undefined;
6666
let noColors = false;
6767
let keepFailed = false;
68+
let skipPercent = 5;
6869

6970
interface TestConfig {
7071
light?: boolean;
@@ -78,6 +79,7 @@ interface TestConfig {
7879
noColors?: boolean;
7980
timeout?: number;
8081
keepFailed?: boolean;
82+
skipPercent?: number;
8183
}
8284

8385
interface TaskSet {
@@ -109,6 +111,9 @@ function handleTestConfig() {
109111
if (testConfig.keepFailed) {
110112
keepFailed = true;
111113
}
114+
if (testConfig.skipPercent !== undefined) {
115+
skipPercent = testConfig.skipPercent;
116+
}
112117

113118
if (testConfig.stackTraceLimit === "full") {
114119
(<any>Error).stackTraceLimit = Infinity;

tests/.test-cost.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)