Skip to content

Commit c0b0d1d

Browse files
committed
test: Add tests for virtual deduplication
1 parent e5ab340 commit c0b0d1d

File tree

2 files changed

+188
-66
lines changed

2 files changed

+188
-66
lines changed

packages/acceptance-tests/pkg-tests-specs/sources/dragon.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,4 +726,111 @@ describe(`Dragon tests`, () => {
726726
},
727727
),
728728
);
729+
730+
test(`it should pass the dragon test 14`,
731+
makeTemporaryEnv(
732+
{
733+
private: true,
734+
workspaces: [
735+
`pkg-a`,
736+
`pkg-b`,
737+
],
738+
},
739+
async ({path, run, source}) => {
740+
// This dragon test represents the following scenario:
741+
//
742+
// .
743+
// ├── pkg-a/
744+
// │ └── (alias-1) [email protected]
745+
// │ └── (peer) does-not-matter
746+
// └── pkg-b/
747+
// └── (alias-2) [email protected]
748+
// └── (peer) does-not-matter
749+
//
750+
// The same package is installed under two different aliases. This test
751+
// checks that the two instances are properly deduplicated despite
752+
// having different idents.
753+
754+
await xfs.mkdirpPromise(`${path}/pkg-a`);
755+
await xfs.writeJsonPromise(`${path}/pkg-a/package.json`, {
756+
name: `a`,
757+
dependencies: {
758+
[`alias-1`]: `npm:[email protected]`,
759+
[`no-deps`]: `1.0.0`,
760+
},
761+
});
762+
763+
await xfs.mkdirpPromise(`${path}/pkg-b`);
764+
await xfs.writeJsonPromise(`${path}/pkg-b/package.json`, {
765+
name: `b`,
766+
dependencies: {
767+
[`alias-2`]: `npm:[email protected]`,
768+
[`no-deps`]: `1.0.0`,
769+
},
770+
});
771+
772+
await expect(run(`install`)).resolves.toBeTruthy();
773+
774+
// The virtual descriptors should be different but the virtual package should be the same
775+
const aPath = npath.fromPortablePath(ppath.join(path, `pkg-a/package.json`));
776+
const bPath = npath.fromPortablePath(ppath.join(path, `pkg-b/package.json`));
777+
await expect(source(`(
778+
createRequire = require('module').createRequire,
779+
createRequire(${JSON.stringify(aPath)}).resolve('alias-1/package.json') === createRequire(${JSON.stringify(bPath)}).resolve('alias-2/package.json')
780+
)`)).resolves.toBe(true);
781+
},
782+
),
783+
);
784+
785+
test(`it should pass the dragon test 15`,
786+
makeTemporaryEnv(
787+
{
788+
private: true,
789+
workspaces: [
790+
`pkg-a`,
791+
`pkg-b`,
792+
],
793+
},
794+
async ({path, run, source}) => {
795+
// This dragon test represents the following scenario:
796+
//
797+
// .
798+
// ├── pkg-a/
799+
// │ └── (alias-1) [email protected]
800+
// │ └── (peer) does-not-matter
801+
// └── pkg-b/
802+
// └── (alias-1) [email protected]
803+
// │ └── (peer) does-not-matter
804+
// └── (alias-2) [email protected]
805+
// └── (peer) does-not-matter
806+
//
807+
// The same package is installed under two different aliases. When
808+
// traversing pkg-b, we deduplicate the virtual package installed under
809+
// alias-1 to the one under pkg-a, but the virtual package is the same
810+
// one installed under alias-2. This test checks that we don't leave
811+
// dangling references to the virtual package that was removed.
812+
813+
await xfs.mkdirpPromise(`${path}/pkg-a`);
814+
await xfs.writeJsonPromise(`${path}/pkg-a/package.json`, {
815+
name: `a`,
816+
dependencies: {
817+
[`alias-1`]: `npm:[email protected]`,
818+
[`no-deps`]: `1.0.0`,
819+
},
820+
});
821+
822+
await xfs.mkdirpPromise(`${path}/pkg-b`);
823+
await xfs.writeJsonPromise(`${path}/pkg-b/package.json`, {
824+
name: `b`,
825+
dependencies: {
826+
[`alias-1`]: `npm:[email protected]`,
827+
[`alias-2`]: `npm:[email protected]`,
828+
[`no-deps`]: `1.0.0`,
829+
},
830+
});
831+
832+
await expect(run(`install`)).resolves.toBeTruthy();
833+
},
834+
),
835+
);
729836
});

packages/yarnpkg-core/sources/Project.ts

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,11 +2191,10 @@ function applyVirtualResolutionMutations({
21912191

21922192
const allIdents = new Map<IdentHash, Ident>();
21932193

2194-
/** Maps dependency hashes to the first virtual locator encountered with that hash, for deduplication */
2195-
const allVirtualInstances = new Map<string, Locator>();
2196-
const allVirtualDependents = new Map<LocatorHash, Set<LocatorHash>>();
2197-
/** Maps virtual locators to all (virtual) descriptors that resolve to them, for deduplication */
2198-
const allVirtualResolutions = new Map<LocatorHash, Set<DescriptorHash>>();
2194+
// We'll be keeping track of all virtual descriptors; once they have all
2195+
// been generated we'll check whether they can be deduplicated into one.
2196+
const allVirtualInstances = new Map<LocatorHash, Map<string, Descriptor>>();
2197+
const allVirtualDependents = new Map<DescriptorHash, Set<LocatorHash>>();
21992198

22002199
const allPeerRequests = new Map<LocatorHash, Map<IdentHash, PeerRequestNode>>();
22012200

@@ -2264,7 +2263,7 @@ function applyVirtualResolutionMutations({
22642263
if (!parentPackage)
22652264
throw new Error(`Assertion failed: The package (${structUtils.prettyLocator(project.configuration, parentLocator)}) should have been registered`);
22662265

2267-
const dedupeCandidates = new Set<LocatorHash>();
2266+
const newVirtualInstances: Array<[Locator, Descriptor, Package]> = [];
22682267
const parentPeerRequirements = new Map<IdentHash, PeerRequirementNode>();
22692268

22702269
const firstPass = [];
@@ -2323,15 +2322,16 @@ function applyVirtualResolutionMutations({
23232322
virtualizedDescriptor = structUtils.virtualizeDescriptor(descriptor, parentLocator.locatorHash);
23242323
virtualizedPackage = structUtils.virtualizePackage(pkg, parentLocator.locatorHash);
23252324

2326-
parentPackage.dependencies.set(descriptor.identHash, virtualizedDescriptor);
2325+
parentPackage.dependencies.delete(descriptor.identHash);
2326+
parentPackage.dependencies.set(virtualizedDescriptor.identHash, virtualizedDescriptor);
23272327

23282328
allResolutions.set(virtualizedDescriptor.descriptorHash, virtualizedPackage.locatorHash);
23292329
allDescriptors.set(virtualizedDescriptor.descriptorHash, virtualizedDescriptor);
23302330

23312331
allPackages.set(virtualizedPackage.locatorHash, virtualizedPackage);
23322332

2333-
miscUtils.getSetWithDefault(allVirtualResolutions, virtualizedPackage.locatorHash).add(virtualizedDescriptor.descriptorHash);
2334-
dedupeCandidates.add(virtualizedPackage.locatorHash);
2333+
// Keep track of all new virtual packages since we'll want to dedupe them
2334+
newVirtualInstances.push([pkg, virtualizedDescriptor, virtualizedPackage]);
23352335
});
23362336

23372337
// In the second pass we resolve the peer requests to their provision.
@@ -2397,12 +2397,10 @@ function applyVirtualResolutionMutations({
23972397

23982398
virtualizedPackage.dependencies.set(peerDescriptor.identHash, peerProvision);
23992399

2400-
// Need to keep track when a virtual depends on a sibling virtual so
2401-
// that if and when the latter is deduplicated, we know the former
2402-
// needs to be deduplicated again
2400+
// Need to track when a virtual descriptor is set as a dependency in case
2401+
// the descriptor will be deduplicated.
24032402
if (structUtils.isVirtualDescriptor(peerProvision)) {
2404-
const dependentLocatorHash = allResolutions.get(peerProvision.descriptorHash);
2405-
const dependents = miscUtils.getSetWithDefault(allVirtualDependents, dependentLocatorHash);
2403+
const dependents = miscUtils.getSetWithDefault(allVirtualDependents, peerProvision.descriptorHash);
24062404
dependents.add(virtualizedPackage.locatorHash);
24072405
}
24082406

@@ -2449,7 +2447,11 @@ function applyVirtualResolutionMutations({
24492447
// In the fourth pass, we register information about the peer requirement
24502448
// and peer request trees, using the post-deduplication information.
24512449
fourthPass.push(() => {
2452-
const finalResolution = allResolutions.get(virtualizedDescriptor.descriptorHash)!;
2450+
const finalDescriptor = parentPackage.dependencies.get(descriptor.identHash);
2451+
if (typeof finalDescriptor === `undefined`)
2452+
throw new Error(`Assertion failed: Expected the peer dependency to have been turned into a dependency`);
2453+
2454+
const finalResolution = allResolutions.get(finalDescriptor.descriptorHash)!;
24532455
if (typeof finalResolution === `undefined`)
24542456
throw new Error(`Assertion failed: Expected the descriptor to be registered`);
24552457

@@ -2462,10 +2464,10 @@ function applyVirtualResolutionMutations({
24622464
if (!peerRequest)
24632465
continue;
24642466

2465-
peerRequirement.requests.set(virtualizedDescriptor.descriptorHash, peerRequest);
2467+
peerRequirement.requests.set(finalDescriptor.descriptorHash, peerRequest);
24662468
peerRequirementNodes.set(peerRequirement.hash, peerRequirement);
24672469
if (!peerRequirement.root) {
2468-
parentPeerRequests.get(peerRequirement.ident.identHash)?.children.set(virtualizedDescriptor.descriptorHash, peerRequest);
2470+
parentPeerRequests.get(peerRequirement.ident.identHash)?.children.set(finalDescriptor.descriptorHash, peerRequest);
24692471
}
24702472
}
24712473

@@ -2481,63 +2483,76 @@ function applyVirtualResolutionMutations({
24812483
for (const fn of [...firstPass, ...secondPass])
24822484
fn();
24832485

2484-
for (const locatorHash of dedupeCandidates) {
2485-
// Remove locatorHash here so that if a dependency is deduped, it will be
2486-
// deduped again when added to the dedupe candidates
2487-
dedupeCandidates.delete(locatorHash);
2486+
let stable: boolean;
2487+
do {
2488+
stable = true;
24882489

2489-
const virtualPackage = allPackages.get(locatorHash)!;
2490+
for (const [physicalLocator, virtualDescriptor, virtualPackage] of newVirtualInstances) {
2491+
const otherVirtualInstances = miscUtils.getMapWithDefault(allVirtualInstances, physicalLocator.locatorHash);
24902492

2491-
// We take all the dependencies from the new virtual instance and
2492-
// generate a hash from it. By checking if this hash is already
2493-
// registered, we know whether we can trim the new version.
2494-
const dependencyHash = hashUtils.makeHash(
2495-
structUtils.devirtualizeLocator(virtualPackage).locatorHash,
2496-
...Array.from(virtualPackage.dependencies.values(), descriptor => {
2497-
const resolution = descriptor.range !== `missing:`
2498-
? allResolutions.get(descriptor.descriptorHash)
2499-
: `missing:`;
2493+
// We take all the dependencies from the new virtual instance and
2494+
// generate a hash from it. By checking if this hash is already
2495+
// registered, we know whether we can trim the new version.
2496+
const dependencyHash = hashUtils.makeHash(
2497+
...[...virtualPackage.dependencies.values()].map(descriptor => {
2498+
const resolution = descriptor.range !== `missing:`
2499+
? allResolutions.get(descriptor.descriptorHash)
2500+
: `missing:`;
25002501

2501-
if (typeof resolution === `undefined`)
2502-
throw new Error(`Assertion failed: Expected the resolution for ${structUtils.prettyDescriptor(project.configuration, descriptor)} to have been registered`);
2502+
if (typeof resolution === `undefined`)
2503+
throw new Error(`Assertion failed: Expected the resolution for ${structUtils.prettyDescriptor(project.configuration, descriptor)} to have been registered`);
25032504

2504-
return resolution === top ? `${resolution} (top)` : resolution;
2505-
}),
2506-
);
2505+
return resolution === top ? `${resolution} (top)` : resolution;
2506+
}),
2507+
// We use the identHash to disambiguate between virtual descriptors
2508+
// with different base idents being resolved to the same virtual package.
2509+
// Note: We don't use the descriptorHash because the whole point of duplicate
2510+
// virtual descriptors is that they have different `virtual:` ranges.
2511+
// This causes the virtual descriptors with different base idents
2512+
// to be preserved, while the virtual package they resolve to gets deduped.
2513+
virtualDescriptor.identHash,
2514+
);
25072515

2508-
const masterLocator = allVirtualInstances.get(dependencyHash);
2509-
if (typeof masterLocator === `undefined`) {
2510-
allVirtualInstances.set(dependencyHash, virtualPackage);
2511-
continue;
2512-
}
2516+
const masterDescriptor = otherVirtualInstances.get(dependencyHash);
2517+
if (typeof masterDescriptor === `undefined`) {
2518+
otherVirtualInstances.set(dependencyHash, virtualDescriptor);
2519+
continue;
2520+
}
25132521

2514-
// Change every descriptor that is resolving to the virtual package to
2515-
// resolve to the master locator instead, then discard the virtual
2516-
// package
2517-
const masterResolutions = miscUtils.getSetWithDefault(allVirtualResolutions, masterLocator.locatorHash);
2518-
for (const descriptorHash of allVirtualResolutions.get(virtualPackage.locatorHash) ?? []) {
2519-
allResolutions.set(descriptorHash, masterLocator.locatorHash);
2520-
masterResolutions.add(descriptorHash);
2521-
}
2522-
allPackages.delete(virtualPackage.locatorHash);
2523-
accessibleLocators.delete(virtualPackage.locatorHash);
2524-
dedupeCandidates.delete(virtualPackage.locatorHash);
2525-
2526-
const dependents = allVirtualDependents.get(virtualPackage.locatorHash);
2527-
if (dependents !== undefined) {
2528-
const masterDependents = miscUtils.getSetWithDefault(allVirtualDependents, masterLocator.locatorHash);
2529-
for (const dependent of dependents) {
2530-
// A dependent of the virtual package is now a dependent of the
2531-
// master package
2532-
masterDependents.add(dependent);
2533-
2534-
// Virtual packages that depended on the deduplicated package would
2535-
// get a different dependency hash now, so we need to deduplicate
2536-
// them again
2537-
dedupeCandidates.add(dependent);
2522+
// Since we're applying multiple pass, we might have already registered
2523+
// ourselves as the "master" descriptor in the previous pass.
2524+
if (masterDescriptor === virtualDescriptor)
2525+
continue;
2526+
2527+
allPackages.delete(virtualPackage.locatorHash);
2528+
allDescriptors.delete(virtualDescriptor.descriptorHash);
2529+
allResolutions.delete(virtualDescriptor.descriptorHash);
2530+
2531+
accessibleLocators.delete(virtualPackage.locatorHash);
2532+
2533+
const dependents = allVirtualDependents.get(virtualDescriptor.descriptorHash) || [];
2534+
const allDependents = [parentPackage.locatorHash, ...dependents];
2535+
2536+
allVirtualDependents.delete(virtualDescriptor.descriptorHash);
2537+
2538+
for (const dependent of allDependents) {
2539+
const pkg = allPackages.get(dependent);
2540+
if (typeof pkg === `undefined`)
2541+
continue;
2542+
2543+
if (pkg.dependencies.get(virtualDescriptor.identHash)!.descriptorHash !== masterDescriptor.descriptorHash)
2544+
stable = false;
2545+
2546+
pkg.dependencies.set(virtualDescriptor.identHash, masterDescriptor);
2547+
}
2548+
2549+
for (const peerRequirement of parentPeerRequirements.values()) {
2550+
if (peerRequirement.provided.descriptorHash === virtualDescriptor.descriptorHash) {
2551+
peerRequirement.provided = masterDescriptor;
2552+
}
25382553
}
25392554
}
2540-
}
2555+
} while (!stable);
25412556

25422557
for (const fn of [...thirdPass, ...fourthPass]) {
25432558
fn();

0 commit comments

Comments
 (0)