ohayolibs
Version:
Ohayo is a set of essential modules for ohayojp.
140 lines (117 loc) • 6.21 kB
text/typescript
// tslint:disable:no-string-literal
import { getSystemPath, normalize } from '@angular-devkit/core';
import { TempScopedNodeJsSyncHost } from '@angular-devkit/core/node/testing';
import * as virtualFs from '@angular-devkit/core/src/virtual-fs/host';
import { EngineHost, TaskExecutor, TaskScheduler } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { mkdirpSync, readFileSync, writeFileSync } from 'fs-extra';
import { dirname, join } from 'path';
import { from as observableFrom, Observable } from 'rxjs';
import { concatMap, filter, last } from 'rxjs/operators';
/** Path to the schematic collection that includes the ohayo migrations. */
export const migrationCollection = require.resolve('../../migration.json');
/** Module name suffix for data files of the `jasmine_node_test` Bazel rule. */
const bazelModuleSuffix = 'ohayo/packages/schematics/ng-update/test-cases';
/**
* Due to the fact that the Angular devkit does not support running scheduled tasks from a
* schematic that has been launched through the TestRunner, we need to manually find the task
* executor for the given task name and run all scheduled instances.
*
* Note that this means that there can be multiple tasks with the same name. The observable emits
* only when all tasks finished executing.
*/
export function runPostScheduledTasks(runner: SchematicTestRunner, taskName: string): Observable<any> {
// Workaround until there is a public API to run scheduled tasks in the @angular-devkit.
// See: https://github.com/angular/angular-cli/issues/11739
const host = runner.engine['_host'] as EngineHost<{}, {}>;
const tasks = runner.engine['_taskSchedulers'] as TaskScheduler[];
const createTaskExecutor = (name: string) => (host.createTaskExecutor(name) as any) as Observable<TaskExecutor<any>>;
return observableFrom(tasks).pipe(
concatMap(scheduler => scheduler.finalize()),
filter(task => task.configuration.name === taskName),
concatMap(task => {
return createTaskExecutor(task.configuration.name).pipe(
concatMap(executor => executor(task.configuration.options, task.context)),
);
}),
// Only emit the last emitted value because there can be multiple tasks with the same name.
// The observable should only emit a value if all tasks completed.
last(),
);
}
export async function createTestApp(runner: SchematicTestRunner, appOptions: {} = {}): Promise<UnitTestTree> {
const workspaceTree = await runner.runExternalSchematicAsync('@schematics/angular', 'workspace', {
name: 'workspace',
version: '6.0.0',
newProjectRoot: 'projects',
}).toPromise();
const res = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
{ ...appOptions, name: 'ohayojp' },
workspaceTree,
).toPromise();
return res;
}
/** Reads the UTF8 content of the specified file. Normalizes the path and ensures that */
export function readFileContent(filePath: string): string {
return readFileSync(filePath, 'utf8');
}
/**
* Resolves the original file path of the specified file that has been to the `data` of the
* jasmine_node_test Bazel rule.
*
* Adding the test case files to the data of the `jasmine_node_test` Bazel rule does not mean
* that the files are being copied over to the Bazel bin output. Bazel just patches the NodeJS
* resolve function and maps the module paths to the original file location.
*/
export function resolveBazelDataFile(filePath: string): string {
return require.resolve(`./${filePath}`);
// Only for bazel
return require.resolve(`${bazelModuleSuffix}/${filePath}`);
}
/**
* Creates a test app schematic tree that will be copied over to a real filesystem location.
* This is necessary because TSLint is not able to read from the virtual filesystem tree.
*/
export async function createFileSystemTestApp(runner: SchematicTestRunner): Promise<{ tree: UnitTestTree, tempPath: string }> {
const tempFileSystemHost = new TempScopedNodeJsSyncHost();
const tree = await createTestApp(runner);
const tempPath = getSystemPath(tempFileSystemHost.root);
// Since the TSLint fix task expects all files to be present on the real file system, we
// map every file in the app tree to a temporary location on the file system.
tree.files
.map(f => normalize(f))
.forEach(f => {
tempFileSystemHost.sync.write(f, virtualFs.stringToFileBuffer(tree.readContent(f)));
});
return { tree, tempPath };
}
export async function runTestCases(migrationName: string, inputs: { [name: string]: string }): Promise<{ tempPath: string; logOutput: string; appTree: UnitTestTree; }> {
const runner = new SchematicTestRunner('schematics', migrationCollection);
const inputNames = Object.keys(inputs);
const initialWorkingDir = process.cwd();
let logOutput = '';
runner.logger.subscribe(entry => (logOutput += entry.message));
const { tree, tempPath } = await createFileSystemTestApp(runner);
// Write each test-case input to the file-system. This is necessary because otherwise
// TSLint won't be able to pick up the test cases.
inputNames.forEach(inputName => {
const inputFullName = `projects/ohayojp/src/test-cases/${inputName}.ts`;
const tempInputPath = join(tempPath, inputFullName);
mkdirpSync(dirname(tempInputPath));
writeFileSync(tempInputPath, readFileContent(inputs[inputName]));
// TODO: add to tree
tree.create(inputFullName, readFileContent(inputs[inputName]));
});
await runner.runSchematicAsync(migrationName, {}, tree).toPromise();
// Switch to the new temporary directory because otherwise TSLint cannot read the files.
process.chdir(tempPath);
// Run the scheduled TSLint fix task from the update schematic. This task is responsible for
// identifying outdated code parts and performs the fixes. Since tasks won't run automatically
// within a `SchematicTestRunner`, we manually need to run the scheduled task.
await runPostScheduledTasks(runner, 'tslint-fix').toPromise();
// Switch back to the initial working directory.
process.chdir(initialWorkingDir);
return { tempPath, logOutput, appTree: tree };
}