Skip to content

Commit 23667ed

Browse files
jkremsalan-agius4
authored andcommittedDec 5, 2024·
fix(@angular-devkit/build-angular): handle windows spec collisions
(cherry picked from commit 9e2d3fb)
1 parent 4a5c5e4 commit 23667ed

File tree

4 files changed

+118
-25
lines changed

4 files changed

+118
-25
lines changed
 

‎packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts

+2-23
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap
2626
import { Configuration } from 'webpack';
2727
import { ExecutionTransformer } from '../../transforms';
2828
import { OutputHashing } from '../browser-esbuild/schema';
29-
import { findTests } from './find-tests';
29+
import { findTests, getTestEntrypoints } from './find-tests';
3030
import { Schema as KarmaBuilderOptions } from './schema';
3131

3232
interface BuildOptions extends ApplicationBuilderInternalOptions {
@@ -268,28 +268,7 @@ async function collectEntrypoints(
268268
projectSourceRoot,
269269
);
270270

271-
const seen = new Set<string>();
272-
273-
return new Map(
274-
Array.from(testFiles, (testFile) => {
275-
const relativePath = path
276-
.relative(
277-
testFile.startsWith(projectSourceRoot) ? projectSourceRoot : context.workspaceRoot,
278-
testFile,
279-
)
280-
.replace(/^[./]+/, '_')
281-
.replace(/\//g, '-');
282-
let uniqueName = `spec-${path.basename(relativePath, path.extname(relativePath))}`;
283-
let suffix = 2;
284-
while (seen.has(uniqueName)) {
285-
uniqueName = `${relativePath}-${suffix}`;
286-
++suffix;
287-
}
288-
seen.add(uniqueName);
289-
290-
return [uniqueName, testFile];
291-
}),
292-
);
271+
return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
293272
}
294273

295274
async function initializeApplication(

‎packages/angular_devkit/build_angular/src/builders/karma/find-tests.ts

+43
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ export async function findTests(
2626
return [...new Set(files.flat())];
2727
}
2828

29+
interface TestEntrypointsOptions {
30+
projectSourceRoot: string;
31+
workspaceRoot: string;
32+
}
33+
34+
/** Generate unique bundle names for a set of test files. */
35+
export function getTestEntrypoints(
36+
testFiles: string[],
37+
{ projectSourceRoot, workspaceRoot }: TestEntrypointsOptions,
38+
): Map<string, string> {
39+
const seen = new Set<string>();
40+
41+
return new Map(
42+
Array.from(testFiles, (testFile) => {
43+
const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot])
44+
// Strip leading dots and path separators.
45+
.replace(/^[./\\]+/, '')
46+
// Replace any path separators with dashes.
47+
.replace(/[/\\]/g, '-');
48+
const baseName = `spec-${basename(relativePath, extname(relativePath))}`;
49+
let uniqueName = baseName;
50+
let suffix = 2;
51+
while (seen.has(uniqueName)) {
52+
uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
53+
++suffix;
54+
}
55+
seen.add(uniqueName);
56+
57+
return [uniqueName, testFile];
58+
}),
59+
);
60+
}
61+
2962
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
3063

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

80+
function removeRoots(path: string, roots: string[]): string {
81+
for (const root of roots) {
82+
if (path.startsWith(root)) {
83+
return path.substring(root.length);
84+
}
85+
}
86+
87+
return basename(path);
88+
}
89+
4790
async function findMatchingTests(
4891
pattern: string,
4992
ignore: string[],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { getTestEntrypoints } from './find-tests';
10+
11+
const UNIX_ENTRYPOINTS_OPTIONS = {
12+
pathSeparator: '/',
13+
workspaceRoot: '/my/workspace/root',
14+
projectSourceRoot: '/my/workspace/root/src-root',
15+
};
16+
17+
const WINDOWS_ENTRYPOINTS_OPTIONS = {
18+
pathSeparator: '\\',
19+
workspaceRoot: 'C:\\my\\workspace\\root',
20+
projectSourceRoot: 'C:\\my\\workspace\\root\\src-root',
21+
};
22+
23+
describe('getTestEntrypoints', () => {
24+
for (const options of [UNIX_ENTRYPOINTS_OPTIONS, WINDOWS_ENTRYPOINTS_OPTIONS]) {
25+
describe(`with path separator "${options.pathSeparator}"`, () => {
26+
function joinWithSeparator(base: string, rel: string) {
27+
return `${base}${options.pathSeparator}${rel.replace(/\//g, options.pathSeparator)}`;
28+
}
29+
30+
function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) {
31+
return getTestEntrypoints(
32+
[
33+
...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)),
34+
...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)),
35+
],
36+
options,
37+
);
38+
}
39+
40+
it('returns an empty map without test files', () => {
41+
expect(getEntrypoints([])).toEqual(new Map());
42+
});
43+
44+
it('strips workspace root and/or project source root', () => {
45+
expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual(
46+
new Map<string, string>([
47+
['spec-a-b.spec', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')],
48+
['spec-c-d.spec', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')],
49+
]),
50+
);
51+
});
52+
53+
it('adds unique prefixes to distinguish between similar names', () => {
54+
expect(getEntrypoints(['a/b/c/d.spec.js', 'a-b/c/d.spec.js'], ['a/b-c/d.spec.js'])).toEqual(
55+
new Map<string, string>([
56+
['spec-a-b-c-d.spec', joinWithSeparator(options.workspaceRoot, 'a/b/c/d.spec.js')],
57+
['spec-a-b-c-d-2.spec', joinWithSeparator(options.workspaceRoot, 'a-b/c/d.spec.js')],
58+
[
59+
'spec-a-b-c-d-3.spec',
60+
joinWithSeparator(options.projectSourceRoot, 'a/b-c/d.spec.js'),
61+
],
62+
]),
63+
);
64+
});
65+
});
66+
}
67+
});

‎packages/angular_devkit/build_angular/src/builders/karma/tests/behavior/specs_spec.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp)
2424

2525
// src/app/app.component.spec.ts conflicts with this one:
2626
await harness.writeFiles({
27-
[`src/app/a/${collidingBasename}`]: `/** Success! */`,
27+
[`src/app/a/foo-bar/${collidingBasename}`]: `/** Success! */`,
28+
[`src/app/a-foo/bar/${collidingBasename}`]: `/** Success! */`,
29+
[`src/app/a-foo-bar/${collidingBasename}`]: `/** Success! */`,
2830
[`src/app/b/${collidingBasename}`]: `/** Success! */`,
2931
});
3032

@@ -36,7 +38,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp)
3638
const bundleLog = logs.find((log) =>
3739
log.message.includes('Application bundle generation complete.'),
3840
);
39-
expect(bundleLog?.message).toContain('spec-app-a-collision.spec.js');
41+
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision.spec.js');
42+
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-2.spec.js');
43+
expect(bundleLog?.message).toContain('spec-app-a-foo-bar-collision-3.spec.js');
4044
expect(bundleLog?.message).toContain('spec-app-b-collision.spec.js');
4145
}
4246
});

0 commit comments

Comments
 (0)
Please sign in to comment.