Update the license headers throughout the repository to reference Google LLC rather than Google Inc, for the required license headers. PR Close #37205
		
			
				
	
	
		
			165 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC All Rights Reserved.
 | |
|  *
 | |
|  * 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 * as crypto from 'crypto';
 | |
| import {createPatch} from 'diff';
 | |
| import * as fs from 'fs';
 | |
| import * as path from 'path';
 | |
| 
 | |
| type TestPackage = {
 | |
|   displayName: string; packagePath: string; goldenFilePath: string;
 | |
| };
 | |
| 
 | |
| const packagesToTest: TestPackage[] = [
 | |
|   {
 | |
|     displayName: 'Example NPM package',
 | |
|     // Resolve the "npm_package" directory by using the runfile resolution. Note that we need to
 | |
|     // resolve the "package.json" of the package since otherwise NodeJS would resolve the "main"
 | |
|     // file, which is not necessarily at the root of the "npm_package".
 | |
|     packagePath: path.dirname(
 | |
|         require.resolve('angular/packages/bazel/test/ng_package/example/npm_package/package.json')),
 | |
|     goldenFilePath: require.resolve('./example_package.golden')
 | |
|   },
 | |
|   {
 | |
|     displayName: 'Example with ts_library NPM package',
 | |
|     // Resolve the "npm_package" directory by using the runfile resolution. Note that we need to
 | |
|     // resolve the "package.json" of the package since otherwise NodeJS would resolve the "main"
 | |
|     // file, which is not necessarily at the root of the "npm_package".
 | |
|     packagePath: path.dirname(require.resolve(
 | |
|         'angular/packages/bazel/test/ng_package/example-with-ts-library/npm_package/package.json')),
 | |
|     goldenFilePath: require.resolve('./example_with_ts_library_package.golden')
 | |
|   },
 | |
| ];
 | |
| 
 | |
| /**
 | |
|  * Gets all entries in a given directory (files and directories) recursively,
 | |
|  * indented based on each entry's depth.
 | |
|  *
 | |
|  * @param directoryPath Path of the directory for which to get entries.
 | |
|  * @param depth The depth of this directory (used for indentation).
 | |
|  * @returns Array of all indented entries (files and directories).
 | |
|  */
 | |
