Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bazel/integration/test_runner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
4 changes: 4 additions & 0 deletions bazel/integration/test_runner/runner.mts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from './process_utils.mjs';
import {ENVIRONMENT_TMP_PLACEHOLDER} from './constants.mjs';
import {debug} from './debug.mjs';
import {SizeTracker} from './size-tracking.mjs';

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

constructor(
private readonly isTestDebugMode: boolean,
Expand All @@ -59,6 +61,7 @@ export class TestRunner {
environment: EnvironmentConfig,
) {
this.environment = this._assignDefaultEnvironmentVariables(environment);
this.sizeTracker = new SizeTracker(this.testPackage);
}

async run() {
Expand All @@ -82,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.');

Expand Down
124 changes: 124 additions & 0 deletions bazel/integration/test_runner/size-tracking.mts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
1 change: 1 addition & 0 deletions bazel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions bazel/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading