Skip to content

Commit cacc873

Browse files
authored
Add Shrinking functionality
1 parent 8443135 commit cacc873

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3431
-82
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.swift.gyb linguist-language=Swift

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ DerivedData/
66
.swiftpm/configuration/registries.json
77
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
88
.netrc
9+
10+
/gyb
11+
/gyb.py
12+
/__pycache__

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Lennard Sprong
3+
Copyright (c) 2025 Lennard Sprong. Portions copyright Point-Free, Inc.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
66

Package.resolved

Lines changed: 0 additions & 15 deletions
This file was deleted.

Package.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ let package = Package(
1111
targets: ["PropertyBased"]
1212
),
1313
],
14-
dependencies: [
15-
.package(url: "https://github.com/pointfreeco/swift-gen.git", from: "0.4.0"),
16-
],
14+
dependencies: [],
1715
targets: [
18-
.target(name: "PropertyBased", dependencies: [
19-
.product(name: "Gen", package: "swift-gen"),
20-
]),
16+
.target(
17+
name: "PropertyBased",
18+
dependencies: [],
19+
exclude: ["PropertyCheck+Pack.swift.gyb", "Zip.swift.gyb"]
20+
),
2121
.testTarget(
2222
name: "PropertyBasedTests",
23-
dependencies: ["PropertyBased"]
23+
dependencies: ["PropertyBased"],
24+
exclude: ["ZipTests.swift.gyb"]
2425
),
2526
]
2627
)

