build: fix ts-api-guardian golden approval not working on windows (#36115)

Currently on Windows, it's not possible to approve goldens in
`ts-api-guardian`. This is because paths are resolved relatively
to the working directory. In Windows, golden files are resolved
to the actual workspace directory. The current logic tries to
compute a relative path to the runfile from the working directory.

This causes the file paths to have a lot of parent directory
path segments. Eventually, when joined with the build workspace
directory, the paths end up being incorrect. e.g.

```
fileName = ../../../../../../projects/angular/golden/<..>/common.d.ts`
outFile = BUILD_WORKSPACE_DIR + fileName;
```

To fix this, we no longer deal with confusing relative paths, but
instead always use absolute file system paths.

Additionally, this fixes that new goldens are generated at the wrong
location on all platforms.

PR Close #36115
This commit is contained in:
Paul Gschwendtner 2020-03-17 11:30:36 +01:00 committed by Andrew Kushnir
parent a1cae28283
commit a1e00f82f4
2 changed files with 56 additions and 31 deletions

View File

@ -16,8 +16,23 @@ import * as path from 'path';
import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile, discoverAllEntrypoints} from './main';
/** Name of the CLI */
const CMD = 'ts-api-guardian';
/** Name of the Bazel workspace that runs the CLI. */
const bazelWorkspaceName = process.env.BAZEL_WORKSPACE;
/**
* Path to the Bazel workspace directory. Only set if the CLI is run with `bazel run`.
* https://docs.bazel.build/versions/master/user-manual.html#run.
*/
const bazelWorkspaceDirectory = process.env.BUILD_WORKSPACE_DIRECTORY;
/**
* Regular expression that matches Bazel manifest paths that start with the
* current Bazel workspace, followed by a path delimiter.
*/
const bazelWorkspaceManifestPathRegex =
bazelWorkspaceName ? new RegExp(`^${bazelWorkspaceName}[/\\\\]`) : null;
export function startCli() {
const {argv, mode, errors} = parseArguments(process.argv.slice(2));
@ -210,35 +225,48 @@ Options:
}
/**
* Resolves a given path to the associated relative path if the current process runs within
* Bazel. We need to use the wrapped NodeJS resolve logic in order to properly handle the given
* runfiles files which are only part of the runfile manifest on Windows.
* Resolves a given path in the file system. If `ts-api-guardian` runs with Bazel, file paths
* are resolved through runfiles. Additionally in Bazel, this method handles the case where
* manifest file paths are not existing, but need to resolve to the Bazel workspace directory.
* This happens commonly when goldens are approved, but the golden file does not exist yet.
*/
function resolveBazelFilePath(fileName: string): string {
// If the CLI has been launched through the NodeJS Bazel rules, we need to resolve the
// actual file paths because otherwise this script won't work on Windows where runfiles
// are not available in the working directory. In order to resolve the real path for the
// runfile, we need to use `require.resolve` which handles runfiles properly on Windows.
if (process.env['BAZEL_TARGET']) {
// This try/catch block is necessary because if the path is to the source file directly
// rather than via symlinks in the bazel output directories, require is not able to
// resolve it.
try {
return path.relative(process.cwd(), require.resolve(fileName));
} catch (err) {
return path.relative(process.cwd(), fileName);
}
function resolveFilePath(fileName: string): string {
// If an absolute path is specified, the path is already resolved.
if (path.isAbsolute(fileName)) {
return fileName;
}
return fileName;
// Outside of Bazel, file paths are resolved based on the current working directory.
if (!bazelWorkspaceName) {
return path.resolve(fileName);
}
// In Bazel, we first try to resolve the file through the runfiles. We do this by calling
// the `require.resolve` function that is patched by the Bazel NodeJS rules. Note that we
// need to catch errors because files inside tree artifacts cannot be resolved through
// runfile manifests. Hence, we need to have alternative resolution logic when resolving
// file paths. Additionally, it could happen that manifest paths which aren't part of the
// runfiles are specified (i.e. golden is approved but does not exist in the workspace yet).
try {
return require.resolve(fileName);
} catch {
}
// This handles cases where file paths cannot be resolved through runfiles. This happens
// commonly when goldens are approved while the golden does not exist in the workspace yet.
// In those cases, we want to build up a relative path based on the manifest path, and join
// it with the absolute bazel workspace directory (which is only set in `bazel run`).
// e.g. `angular/goldens/<..>/common` should become `{workspace_dir}/goldens/<...>/common`.
if (bazelWorkspaceManifestPathRegex !== null && bazelWorkspaceDirectory &&
bazelWorkspaceManifestPathRegex.test(fileName)) {
return path.join(bazelWorkspaceDirectory, fileName.substr(bazelWorkspaceName.length + 1));
}
throw Error(`Could not resolve file path in runfiles: ${fileName}`);
}
function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoints: string[]):
{entrypoint: string, goldenFile: string}[] {
if (argv[mode]) {
return [{
entrypoint: resolveBazelFilePath(entrypoints[0]),
goldenFile: resolveBazelFilePath(argv[mode]),
entrypoint: resolveFilePath(entrypoints[0]),
goldenFile: resolveFilePath(argv[mode]),
}];
} else { // argv[mode + 'Dir']
let rootDir = argv['rootDir'] || '.';
@ -246,8 +274,8 @@ function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoin
return entrypoints.map((fileName: string) => {
return {
entrypoint: resolveBazelFilePath(fileName),
goldenFile: resolveBazelFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
entrypoint: resolveFilePath(fileName),
goldenFile: resolveFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
};
});
}

View File

@ -17,13 +17,6 @@ export function generateGoldenFile(
entrypoint: string, outFile: string, options: SerializationOptions = {}): void {
const output = publicApi(entrypoint, options);
// BUILD_WORKSPACE_DIRECTORY environment variable is only available during bazel
// run executions. This workspace directory allows us to generate golden files directly
// in the source file tree rather than via a symlink.
if (process.env['BUILD_WORKSPACE_DIRECTORY']) {
outFile = path.join(process.env['BUILD_WORKSPACE_DIRECTORY'], outFile);
}
ensureDirectory(path.dirname(outFile));
fs.writeFileSync(outFile, output);
}
@ -36,7 +29,11 @@ export function verifyAgainstGoldenFile(
if (actual === expected) {
return '';
} else {
const patch = createPatch(goldenFile, expected, actual, 'Golden file', 'Generated API');
// The patch should not show absolute paths, as these are pretty long and obfuscated
// the printed golden diff. Additionally, path separators in the patch should be forward
// slashes for consistency and to enable easier integration testing.
const displayFileName = path.relative(process.cwd(), goldenFile).replace(/\\/g, '/');
const patch = createPatch(displayFileName, expected, actual, 'Golden file', 'Generated API');
// Remove the header of the patch
const start = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1;