Skip to content

Commit e313d70

Browse files
committed
Fix a few bugs
1 parent a3009ad commit e313d70

File tree

11 files changed

+541
-85
lines changed

11 files changed

+541
-85
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ BUILD_TYPE ?= Debug
66
BUILD_OPTS ?= -Doptimize=$(BUILD_TYPE)
77
JOBS ?= $(shell nproc || echo 2)
88

9-
# Helper macro to ensure the Zig compiler exists
9+
# Helper macro to guarantee the Zig compiler exists
1010
check_zig = \
1111
if [ ! -x "$(ZIG)" ]; then \
1212
echo "ERROR: Zig compiler not found at '$(ZIG)'."; \

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ following steps:
3838
2. Finding cases where the input causes the property to fail.
3939
3. Finding smaller subsets of the failing input that still cause the failure (this is called "shrinking").
4040

41-
For example, consider the property of a `reverse(s: string)` function that states that reversing a string twice should
42-
return the original string.
41+
For example, consider the property of a `reverse(s: []const u8)` function that states that reversing
42+
a string twice should return the original string.
4343
In property-based testing, you would define this property and let the framework generate a lot of random strings to
4444
test it.
4545
If it finds a string that makes the property fail (due to a bug in the reverse function, for example), it will then try
4646
to shrink that string to a simpler or shorter case that still makes the property fail.
4747

4848
Here is a brief comparison between example-based testing and property-based testing paradigms:
4949

50-
| Aspect | Example-based Testing | Property-based Testing |
51-
|---------------|--------------------------------|--------------------------------------------|
52-
| **Input** | Hand-written specific values | Auto-generated random values |
53-
| **Coverage** | Only cases you can think of | Discovers edge cases automatically |
54-
| **Debugging** | Exact failing inputs are known | Shrinks to minimal failing case |
55-
| **Effort** | Write a lot of test cases | Define one property, test with many inputs |
50+
| Criterion | Example-based Testing | Property-based Testing |
51+
|-----------|--------------------------------|--------------------------------------------|
52+
| Input | Hand-written specific values | Auto-generated random values |
53+
| Coverage | Only cases you can think of | Discovers edge cases automatically |
54+
| Debugging | Exact failing inputs are known | Shrinks to minimal failing case |
55+
| Effort | Write a lot of test cases | Define one property, test with many inputs |
5656

5757
### Why Minish?
5858

@@ -63,7 +63,7 @@ Here is a brief comparison between example-based testing and property-based test
6363
- Supports reproducible failures and verbose mode
6464
- Configurable and easy to integrate into existing Zig projects
6565

66-
See the [ROADMAP.md](ROADMAP.md) for the list of implemented and planned features.
66+
See [ROADMAP.md](ROADMAP.md) for the list of implemented and planned features.
6767

6868
> [!IMPORTANT]
6969
> Minish is in early development, so bugs and breaking changes are expected.

