File: Validation.md

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (343 lines) | stat: -rw-r--r-- 19,063 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# AsyncSequence Validation

* Author(s): [Philippe Hausler](https://github.com/phausler)
* Implementation: [AsyncSequenceValidation](https://github.com/apple/swift-async-algorithms/tree/main/Sources/AsyncSequenceValidation)

## Introduction

Testing is a critical area of focus for any package to make it robust, catch bugs, and explain the expected behaviors in a documented manner. Testing things that are asynchronous can be difficult, testing things that are asynchronous multiple times can be even more difficult.

Types that implement `AsyncSequence` can often be described in deterministic actions given particular inputs. For the inputs, the events can be described as a discrete set: values, errors being thrown, the terminal state of returning a `nil` value from the iterator, or advancing in time and not doing anything. Likewise, the expected output has a discrete set of events: values, errors being caught, the terminal state of receiving a `nil` value from the iterator, or advancing in time and not doing anything. 

## Proposed Solution

By restricting the domain space of values to `String` we can describe the events as a domain specific language, and with monospaced characters that domain space can be used to show values over time for both the input to an `AsyncSequence` but also the expected output. 

```swift
validate {
  "a--b--c---|"
  $0.inputs[0].map { $0.capitalized }
  "A--B--C---|"
}
```

This syntax can be accomplished with a confluence of utilizing some of the advanced features of XCTest, the concurrency runtime, and result builders. The diagram as listed flows as if each event that would propagate is an event flowing along a column but also it shows the expression progressed over time; describing each event. 

By utilizing result builders, this same function can accommodate more than one input specification for testing things like `merge`.

```swift
validate {
  "a-c--f-|"
  "-b-de-g|"
  merge($0.inputs[0], $0.inputs[1])
  "abcdefg|"
}
```

Normally testing a function like `merge` would result in either limited expectations or be stochastic in nature. Those approaches are to account for the potential ordering not being deterministic. Taking the approach of having explicit ordering of time defined by the diagram allows for the test to be predictable. That determinism is sourced directly from the input sequences and the expected correlative output sequence. In short, the syntax of the test inputs and expectations make the execution reliable.

The syntax is trivially parsable (and consequently customizable). By default, the events require only a limited subset of characters for control; such as the advancing in time `-`, or the termination of a sequence by returning nil `|`. However some events may produce strings greater than just one character, other events may happen at the same time, and there is also the cancellation event. This all culminates into a test theme definition of:

|  Symbol |  Description      | Example    |  
| ------- | ----------------- | ---------- |
|   `-`   | Advance time      | `"a--b--"` |
|   `\|`  | Termination       | `"ab-\|"`  |
|   `^`   | Thrown error      | `"ab-^"`   |
|   `;`   | Cancellation      | `"ab;-"`   |
|   `[`   | Begin group       | `"[ab]-"`  |
|   `]`   | End group         | `"[ab]-"`  |
|   `'`   | Begin/End Value   | `"'foo'-"` |
|   `,`   | Delay next        | `",[a,]b"` |

Because some events may take up more than one character and the alignment is important to the visual progression of events, spaces are not counted as part of the parsed events. A space means that no time is advanced and not event is produced or expected. This means that the string `"a -    -b- -"` is equivalent to `"a--b--"`.

Defining a custom theme can then be trivial, since the list of expected interactions are well known. For example an emoji based diagram can be easily constructed:

```swift
struct EmojiTokens: AsyncSequenceValidationTheme {
  func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token {
    switch character {
    case "➖": return .step
    case "❗️": return .error
    case "❌": return .finish
    case "➡️": return .beginValue
    case "⬅️": return .endValue
    case "⏳": return .delayNext
    case " ": return .skip
    default: return .value(String(character))
    }
  }
}

validate(theme: EmojiTokens()) {
  "➖🔴➖🟠➖🟡➖🟢➖❌"
  $0.inputs[0]
  "➖🔴➖🟠➖🟡➖🟢➖❌"
}
```

## Detailed Design

The public interface for this system comes in two parts: the `AsyncSequenceValidationDiagram` subsystem and the `XCTest` extensions. The most commonly interacted-with and most approachable portion is the `XCTest` extension.

```swift
extension XCTestCase {
  public func validate<Test: AsyncSequenceValidationTest, Theme: AsyncSequenceValidationTheme>(theme: Theme, @AsyncSequenceValidationDiagram _ build: (inout AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line)
  
  public func validate<Test: AsyncSequenceValidationTest>(@AsyncSequenceValidationDiagram _ build: (inout AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line)
}
```

These two methods break down to usage like some of the previously used examples. However, the reality of how it works is perhaps the more important portion. For example, the code listed below has some points of interest worth mentioning.

```swift
validate {
  "a--b--c---|"
  $0.inputs[0].map { item in await Task { item.capitalized }.value }
  "A--B--C---|"
}
``` 

The progression of the input sequence can be derived from the `$0.inputs[0]`. This is an `AsyncSequence` with the `Element` type of `String` which at the given input emits an `"a"` at tick 0, a `"b"` at tick 3, a `"c"` at tick 6 and a finish event at tick 10. The output of the middle expression `$0.inputs[0].map { item in await Task { item.capitalized }.value }` is expected to emit an `"A"` at tick 0, a `"B"` at tick 3, a `"C"` at tick 6 and a finish event at tick 10.

Careful readers may immediately recognize that the `map` function is asynchronous and schedules work on a separate task. Normally, this would pose a distinct hazard to deterministic testing for timing of events. However, the `validate` utilizes a specialized hook into the Swift concurrency runtime to schedule the events stepwise and deterministically. This works in a two-fold manner: first, it uses a custom `Clock` to schedule events. Second, it ties that clock into a task driver that ensures enqueued jobs are executed in lockstep with that clock.

The underpinnings to make that work are the actual `AsyncSequenceValidationDiagram` subsystem. The `XCTest` interface does offer a considerably more simple surface area so the diagrams will be broken down into a few key sections for approachability. Those sections are the result builder, the diagram clock, the inputs, themes, and expectations/tests.

### Result Builder

The result builder syntax allows for simple and concise diagrams to be built. Those diagrams can come in a few forms, ranging from no inputs to three inputs. It is worth noting the implementation is not limited to just three inputs and can easily be expanded to more as we deem it needed. The builder itself uses the multiple parameter build block functions to ensure the proper ordering of inputs, tested sequences, and outputs. 

```swift
@resultBuilder
public struct AsyncSequenceValidationDiagram : Sendable {
  public static func buildBlock<Operation: AsyncSequence>(
    _ sequence: Operation,
    _ output: String
  ) -> some AsyncSequenceValidationTest where Operation.Element == String
  
  public static func buildBlock<Operation: AsyncSequence>(
    _ input: String, 
    _ sequence: Operation, 
    _ output: String
  ) -> some AsyncSequenceValidationTest where Operation.Element == String
  
  public static func buildBlock<Operation: AsyncSequence>(
    _ input1: String, 
    _ input2: String, 
    _ sequence: Operation, 
    _ output: String
  ) -> some AsyncSequenceValidationTest where Operation.Element == String 
  
  public static func buildBlock<Operation: AsyncSequence>(
    _ input1: String, 
    _ input2: String, 
    _ input3: String, 
    _ sequence: Operation, 
    _ output: String
  ) -> some AsyncSequenceValidationTest where Operation.Element == String
  
  public var inputs: InputList { get }
  public var clock: Clock { get }
}
```

The `AsyncSequenceValidationTest`, `InputList`, and `Clock` will be covered in subsequent sections.

### Validation Diagram Clock

One of the key functionalities of the validation diagrams is being able to control time. For proper usage of this testing infrastructure, all clock sources must be tied to the `AsyncSequenceValidationDiagram.Clock` that is exposed on the diagram itself. This is the heartbeat of how each columnar input and expectation are produced and consumed. It measures time in an integral manner of `steps`. One step is advanced per event symbol; in the default ASCII diagrams that means:

*  `-`, `;`, `|`, `^`, and any character value event.
* Quoted values like `"'foo'"`.
* Grouped events like `"[ab]"`.

```swift
extension AsyncSequenceValidationDiagram {
  public struct Clock { }
}

extension AsyncSequenceValidationDiagram.Clock: Clock {
  public struct Step: DurationProtocol, Hashable, CustomStringConvertible {
    public static func + (lhs: Step, rhs: Step) -> Step
    public static func - (lhs: Step, rhs: Step) -> Step
    public static func / (lhs: Step, rhs: Int) -> Step
    public static func * (lhs: Step, rhs: Int) -> Step
    public static func / (lhs: Step, rhs: Step) -> Double
    public static func < (lhs: Step, rhs: Step) -> Bool
  
    public static var zero: Step
  
    public static func steps(_ amount: Int) -> Step
  }
  
  public struct Instant: InstantProtocol, CustomStringConvertible {
    public func advanced(by duration: Step) -> Instant
    
    public func duration(to other: Instant) -> Step
  }
  
  public var now: Instant { get }
  public var minimumResolution: Step { get }
  
  public func sleep(
    until deadline: Instant,
    tolerance: Step? = nil
  ) async throws
}
```

Key notes: the `minimumResolution` of the `AsyncSequenceValidationDiagram.Clock` is fixed at `.steps(1)`, and the tolerance to the `sleep` function is ignored. These two behaviors were chosen because there is no sub-step granularity besides the order of execution and any coalescing due to tolerance would detract from the explicit expectations of deterministic execution order.

### Inputs

The inputs to the validation diagram are lazily constructed with the input parameters built by the result builder syntax. The inputs are `Sendable` and `AsyncSequence` conforming types that have their `Element` defined as `String`. The elements are produced as defined by the input specification in the result builder. This means that on each tick that an element is defined, the `next` function will resume to return that element (or return `nil` or throw an error, depending on the input specification). The `InputList` grants access to the defined inputs lazily. 

```swift
extension AsyncSequenceValidationDiagram {
  public struct Input: AsyncSequence, Sendable {
    public typealias Element = String
    
    public struct Iterator: AsyncIteratorProtocol {
      public mutating func next() async throws -> String?
    }
    
    public func makeAsyncIterator() -> Iterator 
  }
  
  public struct InputList: RandomAccessCollection, Sendable {
    public typealias Element = Input
  }
}
```

Access to the validation diagram input list is done through calls such as `$0.inputs[0]` seen in other examples. This access fetches lazily the first input specification and creates an `Input` `AsyncSequence` out of that domain specific language symbology. 

### Themes

|  Symbol | Token                     |  Description      | Example    |  
| ------- | ------------------------- | ----------------- | ---------- |
|   `-`   | `.step`                   | Advance time      | `"a--b--"` |
|   `\|`   | `.finish`                 | Termination       | `"ab-\|"`   |
|   `^`   | `.error`                  | Thrown error      | `"ab-^"`   |
|   `;`   | `.cancel`                 | Cancellation      | `"ab;-"`   |
|   `[`   | `.beginGroup`             | Begin group       | `"[ab]-"`  |
|   `]`   | `.endGroup`               | End group         | `"[ab]-"`  |
|   `'`   | `.beginValue` `.endValue` | Begin/End Value   | `"'foo'-"` |
|   `,`   | `.delayNext`              | Delay next        | `",[a,]b"` |
|   ` `   | `.skip`                   | Skip/Ignore       | `"a b- \|"` |
|         | `.value`                  | Values.           | `"ab-\|"`   |

There are some diagram input specifications that are not valid. The three cases are:

* A step being specified in a group (`"[a-]b|"`).
* A nested group (`"[[ab]]|"`).
* An unbalanced nesting (`"[ab|"`).

```swift
public protocol AsyncSequenceValidationTheme {
  func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token
}

extension AsyncSequenceValidationTheme where Self == AsyncSequenceValidationDiagram.ASCIITheme {
  public static var ascii: AsyncSequenceValidationDiagram.ASCIITheme
}

extension AsyncSequenceValidationDiagram {
  public enum Token {
    case step
    case error
    case finish
    case cancel
    case delayNext
    case beginValue
    case endValue
    case beginGroup
    case endGroup
    case skip
    case value(String)
  }
  
  public struct ASCIITheme: AsyncSequenceValidationTheme {
    public func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token
  }
}
```

### Expectations and Tests

This set of interfaces are the primary mechanism in which the simplified XCTest extension rests upon. 

Expectations defined by the domain specific language symbology can be roughly expressed as expected results and actual results. This notably avoids cancellation and steps, since those are better expressed through the failure reporting system. The expectation failures can express the combination of these expected and actual values. This can also show when the expectation failure occurred and the kind of expectation failure that happened, along with the payload of the actual and expected values.

```swift
extension AsyncSequenceValidationDiagram {
  public struct ExpectationResult {
    public var expected: [(Clock.Instant, Result<String?, Error>)]
    public var actual: [(Clock.Instant, Result<String?, Error>)]
  }
  
  public struct ExpectationFailure: CustomStringConvertible {
    public enum Kind {
      case expectedFinishButGotValue(String)
      case expectedMismatch(String, String)
      case expectedValueButGotFinished(String)
      case expectedFailureButGotValue(Error, String)
      case expectedFailureButGotFinish(Error)
      case expectedValueButGotFailure(String, Error)
      case expectedFinishButGotFailure(Error)
      case expectedValue(String)
      case expectedFinish
      case expectedFailure(Error)
      case unexpectedValue(String)
      case unexpectedFinish
      case unexpectedFailure(Error)
    }
    
    public var when: Clock.Instant
    public var kind: Kind
  }
}
```

The testing itself reduces down to two methods, one being a default theme parameter of `.ascii`. The test methods execute the validation diagram using a custom scheduling hook from the concurrency runtime such that all events are sequentially processed on a single cooperatively-multitasking executed thread. That thread is responsible for ensuring the ordering of the events and the execution of each time delineation such that the order of emissions of any input events are sequential top to bottom: input 0 is emitted first, then input 1, etc. After the ordering of input events, the jobs enqueued onto that task driver thread are executed in order of receipt. This ensures the overall order of execution is stable and deterministic but, most importantly, predictable.

```swift
public protocol AsyncSequenceValidationTest: Sendable {
  var inputs: [String] { get }
  var output: String { get }
  
  func test(_ event: (String) -> Void) async throws
}

extension AsyncSequenceValidationDiagram {
  public static func test<Test: AsyncSequenceValidationTest, Theme: AsyncSequenceValidationTheme>(
    theme: Theme,
    @AsyncSequenceValidationDiagram _ build: (inout AsyncSequenceValidationDiagram) -> Test
  ) throws -> (ExpectationResult, [ExpectationFailure])
  
  public static func test<Test: AsyncSequenceValidationTest>(
    @AsyncSequenceValidationDiagram _ build: (inout AsyncSequenceValidationDiagram) -> Test
  ) throws -> (ExpectationResult, [ExpectationFailure])
}
```

## Future Directions/Improvements

The emoji diagram theme could be made to be a built-in system. It makes for really flashy slides/demos and makes it really easy to see what is going on, but at the cost of being slightly harder to type.

The testing infrastructure could support (with minor alteration) testing iteration beyond the terminal cases, either errors being thrown from the iterator or past the first `nil` return value from `next`. This could help enforce some of the semantical expectations of `AsyncSequence`.

In addition to hooking into the runtime for execution of jobs, the deferred execution of jobs could also be hooked so that a time scale conversion could be made such that any sleep using any clock could map directly to the validation diagram internal clock ticks. 

The testing infrastructure could also have support for testing for values that do not have specified order for a given tick. Some race conditions from external systems not under the control of the concurrency runtime may not be accountable. If that were to be a consideration, unordered group tokens could be added. For example the symbols `{` and `}` could be used to represent a group that is unordered. However, since there are not any cases where this really seems useful for the swift-async-algorithms package, this is not a priority at this time.

## Alternatives Considered

The validation diagram system could be retrofitted to accommodate other value types other than strings, however most use cases can easily be expressed in a readable form with minor adjustments to use strings. 

The builder functions could pass in N-ary variants of the diagram to enforce the inputs to be specific instead of accessed via the lazy `InputList`. As we may potentially add additional numbers of inputs in the future this seems like a less maintainable implementation even though it may offer slightly more safety and only marginally better spelling,  i.e., `$0.inputs[0]` versus `$0.input0` etc.

## Credits/Inspiration

Some of the major sources of inspiration for the validation diagram system were https://rxjs.dev/guide/testing/marble-testing and the graphical representations from https://rxmarbles.com. Both of these were immensely useful to help discuss the expected behaviors of how asynchronous sequences should behave.