Skip to content

Commit 8e44fc9

Browse files
authored
Fix a few bugs (#1)
1 parent e313d70 commit 8e44fc9

File tree

9 files changed

+718
-104
lines changed

9 files changed

+718
-104
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ 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: []const u8)` function that states that reversing
41+
For example, consider the property of a `reverse` function that states that reversing
4242
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.
@@ -112,6 +112,15 @@ const std = @import("std");
112112
const minish = @import("minish");
113113
const gen = minish.gen;
114114
115+
// Helper function to reverse a string
116+
fn reverse(allocator: std.mem.Allocator, s: []const u8) ![]u8 {
117+
const result = try allocator.alloc(u8, s.len);
118+
for (s, 0..) |c, i| {
119+
result[s.len - 1 - i] = c;
120+
}
121+
return result;
122+
}
123+
115124
// Property: reversing a string twice returns the original
116125
fn reverse_twice_is_identity(s: []const u8) !void {
117126
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .minish,
3-
.version = "0.1.0-alpha.1",
3+
.version = "0.1.0",
44
.fingerprint = 0x4a0e1bdb4d520b49, // Changing this has security and trust implications.
55
.minimum_zig_version = "0.15.2",
66
.dependencies = .{ .chilli = .{

logo.svg

Lines changed: 92 additions & 92 deletions
Loading

src/lib.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
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)
5+
//! Minish automatically generates random test cases and, on failure, minimises (or shrinks)
66
//! the input to the smallest value that still reproduces the failure. This makes debugging
77
//! property violations a lot easier.
8+
//!
89
//! ## Quick Start
910
//!
1011
//! ```zig
@@ -49,7 +50,7 @@ pub const TestCase = @import("minish/core.zig").TestCase;
4950
/// Errors that can occur during generation.
5051
pub const GenError = @import("minish/core.zig").GenError;
5152

52-
/// Run property-based tests with the given generator and property function.
53+
/// Run the tests with the given generator and property function.
5354
pub const check = @import("minish/runner.zig").check;
5455

5556
/// Configuration options for property tests.

src/minish/combinators.zig

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const Generator = gen.Generator;
2424
/// // Convert generated integers to strings
2525
/// const string_gen = combinators.map(i32, []const u8, gen.int(i32), intToString);
2626
/// ```
27+
///
28+
/// Note: If base_gen allocates memory and map_fn transforms to a different type,
29+
/// the base value is freed after transformation. If map_fn returns a type that
30+
/// also needs freeing, you must provide that through the result generator.
2731
pub fn map(
2832
comptime T: type,
2933
comptime U: type,
@@ -33,9 +37,16 @@ pub fn map(
3337
const MapGenerator = struct {
3438
fn generate(tc: *TestCase) core.GenError!U {
3539
const base_value = try base_gen.generateFn(tc);
36-
return map_fn(base_value);
40+
const result = map_fn(base_value);
41+
// Free the base value after transformation if it was allocated
42+
if (base_gen.freeFn) |freeFn| {
43+
freeFn(tc.allocator, base_value);
44+
}
45+
return result;
3746
}
3847
};
48+
// Note: freeFn is null because the mapped result U may have different
49+
// memory semantics. Users should use a wrapper if U needs freeing.
3950
return .{ .generateFn = MapGenerator.generate, .shrinkFn = null, .freeFn = null };
4051
}
4152

@@ -50,6 +61,8 @@ pub fn map(
5061
/// // Generate a length, then a list of that length
5162
/// const list_gen = combinators.flatMap(usize, []const u8, gen.intRange(usize, 1, 10), makeListGen);
5263
/// ```
64+
///
65+
/// Note: The base value is freed after the next generator is created and produces a result.
5366
pub fn flatMap(
5467
comptime T: type,
5568
comptime U: type,
@@ -60,7 +73,20 @@ pub fn flatMap(
6073
fn generate(tc: *TestCase) core.GenError!U {
6174
const base_value = try base_gen.generateFn(tc);
6275
const next_gen = flat_fn(base_value);
63-
return next_gen.generateFn(tc);
76+
const result = try next_gen.generateFn(tc);
77+
// Free the base value after we're done using it
78+
if (base_gen.freeFn) |freeFn| {
79+
freeFn(tc.allocator, base_value);
80+
}
81+
return result;
82+
}
83+
84+
fn free(allocator: std.mem.Allocator, value: U) void {
85+
// Try to free using the result generator's freeFn
86+
// Note: This is a best-effort approach since we don't know which
87+
// specific generator was used (depends on base_value at runtime)
88+
_ = allocator;
89+
_ = value;
6490
}
6591
};
6692
return .{ .generateFn = FlatMapGenerator.generate, .shrinkFn = null, .freeFn = null };
@@ -219,3 +245,66 @@ test "filter memory leak regression test" {
219245

220246
try runner.check(allocator, filtered_gen, Props.prop_no_op, opts);
221247
}
248+
249+
test "regression: map combinator frees base value" {
250+
// Bug: Map combinator didn't free the base value after transformation
251+
// Fix: Added freeFn call after map_fn is applied
252+
const runner = @import("runner.zig");
253+
const allocator = std.testing.allocator;
254+
255+
// Generate a string (which allocates) and map it to its length (which doesn't)
256+
const str_gen = comptime gen.string(.{ .min_len = 1, .max_len = 10 });
257+
258+
const getLen = struct {
259+
fn func(s: []const u8) usize {
260+
return s.len;
261+
}
262+
}.func;
263+
264+
const len_gen = map([]const u8, usize, str_gen, getLen);
265+
266+
const opts = runner.Options{ .seed = 222, .num_runs = 20 };
267+
268+
const Props = struct {
269+
fn prop_check_len(len: usize) !void {
270+
// Just verify the length is in expected range
271+
try std.testing.expect(len >= 1 and len <= 10);
272+
}
273+
};
274+
275+
// If map doesn't free the base string, this will leak memory
276+
// and the test allocator will catch it
277+
try runner.check(allocator, len_gen, Props.prop_check_len, opts);
278+
}
279+
280+
test "flatMap combinator chains generators" {
281+
const runner = @import("runner.zig");
282+
const allocator = std.testing.allocator;
283+
284+
// Generate a number, then use it to determine which generator to use
285+
const makeGen = struct {
286+
fn make(x: i32) gen.Generator(i32) {
287+
// If x is even, return 0; if odd, return 1
288+
if (@mod(x, 2) == 0) {
289+
return gen.constant(@as(i32, 0));
290+
} else {
291+
return gen.constant(@as(i32, 1));
292+
}
293+
}
294+
}.make;
295+
296+
const flat_gen = flatMap(i32, i32, gen.intRange(i32, 0, 10), makeGen);
297+
298+
const Props = struct {
299+
fn prop(x: i32) !void {
300+
// Result should be either 0 or 1
301+
try std.testing.expect(x == 0 or x == 1);
302+
}
303+
};
304+
305+
try runner.check(allocator, flat_gen, Props.prop, .{ .seed = 333, .num_runs = 20 });
306+
}
307+
308+
// Note: sized combinator not tested because it requires generators that take
309+
// runtime size parameters, but most generators use comptime parameters.
310+
// It's primarily for custom use cases.

0 commit comments

Comments
 (0)