Skip to content

Commit a69a7c6

Browse files
committed
Add support for handling Issues with lower severity
1 parent 2dcd912 commit a69a7c6

4 files changed

Lines changed: 102 additions & 46 deletions

File tree

Sources/PropertyBased/FixedSeedTrait.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public struct FixedSeedTrait: TestTrait, TestScoping {
4141
}
4242
}
4343

44-
if foundIssues == 0 {
44+
if foundIssues.errors == 0 {
4545
Issue.record(
4646
"A fixed seed was used, but no expectation failure occured. The property was not tested fully.",
4747
sourceLocation: sourceLocation)

Sources/PropertyBased/IssueCounting.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,34 @@
77

88
import Testing
99

10+
struct CountResult {
11+
var errors: Int = 0
12+
var warnings: Int = 0
13+
}
14+
1015
func countIssues(isolation: isolated (any Actor)? = #isolation, suppress: Bool, perform: () async throws -> Void) async
11-
-> Int
16+
-> CountResult
1217
{
13-
let found = Mutex(0)
18+
let found = Mutex(CountResult())
1419

1520
#if swift(>=6.2)
1621
nonisolated(unsafe) let closure = perform
1722

18-
let handler = IssueHandlingTrait.filterIssues { _ in
19-
found.withLock { $0 += 1 }
20-
return !suppress
23+
let handler = IssueHandlingTrait.compactMapIssues { input in
24+
var issue = input
25+
if FixedSeedTrait.fixedRandom != nil {
26+
issue.isWarning = false
27+
}
28+
29+
found.withLock {
30+
if issue.isWarning {
31+
$0.warnings += 1
32+
} else {
33+
$0.errors += 1
34+
}
35+
}
36+
guard !suppress else { return nil }
37+
return issue
2138
}
2239

2340
try? await handler.provideScope(for: Test.current!, testCase: Test.Case.current) {
@@ -27,14 +44,30 @@ func countIssues(isolation: isolated (any Actor)? = #isolation, suppress: Bool,
2744
try? await withKnownIssue(isIntermittent: true, isolation: isolation) {
2845
try await perform()
2946
} matching: { _ in
30-
found.withLock { $0 += 1 }
47+
found.withLock { $0.errors += 1 }
3148
return suppress
3249
}
3350
#endif
3451

3552
return found.withLock { $0 }
3653
}
3754

55+
extension Issue {
56+
#if swift(>=6.3)
57+
var isWarning: Bool {
58+
get { severity == .warning }
59+
set {
60+
severity = newValue ? .warning : .error
61+
}
62+
}
63+
#else
64+
var isWarning: Bool {
65+
get { false }
66+
set {}
67+
}
68+
#endif
69+
}
70+
3871
@inlinable
3972
func run(_ closure: () async throws -> Void, in isolation: isolated (any Actor)?) async throws {
4073
try await closure()

Sources/PropertyBased/PropertyCheck.swift

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public func propertyCheck<InputValue, ResultValue>(
115115
guard count > 0 else { return }
116116

117117
let fixedRng = FixedSeedTrait.fixedRandom
118+
var rngWithIssues: (rng: Xoshiro, value: InputValue, isError: Bool)?
118119

119120
let actualCount = fixedRng != nil ? 1 : count
120121

@@ -130,62 +131,84 @@ public func propertyCheck<InputValue, ResultValue>(
130131
try await body(resultValue)
131132
}
132133

133-
if foundIssues > 0 {
134-
let seed = rngCopy.traitHint
134+
if foundIssues.errors > 0 {
135+
rngWithIssues = (rngCopy, inputValue, isError: true)
136+
break
137+
} else if rngWithIssues == nil, foundIssues.warnings > 0 {
138+
rngWithIssues = (rngCopy, inputValue, isError: false)
139+
}
140+
}
135141

136-
var shrunkenInput = inputValue
142+
if let rngWithIssues {
143+
let seed = rngWithIssues.rng.traitHint
137144

138-
var didShrink = EnableShrinkTrait.isEnabled
139-
var shrinkCount = 0
140-
while didShrink {
141-
didShrink = false
142-
let candidates = input._shrinker(shrunkenInput)
145+
var shrunkenInput = rngWithIssues.value
143146

144-
for c in candidates {
145-
guard !Task.isCancelled else { return }
147+
var didShrink = EnableShrinkTrait.isEnabled
148+
var shrinkCount = 0
149+
while didShrink {
150+
didShrink = false
151+
let candidates = input._shrinker(shrunkenInput)
146152

147-
guard let mappedShrunk = input._mapFilter(c) else { continue }
153+
for c in candidates {
154+
guard !Task.isCancelled else { return }
148155

149-
let shrunkIssues = await countIssues(isolation: isolation, suppress: true) {
150-
try await body(mappedShrunk)
151-
}
156+
guard let mappedShrunk = input._mapFilter(c) else { continue }
152157

153-
if shrunkIssues > 0 {
154-
didShrink = true
155-
shrinkCount += 1
156-
shrunkenInput = c
157-
break
158-
}
158+
let shrunkIssues = await countIssues(isolation: isolation, suppress: true) {
159+
try await body(mappedShrunk)
159160
}
160-
}
161161

162-
// If previous inputs were suppressed, run the block one more time to fully record all issues.
163-
if EnableShrinkTrait.isEnabled {
164-
_ = await countIssues(isolation: isolation, suppress: false) {
165-
try await body(input._mapFilter(shrunkenInput)!)
162+
if shrunkIssues.errors > 0 || (shrunkIssues.warnings > 0 && !rngWithIssues.isError) {
163+
didShrink = true
164+
shrinkCount += 1
165+
shrunkenInput = c
166+
break
166167
}
167168
}
169+
}
170+
171+
// If previous inputs were suppressed, run the block one more time to fully record all issues.
172+
if EnableShrinkTrait.isEnabled {
173+
_ = await countIssues(isolation: isolation, suppress: false) {
174+
try await body(input._mapFilter(shrunkenInput)!)
175+
}
176+
}
168177

169-
let originalParamLabel = String(describingForTest: resultValue)
178+
let resultValue = input._mapFilter(rngWithIssues.value)!
179+
let originalParamLabel = String(describingForTest: resultValue)
170180

171-
let failureMessage: String
181+
let failureMessage: String
172182

173-
if shrinkCount == 0 {
174-
failureMessage = "Failure occured with input \(originalParamLabel)."
175-
} else {
176-
let shrunkParamLabel = String(describingForTest: input._mapFilter(shrunkenInput)!)
177-
failureMessage =
178-
"Failure occured with input \(shrunkParamLabel).\n(shrunk down from \(originalParamLabel) after \(shrinkCount) iteration\(shrinkCount != 1 ? "s" : ""))"
183+
let issueTypeLabel = rngWithIssues.isError ? "Failure" : "Warning"
184+
185+
if shrinkCount == 0 {
186+
failureMessage = "\(issueTypeLabel) occured with input \(originalParamLabel)."
187+
} else {
188+
let shrunkParamLabel = String(describingForTest: input._mapFilter(shrunkenInput)!)
189+
failureMessage =
190+
"\(issueTypeLabel) occured with input \(shrunkParamLabel).\n(shrunk down from \(originalParamLabel) after \(shrinkCount) iteration\(shrinkCount != 1 ? "s" : ""))"
191+
}
192+
193+
if fixedRng == nil {
194+
if rngWithIssues.isError {
195+
Issue.record(
196+
"\(failureMessage)\n\nAdd `.fixedSeed\(seed)` to the Test to reproduce this issue.",
197+
sourceLocation: sourceLocation)
179198
}
180199

181-
if fixedRng == nil {
200+
#if swift(>=6.3)
201+
if !rngWithIssues.isError {
182202
Issue.record(
183203
"\(failureMessage)\n\nAdd `.fixedSeed\(seed)` to the Test to reproduce this issue.",
204+
severity: .warning,
184205
sourceLocation: sourceLocation)
185-
} else {
186-
Issue.record("\(failureMessage)", sourceLocation: sourceLocation)
187206
}
188-
return
207+
#endif
208+
209+
} else {
210+
Issue.record("\(failureMessage)", sourceLocation: sourceLocation)
189211
}
212+
return
190213
}
191214
}

Tests/PropertyBasedTests/ActorTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Testing
2424
#expect(i < 500)
2525
}
2626
}
27-
#expect(issues > 0)
27+
#expect(issues.errors > 0)
2828
}
2929

3030
@OtherActor @Test func testOnOtherActor() async {
@@ -35,6 +35,6 @@ import Testing
3535
#expect(i < 500)
3636
}
3737
}
38-
#expect(issues > 0)
38+
#expect(issues.errors > 0)
3939
}
4040
}

0 commit comments

Comments
 (0)