Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): handle windows spec collisions
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrems authored and alan-agius4 committed Dec 5, 2024
1 parent 1ca260e commit 9e2d3fb
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap
import { Configuration } from 'webpack';
import { ExecutionTransformer } from '../../transforms';
import { OutputHashing } from '../browser-esbuild/schema';
import { findTests } from './find-tests';
import { findTests, getTestEntrypoints } from './find-tests';
import { Schema as KarmaBuilderOptions } from './schema';

interface BuildOptions extends ApplicationBuilderInternalOptions {
Expand Down Expand Up @@ -268,28 +268,7 @@ async function collectEntrypoints(
projectSourceRoot,
);

const seen = new Set<string>();

return new Map(
Array.from(testFiles, (testFile) => {
const relativePath = path
.relative(
testFile.startsWith(projectSourceRoot) ? projectSourceRoot : context.workspaceRoot,
testFile,
)
.replace(/^[./]+/, '_')
.replace(/\//g, '-');
let uniqueName = `spec-${path.basename(relativePath, path.extname(relativePath))}`;
let suffix = 2;
while (seen.has(uniqueName)) {
uniqueName = `${relativePath}-${suffix}`;
++suffix;
}
seen.add(uniqueName);

return [uniqueName, testFile];
}),
);
return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
}

async function initializeApplication(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ export async function findTests(
return [...new Set(files.flat())];
}

interface TestEntrypointsOptions {
projectSourceRoot: string;
workspaceRoot: string;
}

/** Generate unique bundle names for a set of test files. */
export function getTestEntrypoints(
testFiles: string[],
{ projectSourceRoot, workspaceRoot }: TestEntrypointsOptions,
): Map<string, string> {
const seen = new Set<string>();

return new Map(
Array.from(testFiles, (testFile) => {
const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot])
// Strip leading dots and path separators.
.replace(/^[./\\]+/, '')
// Replace any path separators with dashes.
.replace(/[/\\]/g, '-');
const baseName = `spec-${basename(relativePath, extname(relativePath))}`;
let uniqueName = baseName;
let suffix = 2;
while (seen.has(uniqueName)) {
uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
++suffix;
}
seen.add(uniqueName);

return [uniqueName, testFile];
}),
);
}

const normalizePath = (path: string): string => path.replace(/\\/g, '/');

const removeLeadingSlash = (pattern: string): string => {
Expand All @@ -44,6 +77,16 @@ const removeRelativeRoot = (path: string, root: string): string => {
return path;
};

function removeRoots(path: string, roots: string[]): string {
for (const root of roots) {
if (path.startsWith(root)) {
return path.substring(root.length);
}
}

return basename(path);
}

async function findMatchingTests(
pattern: string,
ignore: string[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @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.dev/license
*/

import { getTestEntrypoints } from './find-tests';

const UNIX_ENTRYPOINTS_OPTIONS = {
pathSeparator: '/',
workspaceRoot: '/my/workspace/root',
projectSourceRoot: '/my/workspace/root/src-root',
};

const WINDOWS_ENTRYPOINTS_OPTIONS = {
pathSeparator: '\\',
workspaceRoot: 'C:\\my\\workspace\\root',
projectSourceRoot: 'C:\\my\\workspace\\root\\src-root',
};

describe('getTestEntrypoints', () => {
for (const options of [UNIX_ENTRYPOINTS_OPTIONS, WINDOWS_ENTRYPOINTS_OPTIONS]) {
describe(`with path separator "${options.pathSeparator}"`, () => {
function joinWithSeparator(base: string, rel: string) {
return `${base}${options.pathSeparator}${rel.replace(/\//g, options.pathSeparator)}`;
}

function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) {
return getTestEntrypoints(
[
...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)),
...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)),
],
options,
);
}

it('returns an empty map without test files', () => {
expect(getEntrypoints([])).toEqual(new Map());
});

it('strips workspace root and/or project source root', () => {
expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual(
new Map<string, string>([
['spec-a-b.spec', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')],
['spec-c-d.spec', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')],
]),
);
});

it('adds unique prefixes to distinguish between similar names', () => {
expect(getEntrypoints(['a/b/c/d.spec.js', 'a-b/c/d.spec.js'], ['a/b-c/d.spec.js'])).toEqual(
new Map<string, string>([
['spec-a-b-c-d.spec', joinWithSeparator(options.workspaceRoot, 'a/b/c/d.spec.js')],
['spec-a-b-c-d-2.spec', joinWithSeparator(options.workspaceRoot, 'a-b/c/d.spec.js')],
[
'spec-a-b-c-d-3.spec',
joinWithSeparator(options.projectSourceRoot, 'a/b-c/d.spec.js'),
],
]),
);
});
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp)

// src/app/app.component.spec.ts conflicts with this one:
await harness.writeFiles({
[`src/app/a/${collidingBasename}`]: `/** Success! */`,
[`src/app/a/foo-bar/${collidingBasename}`]: `/** Success! */`,
[`src/app/a-foo/bar/${collidingBasename}`]: `/** Success! */`,
[`src/app/a-foo-bar/${collidingBasename}`]: `/** Success! */`,
[`src/app/b/${collidingBasename}`]: `/** Success! */`,
});

Expand All @@ -36,7 +38,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp)
const bundleLog = logs.find((log) =>
log.message.includes('Application bundle generation complete.'),
);
expect(bundleLog?.message).toContain('spec-app-a-collision.spec.js');
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision.spec.js');
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-2.spec.js');
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-3.spec.js');
expect(bundleLog?.message).toContain('spec-app-b-collision.spec.js');
}
});
Expand Down

0 comments on commit 9e2d3fb

Please sign in to comment.