Skip to content

Commit 157700b

Browse files
committed
Add Frequency generators
1 parent f1b0e72 commit 157700b

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// Gen+Frequency.swift
3+
// PropertyBased
4+
//
5+
// Created by Lennard Sprong on 20/06/2025.
6+
//
7+
8+
#if swift(>=6.2)
9+
extension Gen {
10+
public static func oneOf<Input>(_ generators: Generator<Value, AnySequence<Input>>...)
11+
-> Generator<Value, AnySequence<(index: Int, value: Input)>>
12+
{
13+
return frequency(
14+
generators.map { gen in (rate: 1.0, gen) }
15+
)
16+
}
17+
18+
public static func frequency<Input>(
19+
_ generators: [(rate: FloatLiteralType, gen: Generator<Value, AnySequence<Input>>)]
20+
)
21+
-> Generator<Value, AnySequence<(index: Int, value: Input)>>
22+
{
23+
var total: FloatLiteralType = 0
24+
let options = generators.map { rate, gen in
25+
precondition(rate >= 0, "Rate must be non-negative, found a rate of \(rate)")
26+
27+
total += rate
28+
return (limit: total, gen: gen)
29+
}
30+
31+
return Generator(
32+
run: { [total] rng in
33+
let pick = FloatLiteralType.random(in: 0..<total, using: &rng)
34+
let index = options.firstIndex { $0.limit > pick }! as Int
35+
36+
return (index: index, value: options[index].gen._runIntermediate(&rng))
37+
},
38+
shrink: { pair in
39+
let opt = options[pair.index]
40+
let shrunk = opt.gen._shrinker(pair.value).lazy.map { (index: pair.index, value: $0) }
41+
return AnySequence(shrunk)
42+
},
43+
finalResult: { pair in
44+
let opt = options[pair.index]
45+
return opt.gen._mapFilter(pair.value)
46+
}
47+
)
48+
}
49+
50+
@_disfavoredOverload
51+
public static func oneOf<each Seq: Sequence>(_ generators: repeat Generator<Value, each Seq>)
52+
-> Generator<Value, AnySequence<(index: Int, value: Any)>>
53+
{
54+
var gens: [(rate: FloatLiteralType, gen: Generator<Value, AnySequence<Any>>)] = []
55+
56+
for gen in repeat each generators {
57+
gens.append((rate: 1.0, gen.eraseToAny()))
58+
}
59+
60+
return frequency(gens)
61+
}
62+
63+
@_disfavoredOverload
64+
public static func frequency<each Seq: Sequence>(
65+
_ generators: repeat (rate: FloatLiteralType, gen: Generator<Value, each Seq>)
66+
)
67+
-> Generator<Value, AnySequence<(index: Int, value: Any)>>
68+
{
69+
var gens: [(rate: FloatLiteralType, gen: Generator<Value, AnySequence<Any>>)] = []
70+
71+
for (rate, gen) in repeat each generators {
72+
gens.append((rate: rate, gen.eraseToAny()))
73+
}
74+
75+
return frequency(gens)
76+
}
77+
}
78+
#endif // swift(>=6.2)

Sources/PropertyBased/Generator.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ extension Generator {
261261
}
262262