| function getIndentedDirectoryStructure(directoryPath: string, depth = 0): string[] {
 | |
|   const result: string[] = [];
 | |
|   if (fs.statSync(directoryPath).isDirectory()) {
 | |
|     // We need to sort the directories because on Windows "readdirsync" is not sorted. Since we
 | |
|     // compare these in a golden file, the order needs to be consistent across different platforms.
 | |
|     fs.readdirSync(directoryPath).sort().forEach(f => {
 | |
|       const filePath = path.posix.join(directoryPath, f);
 | |
|       result.push(
 | |
|           '  '.repeat(depth) + filePath, ...getIndentedDirectoryStructure(filePath, depth + 1));
 | |
|     });
 | |
|   }
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Gets all file contents in a given directory recursively. Each file's content will be
 | |
|  * prefixed with a one-line header with the file name.
 | |
|  *
 | |
|  * @param directoryPath Path of the directory for which file contents are collected.
 | |
|  * @returns Array of all files' contents.
 | |
|  */
 | |
| function getDescendantFilesContents(directoryPath: string): string[] {
 | |
|   const result: string[] = [];
 | |
|   if (fs.statSync(directoryPath).isDirectory()) {
 | |
|     // We need to sort the directories because on Windows "readdirsync" is not sorted. Since we
 | |
|     // compare these in a golden file, the order needs to be consistent across different platforms.
 | |
|     fs.readdirSync(directoryPath).sort().forEach(dir => {
 | |
|       result.push(...getDescendantFilesContents(path.posix.join(directoryPath, dir)));
 | |
|     });
 | |
|   }
 | |
|   // Binary files should equal the same as in the srcdir.
 | |
|   else if (path.extname(directoryPath) === '.png') {
 | |
|     result.push(`--- ${directoryPath} ---`, '', hashFileContents(directoryPath), '');
 | |
|   }
 | |
|   // Note that we don't want to include ".map" files in the golden file since these are not
 | |
|   // consistent across different environments (e.g. path delimiters)
 | |
|   else if (path.extname(directoryPath) !== '.map') {
 | |
|     result.push(`--- ${directoryPath} ---`, '', readFileContents(directoryPath), '');
 | |
|   }
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| /** Accepts the current package output by overwriting the gold file in source control. */
 | |
| function acceptNewPackageGold(testPackage: TestPackage) {
 | |
|   process.chdir(testPackage.packagePath);
 | |
|   fs.writeFileSync(testPackage.goldenFilePath, getCurrentPackageContent(), 'utf-8');
 | |
| }
 | |
| 
 | |
| /** Gets the content of the current package. Depends on the current working directory. */
 | |
| function getCurrentPackageContent() {
 | |
|   return [...getIndentedDirectoryStructure('.'), ...getDescendantFilesContents('.')]
 | |
|       .join('\n')
 | |
|       .replace(/bazel-out\/.*\/bin/g, 'bazel-bin');
 | |
| }
 | |
| 
 | |
| /** Compares the current package output to the gold file in source control in a jasmine test. */
 | |
| function runPackageGoldTest(testPackage: TestPackage) {
 | |
|   const {displayName, packagePath, goldenFilePath} = testPackage;
 | |
| 
 | |
|   process.chdir(packagePath);
 | |
| 
 | |
|   // Gold file content from source control. We expect that the output of the package matches this.
 | |
|   const expected = readFileContents(goldenFilePath);
 | |
| 
 | |
|   // Actual file content generated from the rule.
 | |
|   const actual = getCurrentPackageContent();
 | |
| 
 | |
|   // Without the `--accept` flag, compare the actual to the expected in a jasmine test.
 | |
|   it(`Package "${displayName}"`, () => {
 | |
|     if (actual !== expected) {
 | |
|       // Compute the patch and strip the header
 | |
|       let patch = createPatch(
 | |
|           goldenFilePath, expected, actual, 'Golden file', 'Generated file', {context: 5});
 | |
|       const endOfHeader = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1;
 | |
|       patch = patch.substring(endOfHeader);
 | |
| 
 | |
|       // Use string concatentation instead of whitespace inside a single template string
 | |
|       // to make the structure message explicit.
 | |
|       const failureMessage = `example ng_package differs from golden file\n` +
 | |
|           `    Diff:\n` +
 | |
|           `    ${patch}\n\n` +
 | |
|           `    To accept the new golden file, run:\n` +
 | |
|           `      yarn bazel run ${process.env['BAZEL_TARGET']}.accept\n`;
 | |
| 
 | |
|       fail(failureMessage);
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Reads the contents of the specified file. Additionally it strips all carriage return (CR)
 | |
|  * characters from the given content. We do this since the content that will be pulled into the
 | |
|  * golden file needs to be consistent across all platforms.
 | |
|  */
 | |
| function readFileContents(filePath: string): string {
 | |
|   return fs.readFileSync(filePath, 'utf8').replace(/\r/g, '');
 | |
| }
 | |
| 
 | |
| function hashFileContents(filePath: string): string {
 | |
|   return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
 | |
| }
 | |
| 
 | |
| if (require.main === module) {
 | |
|   const args = process.argv.slice(2);
 | |
|   const acceptingNewGold = (args[0] === '--accept');
 | |
| 
 | |
|   if (acceptingNewGold) {
 | |
|     for (let p of packagesToTest) {
 | |
|       acceptNewPackageGold(p);
 | |
|     }
 | |
|   }
 | |
| } else {
 | |
|   describe('Comparing test packages to golds', () => {
 | |
|     for (let p of packagesToTest) {
 | |
|       runPackageGoldTest(p);
 | |
|     }
 | |
|   });
 | |
| }
 |