README.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ PropertyBased is a Swift 6 library that enables Property-Based Testing in `swift
77

88
Property-Based Testing can be used as an alternative for (or in addition to) testing with hardcoded values. Run tests with random values, and easily switch to specific values when debugging a test failure.
99

10-
This library uses [swift-gen by Point-Free](https://github.com/pointfreeco/swift-gen) for reproducible random generation.
10+
This project aims to support all platforms which can run Swift Testing, including platforms without [Foundation](https://developer.apple.com/documentation/foundation) support.
1111

1212
## Requirements
1313

@@ -25,21 +25,20 @@ import Testing
2525
import PropertyBased
2626

2727
@Test func testDuplication() async {
28-
await propertyCheck(input: .int(in: 0...100)) { n in
28+
await propertyCheck(input: Gen.int(in: 0...100)) { n in
2929
#expect(n + n == n * 2)
3030
}
3131
}
3232
```
3333
Example with multiple inputs, and a custom repeat count:
3434
```swift
3535
import Testing
36-
import Gen
3736
import PropertyBased
3837

39-
let stringCreator = Gen.letterOrNumber.string(of: .int(in: 1...10))
38+
let stringCreator = Gen.letterOrNumber.string(of: Gen.int(in: 1...10))
4039

4140
@Test func testStringRepeat() async {
42-
await propertyCheck(count: 500, input: stringCreator, .int(in: 0...5)) { str, n in
41+
await propertyCheck(count: 500, input: stringCreator, Gen.int(in: 0...5)) { str, n in
4342
let actual = String(repeating: str, count: n)
4443
#expect(actual.length == str.length * n)
4544
}
@@ -52,7 +51,7 @@ It's possible that a test only fails on very specific inputs that don't trigger
5251

5352
```swift
5453
@Test func failsSometimes() async {
55-
await propertyCheck(input: .int(in: 0...1000)) { n in
54+
await propertyCheck(input: Gen.int(in: 0...1000)) { n in
5655
#expect(n < 990)
5756
}
5857
}
@@ -70,19 +69,46 @@ You can supply the fixed seed to reproduce the issue every time.
7069
```swift
7170
@Test(.fixedSeed("aKPPWDEafU0CGMDYHef/ETcbYUyjWQvRVP1DTNy6qJk="))
7271
func failsSometimes() async {
73-
await propertyCheck(input: .int(in: 0...1000)) { n in
72+
await propertyCheck(input: Gen.int(in: 0...1000)) { n in
7473
#expect(n < 990)
7574
}
7675
}
7776
```
7877

79-
# Limitations
78+
# Shrinking
8079

81-
This library currently does not include shrinking functionality, which would allow for failing inputs to be reduced to simpler values (e.g. numbers closer to zero, or collections with fewer elements).
80+
> [!NOTE]
81+
> This feature is experimental, and disabled by default. The shrinking output will be very verbose, due to a limitation in Swift Testing.
8282
83-
1. The version of the Testing library that's bundled with Swift currently doesn't allow third-party plugins to change the behavior of issue reporting. Without changes, every intermediate step in the shrinking process will be reported as a new issue.
84-
2. Adding shrinker functions to `swift-gen` is possibly out of scope for that package. Since valid shrinker values depend on the specifications of the generator, a new fork of that project would be required in the future.
83+
When a failing case has been found, it's possible that the input is large and contrived, such as arrays with many elements. When _shrinking_ is enabled, PropertyBased will repeat a failing test until it finds the smallest possible input that still causes a failure.
84+
85+
For example, the following test fails when the given numbers sum to a value above a certain threshold:
86+
87+
```swift
88+
@Test func checkSumInRange() async {
89+
await propertyCheck(input: Gen.int(in: 0...100).array(of: 1...10)) { numbers in
90+
let sum = numbers.reduce(0, +)
91+
#expect(sum < 250)
92+
}
93+
}
94+
```
95+
96+
The generator could come up with an array like `[63, 61, 33, 53, 97, 68, 23, 16]`, which sums to `414`. Ideally, we want to have an input that sums to exactly `250`.
97+
98+
Enable the shrinker by adding the `shrinking` trait:
99+
100+
```swift
101+
@Test(.shrinking) func checkSumInRange() async
102+
```
103+
104+
After shrinking, the new failing case is `[46, 97, 68, 23, 16]`, which sums to exactly `250`. The first few elements have been removed, which the middle element has been reduced to be closer to the edge.
105+
106+
When using the built-in generators and the `zip` function, shrinkers will also be composed.
85107

86108
# License
87109

88-
Copyright (c) 2025 Lennard Sprong. [MIT License](./LICENSE)
110+
Copyright (c) 2025 Lennard Sprong.
111+
112+
Includes a modified version of [swift-gen](https://github.com/pointfreeco/swift-gen), which is Copyright (c) 2019 Point-Free, Inc.
113+
114+
[MIT License](./LICENSE)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// ClosedIntegerRange.swift
3+
// PropertyBased
4+
//
5+
// Created by Lennard Sprong on 13/05/2025.
6+
//
7+
8+
extension ClosedRange where Bound: FixedWidthInteger {
9+
/// Coerce any range to a ClosedRange.
10+
///
11+
/// If the range doesn't have a lower or upper bound, `Bound.min` and `Bound.max` are used respectively.
12+
@usableFromInline init(_ range: some RangeExpression<Bound>) {
13+
if !range.contains(Bound.max) {
14+
self = .init(range.relative(to: .min ..< .max))
15+
} else if range.contains(Bound.min) {
16+
self = .min ... .max
17+
} else if range.contains(Bound.max - 1) {
18+
self = range.relative(to: .min ..< .max).lowerBound ... .max
19+
} else {
20+
self = .max ... .max
21+
}
22+
}
23+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ``PropertyBased/Gen``
2+
3+
This namespace can be extended with custom generators, while hiding certain implementation details from call sites.
4+
5+
For example, a new generator for a `Int256` type could be added like this:
6+
7+
```swift
8+
extension Gen where Value == Int256 {
9+
static func int256() -> Generator<Int256, Shrink.Integral<Int256>> {
10+
Gen<Int256>.value()
11+
}
12+
}
13+
```
14+
15+
This new generator can now be used by writing the following:
16+
```swift
17+
Gen.int256()
18+
```
19+
20+
See ``Generator`` for transforming generators into collections and other values.
21+
22+
## Topics
23+
24+
### Generating numbers
25+
26+
- ``/Gen/int(in:)``
27+
- ``/Gen/float(in:)``
28+
- ``/Gen/double(in:)``
29+
- ``/Gen/bool``
30+
31+
### Generating strings
32+
33+
You can generate individual characters, and use ``/Generator/string(of:)`` to form strings.
34+
35+
- ``/Gen/letter``
36+
- ``/Gen/lowercaseLetter``
37+
- ``/Gen/uppercaseLetter``
38+
- ``/Gen/number``
39+
- ``/Gen/letterOrNumber``
40+
- ``/Gen/latin1``
41+
- ``/Gen/ascii``
42+
- ``/Gen/character(in:)``
43+
- ``/Gen/unicodeScalar(in:)``
44+
45+
### Handling immutable collections
46+
47+
- ``/Gen/case``
48+
- ``/Gen/element(of:)``
49+
- ``/Gen/shuffled(_:)``
50+
51+
### Generating specific number types
52+
53+
- ``/Gen/int8(in:)``
54+
- ``/Gen/int16(in:)``
55+
- ``/Gen/int32(in:)``
56+
- ``/Gen/int64(in:)``
57+
- ``/Gen/int128(in:)``
58+
- ``/Gen/uint(in:)``
59+
- ``/Gen/uint8(in:)``
60+
- ``/Gen/uint16(in:)``
61+
- ``/Gen/uint32(in:)``
62+
- ``/Gen/uint64(in:)``
63+
- ``/Gen/uint128(in:)``
64+
- ``/Gen/cgFloat(in:)``
65+
- ``/Gen/float16(in:)``
66+
67+
## See Also
68+
69+
- ``PropertyBased/Generator``
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ``PropertyBased/Generator``
2+
3+
## Topics
4+
5+
### Creating custom generators
6+
7+
- ``init(run:)``
8+
- ``init(run:shrink:)``
9+
10+
### Testing a generator
11+
12+
- ``run(using:)``
13+
14+
### Grouping generated values
15+
16+
- ``pair``
17+
- ``string(of:)``
18+
- ``array(of:)``
19+
- ``set(ofAtMost:)``
20+
- ``dictionary(ofAtMost:)``
21+
22+
### Transforming generators
23+
24+
- ``map(_:)-9wz4v``
25+
- ``compactMap(_:)``
26+
- ``filter(_:)``
27+
- ``optional``
28+
- ``asResult(withFailure:successRate:)``
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ``PropertyBased``
2+
3+
PropertyBased is a Swift 6 library that enables Property-Based Testing in `swift-testing`, similar to QuickCheck for Haskell or FsCheck for F# and C#.
4+
5+
Property-Based Testing can be used as an alternative for (or in addition to) testing with hardcoded values. Run tests with random values, and easily switch to specific values when debugging a test failure.
6+
7+
## Topics
8+
9+
### Getting started
10+
11+
- ``propertyCheck(count:input:perform:isolation:sourceLocation:)``
12+
- ``Gen``

0 commit comments

Comments
 (0)