Skip to content

Commit 4e05856

Browse files
committed
feat(bazel/integration): support tracking sizes within the integration test rule directly (#2894)
Add size tracking support into the integration rule instead of adding it on downstream. PR Close #2894
1 parent b1fcd6b commit 4e05856

File tree

5 files changed

+139
-0
lines changed

5 files changed

+139
-0
lines changed

bazel/integration/test_runner/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ts_project(
88
tsconfig = "//bazel:tsconfig",
99
deps = [
1010
"//bazel:node_modules/@types/node",
11+
"//bazel:node_modules/chalk",
1112
"//bazel:node_modules/true-case-path",
1213
],
1314
)

bazel/integration/test_runner/runner.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from './process_utils.mjs';
3131
import {ENVIRONMENT_TMP_PLACEHOLDER} from './constants.mjs';
3232
import {debug} from './debug.mjs';
33+
import {SizeTracker} from './size-tracking.mjs';
3334

3435
/** Error class that is used when an integration command fails. */
3536
class IntegrationTestCommandError extends Error {}
@@ -47,6 +48,7 @@ type EnvironmentConfig = Record<string, BazelExpandedValue>;
4748
*/
4849
export class TestRunner {
4950
private readonly environment: EnvironmentConfig;
51+
private readonly sizeTracker: SizeTracker;
5052

5153
constructor(
5254
private readonly isTestDebugMode: boolean,
@@ -59,6 +61,7 @@ export class TestRunner {
5961
environment: EnvironmentConfig,
6062
) {
6163
this.environment = this._assignDefaultEnvironmentVariables(environment);
64+
this.sizeTracker = new SizeTracker(this.testPackage);
6265
}
6366

6467
async run() {
@@ -82,6 +85,7 @@ export class TestRunner {
8285

8386
try {
8487
await this._runTestCommands(testWorkingDir, testEnv);
88+
await this.sizeTracker.run(testWorkingDir, testEnv);
8589
} finally {
8690
debug('Finished running integration test commands.');
8791

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {runCommandInChildProcess} from './process_utils.mjs';
10+
import {existsSync} from 'node:fs';
11+
import fs from 'node:fs/promises';
12+
import path from 'node:path';
13+
import chalk from 'chalk';
14+
import {debug} from './debug.mjs';
15+
16+
// Convience access to chalk colors.
17+
const {red, green} = chalk;
18+
/** The size discrepancy we allow in bytes. */
19+
const THRESHOLD_BYTES = 5000;
20+
/** The size discrepancy as a percentage. */
21+
const THRESHOLD_PERCENT = 5;
22+
23+
interface SizeCheckResult {
24+
expected: number;
25+
actual: number | undefined;
26+
failing: boolean;
27+
details:
28+
| 'missing'
29+
| {
30+
raw: string;
31+
percent: string;
32+
};
33+
}
34+
35+
export class SizeTracker {
36+
constructor(private readonly testPackage: string) {}
37+
38+
/**
39+
* Runs the size tracking scripting.
40+
*
41+
* Builds the integration test application and then checks if the size of the generated files varies too
42+
* far from our known file sizes.
43+
*/
44+
async run(testWorkingDir: string, commandEnv: NodeJS.ProcessEnv): Promise<void> {
45+
const sizeJsonFilePath = path.join(testWorkingDir, 'size.json');
46+
// If the integration test provides a size.json file we use it as a size tracking marker.
47+
if (!existsSync(sizeJsonFilePath)) {
48+
debug(`Skipping size tracking as no size.json file was found at ${sizeJsonFilePath}`);
49+
return;
50+
}
51+
const success = await runCommandInChildProcess('yarn', ['build'], testWorkingDir, commandEnv);
52+
if (!success) {
53+
throw Error('Failed to build for size tracking.');
54+
}
55+
56+
const sizes: {[key: string]: SizeCheckResult} = {};
57+
58+
const expectedSizes = JSON.parse(await fs.readFile(sizeJsonFilePath, 'utf-8')) as {
59+
[key: string]: number;
60+
};
61+
62+
for (let [filename, expectedSize] of Object.entries(expectedSizes)) {
63+
const generedFilePath = path.join(testWorkingDir, filename);
64+
if (!existsSync(generedFilePath)) {
65+
sizes[filename] = {
66+
actual: undefined,
67+
failing: true,
68+
expected: expectedSize,
69+
details: 'missing',
70+
};
71+
} else {
72+
const {size: actualSize} = await fs.stat(generedFilePath);
73+
const absoluteSizeDiff = Math.abs(actualSize - expectedSize);
74+
const percentSizeDiff = (absoluteSizeDiff / expectedSize) * 100;
75+
const direction = actualSize === expectedSize ? '' : actualSize > expectedSize ? '+' : '-';
76+
sizes[filename] = {
77+
actual: actualSize,
78+
expected: expectedSize,
79+
failing: absoluteSizeDiff > THRESHOLD_BYTES || percentSizeDiff > THRESHOLD_PERCENT,
80+
details: {
81+
raw: `${direction}${absoluteSizeDiff.toFixed(0)}`,
82+
percent: `${direction}${Math.round(percentSizeDiff * 1000) / 1000}`,
83+
},
84+
};
85+
}
86+
}
87+
88+
console.info();
89+
console.info(Array(80).fill('=').join(''));
90+
console.info(
91+
`${Array(28).fill('=').join('')} SIZE TRACKING RESULTS ${Array(29).fill('=').join('')}`,
92+
);
93+
console.info(Array(80).fill('=').join(''));
94+
let failed = false;
95+
for (let [filename, {actual, expected, failing, details}] of Object.entries(sizes)) {
96+
failed = failed || failing;
97+
const bullet = failing ? red('✘') : green('✔');
98+
console.info(` ${bullet} ${filename}`);
99+
if (details === 'missing') {
100+
console.info(
101+
` File not found in generated integration test application, either ensure the file is created or remove it from the size tracking json file.`,
102+
);
103+
} else {
104+
console.info(
105+
` Actual Size: ${actual} | Expected Size: ${expected} | ${details.raw} bytes (${details.percent}%)`,
106+
);
107+
}
108+
}
109+
console.info();
110+
if (failed) {
111+
const originalSizeJsonFilePath = path.join(this.testPackage, 'size.json');
112+
console.info(
113+
`If this is a desired change, please update the size limits in: ${originalSizeJsonFilePath}`,
114+
);
115+
process.exitCode = 1;
116+
} else {
117+
console.info(
118+
`Payload size check passed. All diffs are less than ${THRESHOLD_PERCENT}% or ${THRESHOLD_BYTES} bytes.`,
119+
);
120+
}
121+
console.info(Array(80).fill('=').join(''));
122+
console.info();
123+
}
124+
}

bazel/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@types/wait-on": "^5.3.4",
1010
"@types/yargs": "17.0.33",
1111
"browser-sync": "3.0.4",
12+
"chalk": "5.4.1",
1213
"piscina": "^5.0.0",
1314
"send": "1.2.0",
1415
"true-case-path": "2.2.1",

bazel/pnpm-lock.yaml

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

0 commit comments

Comments
 (0)