diff --git a/README.md b/README.md index b74cc11..f311bc2 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,27 @@ Run the test suite: 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 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): ```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 -``` \ No newline at end of file +``` diff --git a/build.zig b/build.zig index d1fc5a4..4c9b2f8 100644 --- a/build.zig +++ b/build.zig @@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); 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 test_debug = b.option(bool, "test-debug", "Enable debug output in tests") orelse false; // Add Zeit dependency 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")); + 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 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")); + 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 if (provider) |p| { 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"}, }); 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(.{ .name = "gitlab-tests", @@ -90,6 +102,9 @@ pub fn build(b: *std.Build) void { .filters = &[_][]const u8{"GitLab provider"}, }); 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(.{ .name = "codeberg-tests", @@ -99,6 +114,9 @@ pub fn build(b: *std.Build) void { .filters = &[_][]const u8{"Codeberg provider"}, }); 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(.{ .name = "sourcehut-tests", @@ -108,6 +126,9 @@ pub fn build(b: *std.Build) void { .filters = &[_][]const u8{"SourceHut provider"}, }); 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); gitlab_step.dependOn(&b.addRunArtifact(gitlab_tests).step); diff --git a/src/integration_tests.zig b/src/integration_tests.zig index 728cddc..8f556b3 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -1,6 +1,7 @@ const std = @import("std"); const testing = std.testing; const ArrayList = std.ArrayList; +const build_options = @import("build_options"); const atom = @import("atom.zig"); const Release = @import("main.zig").Release; @@ -10,6 +11,12 @@ const Codeberg = @import("providers/Codeberg.zig"); const SourceHut = @import("providers/SourceHut.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" { const allocator = testing.allocator; @@ -43,26 +50,26 @@ test "Atom feed validates against W3C validator" { try testing.expect(std.mem.indexOf(u8, atom_content, "") != null); try testing.expect(std.mem.indexOf(u8, atom_content, "") != null); - std.debug.print("Atom feed structure validation passed\n", .{}); + testPrint("Atom feed structure validation passed\n", .{}); } test "GitHub provider integration" { const allocator = testing.allocator; // Load config to get token 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; }; defer app_config.deinit(); 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; } var provider = GitHub.init(app_config.github_token.?); const releases = provider.fetchReleases(allocator) catch |err| { - std.debug.print("GitHub provider error: {}\n", .{err}); + testPrint("GitHub provider error: {}\n", .{err}); return; }; defer { @@ -72,7 +79,7 @@ test "GitHub provider integration" { 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 for (releases.items) |release| { @@ -88,19 +95,19 @@ test "GitLab provider integration" { // Load config to get token 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; }; defer app_config.deinit(); 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; } var provider = GitLab.init(app_config.gitlab_token.?); 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 }; defer { @@ -110,7 +117,7 @@ test "GitLab provider integration" { 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 // 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 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; }; defer app_config.deinit(); 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; } var provider = Codeberg.init(app_config.codeberg_token.?); 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 }; defer { @@ -151,7 +158,7 @@ test "Codeberg provider integration" { 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 for (releases.items) |release| { @@ -167,19 +174,19 @@ test "SourceHut provider integration" { // Load config to get repositories 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; }; defer app_config.deinit(); 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; } var provider = SourceHut.init(app_config.sourcehut.?.token.?, app_config.sourcehut.?.repositories); 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 }; defer { @@ -189,7 +196,7 @@ test "SourceHut provider integration" { 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 for (releases.items) |release| { @@ -205,13 +212,13 @@ test "SourceHut commit date fetching" { // Load config to get repositories 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; }; defer app_config.deinit(); 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; } @@ -220,7 +227,7 @@ test "SourceHut commit date fetching" { var provider = SourceHut.init(app_config.sourcehut.?.token.?, test_repos[0..]); 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; }; defer { @@ -230,11 +237,11 @@ test "SourceHut commit date fetching" { 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 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 } @@ -243,7 +250,7 @@ test "SourceHut commit date fetching" { // Check commit dates 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 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 if (std.mem.eql(u8, release.published_at, "1970-01-01T00:00:00Z")) { epoch_dates += 1; - std.debug.print(" -> Using epoch fallback date\n", .{}); + testPrint(" -> Using epoch fallback date\n", .{}); } else { 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) 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 // If all dates are epoch fallbacks, something is wrong with our commit date fetching if (releases.items.len > 0) { 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 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 } diff --git a/src/main.zig b/src/main.zig index 067fb00..e04685e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,5 @@ const std = @import("std"); const builtin = @import("builtin"); -const print = std.debug.print; const ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; const Thread = std.Thread; @@ -18,6 +17,17 @@ const zeit = @import("zeit"); 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 const RELEASE_AGE_LIMIT_SECONDS: i64 = 365 * 24 * 60 * 60; // 1 year in seconds