@@ -20,24 +20,66 @@ APIs or awaiting on an `async` callable in order to block test execution until
2020a callback is called, or an async callable returns. However, this requires the
2121code being tested to support callbacks or return a status as an async callable.
2222
23- This proposal adds another avenue for waiting for code to update to a specified
24- value, by proactively polling the test closure until it passes or a timeout is
25- reached.
26-
27- More concretely, we can imagine a type that updates its status over an
28- indefinite timeframe:
23+ Consider the following class, ` Aquarium ` , modeling raising dolphins:
2924
3025``` swift
31- actor Aquarium {
32- var dolphins: [Dolphin]
33-
34- func raiseDolphins () async {
35- // over a very long timeframe
36- dolphins.append (Dolphin ())
26+ @MainActor
27+ final class Aquarium {
28+ private (set ) var isRaising = false
29+ var hasFunding = true
30+
31+ func raiseDolphins () {
32+ Task {
33+ if hasFunding {
34+ isRaising = true
35+
36+ // Long running work that I'm not qualified to describe.
37+ // ...
38+
39+ isRaising = false
40+ }
41+ }
3742 }
3843}
3944```
4045
46+ As is, it is extremely difficult to check that ` isRaising ` is correctly set to
47+ true once ` raiseDolphins ` is called. The system offers test authors no
48+ control for when the created task runs, leaving test authors add arbitrary sleep
49+ calls. Like this example:
50+
51+ ``` swift
52+ @Test func `raiseDolphins if hasFunding sets isRaising to true `() async throws {
53+ let subject = Aquarium ()
54+ subject.hasFunding = true
55+
56+ subject.raiseDolphins ()
57+
58+ try await Task.sleep (for : .seconds (1 ))
59+
60+ #expect (subject.isRaising == true )
61+ }
62+ ```
63+
64+ This requires test authors to have to figure out how long to wait so that
65+ ` isRaising ` will reliably be set to true, while not waiting too long, such that
66+ the test suite is not unnecessarily delayed or task itself finishes.
67+
68+ As another example, imagine a test author wants to verify that no dolphins are
69+ raised when there isn't any funding. There isn't and can't be a mechanism for
70+ verifying that ` isRaising ` is never set to ` true ` , but if we constrain the
71+ problem to within a given timeframe, then we can have a reasonable assumption
72+ that ` isRaising ` remains set to false. Again, without some other mechanism to
73+ notify the test when to check ` isRaising ` , test authors are left to add
74+ arbitrary sleep calls, when having the ability to fail fast would save a not
75+ insignificant amount of time in the event that ` isRaising ` is mistakenly set to
76+ true.
77+
78+ This proposal introduces polling to help test authors address these cases. In
79+ this and other similar cases, polling makes these tests practical or even
80+ possible, as well as speeding up the execution of individual tests as well as
81+ the entire test suite.
82+
4183## Proposed solution
4284
4385This proposal introduces new members of the ` confirmation ` family of functions:
@@ -63,15 +105,26 @@ When `PollingStopCondition.stopsPassing` is specified, reaching the duration
63105stop point will mark the confirmation as passing.
64106
65107Tests will now be able to poll code updating in the background using either of
66- the stop conditions:
108+ the stop conditions. For the example of ` Aquarium.raiseDolphins ` , valid tests
109+ might look like:
67110
68111``` swift
69- let subject = Aquarium ()
70- Task {
71- await subject.raiseDolphins ()
112+ @Test func `raiseDolphins if hasFunding sets isRaising to true `() async throws {
113+ let subject = Aquarium ()
114+ subject.hasFunding = true
115+
116+ subject.raiseDolphins ()
117+
118+ try await confirmation (until : .firstPass ) { subject.isRaising == true }
72119}
73- await confirmation (until : .firstPass ) {
74- subject.dolphins .count == 1
120+
121+ @Test func `raiseDolphins if no funding keeps isRaising false `() async throws {
122+ let subject = Aquarium ()
123+ subject.hasFunding = false
124+
125+ subject.raiseDolphins ()
126+
127+ try await confirmation (until : .stopsPassing ) { subject.isRaising == false }
75128}
76129```
77130
@@ -368,6 +421,39 @@ Polling will be stopped when either:
368421- the closure returns a value that satisfies the stopping condition, or
369422- the closure throws an error.
370423
424+ ### When Polling should not be used
425+
426+ Polling is not a silver bullet, and should not be abused. In many cases, the
427+ problems that polling solves can be solved through other, better means. Such as
428+ the observability system, using Async sequences, callbacks, or delegates. When
429+ possible, implementation code which requires polling to be tested should be
430+ refactored to support other means. Polling exists for the case where such
431+ refactors are either not possible or require a large amount of overhead.
432+
433+ Polling introduces a small amount of instability to the tests - in the example
434+ of waiting for ` Aquarium.isRaising ` to be set to true, it is entirely possible
435+ that, unless the code covered by
436+ ` // Long running work that I'm not qualified to describe ` has a test-controlled
437+ means to block further execution, the created ` Task ` could finish between
438+ polling attempts - resulting ` Aquarium.isRaising ` to always be read as false,
439+ and failing the test despite the code having done the right thing.
440+
441+ Polling also only offers a snapshot in time of the state. When
442+ ` PollingStopCondition.firstPass ` is used, polling will stop and return a pass
443+ after the first time the ` body ` returns true, even if any subsequent calls
444+ would've returned false.
445+
446+ Furthermore, polling introduces delays to the running code. This isn't that
447+ much of a concern for ` PollingStopCondition.firstPass ` , where the passing
448+ case minimizes test execution time. However, the
449+ passing case when using ` PollingStopCondition.stopsPassing ` utilizes the full
450+ duration specified. If the test author specifies the polling duration to be
451+ 10 minutes, then the test will poll for approximately that long, so long as the
452+ polling body keeps returning true.
453+
454+ Despite all this, we think that polling is an extremely valuable tool, and is
455+ worth adding to the Testing library.
456+
371457## Source compatibility
372458
373459This is a new interface that is unlikely to collide with any existing
0 commit comments