263263
extension Generator {
264-
/// Wrap the shrinking sequence into a type-erased `AnySequence` struct.
264+
/// Wrap the shrinking sequence into an `AnySequence` struct.
265265
///
266266
/// This can be used if multiple generators must have the exact same type.
267267
/// - Returns: A copy of this generator.
@@ -272,4 +272,22 @@ extension Generator {
272272
finalResult: _mapFilter
273273
)
274274
}
275+
276+
/// Wrap the shrinking sequence into a type-erased `AnySequence` struct.
277+
///
278+
/// This can be used if multiple generators must have the exact same type, and the underlying input value must also be hidden.
279+
/// - Returns: A copy of this generator.
280+
@inlinable public func eraseToAny() -> Generator<ResultValue, AnySequence<Any>> {
281+
return .init(
282+
run: { rng in
283+
self._runIntermediate(&rng) as Any
284+
},
285+
shrink: {
286+
AnySequence(_shrinker($0 as! InputValue).lazy.map { $0 as Any })
287+
},
288+
finalResult: {
289+
self._mapFilter($0 as! InputValue)
290+
}
291+
)
292+
}
275293
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// GenTests+Frequency.swift
3+
// PropertyBased
4+
//
5+
// Created by Lennard Sprong on 08/07/2025.
6+
//
7+
8+
import Testing
9+
10+
@testable import PropertyBased
11+
12+
#if swift(>=6.2)
13+
@Suite struct GenFrequencyTests {
14+
enum Choice: Hashable {
15+
case plain
16+
case number(Int)
17+
case text(String)
18+
}
19+
20+
// MARK: oneOf
21+
22+
static let unpackedGen = Gen.oneOf(
23+
Gen.always(Choice.plain).eraseToAny(),
24+
Gen.int().map(Choice.number).eraseToAny(),
25+
Gen.lowercaseLetter.string(of: 8).map(Choice.text).eraseToAny(),
26+
)
27+
28+
static let packedGen = Gen.oneOf(
29+
Gen.always(Choice.plain),
30+
Gen.int().map(Choice.number),
31+
Gen.lowercaseLetter.string(of: 8).map(Choice.text),
32+
)
33+
34+
static let generators = [(0, unpackedGen), (1, packedGen)]
35+
36+
@Test(arguments: generators)
37+
func testGenerateEnum(_ pair: (Int, Generator<Choice, AnySequence<(index: Int, value: Any)>>)) async {
38+
let gen = pair.1
39+
await testGen(gen)
40+
41+
await confirmation(expectedCount: 1...) { confirm1 in
42+
await confirmation(expectedCount: 1...) { confirm2 in
43+
await confirmation(expectedCount: 1...) { confirm3 in
44+
await propertyCheck(count: 200, input: gen) { item in
45+
switch item {
46+
case .plain:
47+
confirm1()
48+
case .number:
49+
confirm2()
50+
case .text:
51+
confirm3()
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
@Test(arguments: generators)
60+
func testShrinkChoice(_ pair: (Int, Generator<Choice, AnySequence<(index: Int, value: Any)>>)) async throws {
61+
let gen = pair.1
62+
let value = (index: 1, value: 500 as Any)
63+
let results = gen._shrinker(value).compactMap(gen._mapFilter)
64+
try #require(results.count > 1)
65+
#expect(results.first == .number(0))
66+
#expect(!results.contains(.number(500)))
67+
}
68+
69+
// MARK: frequency
70+
71+
static let unpackedFreqGen = Gen<Choice>.frequency([
72+
(1, Gen.int().map(Choice.number).eraseToAny()),
73+
(2.0, Gen.lowercaseLetter.string(of: 8).map(Choice.text).eraseToAny()),
74+
(0, Gen.always(Choice.plain).eraseToAny()),
75+
])
76+
77+
static let packedFreqGen = Gen.frequency(
78+
(1, Gen.int().map(Choice.number)),
79+
(2, Gen.lowercaseLetter.string(of: 8).map(Choice.text)),
80+
(0, Gen.always(Choice.plain)),
81+
)
82+
static let freqGenerators = [(0, unpackedFreqGen), (1, packedFreqGen)]
83+
84+
@Test(arguments: freqGenerators)
85+
func testGenerateWithFrequency(_ pair: (Int, Generator<Choice, AnySequence<(index: Int, value: Any)>>)) async {
86+
let gen = pair.1
87+
await testGen(gen)
88+
89+
await propertyCheck(count: 200, input: gen) { item in
90+
#expect(item != .plain)
91+
}
92+
}
93+
}
94+
#endif

0 commit comments

Comments
 (0)