Skip to content

Commit ceb3721

Browse files
authored
Fix unstable reads for XCSchemeManagement (#758)
Resolves: #756 - Reading the same `xcschememanagement.plist` file was resulting in different `XCSchemeManagement` objects - This was due to the internal conversion of the `schemeUserState` from a dictionary to an array without sorting the dictionary key - This lead to having differently ordered `XCSchemeManagement` arrays each time the plist was read - Wrties were stable (going from `XCSchemeManagement` > `plist`) is most likely why this wasn't previously noticed - To address this, they dictionary elements are sorted by key name - Read stability tests have also been added and fixture updated to include more entries to aid with testing Note: it's unclear why the `schemeUserState` is stored as an array, it's a candidate to be changed to dictionary in the next major release as it would be a breaking change to do so now without any compatibility accessors to maintain the same public API. Test Plan: - Verify unit tests pass - Verify writing an `xcschememanagement.plist` file remains unchanged by these changes
1 parent 049a452 commit ceb3721

File tree

3 files changed

+60
-19
lines changed

3 files changed

+60
-19
lines changed

Fixtures/Schemes/xcschememanagement.plist

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,36 @@
44
<dict>
55
<key>SchemeUserState</key>
66
<dict>
7+
<key>App.xcscheme</key>
8+
<dict>
9+
<key>isShown</key>
10+
<false/>
11+
<key>orderHint</key>
12+
<integer>0</integer>
13+
</dict>
14+
<key>Test 0.xcscheme</key>
15+
<dict>
16+
<key>orderHint</key>
17+
<integer>3</integer>
18+
</dict>
19+
<key>Test 1.xcscheme</key>
20+
<dict>
21+
<key>isShown</key>
22+
<false/>
23+
<key>orderHint</key>
24+
<integer>4</integer>
25+
</dict>
726
<key>Tuist.xcscheme_^#shared#^_</key>
827
<dict>
928
<key>isShown</key>
1029
<true/>
1130
<key>orderHint</key>
12-
<integer>0</integer>
31+
<integer>1</integer>
1332
</dict>
1433
<key>XcodeProj.xcscheme</key>
1534
<dict>
1635
<key>orderHint</key>
17-
<integer>1</integer>
36+
<integer>2</integer>
1837
</dict>
1938
</dict>
2039
<key>SuppressBuildableAutocreation</key>

Sources/XcodeProj/Scheme/XCSchemeManagement.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ public struct XCSchemeManagement: Codable, Equatable, Writable {
150150
let container = try decoder.container(keyedBy: CodingKeys.self)
151151
self.suppressBuildableAutocreation = try container.decodeIfPresent(.suppressBuildableAutocreation)
152152
if let schemeUserStateDictionary = try container.decodeIfPresent([String: Any].self, forKey: .schemeUserState) {
153-
self.schemeUserState = try schemeUserStateDictionary.compactMap({ (key, value) -> XCSchemeManagement.UserStateScheme? in
153+
self.schemeUserState = try schemeUserStateDictionary
154+
.sorted(by: { $0.key < $1.key })
155+
.compactMap({ (key, value) -> XCSchemeManagement.UserStateScheme? in
154156
var name = key
155157
guard var valueDictionary = value as? [String: Any] else { return nil }
156158
if key.contains("_^#shared#^_") {

Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,37 @@ final class XCSchemeManagementTests: XCTestCase {
1313
let got = try XCSchemeManagement.init(path: path)
1414

1515
// Then
16-
let autocreationTarget = try XCTUnwrap(got.suppressBuildableAutocreation?["E525238B16245A900012E2BA"])
17-
XCTAssertEqual(autocreationTarget.primary, true)
18-
19-
let tuistScheme = try XCTUnwrap(got.schemeUserState?.first(where: {$0.name == "Tuist.xcscheme"}))
20-
XCTAssertEqual(tuistScheme.name, "Tuist.xcscheme")
21-
XCTAssertTrue(tuistScheme.shared)
22-
XCTAssertEqual(tuistScheme.isShown, true)
23-
XCTAssertEqual(tuistScheme.orderHint, 0)
24-
25-
let xcodeprojScheme = try XCTUnwrap(got.schemeUserState?.first(where: {$0.name == "XcodeProj.xcscheme"}))
26-
XCTAssertEqual(xcodeprojScheme.name, "XcodeProj.xcscheme")
27-
XCTAssertFalse(xcodeprojScheme.shared)
28-
XCTAssertNil(xcodeprojScheme.isShown)
29-
XCTAssertEqual(xcodeprojScheme.orderHint, 1)
16+
XCTAssertEqual(got.suppressBuildableAutocreation, [
17+
"E525238B16245A900012E2BA": .init(primary: true),
18+
])
19+
20+
XCTAssertEqual(got.schemeUserState, [
21+
.init(name: "App.xcscheme", shared: false, orderHint: 0, isShown: false),
22+
.init(name: "Test 0.xcscheme", shared: false, orderHint: 3, isShown: nil),
23+
.init(name: "Test 1.xcscheme", shared: false, orderHint: 4, isShown: false),
24+
.init(name: "Tuist.xcscheme", shared: true, orderHint: 1, isShown: true),
25+
.init(name: "XcodeProj.xcscheme", shared: false, orderHint: 2, isShown: nil),
26+
])
3027
}
3128

3229
func test_read_write_produces_no_diff() throws {
3330
try testReadWriteProducesNoDiff(from: xcschememanagementPath, initModel: XCSchemeManagement.init(path:))
3431
}
3532

33+
func test_read_is_stable() throws {
34+
// Given
35+
let path = xcschememanagementPath
36+
37+
// When
38+
let reads = try (0..<10).map { _ in
39+
try XCSchemeManagement(path: path)
40+
}
41+
42+
// Then
43+
let unstableReads = reads.dropFirst().filter { $0 != reads.first }
44+
XCTAssertTrue(unstableReads.isEmpty)
45+
}
46+
3647
func test_write_produces_no_diff() throws {
3748
let tmpDir = try Path.uniqueTemporary()
3849
defer {
@@ -42,8 +53,17 @@ final class XCSchemeManagementTests: XCTestCase {
4253
try tmpDir.chdir {
4354
// Write
4455
let plistPath = tmpDir + "xcschememanagement.plist"
45-
let subject = XCSchemeManagement(schemeUserState: [.init(name: "Test.xcscheme", shared: true, orderHint: 0, isShown: true)],
46-
suppressBuildableAutocreation: ["E525238B16245A900012E2BA": .init(primary: true)])
56+
let subject = XCSchemeManagement(
57+
schemeUserState: [
58+
.init(name: "Test 0.xcscheme", shared: true, orderHint: 0, isShown: true),
59+
.init(name: "Test 1.xcscheme", shared: true, orderHint: 1, isShown: true),
60+
.init(name: "Test 2.xcscheme", shared: true, orderHint: 2, isShown: false),
61+
.init(name: "Test 3.xcscheme", shared: true, orderHint: 3, isShown: true),
62+
],
63+
suppressBuildableAutocreation: [
64+
"E525238B16245A900012E2BA": .init(primary: true),
65+
]
66+
)
4767
try subject.write(path: plistPath, override: true)
4868

4969
// Create a commit

0 commit comments

Comments
 (0)