66 * found in the LICENSE file at https://angular.io/license
77 */
88
9+ import { existsSync } from 'node:fs' ;
910import fs from 'node:fs/promises' ;
1011import path from 'node:path' ;
1112import os from 'node:os' ;
@@ -30,6 +31,14 @@ import {
3031} from './process_utils.mjs' ;
3132import { ENVIRONMENT_TMP_PLACEHOLDER } from './constants.mjs' ;
3233import { debug } from './debug.mjs' ;
34+ import chalk from 'chalk' ;
35+
36+ // Convience access to chalk colors.
37+ const { red, green} = chalk ;
38+ /** The size discrepancy we allow in bytes. */
39+ const THRESHOLD_BYTES = 5000 ;
40+ /** The size discrepancy as a percentage. */
41+ const THRESHOLD_PERCENT = 5 ;
3342
3443/** Error class that is used when an integration command fails. */
3544class IntegrationTestCommandError extends Error { }
@@ -270,6 +279,11 @@ export class TestRunner {
270279 console . info (
271280 `Successfully ran all commands in test directory: ${ path . normalize ( testWorkingDir ) } ` ,
272281 ) ;
282+
283+ // If the integration test provides a size.json file we use it as a size tracking marker.
284+ if ( existsSync ( path . join ( testWorkingDir , 'size.json' ) ) ) {
285+ await this . _runSizeTracking ( testWorkingDir , commandEnv ) ;
286+ }
273287 }
274288
275289 /**
@@ -299,4 +313,98 @@ export class TestRunner {
299313
300314 return { ...defaults , ...baseEnv } ;
301315 }
316+
317+ /**
318+ * Runs the size tracking scripting.
319+ *
320+ * Builds the integration test application and then checks if the size of the generated files varies too
321+ * far from our known file sizes.
322+ */
323+ private async _runSizeTracking (
324+ testWorkingDir : string ,
325+ commandEnv : NodeJS . ProcessEnv ,
326+ ) : Promise < void > {
327+ const success = await runCommandInChildProcess ( 'yarn' , [ 'build' ] , testWorkingDir , commandEnv ) ;
328+ if ( ! success ) {
329+ throw Error ( 'Failed to build for size tracking.' ) ;
330+ }
331+
332+ const sizes : { [ key : string ] : SizeCheckResult } = { } ;
333+
334+ const expectedSizes = JSON . parse (
335+ await fs . readFile ( path . join ( testWorkingDir , 'size.json' ) , 'utf-8' ) ,
336+ ) as { [ key : string ] : number } ;
337+
338+ for ( let [ filename , expectedSize ] of Object . entries ( expectedSizes ) ) {
339+ const generedFilePath = path . join ( testWorkingDir , 'dist' , filename ) ;
340+ if ( ! existsSync ( generedFilePath ) ) {
341+ sizes [ filename ] = {
342+ actual : undefined ,
343+ failing : false ,
344+ expected : expectedSize ,
345+ details : 'missing' ,
346+ } ;
347+ } else {
348+ const { size : actualSize } = await fs . stat ( path . join ( testWorkingDir , 'dist' , filename ) ) ;
349+ const absoluteSizeDiff = Math . abs ( actualSize - expectedSize ) ;
350+ const percentSizeDiff = ( absoluteSizeDiff / expectedSize ) * 100 ;
351+ const direction = actualSize === expectedSize ? '' : actualSize > expectedSize ? '+' : '-' ;
352+ sizes [ filename ] = {
353+ actual : actualSize ,
354+ expected : expectedSize ,
355+ failing : absoluteSizeDiff > THRESHOLD_BYTES || percentSizeDiff > THRESHOLD_PERCENT ,
356+ details : {
357+ raw : `${ direction } ${ absoluteSizeDiff . toFixed ( 0 ) } ` ,
358+ percent : `${ direction } ${ percentSizeDiff . toFixed ( 3 ) } ` ,
359+ } ,
360+ } ;
361+ }
362+ }
363+
364+ console . info ( Array ( 80 ) . fill ( '=' ) . join ( '' ) ) ;
365+ console . info (
366+ `${ Array ( 28 ) . fill ( '=' ) . join ( '' ) } SIZE TRACKING RESULTS ${ Array ( 29 ) . fill ( '=' ) . join ( '' ) } ` ,
367+ ) ;
368+ console . info ( Array ( 80 ) . fill ( '=' ) . join ( '' ) ) ;
369+ let failed = false ;
370+ for ( let [ filename , { actual, expected, failing, details} ] of Object . entries ( sizes ) ) {
371+ failed = failed || failing ;
372+ const bullet = failing ? green ( '✔' ) : red ( '✘' ) ;
373+ console . info ( ` ${ bullet } ${ filename } ` ) ;
374+ if ( details === 'missing' ) {
375+ console . info (
376+ ` File not found in generated integration test application, either ensure the file is created or remove it from the size tracking json file.` ,
377+ ) ;
378+ } else {
379+ console . info (
380+ ` Actual Size: ${ actual } | Expected Size: ${ expected } | ${ details . raw } bytes (${ details . raw } %)` ,
381+ ) ;
382+ }
383+ }
384+ console . info ( ) ;
385+ if ( failed ) {
386+ const sizeFileLocation = path . join ( this . testPackage , 'size.json' ) ;
387+ console . info (
388+ `If this is a desired change, please update the size limits in: ${ sizeFileLocation } ` ,
389+ ) ;
390+ process . exitCode = 1 ;
391+ } else {
392+ console . info (
393+ `Payload size check passed. All diffs are less than ${ THRESHOLD_PERCENT } % or ${ THRESHOLD_BYTES } bytes.` ,
394+ ) ;
395+ }
396+ console . info ( Array ( 80 ) . fill ( '=' ) . join ( '' ) ) ;
397+ }
398+ }
399+
400+ interface SizeCheckResult {
401+ expected : number ;
402+ actual : number | undefined ;
403+ failing : boolean ;
404+ details :
405+ | 'missing'
406+ | {
407+ raw : string ;
408+ percent : string ;
409+ } ;
302410}
0 commit comments