add test-debug option for cleaner tests

This commit is contained in:
Emil Lerch 2025-07-14 07:09:20 -07:00
parent a0f8b27ea5
commit a8f802c02f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 89 additions and 30 deletions

View file

@ -65,6 +65,27 @@ Run the test suite:
zig build test zig build test
``` ```
Run integration tests:
```bash
zig build test -Dintegration=true
```
Enable debug output in tests (useful for debugging test failures):
```bash
zig build test -Dintegration=true -Dtest-debug=true
```
Test individual providers:
```bash
zig build test-github
zig build test-gitlab
zig build test-codeberg
zig build test-sourcehut
```
## Deployment ## Deployment
This tool is designed to be run periodically (e.g., via cron) and commit the generated RSS file to a Git repository that can be deployed via Cloudflare Pages or similar static hosting services. This tool is designed to be run periodically (e.g., via cron) and commit the generated RSS file to a Git repository that can be deployed via Cloudflare Pages or similar static hosting services.
@ -72,4 +93,4 @@ This tool is designed to be run periodically (e.g., via cron) and commit the gen
Example cron job (runs every hour): Example cron job (runs every hour):
```bash ```bash
0 * * * * cd /path/to/release-tracker && ./zig-out/bin/release-tracker config.json && git add releases.xml && git commit -m "Update releases" && git push 0 * * * * cd /path/to/release-tracker && ./zig-out/bin/release-tracker config.json && git add releases.xml && git commit -m "Update releases" && git push
``` ```

View file

