Skip to content

fix(@angular-devkit/build-angular): handle windows spec collisions #29034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading