defensive tests for double free seen in prod
This commit is contained in:
parent
f54faf4732
commit
4639edd813
1 changed files with 92 additions and 0 deletions
92
src/cache/store.zig
vendored
92
src/cache/store.zig
vendored
|
|
@ -1913,6 +1913,98 @@ test "writeMerged Dividend: field-level upgrade fills nulls (Tiingo-then-Polygon
|
|||
try std.testing.expectEqual(DividendType.regular, result.data[0].type);
|
||||
}
|
||||
|
||||
test "writeMerged Dividend: currency upgrade does not double-free" {
|
||||
// Tiingo writes a sparse record (no currency). Polygon's later
|
||||
// write supplies a heap-allocated currency string. The merge
|
||||
// path must not let `existing.currency = incoming.currency`
|
||||
// create two records that both believe they own the same
|
||||
// buffer, otherwise std.testing.allocator's double-free
|
||||
// detection trips when the caller's deinit runs later.
|
||||
const allocator = std.testing.allocator;
|
||||
const io = std.testing.io;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
|
||||
defer allocator.free(dir_path);
|
||||
|
||||
var s = Store.init(io, allocator, dir_path);
|
||||
|
||||
// Tiingo first: sparse — no currency.
|
||||
var tiingo_view = [_]Dividend{
|
||||
.{ .ex_date = Date.fromYmd(2024, 5, 15), .amount = 0.50 },
|
||||
};
|
||||
s.writeWithSource(Dividend, "TEST", tiingo_view[0..], .{ .seconds = Ttl.dividends }, "tiingo");
|
||||
|
||||
// Polygon second: same ex_date, but supplies currency. Caller
|
||||
// owns the heap allocation and frees it after writeMerged
|
||||
// returns (mirrors how Polygon's fetchDividends works in
|
||||
// production: returns slice with heap-allocated currency
|
||||
// strings, caller deinits).
|
||||
var polygon_view = [_]Dividend{
|
||||
.{
|
||||
.ex_date = Date.fromYmd(2024, 5, 15),
|
||||
.amount = 0.50,
|
||||
.currency = try allocator.dupe(u8, "USD"),
|
||||
},
|
||||
};
|
||||
defer for (polygon_view) |d| d.deinit(allocator);
|
||||
s.writeWithSource(Dividend, "TEST", polygon_view[0..], .{ .seconds = Ttl.dividends }, "polygon");
|
||||
|
||||
// Read back and verify the upgrade landed.
|
||||
const result = s.read(Dividend, "TEST", null, .any) orelse return error.NoCache;
|
||||
defer allocator.free(result.data);
|
||||
defer for (result.data) |d| d.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), result.data.len);
|
||||
try std.testing.expect(result.data[0].currency != null);
|
||||
try std.testing.expectEqualStrings("USD", result.data[0].currency.?);
|
||||
}
|
||||
|
||||
test "writeMerged Dividend: existing currency preserved on second write with different currency" {
|
||||
// Polygon writes USD first. A later write with a different
|
||||
// currency (CAD) must NOT overwrite — first non-null wins.
|
||||
// This exercises the path where both existing and incoming
|
||||
// have non-null currency strings, which is the trickiest
|
||||
// shape for the merge primitive's lifetime management.
|
||||
const allocator = std.testing.allocator;
|
||||
const io = std.testing.io;
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
|
||||
defer allocator.free(dir_path);
|
||||
|
||||
var s = Store.init(io, allocator, dir_path);
|
||||
|
||||
var first = [_]Dividend{
|
||||
.{
|
||||
.ex_date = Date.fromYmd(2024, 5, 15),
|
||||
.amount = 0.50,
|
||||
.currency = try allocator.dupe(u8, "USD"),
|
||||
},
|
||||
};
|
||||
defer for (first) |d| d.deinit(allocator);
|
||||
s.writeWithSource(Dividend, "TEST", first[0..], .{ .seconds = Ttl.dividends }, "polygon");
|
||||
|
||||
var second = [_]Dividend{
|
||||
.{
|
||||
.ex_date = Date.fromYmd(2024, 5, 15),
|
||||
.amount = 0.50,
|
||||
.currency = try allocator.dupe(u8, "CAD"),
|
||||
},
|
||||
};
|
||||
defer for (second) |d| d.deinit(allocator);
|
||||
s.writeWithSource(Dividend, "TEST", second[0..], .{ .seconds = Ttl.dividends }, "polygon");
|
||||
|
||||
const result = s.read(Dividend, "TEST", null, .any) orelse return error.NoCache;
|
||||
defer allocator.free(result.data);
|
||||
defer for (result.data) |d| d.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), result.data.len);
|
||||
try std.testing.expect(result.data[0].currency != null);
|
||||
// First write's currency wins.
|
||||
try std.testing.expectEqualStrings("USD", result.data[0].currency.?);
|
||||
}
|
||||
|
||||
test "writeMerged Dividend: type unknown counts as null and gets upgraded" {
|
||||
// Tiingo's dividend records always carry type = .unknown. A
|
||||
// later Polygon write with type = .regular must upgrade the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue