From ea8380fb7db6100e6cdbcc8e1ef5de26f423d622 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 2 Jul 2026 23:38:18 -0700 Subject: [PATCH 1/7] Add `CustomDump` trait --- Package.resolved | 2 +- Package.swift | 14 +++++++++++- Package@swift-5.9.swift | 1 + Package@swift-6.0.swift | 1 + Sources/Sharing/Shared.swift | 20 ---------------- Sources/Sharing/SharedReader.swift | 7 ------ Sources/Sharing/Traits/CustomDump.swift | 28 +++++++++++++++++++++++ Tests/SharingTests/FileStorageTests.swift | 6 +++-- Tests/SharingTests/SharedTests.swift | 1 + 9 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 Sources/Sharing/Traits/CustomDump.swift diff --git a/Package.resolved b/Package.resolved index a788e65..5225cb5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "98b3833b06d0cd8dbef93247a8df8c7206690db68aab6a8d301f76c1e3b959ea", + "originHash" : "62d09cddc4ab84131cb5e2cef0a442b1197e705e7023b538a05462a3e113a8f4", "pins" : [ { "identity" : "combine-schedulers", diff --git a/Package.swift b/Package.swift index a0826d0..6284c82 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,10 @@ let package = Package( name: "CasePaths", description: "Derive Shared cases from Shared enums using CasePaths" ), + .trait( + name: "CustomDump", + description: "Pretty-print and diff Sharing's data types using CustomDump" + ), .trait( name: "IdentifiedCollections", description: "Derive Shared elements from Shared collections using IdentifiedCollections" @@ -45,7 +49,13 @@ let package = Package( "Sharing1", "Sharing2", .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "CustomDump", package: "swift-custom-dump"), + .product( + name: "CustomDump", + package: "swift-custom-dump", + condition: .when( + traits: ["CustomDump"] + ) + ), .product(name: "Dependencies", package: "swift-dependencies"), .product( name: "IdentifiedCollections", @@ -73,6 +83,7 @@ let package = Package( dependencies: [ "Sharing", .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product( name: "CasePaths", @@ -103,6 +114,7 @@ package.traits.insert( enableAllTraits ? package.traits.map(\.name) : [ + "CustomDump", "IdentifiedCollections", ] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 4dd5202..e8139b5 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -56,6 +56,7 @@ let package = Package( for target in package.targets { target.swiftSettings = target.swiftSettings ?? [] target.swiftSettings?.append(contentsOf: [ + .define("CustomDump"), .define("IdentifiedCollections"), ]) } diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 405acbb..a2c79b0 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -67,6 +67,7 @@ let package = Package( for target in package.targets { target.swiftSettings = target.swiftSettings ?? [] target.swiftSettings?.append(contentsOf: [ + .define("CustomDump"), .define("IdentifiedCollections"), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("ImmutableWeakCaptures"), diff --git a/Sources/Sharing/Shared.swift b/Sources/Sharing/Shared.swift index 65ff689..e5fcdfb 100644 --- a/Sources/Sharing/Shared.swift +++ b/Sources/Sharing/Shared.swift @@ -1,4 +1,3 @@ -public import CustomDump import Dependencies import Foundation public import Observation @@ -474,25 +473,6 @@ extension Shared: Observable {} extension Shared: Perceptible {} -extension Shared: CustomDumpRepresentable { - public var customDumpValue: Any { - wrappedValue - } -} - -extension Shared: _CustomDiffObject { - public var _customDiffValues: (Any, Any) { - (reference.snapshot ?? reference.wrappedValue, reference.wrappedValue) - } - - public var _objectIdentifier: ObjectIdentifier { - func open(_ reference: some MutableReference) -> ObjectIdentifier { - reference.id - } - return open(reference) - } -} - #if canImport(SwiftUI) extension Shared: DynamicProperty { public func update() { diff --git a/Sources/Sharing/SharedReader.swift b/Sources/Sharing/SharedReader.swift index 772d34e..1446474 100644 --- a/Sources/Sharing/SharedReader.swift +++ b/Sources/Sharing/SharedReader.swift @@ -1,4 +1,3 @@ -public import CustomDump import Foundation import IssueReporting public import Observation @@ -345,12 +344,6 @@ extension SharedReader: Observable {} extension SharedReader: Perceptible {} -extension SharedReader: CustomDumpRepresentable { - public var customDumpValue: Any { - wrappedValue - } -} - #if canImport(SwiftUI) extension SharedReader: DynamicProperty { public func update() { diff --git a/Sources/Sharing/Traits/CustomDump.swift b/Sources/Sharing/Traits/CustomDump.swift new file mode 100644 index 0000000..caaa1a6 --- /dev/null +++ b/Sources/Sharing/Traits/CustomDump.swift @@ -0,0 +1,28 @@ +#if CustomDump + public import CustomDump + + extension Shared: CustomDumpRepresentable { + public var customDumpValue: Any { + wrappedValue + } + } + + extension Shared: _CustomDiffObject { + public var _customDiffValues: (Any, Any) { + (reference.snapshot ?? reference.wrappedValue, reference.wrappedValue) + } + + public var _objectIdentifier: ObjectIdentifier { + func open(_ reference: some MutableReference) -> ObjectIdentifier { + reference.id + } + return open(reference) + } + } + + extension SharedReader: CustomDumpRepresentable { + public var customDumpValue: Any { + wrappedValue + } + } +#endif diff --git a/Tests/SharingTests/FileStorageTests.swift b/Tests/SharingTests/FileStorageTests.swift index b1e9752..d1e2adc 100644 --- a/Tests/SharingTests/FileStorageTests.swift +++ b/Tests/SharingTests/FileStorageTests.swift @@ -20,7 +20,8 @@ @Shared(.fileStorage(.fileURL)) var users = [User]() #expect($users.loadError == nil) expectNoDifference( - fileSystem.value, [.fileURL: Data()] + fileSystem.value, + [.fileURL: Data()] ) $users.withLock { $0.append(.blob) } try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob]) @@ -34,7 +35,8 @@ @Shared(.utf8String) var string = "" #expect($string.loadError == nil) expectNoDifference( - fileSystem.value, [.utf8StringURL: Data()] + fileSystem.value, + [.utf8StringURL: Data()] ) $string.withLock { $0 = "hello" } expectNoDifference( diff --git a/Tests/SharingTests/SharedTests.swift b/Tests/SharingTests/SharedTests.swift index fa77db2..121490f 100644 --- a/Tests/SharingTests/SharedTests.swift +++ b/Tests/SharingTests/SharedTests.swift @@ -1,6 +1,7 @@ import CustomDump import Dependencies import Foundation +import IdentifiedCollections import PerceptionCore import Sharing import Testing From 8eceaa1726f91ac1bf4576185057547a5f7a4a63 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 11:54:46 -0700 Subject: [PATCH 2/7] wip --- Tests/SharingTests/SharedTests.swift | 95 ++++++++++++++-------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/Tests/SharingTests/SharedTests.swift b/Tests/SharingTests/SharedTests.swift index 121490f..d756500 100644 --- a/Tests/SharingTests/SharedTests.swift +++ b/Tests/SharingTests/SharedTests.swift @@ -1,11 +1,12 @@ -import CustomDump import Dependencies import Foundation -import IdentifiedCollections import PerceptionCore import Sharing import Testing +#if CustomDump + import CustomDump +#endif #if IdentifiedCollections import IdentifiedCollections #endif @@ -278,53 +279,55 @@ import Testing #expect($count.description == #"Shared(.inMemory("count"))"#) } - @Test func customDump() { - @Shared(value: 0) var count + #if CustomDump + @Test func customDump() { + @Shared(value: 0) var count - #expect(String(customDumping: $count) == "#1 0") - #expect( - String(customDumping: [$count, $count]) == """ - [ - [0]: #1 0, - [1]: #1 Int(↩︎) - ] - """ - ) - - @Shared(value: 0) var anotherCount - #expect( - String(customDumping: [$count, $anotherCount, $count, $anotherCount]) == """ - [ - [0]: #1 0, - [1]: #2 0, - [2]: #1 Int(↩︎), - [3]: #2 Int(↩︎) - ] - """ - ) - } + #expect(String(customDumping: $count) == "#1 0") + #expect( + String(customDumping: [$count, $count]) == """ + [ + [0]: #1 0, + [1]: #1 Int(↩︎) + ] + """ + ) - @Test func customDumpWithProjection() { - struct Stats { - var count = 0 + @Shared(value: 0) var anotherCount + #expect( + String(customDumping: [$count, $anotherCount, $count, $anotherCount]) == """ + [ + [0]: #1 0, + [1]: #2 0, + [2]: #1 Int(↩︎), + [3]: #2 Int(↩︎) + ] + """ + ) } - struct State { - @Shared var count: Int - @Shared var stats: Stats + + @Test func customDumpWithProjection() { + struct Stats { + var count = 0 + } + struct State { + @Shared var count: Int + @Shared var stats: Stats + } + @Shared(value: Stats()) var stats + #expect( + String(customDumping: State(count: $stats.count, stats: $stats)) == """ + SharedTests.StringRepresentations.State( + _count: #1 0, + _stats: #1 SharedTests.StringRepresentations.Stats(↩︎) + ) + """, + """ + This test shows that the custom dump behavior identifying by root object identifier is not \ + ideal: it causes a larger dump to be truncated if a smaller slice of it was dumped earlier. + """ + ) } - @Shared(value: Stats()) var stats - #expect( - String(customDumping: State(count: $stats.count, stats: $stats)) == """ - SharedTests.StringRepresentations.State( - _count: #1 0, - _stats: #1 SharedTests.StringRepresentations.Stats(↩︎) - ) - """, - """ - This test shows that the custom dump behavior identifying by root object identifier is not \ - ideal: it causes a larger dump to be truncated if a smaller slice of it was dumped earlier. - """ - ) - } + #endif } } From afd383754401b7aaafbb512bb9f6a3a1b46266f8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 13:36:27 -0700 Subject: [PATCH 3/7] wip --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239010c..69ef379 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: matrix: xcode: ['26.5'] config: ['debug', 'release'] - traits: ['', 'CasePaths', 'IdentifiedCollections'] + traits: ['', 'CasePaths', 'CustomDump', 'IdentifiedCollections'] runs-on: macos-26 steps: - uses: actions/checkout@v4 From d6d7565298417c9ac3667a8d980acc34f2055b95 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 13:41:47 -0700 Subject: [PATCH 4/7] wip --- .../SharedChangeTrackerTests.swift | 228 +++++++++--------- 1 file changed, 115 insertions(+), 113 deletions(-) diff --git a/Tests/SharingTests/SharedChangeTrackerTests.swift b/Tests/SharingTests/SharedChangeTrackerTests.swift index 949f133..d55fcf7 100644 --- a/Tests/SharingTests/SharedChangeTrackerTests.swift +++ b/Tests/SharingTests/SharedChangeTrackerTests.swift @@ -1,153 +1,155 @@ -import Combine -import CustomDump -import PerceptionCore -@_spi(SharedChangeTracking) import Sharing -import Testing - -@Suite struct SharedChangeTrackingTests { - @Test func basics() { - @Shared(value: 0) var count - - let tracker = SharedChangeTracker() - tracker.track { - $count.withLock { $0 += 1 } - } +#if CustomDump + import Combine + import CustomDump + import PerceptionCore + @_spi(SharedChangeTracking) import Sharing + import Testing + + @Suite struct SharedChangeTrackingTests { + @Test func basics() { + @Shared(value: 0) var count + + let tracker = SharedChangeTracker() + tracker.track { + $count.withLock { $0 += 1 } + } - tracker.assert { - #expect($count != $count) - #expect( - diff($count, $count) == """ - - #1 0 - + #1 1 - """ - ) + tracker.assert { + #expect($count != $count) + #expect( + diff($count, $count) == """ + - #1 0 + + #1 1 + """ + ) - $count.withLock { $0 = 1 } + $count.withLock { $0 = 1 } - #expect($count == $count) + #expect($count == $count) + } } - } - @Test func independentShareds() { - @Shared(value: 0) var count1 - @Shared(value: 0) var count2 + @Test func independentShareds() { + @Shared(value: 0) var count1 + @Shared(value: 0) var count2 - #expect($count1 == $count2) + #expect($count1 == $count2) - let tracker = SharedChangeTracker() - tracker.track { - $count1.withLock { $0 += 1 } - } + let tracker = SharedChangeTracker() + tracker.track { + $count1.withLock { $0 += 1 } + } - tracker.assert { - #expect($count1 != $count1) - #expect($count1 == $count2) - #expect($count2 == $count2) + tracker.assert { + #expect($count1 != $count1) + #expect($count1 == $count2) + #expect($count2 == $count2) - $count1.withLock { $0 = 1 } + $count1.withLock { $0 = 1 } - #expect($count1 == $count1) - #expect($count1 != $count2) + #expect($count1 == $count1) + #expect($count1 != $count2) + } } - } - @Test func differentProjections() { - struct Stats: Equatable { - var bool1 = false - var bool2 = false - } - @Shared(value: Stats()) var stats + @Test func differentProjections() { + struct Stats: Equatable { + var bool1 = false + var bool2 = false + } + @Shared(value: Stats()) var stats - #expect($stats.bool1 == $stats.bool2) + #expect($stats.bool1 == $stats.bool2) - let tracker = SharedChangeTracker() - tracker.track { - $stats.bool1.withLock { $0.toggle() } - } + let tracker = SharedChangeTracker() + tracker.track { + $stats.bool1.withLock { $0.toggle() } + } - tracker.assert { - #expect($stats.bool1 != $stats.bool1) - #expect($stats.bool1 == $stats.bool2) - #expect($stats.bool2 == $stats.bool2) + tracker.assert { + #expect($stats.bool1 != $stats.bool1) + #expect($stats.bool1 == $stats.bool2) + #expect($stats.bool2 == $stats.bool2) - $stats.bool1.withLock { $0.toggle() } + $stats.bool1.withLock { $0.toggle() } - #expect($stats.bool1 == $stats.bool1) - #expect($stats.bool1 != $stats.bool2) + #expect($stats.bool1 == $stats.bool1) + #expect($stats.bool1 != $stats.bool2) - #expect($stats == $stats) + #expect($stats == $stats) + } } - } - @Test func assertedChanges() { - @Shared(value: 0) var count + @Test func assertedChanges() { + @Shared(value: 0) var count - let counts = Mutex<[Int]>([]) - let cancellable = $count.publisher.sink { @Sendable value in - counts.withLock { $0.append(value) } - } - defer { _ = cancellable } + let counts = Mutex<[Int]>([]) + let cancellable = $count.publisher.sink { @Sendable value in + counts.withLock { $0.append(value) } + } + defer { _ = cancellable } - let tracker = SharedChangeTracker() - tracker.track { - $count.withLock { $0 += 1 } - } + let tracker = SharedChangeTracker() + tracker.track { + $count.withLock { $0 += 1 } + } - tracker.assert { - #expect($count != $count) + tracker.assert { + #expect($count != $count) - $count.withLock { $0 = 1 } + $count.withLock { $0 = 1 } - #expect($count == $count) - #expect(counts.withLock(\.self) == [0, 1]) + #expect($count == $count) + #expect(counts.withLock(\.self) == [0, 1]) + } } - } - @Test func unassertedChanges() { - @Shared(value: 0) var count + @Test func unassertedChanges() { + @Shared(value: 0) var count - withKnownIssue { - do { - let tracker = SharedChangeTracker() - tracker.track { - $count.withLock { $0 += 1 } + withKnownIssue { + do { + let tracker = SharedChangeTracker() + tracker.track { + $count.withLock { $0 += 1 } + } } + } matching: { + $0.description.hasSuffix( + """ + Tracked unasserted changes to 'Shared(value: 1)': 0 → 1 + """ + ) } - } matching: { - $0.description.hasSuffix( - """ - Tracked unasserted changes to 'Shared(value: 1)': 0 → 1 - """ - ) } - } - @Test func unreportedChanges() { - @Shared(value: 0) var count + @Test func unreportedChanges() { + @Shared(value: 0) var count - let tracker = SharedChangeTracker(reportUnassertedChanges: false) - tracker.track { - $count.withLock { $0 += 1 } + let tracker = SharedChangeTracker(reportUnassertedChanges: false) + tracker.track { + $count.withLock { $0 += 1 } + } } - } - @Test func unwrappedShared() { - let optionalShared = Shared(value: 1) - let unwrappedShared = Shared(optionalShared)! + @Test func unwrappedShared() { + let optionalShared = Shared(value: 1) + let unwrappedShared = Shared(optionalShared)! - withKnownIssue { - do { - let tracker = SharedChangeTracker() - tracker.track { - unwrappedShared.withLock { $0 += 1 } + withKnownIssue { + do { + let tracker = SharedChangeTracker() + tracker.track { + unwrappedShared.withLock { $0 += 1 } + } } + } matching: { + $0.description.hasSuffix( + """ + Tracked unasserted changes to 'Shared(value: Optional(2))': Optional(1) → Optional(2) + """ + ) } - } matching: { - $0.description.hasSuffix( - """ - Tracked unasserted changes to 'Shared(value: Optional(2))': Optional(1) → Optional(2) - """ - ) } } -} +#endif From 46c6015cb71527170b25d469c92269a02f97ab50 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 13:42:50 -0700 Subject: [PATCH 5/7] wip --- Package.swift | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index 6284c82..c00c801 100644 --- a/Package.swift +++ b/Package.swift @@ -48,6 +48,13 @@ let package = Package( dependencies: [ "Sharing1", "Sharing2", + .product( + name: "CasePaths", + package: "swift-case-paths", + condition: .when( + traits: ["CasePaths"] + ) + ), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product( name: "CustomDump", @@ -66,13 +73,6 @@ let package = Package( ), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "PerceptionCore", package: "swift-perception"), - .product( - name: "CasePaths", - package: "swift-case-paths", - condition: .when( - traits: ["CasePaths"] - ) - ), ], resources: [ .process("PrivacyInfo.xcprivacy") @@ -82,9 +82,6 @@ let package = Package( name: "SharingTests", dependencies: [ "Sharing", - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product( name: "CasePaths", package: "swift-case-paths", @@ -92,6 +89,15 @@ let package = Package( traits: ["CasePaths"] ) ), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product( + name: "CustomDump", + package: "swift-custom-dump", + condition: .when( + traits: ["CustomDump"] + ) + ), + .product(name: "DependenciesTestSupport", package: "swift-dependencies"), ], exclude: ["Sharing.xctestplan"] ), From 2d4b2ca6267abcafc075d799fcd959149b774811 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 13:43:22 -0700 Subject: [PATCH 6/7] wip --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69ef379..6bc36fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: matrix: swift: - '6.3' - traits: ['', 'CasePaths'] + traits: ['', 'CasePaths', 'CustomDump', 'IdentifiedCollections'] runs-on: ubuntu-latest container: swift:${{ matrix.swift }} steps: From 168eb7b47abe121bc503c939294cef04aba7c47d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 3 Jul 2026 13:48:21 -0700 Subject: [PATCH 7/7] wip --- Package.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index c00c801..c2b8cc7 100644 --- a/Package.swift +++ b/Package.swift @@ -90,13 +90,7 @@ let package = Package( ) ), .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product( - name: "CustomDump", - package: "swift-custom-dump", - condition: .when( - traits: ["CustomDump"] - ) - ), + .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "DependenciesTestSupport", package: "swift-dependencies"), ], exclude: ["Sharing.xctestplan"]