Skip to content

Commit f254dcd

Browse files
authored
Merge pull request #8 from XcodesOrg/fix/runtime-beta-number
fix: beta numbers for UUID runtime identifiers
2 parents b4dd917 + 4071a4a commit f254dcd

4 files changed

Lines changed: 157 additions & 9 deletions

File tree

‎Sources/XcodesKit/Models/Runtimes/Runtimes.swift‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable, Sendable {
4747
public var installState: RuntimeInstallState = .notInstalled
4848
/// SDK build updates that map to this simulator runtime.
4949
public var sdkBuildUpdate: [String]?
50+
/// The beta seed number supplied by Apple's runtime index.
51+
public var seedNumber: Int?
5052

5153
enum CodingKeys: CodingKey {
5254
case category
@@ -62,15 +64,54 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable, Sendable {
6264
case name
6365
case authentication
6466
case sdkBuildUpdate
67+
case seedNumber
6568
case architectures
6669
}
6770

68-
/// The beta seed number parsed from the runtime identifier, when present.
71+
/// The beta seed number supplied by Apple, or inferred from legacy runtime metadata.
6972
public var betaNumber: Int? {
70-
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+") }
71-
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
72-
foundString.removeFirst()
73-
return Int(foundString)!
73+
seedNumber ?? legacyIdentifierBetaNumber ?? nameBetaNumber
74+
}
75+
76+
private var legacyIdentifierBetaNumber: Int? {
77+
// Apple's older runtime identifiers encoded the beta seed as a distinct component,
78+
// for example `com.apple.dmg.iPhoneSimulatorSDK27_0_b3_1`.
79+
//
80+
// Newer runtimes can use UUID identifiers. Keep the match delimiter-bound so a
81+
// random UUID fragment such as `7cb21db4a45b` is not misread as beta 21.
82+
enum Regex {
83+
static let shared = try! NSRegularExpression(
84+
pattern: "(?:^|[_-])b([0-9]+)(?:[_-]|$)",
85+
options: .caseInsensitive
86+
)
87+
}
88+
89+
let searchRange = NSRange(identifier.startIndex..., in: identifier)
90+
guard
91+
let match = Regex.shared.firstMatch(in: identifier, range: searchRange),
92+
let betaRange = Range(match.range(at: 1), in: identifier)
93+
else { return nil }
94+
95+
return Int(identifier[betaRange])
96+
}
97+
98+
private var nameBetaNumber: Int? {
99+
enum Regex {
100+
static let shared = try! NSRegularExpression(
101+
pattern: "\\bbeta(?:\\s+([0-9]+))?\\b",
102+
options: .caseInsensitive
103+
)
104+
}
105+
106+
let searchRange = NSRange(name.startIndex..., in: name)
107+
guard let match = Regex.shared.firstMatch(in: name, range: searchRange) else { return nil }
108+
109+
guard
110+
match.range(at: 1).location != NSNotFound,
111+
let betaRange = Range(match.range(at: 1), in: name)
112+
else { return 1 }
113+
114+
return Int(name[betaRange])
74115
}
75116

76117
/// The OS version plus beta suffix when this is a beta runtime.

‎Sources/XcodesKit/Services/RuntimeListPresentationService.swift‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,18 @@ public struct RuntimeListPresentationService: Sendable {
141141
}
142142

143143
public extension DownloadableRuntimesResponse {
144-
/// Returns downloadable runtimes enriched with SDK build update mappings.
144+
/// Returns downloadable runtimes enriched with SDK build and beta seed mappings.
145145
func downloadablesWithSDKBuildUpdates() -> [DownloadableRuntime] {
146146
downloadables.map { runtime in
147147
var updatedRuntime = runtime
148148
let mappings = sdkToSimulatorMappings.filter {
149149
$0.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate
150150
}
151151
updatedRuntime.sdkBuildUpdate = mappings.map(\.sdkBuildUpdate)
152+
updatedRuntime.seedNumber = sdkToSeedMappings.first {
153+
$0.buildUpdate == runtime.simulatorVersion.buildUpdate &&
154+
$0.platform == runtime.platform
155+
}?.seedNumber
152156
return updatedRuntime
153157
}
154158
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import XCTest
2+
@testable import XcodesKit
3+
4+
final class DownloadableRuntimeBetaNumberTests: XCTestCase {
5+
func testUsesSeedMappingForUUIDRuntime() throws {
6+
let response = DownloadableRuntimesResponse(
7+
sdkToSimulatorMappings: [],
8+
sdkToSeedMappings: [
9+
SDKToSeedMapping(buildUpdate: "24A5423a", platform: .iOS, seedNumber: 6)
10+
],
11+
refreshInterval: 3600,
12+
downloadables: [Self.runtime()],
13+
version: "2"
14+
)
15+
16+
let runtime = try XCTUnwrap(response.downloadablesWithSDKBuildUpdates().first)
17+
18+
XCTAssertEqual(runtime.betaNumber, 6)
19+
XCTAssertEqual(runtime.completeVersion, "27.0-beta6")
20+
XCTAssertEqual(runtime.visibleIdentifier, "iOS 27.0-beta6")
21+
}
22+
23+
func testUUIDRuntimeFallsBackToNameWithoutMatchingSeedData() {
24+
let runtime = Self.runtime()
25+
26+
XCTAssertEqual(runtime.betaNumber, 6)
27+
XCTAssertNotEqual(runtime.betaNumber, 21)
28+
}
29+
30+
func testStableUUIDContainingBFollowedByDigitsIsNotABeta() {
31+
let runtime = Self.runtime(name: "iOS 27.0 Simulator Runtime")
32+
33+
XCTAssertNil(runtime.betaNumber)
34+
XCTAssertEqual(runtime.visibleIdentifier, "iOS 27.0")
35+
}
36+
37+
func testLegacyStructuredIdentifierStillProvidesBetaNumber() {
38+
let runtime = Self.runtime(
39+
identifier: "com.apple.dmg.iPhoneSimulatorSDK27_0_b3_1",
40+
name: "iOS 27.0 Simulator Runtime"
41+
)
42+
43+
XCTAssertEqual(runtime.betaNumber, 3)
44+
}
45+
46+
func testUnnumberedBetaNameRepresentsFirstBeta() {
47+
let runtime = Self.runtime(
48+
identifier: "com.apple.dmg.iPhoneSimulatorSDK27_0_b1",
49+
name: "iOS 27.0 beta Simulator Runtime"
50+
)
51+
52+
XCTAssertEqual(runtime.betaNumber, 1)
53+
}
54+
55+
func testSeedNumberSurvivesCacheCodingRoundTrip() throws {
56+
var runtime = Self.runtime()
57+
runtime.seedNumber = 6
58+
59+
let data = try JSONEncoder().encode([runtime])
60+
let decodedRuntime = try XCTUnwrap(JSONDecoder().decode([DownloadableRuntime].self, from: data).first)
61+
62+
XCTAssertEqual(decodedRuntime.seedNumber, 6)
63+
XCTAssertEqual(decodedRuntime.betaNumber, 6)
64+
}
65+
66+
func testCacheWithoutSeedNumberStillDecodesAndUsesMetadataFallback() throws {
67+
let data = try JSONEncoder().encode([Self.runtime()])
68+
let decodedRuntime = try XCTUnwrap(JSONDecoder().decode([DownloadableRuntime].self, from: data).first)
69+
70+
XCTAssertNil(decodedRuntime.seedNumber)
71+
XCTAssertEqual(decodedRuntime.betaNumber, 6)
72+
}
73+
74+
private static func runtime(
75+
identifier: String = "0218a56d-df74-59c7-a193-7cb21db4a45b",
76+
name: String = "iOS 27.0 beta 6 Simulator Runtime"
77+
) -> DownloadableRuntime {
78+
DownloadableRuntime(
79+
category: .simulator,
80+
simulatorVersion: .init(buildUpdate: "24A5423a", version: "27.0"),
81+
source: nil,
82+
architectures: [.arm64],
83+
dictionaryVersion: 2,
84+
contentType: .cryptexDiskImage,
85+
platform: .iOS,
86+
identifier: identifier,
87+
version: "27.0.0.6",
88+
fileSize: 7_986_041_344,
89+
hostRequirements: nil,
90+
name: name,
91+
authentication: DownloadableRuntime.Authentication.none
92+
)
93+
}
94+
}

‎Tests/XcodesKitTests/RuntimeListStoreTests.swift‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class RuntimeListStoreTests: XCTestCase {
2020
XCTAssertEqual(store.downloadableRuntimes, [runtime])
2121
}
2222

23-
func testUpdateFetchesAddsSDKBuildUpdatesAndSavesRuntimes() async throws {
23+
func testUpdateAddsRuntimeMappingsAndSavesRuntimes() async throws {
2424
let runtime = Self.downloadableRuntime(buildUpdate: "20A360")
2525
let response = Self.downloadableResponse(
2626
downloadables: [runtime],
@@ -31,6 +31,13 @@ final class RuntimeListStoreTests: XCTestCase {
3131
sdkIdentifier: "com.apple.platform.iphonesimulator",
3232
downloadableIdentifiers: nil
3333
)
34+
],
35+
sdkToSeedMappings: [
36+
SDKToSeedMapping(
37+
buildUpdate: "20A360",
38+
platform: .iOS,
39+
seedNumber: 2
40+
)
3441
]
3542
)
3643
let savedRuntimes = RuntimeCacheSaveRecorder()
@@ -47,6 +54,7 @@ final class RuntimeListStoreTests: XCTestCase {
4754
let runtimes = try await store.updateDownloadableRuntimes()
4855

4956
XCTAssertEqual(runtimes.map(\.sdkBuildUpdate), [["20A361"]])
57+
XCTAssertEqual(runtimes.map(\.seedNumber), [2])
5058
XCTAssertEqual(store.downloadableRuntimes, runtimes)
5159
XCTAssertEqual(savedRuntimes.value, runtimes)
5260
}
@@ -71,11 +79,12 @@ final class RuntimeListStoreTests: XCTestCase {
7179

7280
private static func downloadableResponse(
7381
downloadables: [DownloadableRuntime],
74-
sdkToSimulatorMappings: [SDKToSimulatorMapping] = []
82+
sdkToSimulatorMappings: [SDKToSimulatorMapping] = [],
83+
sdkToSeedMappings: [SDKToSeedMapping] = []
7584
) -> DownloadableRuntimesResponse {
7685
DownloadableRuntimesResponse(
7786
sdkToSimulatorMappings: sdkToSimulatorMappings,
78-
sdkToSeedMappings: [],
87+
sdkToSeedMappings: sdkToSeedMappings,
7988
refreshInterval: 0,
8089
downloadables: downloadables,
8190
version: "1"

0 commit comments

Comments
 (0)