| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @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
 | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  | import {AbsoluteFsPath, FileSystem, NodeJSFileSystem, PathSegment} from '../../../src/ngtsc/file_system'; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | const fs = new NodeJSFileSystem(); | 
					
						
							|  |  |  | const basePath = fs.resolve(__dirname, '../test_cases'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Search the `test_cases` directory, in the real file-system, for all the compliance tests. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Test are indicated by a `TEST_CASES.json` file which contains one or more test cases. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function* getAllComplianceTests(): Generator<ComplianceTest> { | 
					
						
							|  |  |  |   const testConfigPaths = collectPaths(basePath, segment => segment === 'TEST_CASES.json'); | 
					
						
							|  |  |  |   for (const testConfigPath of testConfigPaths) { | 
					
						
							|  |  |  |     yield* getComplianceTests(testConfigPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Extract all the compliance tests from the TEST_CASES.json file at the `testConfigPath`. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @param testConfigPath The path, relative to the `test_cases` basePath, of the `TEST_CASES.json` | 
					
						
							|  |  |  |  *     config file. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function* getComplianceTests(testConfigPath: string): Generator<ComplianceTest> { | 
					
						
							|  |  |  |   const absTestConfigPath = fs.resolve(basePath, testConfigPath); | 
					
						
							|  |  |  |   const realTestPath = fs.dirname(absTestConfigPath); | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |   const testConfigJSON = loadTestCasesFile(fs, absTestConfigPath, basePath).cases; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |   const testConfig = Array.isArray(testConfigJSON) ? testConfigJSON : [testConfigJSON]; | 
					
						
							|  |  |  |   for (const test of testConfig) { | 
					
						
							|  |  |  |     const inputFiles = getStringArrayOrDefault(test, 'inputFiles', realTestPath, ['test.ts']); | 
					
						
							| 
									
										
										
										
											2020-12-02 14:12:03 +00:00
										 |  |  |     const compilationModeFilter = getStringArrayOrDefault( | 
					
						
							|  |  |  |                                       test, 'compilationModeFilter', realTestPath, | 
					
						
							|  |  |  |                                       ['linked compile', 'full compile']) as CompilationMode[]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |     yield { | 
					
						
							|  |  |  |       relativePath: fs.relative(basePath, realTestPath), | 
					
						
							|  |  |  |       realTestPath, | 
					
						
							|  |  |  |       description: getStringOrFail(test, 'description', realTestPath), | 
					
						
							|  |  |  |       inputFiles, | 
					
						
							| 
									
										
										
										
											2020-12-02 14:12:03 +00:00
										 |  |  |       compilationModeFilter, | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |       expectations: parseExpectations(test.expectations, realTestPath, inputFiles), | 
					
						
							|  |  |  |       compilerOptions: getConfigOptions(test, 'compilerOptions', realTestPath), | 
					
						
							|  |  |  |       angularCompilerOptions: getConfigOptions(test, 'angularCompilerOptions', realTestPath), | 
					
						
							|  |  |  |       focusTest: test.focusTest, | 
					
						
							|  |  |  |       excludeTest: test.excludeTest, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  | function loadTestCasesFile( | 
					
						
							|  |  |  |     fs: FileSystem, testCasesPath: AbsoluteFsPath, basePath: AbsoluteFsPath): any { | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     return JSON.parse(fs.readFile(testCasesPath)); | 
					
						
							|  |  |  |   } catch (e) { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         `Failed to load test-cases at "${fs.relative(basePath, testCasesPath)}":\n ${e.message}`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Search the file-system from the `current` path to find all paths that satisfy the `predicate`. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function* | 
					
						
							|  |  |  |     collectPaths(current: AbsoluteFsPath, predicate: (segment: PathSegment) => boolean): | 
					
						
							|  |  |  |         Generator<AbsoluteFsPath> { | 
					
						
							|  |  |  |   if (!fs.exists(current)) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   for (const segment of fs.readdir(current)) { | 
					
						
							|  |  |  |     const absPath = fs.resolve(current, segment); | 
					
						
							|  |  |  |     if (predicate(segment)) { | 
					
						
							|  |  |  |       yield absPath; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       if (fs.lstat(absPath).isDirectory()) { | 
					
						
							|  |  |  |         yield* collectPaths(absPath, predicate); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getStringOrFail(container: any, property: string, testPath: AbsoluteFsPath): string { | 
					
						
							|  |  |  |   const value = container[property]; | 
					
						
							|  |  |  |   if (typeof value !== 'string') { | 
					
						
							|  |  |  |     throw new Error(`Test is missing "${property}" property in TEST_CASES.json: ` + testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return value; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  | function getBooleanOrDefault( | 
					
						
							|  |  |  |     container: any, property: string, testPath: AbsoluteFsPath, defaultValue: boolean): boolean { | 
					
						
							|  |  |  |   const value = container[property]; | 
					
						
							|  |  |  |   if (typeof value === 'undefined') { | 
					
						
							|  |  |  |     return defaultValue; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (typeof value !== 'boolean') { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         `Test has invalid "${property}" property in TEST_CASES.json - expected boolean: ` + | 
					
						
							|  |  |  |         testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return value; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | function getStringArrayOrDefault( | 
					
						
							|  |  |  |     container: any, property: string, testPath: AbsoluteFsPath, defaultValue: string[]): string[] { | 
					
						
							|  |  |  |   const value = container[property]; | 
					
						
							|  |  |  |   if (typeof value === 'undefined') { | 
					
						
							|  |  |  |     return defaultValue; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         `Test has invalid "${property}" property in TEST_CASES.json - expected array of strings: ` + | 
					
						
							|  |  |  |         testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return value; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function parseExpectations( | 
					
						
							|  |  |  |     value: any, testPath: AbsoluteFsPath, inputFiles: string[]): Expectation[] { | 
					
						
							|  |  |  |   const defaultFailureMessage = 'Incorrect generated output.'; | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |   const tsFiles = inputFiles.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts')); | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |   const defaultFiles = tsFiles.map(inputFile => { | 
					
						
							|  |  |  |     const outputFile = inputFile.replace(/\.ts$/, '.js'); | 
					
						
							|  |  |  |     return {expected: outputFile, generated: outputFile}; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (typeof value === 'undefined') { | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |     return [{ | 
					
						
							|  |  |  |       failureMessage: defaultFailureMessage, | 
					
						
							|  |  |  |       files: defaultFiles, | 
					
						
							|  |  |  |       expectedErrors: [], | 
					
						
							|  |  |  |       extraChecks: [] | 
					
						
							|  |  |  |     }]; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!Array.isArray(value)) { | 
					
						
							|  |  |  |     return parseExpectations([value], testPath, inputFiles); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return value.map((expectation, i) => { | 
					
						
							|  |  |  |     if (typeof expectation !== 'object') { | 
					
						
							|  |  |  |       throw new Error( | 
					
						
							|  |  |  |           `Test has invalid "expectations" property in TEST_CASES.json - expected array of "expectation" objects: ${ | 
					
						
							|  |  |  |               testPath}`);
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |     const failureMessage: string = expectation.failureMessage ?? defaultFailureMessage; | 
					
						
							|  |  |  |     const expectedErrors = parseExpectedErrors(expectation.expectedErrors, testPath); | 
					
						
							|  |  |  |     const extraChecks = parseExtraChecks(expectation.extraChecks, testPath); | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (typeof expectation.files === 'undefined') { | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |       return {failureMessage, files: defaultFiles, expectedErrors, extraChecks}; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!Array.isArray(expectation.files)) { | 
					
						
							|  |  |  |       throw new Error(`Test has invalid "expectations[${ | 
					
						
							|  |  |  |           i}].files" property in TEST_CASES.json - expected array of "expected files": ${ | 
					
						
							|  |  |  |           testPath}`);
 | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |     const files: ExpectedFile[] = expectation.files.map((file: any) => { | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |       if (typeof file === 'string') { | 
					
						
							|  |  |  |         return {expected: file, generated: file}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (typeof file === 'object' && typeof file.expected === 'string' && | 
					
						
							|  |  |  |           typeof file.generated === 'string') { | 
					
						
							|  |  |  |         return file; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       throw new Error(`Test has invalid "expectations[${ | 
					
						
							|  |  |  |           i}].files" property in TEST_CASES.json - expected each item to be a string or an "expected file" object: ${ | 
					
						
							|  |  |  |           testPath}`);
 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |     return {failureMessage, files, expectedErrors, extraChecks}; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  | function parseExpectedErrors(expectedErrors: any = [], testPath: AbsoluteFsPath): ExpectedError[] { | 
					
						
							|  |  |  |   if (!Array.isArray(expectedErrors)) { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         'Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array: ' + | 
					
						
							|  |  |  |         testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return expectedErrors.map(error => { | 
					
						
							|  |  |  |     if (typeof error !== 'object' || typeof error.message !== 'string' || | 
					
						
							|  |  |  |         (error.location && typeof error.location !== 'string')) { | 
					
						
							|  |  |  |       throw new Error( | 
					
						
							|  |  |  |           `Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array of ExpectedError objects: ` + | 
					
						
							|  |  |  |           testPath); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return {message: parseRegExp(error.message), location: parseRegExp(error.location)}; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function parseExtraChecks(extraChecks: any = [], testPath: AbsoluteFsPath): ExtraCheck[] { | 
					
						
							|  |  |  |   if (!Array.isArray(extraChecks) || | 
					
						
							|  |  |  |       !extraChecks.every(i => typeof i === 'string' || Array.isArray(i))) { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         `Test has invalid "extraChecks" property in TEST_CASES.json - expected an array of strings or arrays: ` + | 
					
						
							|  |  |  |         testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return extraChecks; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function parseRegExp(str: string|undefined): RegExp { | 
					
						
							|  |  |  |   return new RegExp(str || ''); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | function getConfigOptions( | 
					
						
							|  |  |  |     container: any, property: string, testPath: AbsoluteFsPath): ConfigOptions|undefined { | 
					
						
							|  |  |  |   const options = container[property]; | 
					
						
							|  |  |  |   if (options !== undefined && typeof options !== 'object') { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |         `Test have invalid "${ | 
					
						
							|  |  |  |             property}" property in TEST_CASES.json - expected config option object: ` +
 | 
					
						
							|  |  |  |         testPath); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return options; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Describes a compliance test, as defined in a `TEST_CASES.json` file. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export interface ComplianceTest { | 
					
						
							|  |  |  |   /** The path, relative to the test_cases directory, of the directory containing this test. */ | 
					
						
							|  |  |  |   relativePath: string; | 
					
						
							|  |  |  |   /** The absolute path (on the real file-system) to the test case containing this test. */ | 
					
						
							|  |  |  |   realTestPath: AbsoluteFsPath; | 
					
						
							|  |  |  |   /** A description of this particular test. */ | 
					
						
							|  |  |  |   description: string; | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Any additional options to pass to the TypeScript compiler when compiling this test's source | 
					
						
							|  |  |  |    * files. These are equivalent to what you would put in `tsconfig.json`. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   compilerOptions?: ConfigOptions; | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Any additional options to pass to the Angular compiler when compiling this test's source | 
					
						
							|  |  |  |    * files. These are equivalent to what you would put in `tsconfig.json`. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   angularCompilerOptions?: ConfigOptions; | 
					
						
							|  |  |  |   /** A list of paths to source files that should be compiled for this test case. */ | 
					
						
							|  |  |  |   inputFiles: string[]; | 
					
						
							| 
									
										
										
										
											2020-12-02 14:12:03 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Only run this test when the input files are compiled using the given compilation | 
					
						
							|  |  |  |    * modes. The default is to run for all modes. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   compilationModeFilter: CompilationMode[]; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  |   /** A list of expectations to check for this test case. */ | 
					
						
							|  |  |  |   expectations: Expectation[]; | 
					
						
							|  |  |  |   /** If set to `true`, then focus on this test (equivalent to jasmine's 'fit()`). */ | 
					
						
							|  |  |  |   focusTest?: boolean; | 
					
						
							|  |  |  |   /** If set to `true`, then exclude this test (equivalent to jasmine's 'xit()`). */ | 
					
						
							|  |  |  |   excludeTest?: boolean; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-02 14:12:03 +00:00
										 |  |  | export type CompilationMode = 'linked compile'|'full compile'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | export interface Expectation { | 
					
						
							|  |  |  |   /** The message to display if this expectation fails. */ | 
					
						
							|  |  |  |   failureMessage: string; | 
					
						
							|  |  |  |   /** A list of pairs of paths to expected and generated files to compare. */ | 
					
						
							|  |  |  |   files: ExpectedFile[]; | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  |   /** A collection of errors that should be reported when compiling the generated file. */ | 
					
						
							|  |  |  |   expectedErrors: ExpectedError[]; | 
					
						
							|  |  |  |   /** Additional checks to run against the generated code. */ | 
					
						
							|  |  |  |   extraChecks: ExtraCheck[]; | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * A pair of paths to expected and generated files that should be compared in an `Expectation`. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export interface ExpectedFile { | 
					
						
							|  |  |  |   expected: string; | 
					
						
							|  |  |  |   generated: string; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 19:43:35 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Regular expressions that should match an error message. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export interface ExpectedError { | 
					
						
							|  |  |  |   message: RegExp; | 
					
						
							|  |  |  |   location: RegExp; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * The name (or name and arguments) of a function to call to run additional checks against the | 
					
						
							|  |  |  |  * generated code. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export type ExtraCheck = (string|[string, ...any]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-11 15:29:43 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Options to pass to configure the compiler. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export type ConfigOptions = Record<string, string|boolean|null>; |