license import { getSystemPath , Path } from '@angular-devkit/core' ; import { HostTree , Tree } from '@angular-devkit/schematics' ; import { SchematicTestRunner , UnitTestTree } from '@angular-devkit/schematics/testing' ; import { readFileSync } from 'fs' ; import { sync as globSync } from 'glob' ; import { parse } from 'jsonc-parser' ; import { basename , extname , join , relative , sep } from 'path' ; import { EMPTY } from 'rxjs' ; import { createTestApp } from './test-app' ; /** Suffix that indicates whether a given file is a test case input. */ const TEST_CASE_INPUT_SUFFIX = '_input.ts' ; /** Suffix that indicates whether a given file is an expected output of a test case. */ const TEST_CASE_OUTPUT_SUFFIX = '_expected_output.ts' ; /** Name of the folder that can contain test case files which should not run automatically. */ const MISC_FOLDER_NAME = 'misc' ; /** Reads the UTF8 content of the specified file. Normalizes the path and ensures that */ export function readFileContent ( filePath ) { return readFileSync ( filePath , 'utf8' ) ; } /**
 * Creates a test app schematic tree that will be copied over to a real filesystem location.
 * This is necessary because otherwise the TypeScript compiler API would not be able to
 * find source files within the tsconfig project.
 * TODO(devversion): we should be able to make the TypeScript config parsing respect the
 * schematic tree. This would allow us to fully take advantage of the virtual file system.
 */ export async function createFileSystemTestApp ( runner ) { const hostTree = new HostTree ( ) ; const appTree = await createTestApp ( runner , { name : 'cdk-testing' } , hostTree ) ; appTree . files . forEach ( ( f ) => writeFile ( f , appTree . readContent ( f ) ) ) ; return { appTree , writeFile } ; function writeFile ( filePath , content ) { if ( hostTree . exists ( filePath ) ) { hostTree . overwrite ( filePath , content ) ; } else { hostTree . create ( filePath , content ) ; } } } export async function createTestCaseSetup ( migrationName , collectionPath , inputFiles ) { const runner = new SchematicTestRunner ( 'schematics' , collectionPath ) ; let logOutput = '' ; runner . logger . subscribe ( ( entry ) => ( logOutput += `${ entry . message }\n` ) ) ; const { appTree , writeFile } = await createFileSystemTestApp ( runner ) ; _patchTypeScriptDefaultLib ( appTree ) ; inputFiles . forEach ( ( inputFilePath ) => { const inputTestName = basename ( inputFilePath , extname ( inputFilePath ) ) ; const relativePath = `projects/cdk-testing/src/test-cases/${ inputTestName }.ts` ; const inputContent = readFileContent ( inputFilePath ) ; writeFile ( relativePath , inputContent ) ; } ) ; const testAppTsconfigPath = 'projects/cdk-testing/tsconfig.app.json' ; const testAppTsconfig = parse ( appTree . readContent ( testAppTsconfigPath ) , [ ] , { allowTrailingComma : true } ) ; testAppTsconfig . include . push ( 'src/**/*.ts' ) ; writeFile ( testAppTsconfigPath , JSON . stringify ( testAppTsconfig , null , 2 ) ) ; const migrationNames = Array . isArray ( migrationName ) ? migrationName : [ migrationName ] ; const runFixers = async function ( ) { runner . engine . executePostTasks = ( ) => EMPTY as any ; for ( const migration of migrationNames ) { await runner . runSchematicAsync ( migration , { } , appTree ) . toPromise ( ) ; } return { logOutput } ; } ; return { runner , appTree , writeFile , runFixers } ; } /**
 * Resolves all test cases for specified path using Bazel's runfile manifest. Note that we
 * cannot just use "glob" since the test case files are not copied to the Bazel bin directory
 * and are just runfiles.
 */ export function findBazelVersionTestCases ( basePath ) { const testCasesMap = new Map < string , string [ ] > ( ) ; const manifestPath = process . env [ 'RUNFILES_MANIFEST_FILE' ] ! ; const runfilesDir = process . env [ 'RUNFILES' ] ; if ( ! manifestPath ) { const runfilesBaseDir = join ( runfilesDir ! , basePath ) ; const inputFiles = globSync ( `**/!(${ MISC_FOLDER_NAME })/*${ TEST_CASE_INPUT_SUFFIX }` , { cwd : runfilesBaseDir } ) ; inputFiles . forEach ( ( inputFile ) => { const targetVersion = inputFile . split ( sep ) [ 0 ] ; const resolvedInputPath = join ( runfilesBaseDir , inputFile ) ; testCasesMap . set ( targetVersion , ( testCasesMap . get ( targetVersion ) || [ ] ) . concat ( resolvedInputPath ) ) ; } ) ; return testCasesMap ; } readFileSync ( manifestPath , 'utf8' ) . split ( '\n' ) . forEach ( ( line ) => { const [ runfilePath , realPath ] = line . split ( ' ' ) ; if ( runfilePath . startsWith ( basePath ) && runfilePath . endsWith ( TEST_CASE_INPUT_SUFFIX ) ) { const pathSegments = relative ( basePath , runfilePath ) . split ( sep ) ; if ( pathSegments . includes ( MISC_FOLDER_NAME ) ) { return ; } const targetVersion = pathSegments [ 0 ] ; testCasesMap . set ( targetVersion , ( testCasesMap . get ( targetVersion ) || [ ] ) . concat ( realPath ) ) ; } } ) ; return testCasesMap ; } /**
 * Sets up the specified test cases using Jasmine by creating the appropriate jasmine
 * spec definitions. This should be used within a "describe" jasmine closure.
 */ export function defineJasmineTestCases ( versionName , collectionFile , inputFiles ) { if ( ! inputFiles ) { return ; } jasmine . MAX_PRETTY_PRINT_CHARS = 10000 ; let appTree ; let testCasesOutputPath ; beforeAll ( async ( ) => { const { appTree : _tree , runFixers } = await createTestCaseSetup ( versionName , collectionFile , inputFiles ) ; await runFixers ( ) ; appTree = _tree ; testCasesOutputPath = '/projects/cdk-testing/src/test-cases/' ; } ) ; inputFiles . forEach ( ( inputFile ) => { const inputTestName = basename ( inputFile , extname ( inputFile ) ) ; it ( `should apply update schematics to test case: ${ inputTestName }` , ( ) => { expect ( appTree . readContent ( join ( testCasesOutputPath , `${ inputTestName }.ts` ) ) ) . toBe ( readFileContent ( inputFile . replace ( TEST_CASE_INPUT_SUFFIX , TEST_CASE_OUTPUT_SUFFIX ) ) ) ; } ) ; } ) ; } /**
 * Patches the specified virtual file system tree to be able to read the TypeScript
 * default library typings. These need to be readable in unit tests because otherwise
 * type checking within migration rules is not working as in real applications.
 */ export function _patchTypeScriptDefaultLib ( tree ) { const _originalRead = tree . read ; tree . read = function ( filePath ) { if ( filePath . match ( /node_modules[/\\]typescript/ ) ) { return readFileSync ( getSystemPath ( filePath ) ) ; } else { return _originalRead . call ( this , filePath ) ; } } ; }