From 9dad7cabbab05e1c70498b63ed5588b2714f3984 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 27 Nov 2023 11:33:33 -0800 Subject: [PATCH 1/2] DocC --- .github/workflows/documentation.yml | 33 -- .swi.yml | 14 + Package@swift-5.6.swift | 36 ++ .../Conformances/CoreLocation.swift | 4 +- .../Documentation.docc/CustomDump.md | 456 ++++++++++++++++++ Sources/CustomDump/Documentation.docc/Diff.md | 7 + .../Documentation.docc/XCTAssertDifference.md | 7 + Sources/CustomDump/XCTAssertDifference.swift | 4 +- 8 files changed, 524 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/documentation.yml create mode 100644 .swi.yml create mode 100644 Package@swift-5.6.swift create mode 100644 Sources/CustomDump/Documentation.docc/CustomDump.md create mode 100644 Sources/CustomDump/Documentation.docc/Diff.md create mode 100644 Sources/CustomDump/Documentation.docc/XCTAssertDifference.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index ef72b4fc..00000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Documentation -on: - release: - types: - - published - workflow_dispatch: - -concurrency: - group: documentation-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Generate Documentation - uses: SwiftDocOrg/swift-doc@master - with: - base-url: /swift-custom-dump/ - format: html - inputs: Sources/CustomDump - module-name: CustomDump - output: Documentation - - name: Update Permissions - run: 'sudo chown --recursive $USER Documentation' - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@releases/v3 - with: - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - BRANCH: gh-pages - FOLDER: Documentation diff --git a/.swi.yml b/.swi.yml new file mode 100644 index 00000000..17142baa --- /dev/null +++ b/.swi.yml @@ -0,0 +1,14 @@ +version: 1 +builder: + configs: + - platform: ios + scheme: CustomDump + - platform: macos-xcodebuild + scheme: CustomDump + - platform: tvos + scheme: CustomDump + - platform: watchos + scheme: CustomDump + - documentation_targets: [CustomDump] + swift_version: 5.9 + diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift new file mode 100644 index 00000000..47abbdd3 --- /dev/null +++ b/Package@swift-5.6.swift @@ -0,0 +1,36 @@ +// swift-tools-version:5.6 + +import PackageDescription + +let package = Package( + name: "swift-custom-dump", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "CustomDump", + targets: ["CustomDump"] + ) + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0") + ], + targets: [ + .target( + name: "CustomDump", + dependencies: [ + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay") + ] + ), + .testTarget( + name: "CustomDumpTests", + dependencies: [ + "CustomDump" + ] + ), + ] +) diff --git a/Sources/CustomDump/Conformances/CoreLocation.swift b/Sources/CustomDump/Conformances/CoreLocation.swift index c1f66715..db74aa55 100644 --- a/Sources/CustomDump/Conformances/CoreLocation.swift +++ b/Sources/CustomDump/Conformances/CoreLocation.swift @@ -80,8 +80,8 @@ #if compiler(>=5.9) @available(iOS 7, macOS 10.15, *) @available(tvOS, unavailable) + @available(visionOS, unavailable) @available(watchOS, unavailable) - @available(xrOS, unavailable) extension CLProximity: CustomDumpStringConvertible { public var customDumpDescription: String { switch self { @@ -123,8 +123,8 @@ #if compiler(>=5.9) @available(iOS 7, macOS 10, *) @available(tvOS, unavailable) + @available(visionOS, unavailable) @available(watchOS, unavailable) - @available(xrOS, unavailable) extension CLRegionState: CustomDumpStringConvertible { public var customDumpDescription: String { switch self { diff --git a/Sources/CustomDump/Documentation.docc/CustomDump.md b/Sources/CustomDump/Documentation.docc/CustomDump.md new file mode 100644 index 00000000..f3fa4e98 --- /dev/null +++ b/Sources/CustomDump/Documentation.docc/CustomDump.md @@ -0,0 +1,456 @@ +# ``CustomDump`` + +A collection of tools for debugging, diffing, and testing your application's data structures. + +## Overview + +Swift comes with a wonderful tool for dumping the contents of any value to a string, and it's called +`dump`. It prints all the fields and sub-fields of a value into a tree-like description: + +```swift +struct User { + var favoriteNumbers: [Int] + var id: Int + var name: String +} + +let user = User( + favoriteNumbers: [42, 1729], + id: 2, + name: "Blob" +) + +dump(user) +``` +```text +▿ User + ▿ favoriteNumbers: 2 elements + - 42 + - 1729 + - id: 2 + - name: "Blob" +``` + +This is really useful, and can be great for building debug tools that visualize the data held in +runtime values of our applications, but sometimes its output is not ideal. + +For example, dumping dictionaries leads to a verbose output that can be hard to read (also note that +the keys are unordered): + +```swift +dump([1: "one", 2: "two", 3: "three"]) +``` +```text +▿ 3 key/value pairs + ▿ (2 elements) + - key: 2 + - value: "two" + ▿ (2 elements) + - key: 3 + - value: "three" + ▿ (2 elements) + - key: 1 + - value: "one" +``` + +Similarly enums have a very verbose output: + +```swift +dump(Result.success(42)) +``` +```text +▿ Swift.Result.success + - success: 42 +``` + +It gets even harder to read when dealing with deeply nested structures: + +```swift +dump([1: Result.success(user)]) +``` +```text +▿ 1 key/value pair + ▿ (2 elements) + - key: 1 + ▿ value: Swift.Result.success + ▿ success: User + ▿ favoriteNumbers: 2 elements + - 42 + - 1729 + - id: 2 + - name: "Blob" +``` + +There are also times that `dump` simply does not print useful information, such as enums imported +from Objective-C: + +```swift +import UserNotifications + +dump(UNNotificationSetting.disabled) +``` +```text +- __C.UNNotificationSetting +``` + +So, while the `dump` function can be handy, it is often too crude of a tool to use. This is the +motivation for the `customDump` function. + +### customDump + +The ``customDump(_:name:indent:maxDepth:)`` function emulates the behavior of `dump`, but provides a +more refined output of nested structures, optimizing for readability. For example, structs are +dumped in a format that more closely mimics the struct syntax in Swift, and arrays are dumped with +the indices of each element: + +```swift +import CustomDump + +customDump(user) +``` +```text +User( + favoriteNumbers: [ + [0]: 42, + [1]: 1729 + ], + id: 2, + name: "Blob" +) +``` + +Dictionaries are dumped in a more compact format that mimics Swift's syntax, and automatically +orders the keys: + +```swift +customDump([1: "one", 2: "two", 3: "three"]) +``` +```text +[ + 1: "one", + 2: "two", + 3: "three" +] +``` + +Similarly, enums also dump in a more compact, readable format: + +```swift +customDump(Result.success(42)) +``` +```text +Result.success(42) +``` + +And deeply nested structures have a simplified tree-structure: + +```swift +customDump([1: Result.success(user)]) +``` +```text +[ + 1: Result.success( + User( + favoriteNumbers: [ + [0]: 42, + [1]: 1729 + ], + id: 2, + name: "Blob" + ) + ) +] +``` + +### diff + +Using the output of the `customDump` function we can build a very lightweight way to textually diff +any two values in Swift using the ``diff(_:_:format:)`` function: + +```swift +var other = user +other.favoriteNumbers[1] = 91 + +print(diff(user, other)!) +``` +```diff +  User( +  favoriteNumbers: [ +  [0]: 42, +- [1]: 1729 ++ [1]: 91 +  ], +  id: 2, +  name: "Blob" +  ) +``` + +Further, extra work is done to minimize the size of the diff when parts of the structure haven't +changed, such as a single element changing in a large collection: + +```swift +let users = (1...5).map { + User( + favoriteNumbers: [$0], + id: $0, + name: "Blob \($0)" + ) +} + +var other = users +other.append( + .init( + favoriteNumbers: [42, 1729], + id: 100, + name: "Blob Sr." + ) +) + +print(diff(users, other)!) +``` +```diff +  [ +  … (4 unchanged), ++ [4]: User( ++ favoriteNumbers: [ ++ [0]: 42, ++ [1]: 1729 ++ ], ++ id: 100, ++ name: "Blob Sr." ++ ) +  ] +``` + +For a real world use case we modified Apple's + [Landmarks](https://developer.apple.com/tutorials/swiftui/working-with-ui-controls) tutorial +application to print the before and after state when favoriting a landmark: + +```diff +  [ +  [0]: Landmark( +  id: 1001, +  name: "Turtle Rock", +  park: "Joshua Tree National Park", +  state: "California", +  description: "This very large formation lies south of the large Real Hidden Valley parking lot and immediately adjacent to (south of) the picnic areas.", +- isFavorite: true, ++ isFavorite: false, +  isFeatured: true, +  category: Category.rivers, +  imageName: "turtlerock", +  coordinates: Coordinates(…) +  ), +  … (11 unchanged) +  ] +``` + +### XCTAssertNoDifference + +The `XCTAssertEqual` function from `XCTest` allows you to assert that two values are equal, and if +they are not the test suite will fail with a message: + +```swift +var other = user +other.name += "!" + +XCTAssertEqual(user, other) +``` +```text +XCTAssertEqual failed: ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")") is not equal to ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!")") +``` + +Unfortunately this failure message is quite difficult to visually parse and understand. It takes a +few moments of hunting through the message to see that the only difference is the exclamation mark +at the end of the name. The problem gets worse if the type is more complex, consisting of nested +structures and large collections. + +This library also ships with an ``XCTAssertNoDifference(_:_:_:file:line:)`` function to mitigate +these problems. It works like `XCTAssertEqual` except the failure message uses a nicely formatted +diff to show exactly what is different between the two values: + +```swift +XCTAssertNoDifference(user, other) +``` +```diff +XCTAssertNoDifference failed: … + +  User( +  favoriteNumbers: […], +  id: 2, +- name: "Blob" ++ name: "Blob!" +  ) + +(First: -, Second: +) +``` + +### XCTAssertDifference + +``XCTAssertDifference(_:_:operation:changes:file:line:)-8xfxw`` provides the inverse of +`XCTAssertNoDifference`: it asserts that a value has a set of changes by evaluating a given +expression before and after a given operation and then comparing the results. + +For example, given a very simple counter structure, we can write a test against its incrementing +functionality: + +```swift +struct Counter { + var count = 0 + var isOdd = false + mutating func increment() { + self.count += 1 + self.isOdd.toggle() + } +} + +var counter = Counter() +XCTAssertDifference(counter) { + counter.increment() +} changes: { + $0.count = 1 + $0.isOdd = true +} +``` + +If the `changes` does not exhaustively describe all changed fields, the assertion will fail. + +By omitting the operation you can write a "non-exhaustive" assertion against a value by describing +just the fields you want to assert against in the `changes` closure: + +```swift +counter.increment() +XCTAssertDifference(counter) { + $0.count = 1 + // Don't need to further describe how `isOdd` has changed +} +``` + +## Customization + +Custom Dump provides a few important ways to customize how a data type is dumped: +``CustomDumpStringConvertible``, ``CustomDumpReflectable``, and ``CustomDumpRepresentable``. + +### CustomDumpStringConvertible + +The ``CustomDumpStringConvertible`` protocol provides a simple way of converting a type to a raw +string for the purpose of dumping. It is most appropriate for types that have a simple, un-nested +internal representation, and typically its output fits on a single line, for example dates, UUIDs, +URLs, etc: + +```swift +extension URL: CustomDumpStringConvertible { + public var customDumpDescription: String { + "URL(\(self.absoluteString))" + } +} + +customDump(URL(string: "https://www.pointfree.co/")!) +``` +```text +URL(https://www.pointfree.co/) +``` + +Custom Dump also uses this protocol internally to provide more useful output for enums imported from +Objective-C: + +```swift +import UserNotifications + +print("dump:") +dump(UNNotificationSetting.disabled) +print("customDump:") +customDump(UNNotificationSetting.disabled) +``` +```text +dump: +- __C.UNNotificationSetting +customDump: +UNNotificationSettings.disabled +``` + +### CustomDumpReflectable + +The ``CustomDumpReflectable`` protocol provides a more comprehensive way of dumping a type into a +more structured output. It allows you to construct a custom mirror that describes the structure that +should be dumped. You can omit, add, and replace fields, or even change the "display style" of how +the structure is dumped. + +For example, let's say you have a struct representing state that holds a secure token in memory that +should never be written to your logs. You can omit the token from `customDump` by providing a mirror +that omits this field: + +```swift +struct LoginState: CustomDumpReflectable { + var username: String + var token: String + + var customDumpMirror: Mirror { + .init( + self, + children: [ + "username": self.username, + // omit token from logs + ], + displayStyle: .struct + ) + } +} + +customDump( + LoginState( + username: "blob", + token: "secret" + ) +) +``` +```text +LoginState(username: "blob") +``` + +And just like that, no token data will be written to the dump. + +### `CustomDumpRepresentable` + +The `CustomDumpRepresentable` protocol allows you to return _any_ value for the purpose of dumping. +This can be useful to flatten the dump representation of wrapper types. For example, a type-safe +identifier may want to dump its raw value directly: + +```swift +struct ID: RawRepresentable { + var rawValue: String +} + +extension ID: CustomDumpRepresentable { + var customDumpValue: Any { + self.rawValue + } +} + +customDump(ID(rawValue: "deadbeef") +``` +```text +"deadbeef" +``` + +## Topics + +### Dumping + +- ``customDump(_:name:indent:maxDepth:)`` +- ``customDump(_:to:name:indent:maxDepth:)`` + +### Diffing + +- ``diff(_:_:format:)`` + +### Test support + +- ``XCTAssertNoDifference(_:_:_:file:line:)`` +- ``XCTAssertDifference(_:_:operation:changes:file:line:)-8xfxw`` + +### Customizing output + +- ``CustomDumpStringConvertible`` +- ``CustomDumpRepresentable`` +- ``CustomDumpReflectable`` diff --git a/Sources/CustomDump/Documentation.docc/Diff.md b/Sources/CustomDump/Documentation.docc/Diff.md new file mode 100644 index 00000000..edda5cb6 --- /dev/null +++ b/Sources/CustomDump/Documentation.docc/Diff.md @@ -0,0 +1,7 @@ +# ``CustomDump/diff(_:_:format:)`` + +## Topics + +### Formatting + +- ``DiffFormat`` diff --git a/Sources/CustomDump/Documentation.docc/XCTAssertDifference.md b/Sources/CustomDump/Documentation.docc/XCTAssertDifference.md new file mode 100644 index 00000000..9fde1206 --- /dev/null +++ b/Sources/CustomDump/Documentation.docc/XCTAssertDifference.md @@ -0,0 +1,7 @@ +# ``CustomDump/XCTAssertDifference(_:_:operation:changes:file:line:)-8xfxw`` + +## Topics + +### Async + +- ``XCTAssertDifference(_:_:operation:changes:file:line:)-3c9r9`` diff --git a/Sources/CustomDump/XCTAssertDifference.swift b/Sources/CustomDump/XCTAssertDifference.swift index 1fe25c58..dad583f9 100644 --- a/Sources/CustomDump/XCTAssertDifference.swift +++ b/Sources/CustomDump/XCTAssertDifference.swift @@ -5,7 +5,7 @@ import XCTestDynamicOverlay /// This function evaluates a given expression before and after a given operation and then compares /// the results. The comparison is done by invoking the `changes` closure with a mutable version of /// the initial value, and then asserting that the modifications made match the final value using -/// ``XCTAssertNoDifference``. +/// ``XCTAssertNoDifference(_:_:_:file:line:)``. /// /// For example, given a very simple counter structure, we can write a test against its incrementing /// functionality: @@ -105,7 +105,7 @@ public func XCTAssertDifference( /// Asserts that a value has a set of changes. /// -/// An async version of ``XCTAssertDifference(_:_:operation:changes:)``. +/// An async version of ``XCTAssertDifference(_:_:operation:changes:file:line:)-8xfxw``. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func XCTAssertDifference( _ expression: @autoclosure @Sendable () throws -> T, From 2766c0b7ac128efb67644d73f6ba2949ee353cd3 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 27 Nov 2023 11:36:46 -0800 Subject: [PATCH 2/2] wip --- Package@swift-5.6.swift => Package@swift-5.5.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Package@swift-5.6.swift => Package@swift-5.5.swift (96%) diff --git a/Package@swift-5.6.swift b/Package@swift-5.5.swift similarity index 96% rename from Package@swift-5.6.swift rename to Package@swift-5.5.swift index 47abbdd3..97034a51 100644 --- a/Package@swift-5.6.swift +++ b/Package@swift-5.5.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.5 import PackageDescription