| 
									
										
										
										
											2018-02-28 06:51:40 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google Inc. 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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-22 10:37:43 -07:00
										 |  |  | import {createPatch} from 'diff'; | 
					
						
							| 
									
										
										
										
											2018-02-28 06:51:40 -08:00
										 |  |  | import * as fs from 'fs'; | 
					
						
							|  |  |  | import * as path from 'path'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  | /** Directory in the Angular repo where package gold tests live. */ | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  | const TEST_DIR = path.resolve(path.join('packages', 'bazel', 'test', 'ng_package')); | 
					
						
							| 
									
										
										
										
											2018-02-28 06:51:40 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  | type TestPackage = { | 
					
						
							|  |  |  |   dir: string; goldPath: string | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | const packagesToTest: TestPackage[] = [ | 
					
						
							|  |  |  |   {dir: 'example', goldPath: 'example_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[] { | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |   const result: string[] = []; | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |   if (fs.statSync(directoryPath).isDirectory()) { | 
					
						
							|  |  |  |     fs.readdirSync(directoryPath).forEach(f => { | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |       result.push( | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |           '  '.repeat(depth) + path.join(directoryPath, f), | 
					
						
							|  |  |  |           ...getIndentedDirectoryStructure(path.join(directoryPath, f), depth + 1)); | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return result; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-02-28 06:51:40 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * 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[] { | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |   const result: string[] = []; | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |   if (fs.statSync(directoryPath).isDirectory()) { | 
					
						
							|  |  |  |     fs.readdirSync(directoryPath).forEach(dir => { | 
					
						
							|  |  |  |       result.push(...getDescendantFilesContents(path.join(directoryPath, dir))); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |     result.push(`--- ${directoryPath} ---`, '', fs.readFileSync(directoryPath, 'utf-8'), ''); | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  |   } | 
					
						
							|  |  |  |   return result; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  | /** Accepts the current package output by overwriting the gold file in source control. */ | 
					
						
							|  |  |  | function acceptNewPackageGold(testPackage: TestPackage) { | 
					
						
							|  |  |  |   const goldenFile = path.join(TEST_DIR, testPackage.goldPath); | 
					
						
							|  |  |  |   process.chdir(path.join(TEST_DIR, `${testPackage.dir}`, 'npm_package')); | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |   const actual = getCurrentPackageContent(); | 
					
						
							|  |  |  |   fs.writeFileSync(require.resolve(goldenFile), actual, '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 goldenFile = path.join(TEST_DIR, testPackage.goldPath); | 
					
						
							|  |  |  |   process.chdir(path.join(TEST_DIR, `${testPackage.dir}`, 'npm_package')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Gold file content from source control. We expect that the output of the package matches this.
 | 
					
						
							|  |  |  |   const expected = fs.readFileSync(goldenFile, 'utf-8'); | 
					
						
							| 
									
										
										
										
											2018-03-22 10:37:43 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |   // 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 "${testPackage.dir}"`, () => { | 
					
						
							|  |  |  |     if (actual !== expected) { | 
					
						
							| 
									
										
										
										
											2018-03-22 10:37:43 -07:00
										 |  |  |       // Compute the patch and strip the header
 | 
					
						
							|  |  |  |       let patch = | 
					
						
							|  |  |  |           createPatch(goldenFile, expected, actual, 'Golden file', 'Generated file', {context: 5}); | 
					
						
							|  |  |  |       const endOfHeader = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1; | 
					
						
							|  |  |  |       patch = patch.substring(endOfHeader); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |       // 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` + | 
					
						
							|  |  |  |           `      bazel run ${process.env['BAZEL_TARGET']}.accept\n`; | 
					
						
							| 
									
										
										
										
											2018-03-22 10:37:43 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  |       fail(failureMessage); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-02-28 06:51:40 -08:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-03-15 18:04:34 -07:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2018-04-02 16:52:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** Gets all errors for missing golden files or packages. Typically missing from the bazel rule. */ | 
					
						
							|  |  |  | function getDependencyErrors(testPackage: TestPackage): string[] { | 
					
						
							|  |  |  |   const errors = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const goldenFile = path.join(TEST_DIR, testPackage.goldPath); | 
					
						
							|  |  |  |   if (!fs.existsSync(goldenFile)) { | 
					
						
							|  |  |  |     errors.push( | 
					
						
							|  |  |  |         `The golden file "${testPackage.goldPath}" cannot be found. ` + | 
					
						
							|  |  |  |         `Ensure that the file exists and is added to the 'data' attribute of the test rule`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!fs.existsSync(path.join(TEST_DIR, `${testPackage.dir}`, 'npm_package'))) { | 
					
						
							|  |  |  |     errors.push( | 
					
						
							|  |  |  |         `The package output for "${testPackage.dir}" cannot be found. Ensure that ` + | 
					
						
							|  |  |  |         `the an ng_package named "npm_package" exists in the "${testPackage.dir }" directory ` + | 
					
						
							|  |  |  |         `and that it is added to the "data" attribute of the test rule.`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return errors; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // If there are any dependency errors, emit the errors and set the exit code.
 | 
					
						
							|  |  |  | let hasError = false; | 
					
						
							|  |  |  | for (let p of packagesToTest) { | 
					
						
							|  |  |  |   const dependencyErrors = getDependencyErrors(p); | 
					
						
							|  |  |  |   if (dependencyErrors.length) { | 
					
						
							|  |  |  |     console.error(dependencyErrors.join('\n\n')); | 
					
						
							|  |  |  |     hasError = true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if (!hasError) { | 
					
						
							|  |  |  |   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); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | process.exitCode = hasError ? 1 : 0; |