Skip to content

Commit 9398f18

Browse files
authored
fix mirrors reverse mapping to be more deterministic (#5559) (#5562)
1 parent b1458d1 commit 9398f18

File tree

2 files changed

+158
-5
lines changed

2 files changed

+158
-5
lines changed

Sources/PackageGraph/DependencyMirrors.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import OrderedCollections
1415
import TSCBasic
1516

1617
/// A collection of dependency mirrors.
1718
public final class DependencyMirrors: Equatable {
1819
private var index: [String: String]
19-
private var reverseIndex: [String: String]
20+
private var reverseIndex: [String: [String]]
21+
private var visited: OrderedCollections.OrderedSet<String>
2022
private let lock = Lock()
2123

2224
public var mapping: [String: String] {
@@ -27,7 +29,11 @@ public final class DependencyMirrors: Equatable {
2729

2830
public init(_ mirrors: [String: String]) {
2931
self.index = mirrors
30-
self.reverseIndex = Dictionary(mirrors.map({ ($0.1, $0.0) }), uniquingKeysWith: { first, _ in first })
32+
self.reverseIndex = [String: [String]]()
33+
for entry in mirrors {
34+
self.reverseIndex[entry.value, default: []].append(entry.key)
35+
}
36+
self.visited = .init()
3137
}
3238

3339
@available(*, deprecated)
@@ -46,7 +52,7 @@ public final class DependencyMirrors: Equatable {
4652
public func set(mirrorURL: String, forURL url: String) {
4753
self.lock.withLock {
4854
self.index[url] = mirrorURL
49-
self.reverseIndex[mirrorURL] = url
55+
self.reverseIndex[mirrorURL, default: []].append(url)
5056
}
5157
}
5258

@@ -105,7 +111,12 @@ public final class DependencyMirrors: Equatable {
105111
/// - Returns: The mirrored URL, if one exists.
106112
public func mirrorURL(for url: String) -> String? {
107113
self.lock.withLock {
108-
return self.index[url]
114+
let value = self.index[url]
115+
if value != nil {
116+
// record visited mirrors for reverse index lookup sorting
117+
self.visited.append(url)
118+
}
119+
return value
109120
}
110121
}
111122

@@ -123,7 +134,23 @@ public final class DependencyMirrors: Equatable {
123134
/// - Returns: The original URL, if one exists.
124135
public func originalURL(for url: String) -> String? {
125136
self.lock.withLock {
126-
return self.reverseIndex[url]
137+
let alternatives = self.reverseIndex[url]
138+
// since there are potentially multiple mapping, we need to sort them to produce deterministic results
139+
let sorted = alternatives?.sorted(by: { lhs, rhs in
140+
// check if it was visited (which means it used by the package)
141+
switch (self.visited.firstIndex(of: lhs), self.visited.firstIndex(of: rhs)) {
142+
case (.some(let lhsIndex), .some(let rhsIndex)):
143+
return lhsIndex < rhsIndex
144+
case (.some, .none):
145+
return true
146+
case (.none, .some):
147+
return false
148+
case (.none, .none):
149+
// otherwise sort alphabetically
150+
return lhs < rhs
151+
}
152+
})
153+
return sorted?.first
127154
}
128155
}
129156
}

Tests/WorkspaceTests/PinsStoreTests.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ final class PinsStoreTests: XCTestCase {
322322
try store.saveState(toolsVersion: ToolsVersion.current)
323323
XCTAssert(fileSystem.exists(pinsFile))
324324

325+
let content: String = try fileSystem.readFileContents(pinsFile)
326+
XCTAssertMatch(content, .contains(fooURL.absoluteString))
327+
XCTAssertNoMatch(content, .contains(fooMirroredURL.absoluteString))
328+
325329
// Load the store again from disk, with no mirrors
326330
let store2 = try PinsStore(pinsFile: pinsFile, workingDirectory: .root, fileSystem: fileSystem, mirrors: .init())
327331
XCTAssert(store2.pinsMap.count == 3)
@@ -334,4 +338,126 @@ final class PinsStoreTests: XCTestCase {
334338
XCTAssert(store3.pinsMap.count == 3)
335339
XCTAssertEqual(store3.pinsMap, store.pinsMap)
336340
}
341+
342+
func testPinsWithMirrorsDeterminism() throws {
343+
let fooIdentity = PackageIdentity.plain("foo")
344+
let fooURL1 = URL(string: "https://github.com/corporate/foo")!
345+
let fooURL2 = URL(string: "https://github.com/corporate/foo.git")!
346+
let fooURL3 = URL(string: "https://github.com/old-corporate/foo")!
347+
let fooURL4 = URL(string: "https://github.com/old-corporate/foo.git")!
348+
let fooMirroredURL = URL(string: "https://github.corporate.com/team/foo")!
349+
350+
let mirrors = DependencyMirrors()
351+
mirrors.set(mirrorURL: fooMirroredURL.absoluteString, forURL: fooURL1.absoluteString)
352+
mirrors.set(mirrorURL: fooMirroredURL.absoluteString, forURL: fooURL2.absoluteString)
353+
mirrors.set(mirrorURL: fooMirroredURL.absoluteString, forURL: fooURL3.absoluteString)
354+
mirrors.set(mirrorURL: fooMirroredURL.absoluteString, forURL: fooURL4.absoluteString)
355+
356+
let fileSystem = InMemoryFileSystem()
357+
let pinsFile = AbsolutePath("/pins.txt")
358+
359+
let store1 = try PinsStore(pinsFile: pinsFile, workingDirectory: .root, fileSystem: fileSystem, mirrors: mirrors)
360+
store1.pin(
361+
packageRef: .remoteSourceControl(identity: fooIdentity, url: fooMirroredURL),
362+
state: .version(v1, revision: "revision")
363+
)
364+
365+
XCTAssert(store1.pinsMap.count == 1)
366+
XCTAssertEqual(store1.pinsMap[fooIdentity]!.packageRef.kind, .remoteSourceControl(fooMirroredURL))
367+
368+
try store1.saveState(toolsVersion: ToolsVersion.current)
369+
XCTAssert(fileSystem.exists(pinsFile))
370+
371+
let content: String = try fileSystem.readFileContents(pinsFile)
372+
XCTAssertMatch(content, .contains(fooURL1.absoluteString))
373+
XCTAssertNoMatch(content, .contains(fooURL2.absoluteString))
374+
XCTAssertNoMatch(content, .contains(fooURL3.absoluteString))
375+
XCTAssertNoMatch(content, .contains(fooURL4.absoluteString))
376+
XCTAssertNoMatch(content, .contains(fooMirroredURL.absoluteString))
377+
378+
// Load the store again from disk, with no mirrors
379+
let store2 = try PinsStore(pinsFile: pinsFile, workingDirectory: .root, fileSystem: fileSystem, mirrors: .init())
380+
XCTAssert(store2.pinsMap.count == 1)
381+
XCTAssertEqual(store2.pinsMap[fooIdentity]!.packageRef.kind, .remoteSourceControl(fooURL1))
382+
383+
// Load the store again from disk, with mirrors
384+
let store3 = try PinsStore(pinsFile: pinsFile, workingDirectory: .root, fileSystem: fileSystem, mirrors: mirrors)
385+
XCTAssert(store3.pinsMap.count == 1)
386+
XCTAssertEqual(store3.pinsMap, store1.pinsMap)
387+
}
388+
389+
func testMirrorsDeterminism() throws {
390+
let URL1 = URL(string: "https://github.com/corporate/foo")!
391+
let URL2 = URL(string: "https://github.com/corporate/foo.git")!
392+
let URL3 = URL(string: "https://github.com/old-corporate/foo")!
393+
let URL4 = URL(string: "https://github.com/old-corporate/foo.git")!
394+
let mirroredURL = URL(string: "https://github.corporate.com/team/foo")!
395+
396+
do {
397+
let mirrors = DependencyMirrors([
398+
URL1.absoluteString: mirroredURL.absoluteString,
399+
URL2.absoluteString: mirroredURL.absoluteString,
400+
URL3.absoluteString: mirroredURL.absoluteString,
401+
URL4.absoluteString: mirroredURL.absoluteString
402+
])
403+
404+
XCTAssertEqual(mirrors.mirrorURL(for: URL2.absoluteString), mirroredURL.absoluteString)
405+
// reverse index is sorted by "visited", then alphabetically
406+
XCTAssertEqual(mirrors.originalURL(for: mirroredURL.absoluteString), URL2.absoluteString)
407+
}
408+
409+
do {
410+
let mirrors = DependencyMirrors([
411+
URL1.absoluteString: mirroredURL.absoluteString,
412+
URL2.absoluteString: mirroredURL.absoluteString,
413+
URL3.absoluteString: mirroredURL.absoluteString,
414+
URL4.absoluteString: mirroredURL.absoluteString
415+
])
416+
417+
XCTAssertEqual(mirrors.mirrorURL(for: URL3.absoluteString), mirroredURL.absoluteString)
418+
// reverse index is sorted by "visited", then alphabetically
419+
XCTAssertEqual(mirrors.originalURL(for: mirroredURL.absoluteString), URL3.absoluteString)
420+
}
421+
422+
do {
423+
let mirrors = DependencyMirrors([
424+
URL1.absoluteString: mirroredURL.absoluteString,
425+
URL2.absoluteString: mirroredURL.absoluteString,
426+
URL3.absoluteString: mirroredURL.absoluteString,
427+
URL4.absoluteString: mirroredURL.absoluteString
428+
])
429+
430+
XCTAssertEqual(mirrors.mirrorURL(for: URL2.absoluteString), mirroredURL.absoluteString)
431+
XCTAssertEqual(mirrors.mirrorURL(for: URL3.absoluteString), mirroredURL.absoluteString)
432+
// reverse index is sorted by "visited", then alphabetically
433+
XCTAssertEqual(mirrors.originalURL(for: mirroredURL.absoluteString), URL2.absoluteString)
434+
}
435+
436+
do {
437+
let mirrors = DependencyMirrors([
438+
URL1.absoluteString: mirroredURL.absoluteString,
439+
URL2.absoluteString: mirroredURL.absoluteString,
440+
URL3.absoluteString: mirroredURL.absoluteString,
441+
URL4.absoluteString: mirroredURL.absoluteString
442+
])
443+
444+
XCTAssertEqual(mirrors.mirrorURL(for: URL3.absoluteString), mirroredURL.absoluteString)
445+
XCTAssertEqual(mirrors.mirrorURL(for: URL2.absoluteString), mirroredURL.absoluteString)
446+
// reverse index is sorted by "visited", then alphabetically
447+
XCTAssertEqual(mirrors.originalURL(for: mirroredURL.absoluteString), URL3.absoluteString)
448+
}
449+
450+
do {
451+
let mirrors = DependencyMirrors([
452+
URL1.absoluteString: mirroredURL.absoluteString,
453+
URL2.absoluteString: mirroredURL.absoluteString,
454+
URL3.absoluteString: mirroredURL.absoluteString,
455+
URL4.absoluteString: mirroredURL.absoluteString
456+
])
457+
458+
// reverse index is sorted by "visited", then alphabetically
459+
XCTAssertEqual(mirrors.originalURL(for: mirroredURL.absoluteString), URL1.absoluteString)
460+
}
461+
}
462+
337463
}

0 commit comments

Comments
 (0)