@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const integration = b.option(bool, "integration", "Run integration tests") orelse false; const integration = b.option(bool, "integration", "Run integration tests") orelse false;
const provider = b.option([]const u8, "provider", "Test specific provider (github, gitlab, codeberg, sourcehut)"); const provider = b.option([]const u8, "provider", "Test specific provider (github, gitlab, codeberg, sourcehut)");
const test_debug = b.option(bool, "test-debug", "Enable debug output in tests") orelse false;
// Add Zeit dependency // Add Zeit dependency
const zeit_dep = b.dependency("zeit", .{ const zeit_dep = b.dependency("zeit", .{
@ -41,6 +42,10 @@ pub fn build(b: *std.Build) void {
unit_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); unit_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const test_debug_option = b.addOptions();
test_debug_option.addOption(bool, "test_debug", test_debug);
unit_tests.root_module.addOptions("build_options", test_debug_option);
const run_unit_tests = b.addRunArtifact(unit_tests); const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
@ -57,6 +62,10 @@ pub fn build(b: *std.Build) void {
integration_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); integration_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const integration_test_debug_option = b.addOptions();
integration_test_debug_option.addOption(bool, "test_debug", test_debug);
integration_tests.root_module.addOptions("build_options", integration_test_debug_option);
// Add filter for specific provider if specified // Add filter for specific provider if specified
if (provider) |p| { if (provider) |p| {
const filter = std.fmt.allocPrint(b.allocator, "{s} provider", .{p}) catch @panic("OOM"); const filter = std.fmt.allocPrint(b.allocator, "{s} provider", .{p}) catch @panic("OOM");
@ -81,6 +90,9 @@ pub fn build(b: *std.Build) void {
.filters = &[_][]const u8{"GitHub provider"}, .filters = &[_][]const u8{"GitHub provider"},
}); });
github_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); github_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const github_test_debug_option = b.addOptions();
github_test_debug_option.addOption(bool, "test_debug", test_debug);
github_tests.root_module.addOptions("build_options", github_test_debug_option);
const gitlab_tests = b.addTest(.{ const gitlab_tests = b.addTest(.{
.name = "gitlab-tests", .name = "gitlab-tests",
@ -90,6 +102,9 @@ pub fn build(b: *std.Build) void {
.filters = &[_][]const u8{"GitLab provider"}, .filters = &[_][]const u8{"GitLab provider"},
}); });
gitlab_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); gitlab_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const gitlab_test_debug_option = b.addOptions();
gitlab_test_debug_option.addOption(bool, "test_debug", test_debug);
gitlab_tests.root_module.addOptions("build_options", gitlab_test_debug_option);
const codeberg_tests = b.addTest(.{ const codeberg_tests = b.addTest(.{
.name = "codeberg-tests", .name = "codeberg-tests",
@ -99,6 +114,9 @@ pub fn build(b: *std.Build) void {
.filters = &[_][]const u8{"Codeberg provider"}, .filters = &[_][]const u8{"Codeberg provider"},
}); });
codeberg_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); codeberg_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const codeberg_test_debug_option = b.addOptions();
codeberg_test_debug_option.addOption(bool, "test_debug", test_debug);
codeberg_tests.root_module.addOptions("build_options", codeberg_test_debug_option);
const sourcehut_tests = b.addTest(.{ const sourcehut_tests = b.addTest(.{
.name = "sourcehut-tests", .name = "sourcehut-tests",
@ -108,6 +126,9 @@ pub fn build(b: *std.Build) void {
.filters = &[_][]const u8{"SourceHut provider"}, .filters = &[_][]const u8{"SourceHut provider"},
}); });
sourcehut_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); sourcehut_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const sourcehut_test_debug_option = b.addOptions();
sourcehut_test_debug_option.addOption(bool, "test_debug", test_debug);
sourcehut_tests.root_module.addOptions("build_options", sourcehut_test_debug_option);
github_step.dependOn(&b.addRunArtifact(github_tests).step); github_step.dependOn(&b.addRunArtifact(github_tests).step);
gitlab_step.dependOn(&b.addRunArtifact(gitlab_tests).step); gitlab_step.dependOn(&b.addRunArtifact(gitlab_tests).step);

View file

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const testing = std.testing; const testing = std.testing;
const ArrayList = std.ArrayList; const ArrayList = std.ArrayList;
const build_options = @import("build_options");
const atom = @import("atom.zig"); const atom = @import("atom.zig");
const Release = @import("main.zig").Release; const Release = @import("main.zig").Release;
@ -10,6 +11,12 @@ const Codeberg = @import("providers/Codeberg.zig");
const SourceHut = @import("providers/SourceHut.zig"); const SourceHut = @import("providers/SourceHut.zig");
const config = @import("config.zig"); const config = @import("config.zig");
fn testPrint(comptime fmt: []const u8, args: anytype) void {
if (build_options.test_debug) {
std.debug.print(fmt, args);
}
}
test "Atom feed validates against W3C validator" { test "Atom feed validates against W3C validator" {
const allocator = testing.allocator; const allocator = testing.allocator;
@ -43,26 +50,26 @@ test "Atom feed validates against W3C validator" {
try testing.expect(std.mem.indexOf(u8, atom_content, "<feed xmlns=\"http://www.w3.org/2005/Atom\">") != null); try testing.expect(std.mem.indexOf(u8, atom_content, "<feed xmlns=\"http://www.w3.org/2005/Atom\">") != null);
try testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null); try testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null);
std.debug.print("Atom feed structure validation passed\n", .{}); testPrint("Atom feed structure validation passed\n", .{});
} }
test "GitHub provider integration" { test "GitHub provider integration" {
const allocator = testing.allocator; const allocator = testing.allocator;
// Load config to get token // Load config to get token
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
std.debug.print("Skipping GitHub test - config not available: {}\n", .{err}); testPrint("Skipping GitHub test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.github_token == null) { if (app_config.github_token == null) {
std.debug.print("Skipping GitHub test - no token configured\n", .{}); testPrint("Skipping GitHub test - no token configured\n", .{});
return; return;
} }
var provider = GitHub.init(app_config.github_token.?); var provider = GitHub.init(app_config.github_token.?);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("GitHub provider error: {}\n", .{err}); testPrint("GitHub provider error: {}\n", .{err});
return; return;
}; };
defer { defer {
@ -72,7 +79,7 @@ test "GitHub provider integration" {
releases.deinit(); releases.deinit();
} }
std.debug.print("GitHub: Found {} releases\n", .{releases.items.len}); testPrint("GitHub: Found {} releases\n", .{releases.items.len});
// Verify releases have required fields // Verify releases have required fields
for (releases.items) |release| { for (releases.items) |release| {
@ -88,19 +95,19 @@ test "GitLab provider integration" {
// Load config to get token // Load config to get token
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
std.debug.print("Skipping GitLab test - config not available: {}\n", .{err}); testPrint("Skipping GitLab test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.gitlab_token == null) { if (app_config.gitlab_token == null) {
std.debug.print("Skipping GitLab test - no token configured\n", .{}); testPrint("Skipping GitLab test - no token configured\n", .{});
return; return;
} }
var provider = GitLab.init(app_config.gitlab_token.?); var provider = GitLab.init(app_config.gitlab_token.?);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("GitLab provider error: {}\n", .{err}); testPrint("GitLab provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
defer { defer {
@ -110,7 +117,7 @@ test "GitLab provider integration" {
releases.deinit(); releases.deinit();
} }
std.debug.print("GitLab: Found {} releases\n", .{releases.items.len}); testPrint("GitLab: Found {} releases\n", .{releases.items.len});
// Note: It's normal for starred projects to have 0 releases if they don't use GitLab's release feature // Note: It's normal for starred projects to have 0 releases if they don't use GitLab's release feature
// The test passes as long as we can successfully fetch the starred projects and check for releases // The test passes as long as we can successfully fetch the starred projects and check for releases
@ -129,19 +136,19 @@ test "Codeberg provider integration" {
// Load config to get token // Load config to get token
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
std.debug.print("Skipping Codeberg test - config not available: {}\n", .{err}); testPrint("Skipping Codeberg test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.codeberg_token == null) { if (app_config.codeberg_token == null) {
std.debug.print("Skipping Codeberg test - no token configured\n", .{}); testPrint("Skipping Codeberg test - no token configured\n", .{});
return; return;
} }
var provider = Codeberg.init(app_config.codeberg_token.?); var provider = Codeberg.init(app_config.codeberg_token.?);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("Codeberg provider error: {}\n", .{err}); testPrint("Codeberg provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
defer { defer {
@ -151,7 +158,7 @@ test "Codeberg provider integration" {
releases.deinit(); releases.deinit();
} }
std.debug.print("Codeberg: Found {} releases\n", .{releases.items.len}); testPrint("Codeberg: Found {} releases\n", .{releases.items.len});
// Verify releases have required fields // Verify releases have required fields
for (releases.items) |release| { for (releases.items) |release| {
@ -167,19 +174,19 @@ test "SourceHut provider integration" {
// Load config to get repositories // Load config to get repositories
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
std.debug.print("Skipping SourceHut test - config not available: {}\n", .{err}); testPrint("Skipping SourceHut test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.sourcehut == null or app_config.sourcehut.?.repositories.len == 0) { if (app_config.sourcehut == null or app_config.sourcehut.?.repositories.len == 0) {
std.debug.print("Skipping SourceHut test - no repositories configured\n", .{}); testPrint("Skipping SourceHut test - no repositories configured\n", .{});
return; return;
} }
var provider = SourceHut.init(app_config.sourcehut.?.token.?, app_config.sourcehut.?.repositories); var provider = SourceHut.init(app_config.sourcehut.?.token.?, app_config.sourcehut.?.repositories);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("SourceHut provider error: {}\n", .{err}); testPrint("SourceHut provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
defer { defer {
@ -189,7 +196,7 @@ test "SourceHut provider integration" {
releases.deinit(); releases.deinit();
} }
std.debug.print("SourceHut: Found {} releases\n", .{releases.items.len}); testPrint("SourceHut: Found {} releases\n", .{releases.items.len});
// Verify releases have required fields // Verify releases have required fields
for (releases.items) |release| { for (releases.items) |release| {
@ -205,13 +212,13 @@ test "SourceHut commit date fetching" {
// Load config to get repositories // Load config to get repositories
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
std.debug.print("Skipping SourceHut commit date test - config not available: {}\n", .{err}); testPrint("Skipping SourceHut commit date test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.sourcehut == null or app_config.sourcehut.?.repositories.len == 0) { if (app_config.sourcehut == null or app_config.sourcehut.?.repositories.len == 0) {
std.debug.print("Skipping SourceHut commit date test - no repositories configured\n", .{}); testPrint("Skipping SourceHut commit date test - no repositories configured\n", .{});
return; return;
} }
@ -220,7 +227,7 @@ test "SourceHut commit date fetching" {
var provider = SourceHut.init(app_config.sourcehut.?.token.?, test_repos[0..]); var provider = SourceHut.init(app_config.sourcehut.?.token.?, test_repos[0..]);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
std.debug.print("SourceHut commit date test error: {}\n", .{err}); testPrint("SourceHut commit date test error: {}\n", .{err});
return; return;
}; };
defer { defer {
@ -230,11 +237,11 @@ test "SourceHut commit date fetching" {
releases.deinit(); releases.deinit();
} }
std.debug.print("SourceHut commit date test: Found {} releases from {s}\n", .{ releases.items.len, test_repos[0] }); testPrint("SourceHut commit date test: Found {} releases from {s}\n", .{ releases.items.len, test_repos[0] });
// Verify we have some releases // Verify we have some releases
if (releases.items.len == 0) { if (releases.items.len == 0) {
std.debug.print("FAIL: No releases found from SourceHut repository {s}\n", .{test_repos[0]}); testPrint("FAIL: No releases found from SourceHut repository {s}\n", .{test_repos[0]});
try testing.expect(false); // Force test failure - we should be able to fetch releases try testing.expect(false); // Force test failure - we should be able to fetch releases
} }
@ -243,7 +250,7 @@ test "SourceHut commit date fetching" {
// Check commit dates // Check commit dates
for (releases.items) |release| { for (releases.items) |release| {
std.debug.print("Release: {s} - Date: {s}\n", .{ release.tag_name, release.published_at }); testPrint("Release: {s} - Date: {s}\n", .{ release.tag_name, release.published_at });
// Verify basic fields // Verify basic fields
try testing.expect(release.repo_name.len > 0); try testing.expect(release.repo_name.len > 0);
@ -255,10 +262,10 @@ test "SourceHut commit date fetching" {
// Check if we got a real commit date vs epoch fallback // Check if we got a real commit date vs epoch fallback
if (std.mem.eql(u8, release.published_at, "1970-01-01T00:00:00Z")) { if (std.mem.eql(u8, release.published_at, "1970-01-01T00:00:00Z")) {
epoch_dates += 1; epoch_dates += 1;
std.debug.print(" -> Using epoch fallback date\n", .{}); testPrint(" -> Using epoch fallback date\n", .{});
} else { } else {
valid_dates += 1; valid_dates += 1;
std.debug.print(" -> Got real commit date\n", .{}); testPrint(" -> Got real commit date\n", .{});
// Verify the date format looks reasonable (should be ISO 8601) // Verify the date format looks reasonable (should be ISO 8601)
try testing.expect(release.published_at.len >= 19); // At least YYYY-MM-DDTHH:MM:SS try testing.expect(release.published_at.len >= 19); // At least YYYY-MM-DDTHH:MM:SS
@ -266,17 +273,17 @@ test "SourceHut commit date fetching" {
} }
} }
std.debug.print("SourceHut commit date summary: {} valid dates, {} epoch fallbacks\n", .{ valid_dates, epoch_dates }); testPrint("SourceHut commit date summary: {} valid dates, {} epoch fallbacks\n", .{ valid_dates, epoch_dates });
// We should have at least some valid commit dates // We should have at least some valid commit dates
// If all dates are epoch fallbacks, something is wrong with our commit date fetching // If all dates are epoch fallbacks, something is wrong with our commit date fetching
if (releases.items.len > 0) { if (releases.items.len > 0) {
const success_rate = (valid_dates * 100) / releases.items.len; const success_rate = (valid_dates * 100) / releases.items.len;
std.debug.print("Commit date success rate: {}%\n", .{success_rate}); testPrint("Commit date success rate: {}%\n", .{success_rate});
// Test should fail if we can't fetch any real commit dates // Test should fail if we can't fetch any real commit dates
if (valid_dates == 0) { if (valid_dates == 0) {
std.debug.print("FAIL: No valid commit dates were fetched from SourceHut\n", .{}); testPrint("FAIL: No valid commit dates were fetched from SourceHut\n", .{});
try testing.expect(false); // Force test failure try testing.expect(false); // Force test failure
} }

View file

@ -1,6 +1,5 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const print = std.debug.print;
const ArrayList = std.ArrayList; const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Thread = std.Thread; const Thread = std.Thread;
@ -18,6 +17,17 @@ const zeit = @import("zeit");
const Provider = @import("Provider.zig"); const Provider = @import("Provider.zig");
fn print(comptime fmt: []const u8, args: anytype) void {
if (comptime @import("builtin").is_test) {
const build_options = @import("build_options");
if (build_options.test_debug) {
std.debug.print(fmt, args);
}
} else {
std.debug.print(fmt, args);
}
}
// Configuration: Only include releases from the last year in the output // Configuration: Only include releases from the last year in the output
const RELEASE_AGE_LIMIT_SECONDS: i64 = 365 * 24 * 60 * 60; // 1 year in seconds const RELEASE_AGE_LIMIT_SECONDS: i64 = 365 * 24 * 60 * 60; // 1 year in seconds