From fd9135abc2148b901ce7e7ade8c52d91c54f1980 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 14 Jul 2025 20:51:35 +0000 Subject: [PATCH 1/2] feat(bazel/integration): support tracking sizes within the integration test rule directly Add size tracking support into the integration rule instead of adding it on downstream. --- bazel/integration/test_runner/BUILD.bazel | 1 + bazel/integration/test_runner/runner.mts | 108 ++++++++++++++++++++++ bazel/package.json | 1 + bazel/pnpm-lock.yaml | 9 ++ 4 files changed, 119 insertions(+) diff --git a/bazel/integration/test_runner/BUILD.bazel b/bazel/integration/test_runner/BUILD.bazel index 5eeca4cc0..d19939ec2 100644 --- a/bazel/integration/test_runner/BUILD.bazel +++ b/bazel/integration/test_runner/BUILD.bazel @@ -8,6 +8,7 @@ ts_project( tsconfig = "//bazel:tsconfig", deps = [ "//bazel:node_modules/@types/node", + "//bazel:node_modules/chalk", "//bazel:node_modules/true-case-path", ], ) diff --git a/bazel/integration/test_runner/runner.mts b/bazel/integration/test_runner/runner.mts index 681ed525b..513b42593 100644 --- a/bazel/integration/test_runner/runner.mts +++ b/bazel/integration/test_runner/runner.mts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {existsSync} from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; @@ -30,6 +31,14 @@ import { } from './process_utils.mjs'; import {ENVIRONMENT_TMP_PLACEHOLDER} from './constants.mjs'; import {debug} from './debug.mjs'; +import chalk from 'chalk'; + +// Convience access to chalk colors. +const {red, green} = chalk; +/** The size discrepancy we allow in bytes. */ +const THRESHOLD_BYTES = 5000; +/** The size discrepancy as a percentage. */ +const THRESHOLD_PERCENT = 5; /** Error class that is used when an integration command fails. */ class IntegrationTestCommandError extends Error {} @@ -270,6 +279,11 @@ export class TestRunner { console.info( `Successfully ran all commands in test directory: ${path.normalize(testWorkingDir)}`, ); + + // If the integration test provides a size.json file we use it as a size tracking marker. + if (existsSync(path.join(testWorkingDir, 'size.json'))) { + await this._runSizeTracking(testWorkingDir, commandEnv); + } } /** @@ -299,4 +313,98 @@ export class TestRunner { return {...defaults, ...baseEnv}; } + + /** + * Runs the size tracking scripting. + * + * Builds the integration test application and then checks if the size of the generated files varies too + * far from our known file sizes. + */ + private async _runSizeTracking( + testWorkingDir: string, + commandEnv: NodeJS.ProcessEnv, + ): Promise { + const success = await runCommandInChildProcess('yarn', ['build'], testWorkingDir, commandEnv); + if (!success) { + throw Error('Failed to build for size tracking.'); + } + + const sizes: {[key: string]: SizeCheckResult} = {}; + + const expectedSizes = JSON.parse( + await fs.readFile(path.join(testWorkingDir, 'size.json'), 'utf-8'), + ) as {[key: string]: number}; + + for (let [filename, expectedSize] of Object.entries(expectedSizes)) { + const generedFilePath = path.join(testWorkingDir, 'dist', filename); + if (!existsSync(generedFilePath)) { + sizes[filename] = { + actual: undefined, + failing: false, + expected: expectedSize, + details: 'missing', + }; + } else { + const {size: actualSize} = await fs.stat(path.join(testWorkingDir, 'dist', filename)); + const absoluteSizeDiff = Math.abs(actualSize - expectedSize); + const percentSizeDiff = (absoluteSizeDiff / expectedSize) * 100; + const direction = actualSize === expectedSize ? '' : actualSize > expectedSize ? '+' : '-'; + sizes[filename] = { + actual: actualSize, + expected: expectedSize, + failing: absoluteSizeDiff > THRESHOLD_BYTES || percentSizeDiff > THRESHOLD_PERCENT, + details: { + raw: `${direction}${absoluteSizeDiff.toFixed(0)}`, + percent: `${direction}${percentSizeDiff.toFixed(3)}`, + }, + }; + } + } + + console.info(Array(80).fill('=').join('')); + console.info( + `${Array(28).fill('=').join('')} SIZE TRACKING RESULTS ${Array(29).fill('=').join('')}`, + ); + console.info(Array(80).fill('=').join('')); + let failed = false; + for (let [filename, {actual, expected, failing, details}] of Object.entries(sizes)) { + failed = failed || failing; + const bullet = failing ? green('✔') : red('✘'); + console.info(` ${bullet} ${filename}`); + if (details === 'missing') { + console.info( + ` File not found in generated integration test application, either ensure the file is created or remove it from the size tracking json file.`, + ); + } else { + console.info( + ` Actual Size: ${actual} | Expected Size: ${expected} | ${details.raw} bytes (${details.raw}%)`, + ); + } + } + console.info(); + if (failed) { + const sizeFileLocation = path.join(this.testPackage, 'size.json'); + console.info( + `If this is a desired change, please update the size limits in: ${sizeFileLocation}`, + ); + process.exitCode = 1; + } else { + console.info( + `Payload size check passed. All diffs are less than ${THRESHOLD_PERCENT}% or ${THRESHOLD_BYTES} bytes.`, + ); + } + console.info(Array(80).fill('=').join('')); + } +} + +interface SizeCheckResult { + expected: number; + actual: number | undefined; + failing: boolean; + details: + | 'missing' + | { + raw: string; + percent: string; + }; } diff --git a/bazel/package.json b/bazel/package.json index 312630df6..7182f1bc7 100644 --- a/bazel/package.json +++ b/bazel/package.json @@ -9,6 +9,7 @@ "@types/wait-on": "^5.3.4", "@types/yargs": "17.0.33", "browser-sync": "3.0.4", + "chalk": "5.4.1", "piscina": "^5.0.0", "send": "1.2.0", "true-case-path": "2.2.1", diff --git a/bazel/pnpm-lock.yaml b/bazel/pnpm-lock.yaml index ab2b50353..dfffa41d5 100644 --- a/bazel/pnpm-lock.yaml +++ b/bazel/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: browser-sync: specifier: 3.0.4 version: 3.0.4 + chalk: + specifier: 5.4.1 + version: 5.4.1 piscina: specifier: ^5.0.0 version: 5.1.2 @@ -442,6 +445,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1962,6 +1969,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.4.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 From bcc4aaf81fb546be8619e7a78e91ed6dc8b90820 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Tue, 15 Jul 2025 18:31:48 +0000 Subject: [PATCH 2/2] fixup! feat(bazel/integration): support tracking sizes within the integration test rule directly --- bazel/integration/test_runner/runner.mts | 112 +--------------- .../integration/test_runner/size-tracking.mts | 124 ++++++++++++++++++ 2 files changed, 128 insertions(+), 108 deletions(-) create mode 100644 bazel/integration/test_runner/size-tracking.mts diff --git a/bazel/integration/test_runner/runner.mts b/bazel/integration/test_runner/runner.mts index 513b42593..3ab11fd9a 100644 --- a/bazel/integration/test_runner/runner.mts +++ b/bazel/integration/test_runner/runner.mts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {existsSync} from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; @@ -31,14 +30,7 @@ import { } from './process_utils.mjs'; import {ENVIRONMENT_TMP_PLACEHOLDER} from './constants.mjs'; import {debug} from './debug.mjs'; -import chalk from 'chalk'; - -// Convience access to chalk colors. -const {red, green} = chalk; -/** The size discrepancy we allow in bytes. */ -const THRESHOLD_BYTES = 5000; -/** The size discrepancy as a percentage. */ -const THRESHOLD_PERCENT = 5; +import {SizeTracker} from './size-tracking.mjs'; /** Error class that is used when an integration command fails. */ class IntegrationTestCommandError extends Error {} @@ -56,6 +48,7 @@ type EnvironmentConfig = Record; */ export class TestRunner { private readonly environment: EnvironmentConfig; + private readonly sizeTracker: SizeTracker; constructor( private readonly isTestDebugMode: boolean, @@ -68,6 +61,7 @@ export class TestRunner { environment: EnvironmentConfig, ) { this.environment = this._assignDefaultEnvironmentVariables(environment); + this.sizeTracker = new SizeTracker(this.testPackage); } async run() { @@ -91,6 +85,7 @@ export class TestRunner { try { await this._runTestCommands(testWorkingDir, testEnv); + await this.sizeTracker.run(testWorkingDir, testEnv); } finally { debug('Finished running integration test commands.'); @@ -279,11 +274,6 @@ export class TestRunner { console.info( `Successfully ran all commands in test directory: ${path.normalize(testWorkingDir)}`, ); - - // If the integration test provides a size.json file we use it as a size tracking marker. - if (existsSync(path.join(testWorkingDir, 'size.json'))) { - await this._runSizeTracking(testWorkingDir, commandEnv); - } } /** @@ -313,98 +303,4 @@ export class TestRunner { return {...defaults, ...baseEnv}; } - - /** - * Runs the size tracking scripting. - * - * Builds the integration test application and then checks if the size of the generated files varies too - * far from our known file sizes. - */ - private async _runSizeTracking( - testWorkingDir: string, - commandEnv: NodeJS.ProcessEnv, - ): Promise { - const success = await runCommandInChildProcess('yarn', ['build'], testWorkingDir, commandEnv); - if (!success) { - throw Error('Failed to build for size tracking.'); - } - - const sizes: {[key: string]: SizeCheckResult} = {}; - - const expectedSizes = JSON.parse( - await fs.readFile(path.join(testWorkingDir, 'size.json'), 'utf-8'), - ) as {[key: string]: number}; - - for (let [filename, expectedSize] of Object.entries(expectedSizes)) { - const generedFilePath = path.join(testWorkingDir, 'dist', filename); - if (!existsSync(generedFilePath)) { - sizes[filename] = { - actual: undefined, - failing: false, - expected: expectedSize, - details: 'missing', - }; - } else { - const {size: actualSize} = await fs.stat(path.join(testWorkingDir, 'dist', filename)); - const absoluteSizeDiff = Math.abs(actualSize - expectedSize); - const percentSizeDiff = (absoluteSizeDiff / expectedSize) * 100; - const direction = actualSize === expectedSize ? '' : actualSize > expectedSize ? '+' : '-'; - sizes[filename] = { - actual: actualSize, - expected: expectedSize, - failing: absoluteSizeDiff > THRESHOLD_BYTES || percentSizeDiff > THRESHOLD_PERCENT, - details: { - raw: `${direction}${absoluteSizeDiff.toFixed(0)}`, - percent: `${direction}${percentSizeDiff.toFixed(3)}`, - }, - }; - } - } - - console.info(Array(80).fill('=').join('')); - console.info( - `${Array(28).fill('=').join('')} SIZE TRACKING RESULTS ${Array(29).fill('=').join('')}`, - ); - console.info(Array(80).fill('=').join('')); - let failed = false; - for (let [filename, {actual, expected, failing, details}] of Object.entries(sizes)) { - failed = failed || failing; - const bullet = failing ? green('✔') : red('✘'); - console.info(` ${bullet} ${filename}`); - if (details === 'missing') { - console.info( - ` File not found in generated integration test application, either ensure the file is created or remove it from the size tracking json file.`, - ); - } else { - console.info( - ` Actual Size: ${actual} | Expected Size: ${expected} | ${details.raw} bytes (${details.raw}%)`, - ); - } - } - console.info(); - if (failed) { - const sizeFileLocation = path.join(this.testPackage, 'size.json'); - console.info( - `If this is a desired change, please update the size limits in: ${sizeFileLocation}`, - ); - process.exitCode = 1; - } else { - console.info( - `Payload size check passed. All diffs are less than ${THRESHOLD_PERCENT}% or ${THRESHOLD_BYTES} bytes.`, - ); - } - console.info(Array(80).fill('=').join('')); - } -} - -interface SizeCheckResult { - expected: number; - actual: number | undefined; - failing: boolean; - details: - | 'missing' - | { - raw: string; - percent: string; - }; } diff --git a/bazel/integration/test_runner/size-tracking.mts b/bazel/integration/test_runner/size-tracking.mts new file mode 100644 index 000000000..450424825 --- /dev/null +++ b/bazel/integration/test_runner/size-tracking.mts @@ -0,0 +1,124 @@ +/** + * @license + * Copyright Google LLC + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {runCommandInChildProcess} from './process_utils.mjs'; +import {existsSync} from 'node:fs'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import chalk from 'chalk'; +import {debug} from './debug.mjs'; + +// Convience access to chalk colors. +const {red, green} = chalk; +/** The size discrepancy we allow in bytes. */ +const THRESHOLD_BYTES = 5000; +/** The size discrepancy as a percentage. */ +const THRESHOLD_PERCENT = 5; + +interface SizeCheckResult { + expected: number; + actual: number | undefined; + failing: boolean; + details: + | 'missing' + | { + raw: string; + percent: string; + }; +} + +export class SizeTracker { + constructor(private readonly testPackage: string) {} + + /** + * Runs the size tracking scripting. + * + * Builds the integration test application and then checks if the size of the generated files varies too + * far from our known file sizes. + */ + async run(testWorkingDir: string, commandEnv: NodeJS.ProcessEnv): Promise { + const sizeJsonFilePath = path.join(testWorkingDir, 'size.json'); + // If the integration test provides a size.json file we use it as a size tracking marker. + if (!existsSync(sizeJsonFilePath)) { + debug(`Skipping size tracking as no size.json file was found at ${sizeJsonFilePath}`); + return; + } + const success = await runCommandInChildProcess('yarn', ['build'], testWorkingDir, commandEnv); + if (!success) { + throw Error('Failed to build for size tracking.'); + } + + const sizes: {[key: string]: SizeCheckResult} = {}; + + const expectedSizes = JSON.parse(await fs.readFile(sizeJsonFilePath, 'utf-8')) as { + [key: string]: number; + }; + + for (let [filename, expectedSize] of Object.entries(expectedSizes)) { + const generedFilePath = path.join(testWorkingDir, filename); + if (!existsSync(generedFilePath)) { + sizes[filename] = { + actual: undefined, + failing: true, + expected: expectedSize, + details: 'missing', + }; + } else { + const {size: actualSize} = await fs.stat(generedFilePath); + const absoluteSizeDiff = Math.abs(actualSize - expectedSize); + const percentSizeDiff = (absoluteSizeDiff / expectedSize) * 100; + const direction = actualSize === expectedSize ? '' : actualSize > expectedSize ? '+' : '-'; + sizes[filename] = { + actual: actualSize, + expected: expectedSize, + failing: absoluteSizeDiff > THRESHOLD_BYTES || percentSizeDiff > THRESHOLD_PERCENT, + details: { + raw: `${direction}${absoluteSizeDiff.toFixed(0)}`, + percent: `${direction}${Math.round(percentSizeDiff * 1000) / 1000}`, + }, + }; + } + } + + console.info(); + console.info(Array(80).fill('=').join('')); + console.info( + `${Array(28).fill('=').join('')} SIZE TRACKING RESULTS ${Array(29).fill('=').join('')}`, + ); + console.info(Array(80).fill('=').join('')); + let failed = false; + for (let [filename, {actual, expected, failing, details}] of Object.entries(sizes)) { + failed = failed || failing; + const bullet = failing ? red('✘') : green('✔'); + console.info(` ${bullet} ${filename}`); + if (details === 'missing') { + console.info( + ` File not found in generated integration test application, either ensure the file is created or remove it from the size tracking json file.`, + ); + } else { + console.info( + ` Actual Size: ${actual} | Expected Size: ${expected} | ${details.raw} bytes (${details.percent}%)`, + ); + } + } + console.info(); + if (failed) { + const originalSizeJsonFilePath = path.join(this.testPackage, 'size.json'); + console.info( + `If this is a desired change, please update the size limits in: ${originalSizeJsonFilePath}`, + ); + process.exitCode = 1; + } else { + console.info( + `Payload size check passed. All diffs are less than ${THRESHOLD_PERCENT}% or ${THRESHOLD_BYTES} bytes.`, + ); + } + console.info(Array(80).fill('=').join('')); + console.info(); + } +}