examples/e5_struct_and_combinators.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ fn adult_flag_is_consistent(person: Person) !void {
2222

2323
// Property: height should be reasonable for the age
2424
fn height_is_reasonable(person: Person) !void {
25-
// Very basic check - just ensure height isn't absurdly large or small
25+
// Very basic check - just make sure height isn't absurdly large or small
2626
try std.testing.expect(person.height_cm >= 30); // Even babies are >30cm
2727
try std.testing.expect(person.height_cm <= 250); // Very tall but possible
2828
}

examples/e7_hashmap_example.zig

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const gen = minish.gen;
44

55
// Property: getting a value we just put should return that value
66
fn put_then_get_returns_value(map: std.AutoHashMap(i32, i32)) !void {
7-
var mut_map = map;
7+
var mut_map = try map.clone();
88
defer mut_map.deinit();
99

1010
// Try putting and getting a known value
@@ -22,10 +22,8 @@ fn put_then_get_returns_value(map: std.AutoHashMap(i32, i32)) !void {
2222

2323
// Property: map size should match number of unique keys inserted
2424
fn map_count_matches_entries(map: std.AutoHashMap(i32, bool)) !void {
25-
var mut_map = map;
26-
defer mut_map.deinit();
27-
28-
const count = mut_map.count();
25+
// Treat as read-only, Minish will free it.
26+
const count = map.count();
2927

3028
// Count should be reasonable (not negative, not absurd)
3129
try std.testing.expect(count >= 0);
@@ -34,7 +32,7 @@ fn map_count_matches_entries(map: std.AutoHashMap(i32, bool)) !void {
3432

3533
// Property: removing a key that doesn't exist returns false
3634
fn remove_nonexistent_key(map: std.AutoHashMap(i32, i32)) !void {
37-
var mut_map = map;
35+
var mut_map = try map.clone();
3836
defer mut_map.deinit();
3937

4038
// Try to remove a key that's very unlikely to exist

examples/e8_misc_features.zig

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const std = @import("std");
2+
const minish = @import("minish");
3+
const gen = minish.gen;
4+
const combinators = minish.combinators;
5+
6+
// ============================================================================
7+
// Misc Features Example
8+
// ============================================================================
9+
// This example showcases features not covered in other examples:
10+
// 1. oneOf (Choice generator)
11+
// 2. dependent (Monadic-like sequencing)
12+
// 3. timestamps
13+
// 4. enums
14+
15+
// 1. oneOf: Generate values from mixed sources
16+
fn test_mixed_integers(val: i32) !void {
17+
// We expect either small ints (0-10) or a specific large constant (1000)
18+
const is_small = (val >= 0 and val <= 10);
19+
const is_large = (val == 1000);
20+
try std.testing.expect(is_small or is_large);
21+
}
22+
23+
// 2. dependent: Generate a boolean, then use it to select a generator
24+
// Dependent generator creates a struct { T, U }
25+
fn test_dependent_logic(pair: struct { bool, i32 }) !void {
26+
const is_pos = pair[0];
27+
const val = pair[1];
28+
29+
if (is_pos) {
30+
try std.testing.expect(val > 0);
31+
} else {
32+
try std.testing.expect(val < 0);
33+
}
34+
}
35+
36+
// 3. Timestamp generator
37+
fn test_timestamp_range(ts: i64) !void {
38+
// Check it's within our requested 1-hour window
39+
// 1672531200 = 2023-01-01 00:00:00 UTC
40+
// 1672534800 = 2023-01-01 01:00:00 UTC
41+
try std.testing.expect(ts >= 1672531200);
42+
try std.testing.expect(ts <= 1672534800);
43+
}
44+
45+
// 4. Enum generator
46+
const Color = enum { Red, Green, Blue };
47+
fn test_enum_colors(c: Color) !void {
48+
switch (c) {
49+
.Red, .Green, .Blue => {}, // All valid
50+
}
51+
}
52+
53+
pub fn main() !void {
54+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
55+
defer _ = gpa.deinit();
56+
const allocator = gpa.allocator();
57+
58+
std.debug.print("\n=== Misc Features Example ===\n\n", .{});
59+
60+
// 1. oneOf
61+
std.debug.print("Test 1: oneOf (mixed ranges)\n", .{});
62+
const mixed_gen = gen.oneOf(i32, &.{
63+
gen.intRange(i32, 0, 10),
64+
gen.constant(@as(i32, 1000)),
65+
});
66+
try minish.check(allocator, mixed_gen, test_mixed_integers, .{ .num_runs = 50 });
67+
68+
// 2. dependent
69+
// Note: dependent is limited to branching/selection logic for runtime values,
70+
// as Minish generators cannot easily capture runtime state in closures.
71+
std.debug.print("\nTest 2: dependent (bool -> branching logic)\n", .{});
72+
const Dep = struct {
73+
const bool_gen = gen.boolean();
74+
const make_int_gen = struct {
75+
fn make(b: bool) gen.Generator(i32) {
76+
if (b) {
77+
return gen.intRange(i32, 1, 100);
78+
} else {
79+
return gen.intRange(i32, -100, -1);
80+
}
81+
}
82+
}.make;
83+
const dep_gen = gen.dependent(bool, i32, bool_gen, make_int_gen);
84+
};
85+
try minish.check(allocator, Dep.dep_gen, test_dependent_logic, .{ .num_runs = 50 });
86+
87+
// 3. Timestamps
88+
std.debug.print("\nTest 3: Timestamp (1 hour range)\n", .{});
89+
const ts_gen = gen.timestampRange(1672531200, 1672534800);
90+
try minish.check(allocator, ts_gen, test_timestamp_range, .{ .num_runs = 50 });
91+
92+
// 4. Enums
93+
std.debug.print("\nTest 4: Enums\n", .{});
94+
const enum_gen = gen.enumValue(Color);
95+
try minish.check(allocator, enum_gen, test_enum_colors, .{ .num_runs = 50 });
96+
97+
std.debug.print("\nAll misc feature tests passed!\n", .{});
98+
}

src/lib.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
//!
33
//! A property-based testing framework for Zig, inspired by [QuickCheck](https://hackage.haskell.org/package/QuickCheck) and [Hypothesis](https://hypothesis.readthedocs.io/en/latest/).
44
//!
5+
//! Minish automatically generates random test cases and, on failure, minimises (shrinks)
6+
//! the input to the smallest value that still reproduces the failure. This makes debugging
7+
//! property violations a lot easier.
58
//! ## Quick Start
69
//!
710
//! ```zig
@@ -43,6 +46,9 @@ pub const combinators = @import("minish/combinators.zig");
4346
/// Internal test case state. Used by generators to make random choices.
4447
pub const TestCase = @import("minish/core.zig").TestCase;
4548

49+
/// Errors that can occur during generation.
50+
pub const GenError = @import("minish/core.zig").GenError;
51+
4652
/// Run property-based tests with the given generator and property function.
4753
pub const check = @import("minish/runner.zig").check;
4854

src/minish/combinators.zig

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ const Generator = gen.Generator;
1818
// ============================================================================
1919

2020
/// Transform the output of a generator using a mapping function.
21+
///
22+
/// Example:
23+
/// ```zig
24+
/// // Convert generated integers to strings
25+
/// const string_gen = combinators.map(i32, []const u8, gen.int(i32), intToString);
26+
/// ```
2127
pub fn map(
2228
comptime T: type,
2329
comptime U: type,
@@ -38,6 +44,12 @@ pub fn map(
3844
// ============================================================================
3945

4046
/// Chain generators - use the output of one generator to create another.
47+
///
48+
/// Example:
49+
/// ```zig
50+
/// // Generate a length, then a list of that length
51+
/// const list_gen = combinators.flatMap(usize, []const u8, gen.intRange(usize, 1, 10), makeListGen);
52+
/// ```
4153
pub fn flatMap(
4254
comptime T: type,
4355
comptime U: type,
@@ -60,6 +72,14 @@ pub fn flatMap(
6072

6173
/// Generate values that satisfy a predicate.
6274
/// WARNING: This can loop indefinitely if the predicate is rarely satisfied.
75+
///
76+
/// Example:
77+
/// ```zig
78+
/// const even_gen = combinators.filter(i32, gen.int(i32), isEven, 100);
79+
/// ```
80+
///
81+
/// Memory lifecycle: Generated values that fail the predicate are automatically freed if associated generator has a freeFn.
82+
/// The retained value is owned by the Minish runner.
6383
pub fn filter(
6484
comptime T: type,
6585
comptime base_gen: Generator(T),
@@ -74,11 +94,21 @@ pub fn filter(
7494
if (predicate(value)) {
7595
return value;
7696
}
97+
// Free rejected value if generator provides freeFn
98+
if (base_gen.freeFn) |freeFn| {
99+
freeFn(tc.allocator, value);
100+
}
77101
}
78102
return error.Overrun;
79103
}
104+
105+
fn free(allocator: std.mem.Allocator, value: T) void {
106+
if (base_gen.freeFn) |freeFn| {
107+
freeFn(allocator, value);
108+
}
109+
}
80110
};
81-
return .{ .generateFn = FilterGenerator.generate, .shrinkFn = null, .freeFn = null };
111+
return .{ .generateFn = FilterGenerator.generate, .shrinkFn = null, .freeFn = FilterGenerator.free };
82112
}
83113

84114
// ============================================================================
@@ -101,6 +131,18 @@ pub fn sized(
101131
// ============================================================================
102132

103133
/// Choose from generators with weighted probabilities.
134+
///
135+
/// Example:
136+
/// ```zig
137+
/// // 90% chance of 0, 10% chance of random int
138+
/// const biased_gen = combinators.frequency(i32, &.{
139+
/// .{ .weight = 90, .gen = gen.constant(@as(i32, 0)) },
140+
/// .{ .weight = 10, .gen = gen.int(i32) }
141+
/// });
142+
/// ```
143+
///
144+
/// Memory lifecycle: The returned value is owned by the Minish runner and will be freed automatically.
145+
/// Assumes all weighted generators share compatible memory management.
104146
pub fn frequency(
105147
comptime T: type,
106148
comptime weighted_gens: []const struct { weight: u64, gen: Generator(T) },
@@ -118,9 +160,62 @@ pub fn frequency(
118160
const idx = try tc.weightedChoice(&weights);
119161
return weighted_gens[idx].gen.generateFn(tc);
120162
}
163+
164+
fn free(allocator: std.mem.Allocator, value: T) void {
165+
// Assume homogeneity: use first generator's freeFn if available
166+
if (weighted_gens.len > 0 and weighted_gens[0].gen.freeFn != null) {
167+
weighted_gens[0].gen.freeFn.?(allocator, value);
168+
}
169+
}
121170
};
122-
return .{ .generateFn = FrequencyGenerator.generate, .shrinkFn = null, .freeFn = null };
171+
return .{ .generateFn = FrequencyGenerator.generate, .shrinkFn = null, .freeFn = FrequencyGenerator.free };
123172
}
124173

125174
// Note: Combinator tests are demonstrated in examples/e5_struct_and_combinators.zig
126175
// They cannot be easily unit tested due to comptime parameter requirements
176+
177+
test "combinator memory leak regression tests" {
178+
const runner = @import("runner.zig");
179+
const allocator = std.testing.allocator;
180+
const str_gen = comptime gen.string(.{ .min_len = 1, .max_len = 5 });
181+
182+
const opts = runner.Options{ .seed = 111, .num_runs = 10 };
183+
184+
const Props = struct {
185+
fn prop_no_op(_: []const u8) !void {}
186+
};
187+
188+
// Test Frequency
189+
const freq_gen = frequency([]const u8, &.{
190+
.{ .weight = 10, .gen = str_gen },
191+
.{ .weight = 10, .gen = str_gen },
192+
});
193+
try runner.check(allocator, freq_gen, Props.prop_no_op, opts);
194+
}
195+
196+
test "filter memory leak regression test" {
197+
const runner = @import("runner.zig");
198+
const allocator = std.testing.allocator;
199+
200+
// Generate strings, keep only those starting with 'A'.
201+
// Rejected strings (allocations) should be freed by filter.
202+
const str_gen = comptime gen.string(.{ .min_len = 1, .max_len = 5, .charset = .alphanumeric });
203+
204+
const startsWithA = struct {
205+
fn func(s: []const u8) bool {
206+
if (s.len == 0) return false;
207+
return s[0] == 'A';
208+
}
209+
}.func;
210+
211+
// We filter, max 1000 attempts to allow many rejections without failure.
212+
const filtered_gen = filter([]const u8, str_gen, startsWithA, 1000);
213+
214+
const opts = runner.Options{ .seed = 111, .num_runs = 10 };
215+
216+
const Props = struct {
217+
fn prop_no_op(_: []const u8) !void {}
218+
};
219+
220+
try runner.check(allocator, filtered_gen, Props.prop_no_op, opts);
221+
}

src/minish/core.zig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ pub const GenError = error{
2020
};
2121

2222
/// TestCase manages the state for a single property test run.
23-
/// It tracks random choices made during generation to enable shrinking.
23+
///
24+
/// Responsibilities:
25+
/// - **Randomness**: Provides the source of random choices for generators.
26+
/// - **Recording**: Records every choice made during generation. This trace is used
27+
/// reproduce failures or to guide shrinking (by modifying the trace).
28+
/// - **Shrinking**: When replaying for shrinking, can force specific choices.
2429
pub const TestCase = struct {
2530
allocator: Allocator,
2631
prng: DefaultPrng,

0 commit comments

Comments
 (0)