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);
|
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" {
|
test "writeMerged Dividend: type unknown counts as null and gets upgraded" {
|
||||||
// Tiingo's dividend records always carry type = .unknown. A
|
// Tiingo's dividend records always carry type = .unknown. A
|
||||||
// later Polygon write with type = .regular must upgrade the
|
// later Polygon write with type = .regular must upgrade the
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue