From 8c4d7e6de3c00ceb5dad8c2e6f7d4940c4f57407 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 19 May 2026 13:44:37 -0700 Subject: [PATCH] avoid output under test --- src/analytics/projections.zig | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/analytics/projections.zig b/src/analytics/projections.zig index af7cb34..5b3296b 100644 --- a/src/analytics/projections.zig +++ b/src/analytics/projections.zig @@ -10,11 +10,24 @@ /// - Success rate for a given spending level /// - Percentile bands of portfolio value at each year (for charting) const std = @import("std"); +const builtin = @import("builtin"); const log = std.log.scoped(.projections); const shiller = @import("../data/shiller.zig"); const srf = @import("srf"); const Date = @import("../Date.zig"); +/// `log.warn` wrapper that no-ops under `zig build test`. Used for +/// validation warnings emitted while parsing user-supplied +/// `projections.srf` records: the test suite intentionally feeds +/// invalid inputs to `validRetirementTarget` and the config-loader +/// to verify they're rejected, but the resulting stderr noise +/// pollutes test output. Keep using `log.warn` directly when the +/// warning is interesting in tests too. +fn warnUser(comptime fmt: []const u8, args: anytype) void { + if (builtin.is_test) return; + log.warn(fmt, args); +} + // ── Life events ───────────────────────────────────────────────── /// A resolved event ready for the simulation loop. All age-based timing @@ -522,7 +535,7 @@ pub fn parseProjectionsConfig(data: ?[]const u8) UserConfig { if (amt >= 0) { config.annual_contribution = amt; } else { - log.warn("projections: annual_contribution must be >= 0 (got {d}); ignoring record", .{amt}); + warnUser("projections: annual_contribution must be >= 0 (got {d}); ignoring record", .{amt}); } } if (c.contribution_inflation_adjusted) |b| { @@ -532,7 +545,7 @@ pub fn parseProjectionsConfig(data: ?[]const u8) UserConfig { if (amt >= 0) { config.target_spending = amt; } else { - log.warn("projections: target_spending must be >= 0 (got {d}); ignoring record", .{amt}); + warnUser("projections: target_spending must be >= 0 (got {d}); ignoring record", .{amt}); } } if (c.target_spending_inflation_adjusted) |b| { @@ -580,7 +593,7 @@ pub fn parseProjectionsConfig(data: ?[]const u8) UserConfig { // fall back to the default rule. Logged as a warning so the // user knows their override was ignored. if (annotation_count > 1) { - log.warn("projections: retirement_target set on multiple horizons; ignoring all annotations and using default promotion rule", .{}); + warnUser("projections: retirement_target set on multiple horizons; ignoring all annotations and using default promotion rule", .{}); config.horizon_targets = @splat(0); config.horizon_age_targets = @splat(0); } @@ -596,7 +609,7 @@ pub fn parseProjectionsConfig(data: ?[]const u8) UserConfig { fn validRetirementTarget(raw: ?u8) ?u8 { const v = raw orelse return null; if (v == 90 or v == 95 or v == 99) return v; - log.warn("projections: retirement_target must be 90, 95, or 99 (got {d}); annotation ignored", .{v}); + warnUser("projections: retirement_target must be 90, 95, or 99 (got {d}); annotation ignored", .{v}); return null; }