use existing data appropriately
This commit is contained in:
parent
ee9cbaada3
commit
c2ca2e6be5
8 changed files with 1296 additions and 136 deletions
|
@ -8,6 +8,5 @@
|
|||
"~sircmpwn/aerc",
|
||||
"~emersion/gamja"
|
||||
]
|
||||
},
|
||||
"last_check": null
|
||||
}
|
||||
}
|
|
@ -38,8 +38,9 @@ pub fn generateFeed(allocator: Allocator, releases: []const Release) ![]u8 {
|
|||
|
||||
// Add current timestamp in proper ISO 8601 format using zeit
|
||||
const now = zeit.instant(.{}) catch zeit.instant(.{ .source = .now }) catch unreachable;
|
||||
const updated_str = try std.fmt.allocPrint(allocator, "{}", .{now});
|
||||
defer allocator.free(updated_str);
|
||||
const time = now.time();
|
||||
var buf: [64]u8 = undefined;
|
||||
const updated_str = try time.bufPrint(&buf, .rfc3339);
|
||||
try writer.print("<updated>{s}</updated>\n", .{updated_str});
|
||||
|
||||
// Add entries
|
||||
|
|
|
@ -68,9 +68,21 @@ pub fn loadConfig(allocator: Allocator, path: []const u8) !Config {
|
|||
}
|
||||
|
||||
return Config{
|
||||
.github_token = if (root.get("github_token")) |v| try allocator.dupe(u8, v.string) else null,
|
||||
.gitlab_token = if (root.get("gitlab_token")) |v| try allocator.dupe(u8, v.string) else null,
|
||||
.codeberg_token = if (root.get("codeberg_token")) |v| try allocator.dupe(u8, v.string) else null,
|
||||
.github_token = if (root.get("github_token")) |v| switch (v) {
|
||||
.string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null,
|
||||
.null => null,
|
||||
else => null,
|
||||
} else null,
|
||||
.gitlab_token = if (root.get("gitlab_token")) |v| switch (v) {
|
||||
.string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null,
|
||||
.null => null,
|
||||
else => null,
|
||||
} else null,
|
||||
.codeberg_token = if (root.get("codeberg_token")) |v| switch (v) {
|
||||
.string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null,
|
||||
.null => null,
|
||||
else => null,
|
||||
} else null,
|
||||
.sourcehut = sourcehut_config,
|
||||
.allocator = allocator,
|
||||
};
|
||||
|
|
538
src/main.zig
538
src/main.zig
|
@ -11,6 +11,9 @@ const Codeberg = @import("providers/Codeberg.zig");
|
|||
const SourceHut = @import("providers/SourceHut.zig");
|
||||
const atom = @import("atom.zig");
|
||||
const config = @import("config.zig");
|
||||
const Config = config.Config;
|
||||
const SourceHutConfig = config.SourceHutConfig;
|
||||
const xml_parser = @import("xml_parser.zig");
|
||||
const zeit = @import("zeit");
|
||||
|
||||
const Provider = @import("Provider.zig");
|
||||
|
@ -78,7 +81,7 @@ pub fn main() !u8 {
|
|||
};
|
||||
defer app_config.deinit();
|
||||
|
||||
// Load existing Atom feed to get current releases
|
||||
// Load existing releases to determine last check time per provider
|
||||
var existing_releases = loadExistingReleases(allocator, output_file) catch ArrayList(Release).init(allocator);
|
||||
defer {
|
||||
for (existing_releases.items) |release| {
|
||||
|
@ -152,7 +155,7 @@ pub fn main() !u8 {
|
|||
try all_releases.appendSlice(new_releases.items);
|
||||
|
||||
// Add existing releases (up to a reasonable limit to prevent Atom feed from growing indefinitely)
|
||||
const max_total_releases = 100;
|
||||
const max_total_releases = 1000;
|
||||
const remaining_slots = if (new_releases.items.len < max_total_releases)
|
||||
max_total_releases - new_releases.items.len
|
||||
else
|
||||
|
@ -181,58 +184,12 @@ pub fn main() !u8 {
|
|||
return 0;
|
||||
}
|
||||
|
||||
test "main functionality" {
|
||||
// Basic test to ensure compilation
|
||||
const allocator = std.testing.allocator;
|
||||
var releases = ArrayList(Release).init(allocator);
|
||||
defer releases.deinit();
|
||||
|
||||
try std.testing.expect(releases.items.len == 0);
|
||||
}
|
||||
|
||||
test "Atom feed has correct structure" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const releases = [_]Release{
|
||||
Release{
|
||||
.repo_name = "test/repo",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo/releases/tag/v1.0.0",
|
||||
.description = "Test release",
|
||||
.provider = "github",
|
||||
},
|
||||
};
|
||||
|
||||
const atom_content = try atom.generateFeed(allocator, &releases);
|
||||
defer allocator.free(atom_content);
|
||||
|
||||
// Check for required Atom elements
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<feed xmlns=\"http://www.w3.org/2005/Atom\">") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<title>Repository Releases</title>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<subtitle>New releases from starred repositories</subtitle>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://github.com\" rel=\"alternate\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://example.com/releases.xml\" rel=\"self\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://example.com/releases</id>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<updated>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<entry>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null);
|
||||
|
||||
// Check entry structure
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<title>test/repo - v1.0.0</title>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://github.com/test/repo/releases/tag/v1.0.0\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://github.com/test/repo/releases/tag/v1.0.0</id>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<updated>2024-01-01T00:00:00Z</updated>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<author><name>github</name></author>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<summary>Test release</summary>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<category term=\"github\"/>") != null);
|
||||
}
|
||||
fn loadExistingReleases(allocator: Allocator, filename: []const u8) !ArrayList(Release) {
|
||||
var releases = ArrayList(Release).init(allocator);
|
||||
|
||||
const file = std.fs.cwd().openFile(filename, .{}) catch |err| switch (err) {
|
||||
error.FileNotFound => return releases, // No existing file, return empty list
|
||||
error.FileNotFound => {
|
||||
print("No existing releases file found, starting fresh\n", .{});
|
||||
return ArrayList(Release).init(allocator);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
|
@ -240,69 +197,19 @@ fn loadExistingReleases(allocator: Allocator, filename: []const u8) !ArrayList(R
|
|||
const content = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
|
||||
defer allocator.free(content);
|
||||
|
||||
// Simple XML parsing to extract existing releases from Atom feed
|
||||
// Look for <entry> blocks and extract the data
|
||||
var lines = std.mem.splitScalar(u8, content, '\n');
|
||||
var current_release: ?Release = null;
|
||||
var in_entry = false;
|
||||
print("Loading existing releases from {s}...\n", .{filename});
|
||||
|
||||
while (lines.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
||||
|
||||
if (std.mem.startsWith(u8, trimmed, "<entry>")) {
|
||||
in_entry = true;
|
||||
current_release = Release{
|
||||
.repo_name = try allocator.dupe(u8, ""),
|
||||
.tag_name = try allocator.dupe(u8, ""),
|
||||
.published_at = try allocator.dupe(u8, ""),
|
||||
.html_url = try allocator.dupe(u8, ""),
|
||||
.description = try allocator.dupe(u8, ""),
|
||||
.provider = try allocator.dupe(u8, ""),
|
||||
const releases = xml_parser.parseAtomFeed(allocator, content) catch |err| {
|
||||
print("Warning: Failed to parse existing releases file: {}\n", .{err});
|
||||
print("Starting fresh with no existing releases\n", .{});
|
||||
return ArrayList(Release).init(allocator);
|
||||
};
|
||||
} else if (std.mem.startsWith(u8, trimmed, "</entry>")) {
|
||||
if (current_release) |release| {
|
||||
try releases.append(release);
|
||||
}
|
||||
in_entry = false;
|
||||
current_release = null;
|
||||
} else if (in_entry and current_release != null) {
|
||||
if (std.mem.startsWith(u8, trimmed, "<title>") and std.mem.endsWith(u8, trimmed, "</title>")) {
|
||||
const title_content = trimmed[7 .. trimmed.len - 8];
|
||||
if (std.mem.indexOf(u8, title_content, " - ")) |dash_pos| {
|
||||
allocator.free(current_release.?.repo_name);
|
||||
allocator.free(current_release.?.tag_name);
|
||||
current_release.?.repo_name = try allocator.dupe(u8, title_content[0..dash_pos]);
|
||||
current_release.?.tag_name = try allocator.dupe(u8, title_content[dash_pos + 3 ..]);
|
||||
}
|
||||
} else if (std.mem.startsWith(u8, trimmed, "<link href=\"") and std.mem.endsWith(u8, trimmed, "\"/>")) {
|
||||
const url_start = 12; // length of "<link href=\""
|
||||
const url_end = trimmed.len - 3; // remove "\"/>"
|
||||
allocator.free(current_release.?.html_url);
|
||||
current_release.?.html_url = try allocator.dupe(u8, trimmed[url_start..url_end]);
|
||||
} else if (std.mem.startsWith(u8, trimmed, "<updated>") and std.mem.endsWith(u8, trimmed, "</updated>")) {
|
||||
allocator.free(current_release.?.published_at);
|
||||
current_release.?.published_at = try allocator.dupe(u8, trimmed[9 .. trimmed.len - 10]);
|
||||
} else if (std.mem.startsWith(u8, trimmed, "<category term=\"") and std.mem.endsWith(u8, trimmed, "\"/>")) {
|
||||
const term_start = 15; // length of "<category term=\""
|
||||
const term_end = trimmed.len - 3; // remove "\"/>"
|
||||
allocator.free(current_release.?.provider);
|
||||
current_release.?.provider = try allocator.dupe(u8, trimmed[term_start..term_end]);
|
||||
} else if (std.mem.startsWith(u8, trimmed, "<summary>") and std.mem.endsWith(u8, trimmed, "</summary>")) {
|
||||
allocator.free(current_release.?.description);
|
||||
current_release.?.description = try allocator.dupe(u8, trimmed[9 .. trimmed.len - 10]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any incomplete release that wasn't properly closed
|
||||
if (current_release) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
|
||||
print("Loaded {} existing releases\n", .{releases.items.len});
|
||||
return releases;
|
||||
}
|
||||
|
||||
fn filterNewReleases(allocator: Allocator, all_releases: []const Release, since_timestamp: i64) !ArrayList(Release) {
|
||||
pub fn filterNewReleases(allocator: Allocator, all_releases: []const Release, since_timestamp: i64) !ArrayList(Release) {
|
||||
var new_releases = ArrayList(Release).init(allocator);
|
||||
|
||||
for (all_releases) |release| {
|
||||
|
@ -326,7 +233,7 @@ fn filterNewReleases(allocator: Allocator, all_releases: []const Release, since_
|
|||
return new_releases;
|
||||
}
|
||||
|
||||
fn parseReleaseTimestamp(date_str: []const u8) !i64 {
|
||||
pub fn parseReleaseTimestamp(date_str: []const u8) !i64 {
|
||||
// Try parsing as direct timestamp first
|
||||
if (std.fmt.parseInt(i64, date_str, 10)) |timestamp| {
|
||||
return timestamp;
|
||||
|
@ -335,11 +242,13 @@ fn parseReleaseTimestamp(date_str: []const u8) !i64 {
|
|||
const instant = zeit.instant(.{
|
||||
.source = .{ .iso8601 = date_str },
|
||||
}) catch return 0;
|
||||
return @intCast(instant.timestamp);
|
||||
// Zeit returns nanoseconds, convert to seconds
|
||||
const seconds = @divTrunc(instant.timestamp, 1_000_000_000);
|
||||
return @intCast(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
fn compareReleasesByDate(context: void, a: Release, b: Release) bool {
|
||||
pub fn compareReleasesByDate(context: void, a: Release, b: Release) bool {
|
||||
_ = context;
|
||||
const timestamp_a = parseReleaseTimestamp(a.published_at) catch 0;
|
||||
const timestamp_b = parseReleaseTimestamp(b.published_at) catch 0;
|
||||
|
@ -351,7 +260,9 @@ fn formatTimestampForDisplay(allocator: Allocator, timestamp: i64) ![]const u8 {
|
|||
return try allocator.dupe(u8, "beginning of time");
|
||||
}
|
||||
|
||||
// Convert timestamp to approximate ISO date for display
|
||||
// Use zeit to format the timestamp properly
|
||||
const instant = zeit.instant(.{ .source = .{ .unix_timestamp = timestamp } }) catch {
|
||||
// Fallback to simple approximation if zeit fails
|
||||
const days_since_epoch = @divTrunc(timestamp, 24 * 60 * 60);
|
||||
const years_since_1970 = @divTrunc(days_since_epoch, 365);
|
||||
const remaining_days = @mod(days_since_epoch, 365);
|
||||
|
@ -363,6 +274,12 @@ fn formatTimestampForDisplay(allocator: Allocator, timestamp: i64) ![]const u8 {
|
|||
const day = 1 + days;
|
||||
|
||||
return try std.fmt.allocPrint(allocator, "{d:0>4}-{d:0>2}-{d:0>2}T00:00:00Z", .{ year, month, day });
|
||||
};
|
||||
|
||||
const time = instant.time();
|
||||
var buf: [64]u8 = undefined;
|
||||
const formatted = try time.bufPrint(&buf, .rfc3339);
|
||||
return try allocator.dupe(u8, formatted);
|
||||
}
|
||||
|
||||
fn fetchReleasesFromAllProviders(
|
||||
|
@ -453,3 +370,398 @@ fn fetchProviderReleases(context: *const ThreadContext) void {
|
|||
print("✗ {s}: {s}\n", .{ provider.getName(), error_msg });
|
||||
}
|
||||
}
|
||||
|
||||
test "main functionality" {
|
||||
// Basic test to ensure compilation
|
||||
const allocator = std.testing.allocator;
|
||||
var releases = ArrayList(Release).init(allocator);
|
||||
defer releases.deinit();
|
||||
|
||||
try std.testing.expect(releases.items.len == 0);
|
||||
}
|
||||
|
||||
test "Atom feed has correct structure" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const releases = [_]Release{
|
||||
Release{
|
||||
.repo_name = "test/repo",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo/releases/tag/v1.0.0",
|
||||
.description = "Test release",
|
||||
.provider = "github",
|
||||
},
|
||||
};
|
||||
|
||||
const atom_content = try atom.generateFeed(allocator, &releases);
|
||||
defer allocator.free(atom_content);
|
||||
|
||||
// Check for required Atom elements
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<feed xmlns=\"http://www.w3.org/2005/Atom\">") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<title>Repository Releases</title>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<subtitle>New releases from starred repositories</subtitle>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://github.com\" rel=\"alternate\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://example.com/releases.xml\" rel=\"self\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://example.com/releases</id>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<updated>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<entry>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "</feed>") != null);
|
||||
|
||||
// Check entry structure
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<title>test/repo - v1.0.0</title>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<link href=\"https://github.com/test/repo/releases/tag/v1.0.0\"/>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<id>https://github.com/test/repo/releases/tag/v1.0.0</id>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<updated>2024-01-01T00:00:00Z</updated>") != null);
|
||||
|
||||
// Check for author - be flexible about exact format
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<author>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "github") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "</author>") != null);
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<summary>Test release</summary>") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, atom_content, "<category term=\"github\"/>") != null);
|
||||
}
|
||||
|
||||
test "loadExistingReleases with valid XML" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Create a temporary file with valid Atom XML
|
||||
const test_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>test/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <summary>Test release</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
const temp_filename = "test_releases.xml";
|
||||
|
||||
// Write test XML to file
|
||||
{
|
||||
const file = try std.fs.cwd().createFile(temp_filename, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(test_xml);
|
||||
}
|
||||
defer std.fs.cwd().deleteFile(temp_filename) catch {};
|
||||
|
||||
// Load existing releases
|
||||
var releases = try loadExistingReleases(allocator, temp_filename);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), releases.items.len);
|
||||
try std.testing.expectEqualStrings("test/repo", releases.items[0].repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0", releases.items[0].tag_name);
|
||||
}
|
||||
|
||||
test "loadExistingReleases with nonexistent file" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var releases = try loadExistingReleases(allocator, "nonexistent_file.xml");
|
||||
defer releases.deinit();
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), releases.items.len);
|
||||
}
|
||||
|
||||
test "loadExistingReleases with malformed XML" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const malformed_xml = "This is not valid XML at all!";
|
||||
const temp_filename = "test_malformed.xml";
|
||||
|
||||
// Write malformed XML to file
|
||||
{
|
||||
const file = try std.fs.cwd().createFile(temp_filename, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(malformed_xml);
|
||||
}
|
||||
defer std.fs.cwd().deleteFile(temp_filename) catch {};
|
||||
|
||||
// Should handle gracefully and return empty list
|
||||
var releases = try loadExistingReleases(allocator, temp_filename);
|
||||
defer releases.deinit();
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), releases.items.len);
|
||||
}
|
||||
|
||||
test "parseReleaseTimestamp with various formats" {
|
||||
// Test ISO 8601 format
|
||||
const timestamp1 = try parseReleaseTimestamp("2024-01-01T00:00:00Z");
|
||||
try std.testing.expect(timestamp1 > 0);
|
||||
|
||||
// Test direct timestamp
|
||||
const timestamp2 = try parseReleaseTimestamp("1704067200");
|
||||
try std.testing.expectEqual(@as(i64, 1704067200), timestamp2);
|
||||
|
||||
// Test invalid format (should return 0)
|
||||
const timestamp3 = parseReleaseTimestamp("invalid") catch 0;
|
||||
try std.testing.expectEqual(@as(i64, 0), timestamp3);
|
||||
|
||||
// Test empty string
|
||||
const timestamp4 = parseReleaseTimestamp("") catch 0;
|
||||
try std.testing.expectEqual(@as(i64, 0), timestamp4);
|
||||
|
||||
// Test different ISO formats
|
||||
const timestamp5 = try parseReleaseTimestamp("2024-12-25T15:30:45Z");
|
||||
try std.testing.expect(timestamp5 > timestamp1);
|
||||
}
|
||||
|
||||
test "filterNewReleases correctly filters by timestamp" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const old_release = Release{
|
||||
.repo_name = "test/old",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/old/releases/tag/v1.0.0",
|
||||
.description = "Old release",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
const new_release = Release{
|
||||
.repo_name = "test/new",
|
||||
.tag_name = "v2.0.0",
|
||||
.published_at = "2024-06-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/new/releases/tag/v2.0.0",
|
||||
.description = "New release",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
const all_releases = [_]Release{ old_release, new_release };
|
||||
|
||||
// Filter with timestamp between the two releases
|
||||
const march_timestamp = try parseReleaseTimestamp("2024-03-01T00:00:00Z");
|
||||
var filtered = try filterNewReleases(allocator, &all_releases, march_timestamp);
|
||||
defer {
|
||||
for (filtered.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
filtered.deinit();
|
||||
}
|
||||
|
||||
// Should only contain the new release
|
||||
try std.testing.expectEqual(@as(usize, 1), filtered.items.len);
|
||||
try std.testing.expectEqualStrings("test/new", filtered.items[0].repo_name);
|
||||
}
|
||||
|
||||
test "loadExistingReleases handles various XML structures" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test with minimal valid XML
|
||||
const minimal_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>minimal/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/minimal/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
const temp_filename = "test_minimal.xml";
|
||||
|
||||
// Write test XML to file
|
||||
{
|
||||
const file = try std.fs.cwd().createFile(temp_filename, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(minimal_xml);
|
||||
}
|
||||
defer std.fs.cwd().deleteFile(temp_filename) catch {};
|
||||
|
||||
// Load existing releases
|
||||
var releases = try loadExistingReleases(allocator, temp_filename);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), releases.items.len);
|
||||
try std.testing.expectEqualStrings("minimal/repo", releases.items[0].repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0", releases.items[0].tag_name);
|
||||
try std.testing.expectEqualStrings("2024-01-01T00:00:00Z", releases.items[0].published_at);
|
||||
}
|
||||
|
||||
test "loadExistingReleases with complex XML content" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test with complex XML including escaped characters and multiple entries
|
||||
const complex_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<subtitle>New releases from starred repositories</subtitle>
|
||||
\\<link href="https://github.com" rel="alternate"/>
|
||||
\\<link href="https://example.com/releases.xml" rel="self"/>
|
||||
\\<id>https://example.com/releases</id>
|
||||
\\<updated>2024-01-01T00:00:00Z</updated>
|
||||
\\<entry>
|
||||
\\ <title>complex/repo & more - v1.0.0 <beta></title>
|
||||
\\ <link href="https://github.com/complex/repo/releases/tag/v1.0.0"/>
|
||||
\\ <id>https://github.com/complex/repo/releases/tag/v1.0.0</id>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <author><n>github</n></author>
|
||||
\\ <summary>Release with "special" characters & symbols</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>another/repo - v2.0.0</title>
|
||||
\\ <link href="https://gitlab.com/another/repo/-/releases/v2.0.0"/>
|
||||
\\ <id>https://gitlab.com/another/repo/-/releases/v2.0.0</id>
|
||||
\\ <updated>2024-01-02T12:30:45Z</updated>
|
||||
\\ <author><n>gitlab</n></author>
|
||||
\\ <summary>Another release</summary>
|
||||
\\ <category term="gitlab"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
const temp_filename = "test_complex.xml";
|
||||
|
||||
// Write test XML to file
|
||||
{
|
||||
const file = try std.fs.cwd().createFile(temp_filename, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(complex_xml);
|
||||
}
|
||||
defer std.fs.cwd().deleteFile(temp_filename) catch {};
|
||||
|
||||
// Load existing releases
|
||||
var releases = try loadExistingReleases(allocator, temp_filename);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), releases.items.len);
|
||||
|
||||
// Check first release with escaped characters
|
||||
try std.testing.expectEqualStrings("complex/repo & more", releases.items[0].repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0 <beta>", releases.items[0].tag_name);
|
||||
try std.testing.expectEqualStrings("Release with \"special\" characters & symbols", releases.items[0].description);
|
||||
try std.testing.expectEqualStrings("github", releases.items[0].provider);
|
||||
|
||||
// Check second release
|
||||
try std.testing.expectEqualStrings("another/repo", releases.items[1].repo_name);
|
||||
try std.testing.expectEqualStrings("v2.0.0", releases.items[1].tag_name);
|
||||
try std.testing.expectEqualStrings("gitlab", releases.items[1].provider);
|
||||
}
|
||||
|
||||
test "formatTimestampForDisplay produces valid ISO dates" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test with zero timestamp
|
||||
const zero_result = try formatTimestampForDisplay(allocator, 0);
|
||||
defer allocator.free(zero_result);
|
||||
try std.testing.expectEqualStrings("beginning of time", zero_result);
|
||||
|
||||
// Test with known timestamp (2024-01-01T00:00:00Z = 1704067200)
|
||||
const known_result = try formatTimestampForDisplay(allocator, 1704067200);
|
||||
defer allocator.free(known_result);
|
||||
try std.testing.expect(std.mem.startsWith(u8, known_result, "20"));
|
||||
try std.testing.expect(std.mem.endsWith(u8, known_result, "Z"));
|
||||
try std.testing.expect(std.mem.indexOf(u8, known_result, "T") != null);
|
||||
}
|
||||
|
||||
test "XML parsing handles malformed entries gracefully" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test with partially malformed XML (missing closing tags, etc.)
|
||||
const malformed_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>good/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/good/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>broken/repo - v2.0.0
|
||||
\\ <link href="https://github.com/broken/repo/releases/tag/v2.0.0"/>
|
||||
\\ <updated>2024-01-02T00:00:00Z</updated>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>another/good - v3.0.0</title>
|
||||
\\ <link href="https://github.com/another/good/releases/tag/v3.0.0"/>
|
||||
\\ <updated>2024-01-03T00:00:00Z</updated>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var releases = try xml_parser.parseAtomFeed(allocator, malformed_xml);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
// Should parse the good entries and skip/handle the malformed one gracefully
|
||||
try std.testing.expect(releases.items.len >= 2);
|
||||
|
||||
// Check that we got the good entries
|
||||
var found_good = false;
|
||||
var found_another_good = false;
|
||||
for (releases.items) |release| {
|
||||
if (std.mem.eql(u8, release.repo_name, "good/repo")) {
|
||||
found_good = true;
|
||||
}
|
||||
if (std.mem.eql(u8, release.repo_name, "another/good")) {
|
||||
found_another_good = true;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found_good);
|
||||
try std.testing.expect(found_another_good);
|
||||
}
|
||||
|
||||
test "compareReleasesByDate" {
|
||||
const release1 = Release{
|
||||
.repo_name = "test/repo1",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo1/releases/tag/v1.0.0",
|
||||
.description = "First release",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
const release2 = Release{
|
||||
.repo_name = "test/repo2",
|
||||
.tag_name = "v2.0.0",
|
||||
.published_at = "2024-01-02T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo2/releases/tag/v2.0.0",
|
||||
.description = "Second release",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
// release2 should come before release1 (more recent first)
|
||||
try std.testing.expect(compareReleasesByDate({}, release2, release1));
|
||||
try std.testing.expect(!compareReleasesByDate({}, release1, release2));
|
||||
}
|
||||
|
||||
// Import XML parser tests
|
||||
test {
|
||||
std.testing.refAllDecls(@import("xml_parser_tests.zig"));
|
||||
}
|
||||
|
||||
// Import timestamp tests
|
||||
test {
|
||||
std.testing.refAllDecls(@import("timestamp_tests.zig"));
|
||||
}
|
||||
|
|
|
@ -126,9 +126,7 @@ fn getRepoTags(allocator: Allocator, client: *http.Client, token: ?[]const u8, r
|
|||
const uri = try std.Uri.parse(graphql_url);
|
||||
|
||||
// Use the exact same GraphQL query that worked in curl, with proper brace escaping
|
||||
const request_body = try std.fmt.allocPrint(allocator,
|
||||
"{{\"query\":\"query {{ user(username: \\\"{s}\\\") {{ repository(name: \\\"{s}\\\") {{ references {{ results {{ name target }} }} }} }} }}\"}}",
|
||||
.{ username, reponame });
|
||||
const request_body = try std.fmt.allocPrint(allocator, "{{\"query\":\"query {{ user(username: \\\"{s}\\\") {{ repository(name: \\\"{s}\\\") {{ references {{ results {{ name target }} }} }} }} }}\"}}", .{ username, reponame });
|
||||
defer allocator.free(request_body);
|
||||
|
||||
const auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{auth_token});
|
||||
|
@ -314,9 +312,7 @@ fn getCommitDate(allocator: Allocator, client: *http.Client, token: []const u8,
|
|||
const uri = try std.Uri.parse(graphql_url);
|
||||
|
||||
// Use the exact same GraphQL query that worked in curl, with proper brace escaping
|
||||
const request_body = try std.fmt.allocPrint(allocator,
|
||||
"{{\"query\":\"query {{ user(username: \\\"{s}\\\") {{ repository(name: \\\"{s}\\\") {{ revparse_single(revspec: \\\"{s}\\\") {{ author {{ time }} committer {{ time }} }} }} }} }}\"}}",
|
||||
.{ username, reponame, commit_id });
|
||||
const request_body = try std.fmt.allocPrint(allocator, "{{\"query\":\"query {{ user(username: \\\"{s}\\\") {{ repository(name: \\\"{s}\\\") {{ revparse_single(revspec: \\\"{s}\\\") {{ author {{ time }} committer {{ time }} }} }} }} }}\"}}", .{ username, reponame, commit_id });
|
||||
defer allocator.free(request_body);
|
||||
|
||||
const auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token});
|
||||
|
|
241
src/timestamp_tests.zig
Normal file
241
src/timestamp_tests.zig
Normal file
|
@ -0,0 +1,241 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
const config = @import("config.zig");
|
||||
const xml_parser = @import("xml_parser.zig");
|
||||
|
||||
const Release = main.Release;
|
||||
const Config = config.Config;
|
||||
const SourceHutConfig = config.SourceHutConfig;
|
||||
|
||||
test "Config loading without last_check field" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Create a test config file without last_check
|
||||
const test_config_content =
|
||||
\\{
|
||||
\\ "github_token": "test_token",
|
||||
\\ "gitlab_token": null,
|
||||
\\ "codeberg_token": null,
|
||||
\\ "sourcehut": {
|
||||
\\ "repositories": ["~test/repo"]
|
||||
\\ }
|
||||
\\}
|
||||
;
|
||||
|
||||
const temp_config_file = "test_config_no_last_check.json";
|
||||
|
||||
// Write test config to file
|
||||
{
|
||||
const file = try std.fs.cwd().createFile(temp_config_file, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(test_config_content);
|
||||
}
|
||||
defer std.fs.cwd().deleteFile(temp_config_file) catch {};
|
||||
|
||||
// Load config
|
||||
const loaded_config = try config.loadConfig(allocator, temp_config_file);
|
||||
defer loaded_config.deinit();
|
||||
|
||||
// Verify config was loaded correctly
|
||||
try std.testing.expectEqualStrings("test_token", loaded_config.github_token.?);
|
||||
try std.testing.expect(loaded_config.gitlab_token == null);
|
||||
}
|
||||
|
||||
test "parseReleaseTimestamp handles edge cases" {
|
||||
// Test various timestamp formats
|
||||
const test_cases = [_]struct {
|
||||
input: []const u8,
|
||||
expected_valid: bool,
|
||||
}{
|
||||
.{ .input = "2024-01-01T00:00:00Z", .expected_valid = true },
|
||||
.{ .input = "2024-12-31T23:59:59Z", .expected_valid = true },
|
||||
.{ .input = "1704067200", .expected_valid = true }, // This is a valid timestamp
|
||||
.{ .input = "2024-01-01", .expected_valid = true }, // Zeit can parse date-only format
|
||||
.{ .input = "", .expected_valid = false },
|
||||
.{ .input = "invalid", .expected_valid = false },
|
||||
.{ .input = "not-a-date", .expected_valid = false },
|
||||
.{ .input = "definitely-not-a-date", .expected_valid = false },
|
||||
};
|
||||
|
||||
for (test_cases) |test_case| {
|
||||
const result = main.parseReleaseTimestamp(test_case.input) catch 0;
|
||||
if (test_case.expected_valid) {
|
||||
try std.testing.expect(result > 0);
|
||||
} else {
|
||||
try std.testing.expectEqual(@as(i64, 0), result);
|
||||
}
|
||||
}
|
||||
|
||||
// Test the special case of "0" timestamp - this should return 0
|
||||
const zero_result = main.parseReleaseTimestamp("0") catch 0;
|
||||
try std.testing.expectEqual(@as(i64, 0), zero_result);
|
||||
|
||||
// Test specific known values
|
||||
const known_timestamp = main.parseReleaseTimestamp("1704067200") catch 0;
|
||||
try std.testing.expectEqual(@as(i64, 1704067200), known_timestamp);
|
||||
|
||||
// Test that date-only format works
|
||||
const date_only_result = main.parseReleaseTimestamp("2024-01-01") catch 0;
|
||||
try std.testing.expectEqual(@as(i64, 1704067200), date_only_result);
|
||||
}
|
||||
|
||||
test "filterNewReleases with various timestamp scenarios" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const releases = [_]Release{
|
||||
Release{
|
||||
.repo_name = "test/very-old",
|
||||
.tag_name = "v0.1.0",
|
||||
.published_at = "2023-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/very-old/releases/tag/v0.1.0",
|
||||
.description = "Very old release",
|
||||
.provider = "github",
|
||||
},
|
||||
Release{
|
||||
.repo_name = "test/old",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/old/releases/tag/v1.0.0",
|
||||
.description = "Old release",
|
||||
.provider = "github",
|
||||
},
|
||||
Release{
|
||||
.repo_name = "test/recent",
|
||||
.tag_name = "v2.0.0",
|
||||
.published_at = "2024-06-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/recent/releases/tag/v2.0.0",
|
||||
.description = "Recent release",
|
||||
.provider = "github",
|
||||
},
|
||||
Release{
|
||||
.repo_name = "test/newest",
|
||||
.tag_name = "v3.0.0",
|
||||
.published_at = "2024-12-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/newest/releases/tag/v3.0.0",
|
||||
.description = "Newest release",
|
||||
.provider = "github",
|
||||
},
|
||||
};
|
||||
|
||||
// Test filtering from beginning of time (should get all)
|
||||
{
|
||||
var filtered = try main.filterNewReleases(allocator, &releases, 0);
|
||||
defer {
|
||||
for (filtered.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
filtered.deinit();
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 4), filtered.items.len);
|
||||
}
|
||||
|
||||
// Test filtering from middle of 2024 (should get recent and newest)
|
||||
{
|
||||
const march_2024 = main.parseReleaseTimestamp("2024-03-01T00:00:00Z") catch 0;
|
||||
var filtered = try main.filterNewReleases(allocator, &releases, march_2024);
|
||||
defer {
|
||||
for (filtered.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
filtered.deinit();
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 2), filtered.items.len);
|
||||
|
||||
// Should contain recent and newest
|
||||
var found_recent = false;
|
||||
var found_newest = false;
|
||||
for (filtered.items) |release| {
|
||||
if (std.mem.eql(u8, release.repo_name, "test/recent")) {
|
||||
found_recent = true;
|
||||
}
|
||||
if (std.mem.eql(u8, release.repo_name, "test/newest")) {
|
||||
found_newest = true;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found_recent);
|
||||
try std.testing.expect(found_newest);
|
||||
}
|
||||
|
||||
// Test filtering from future (should get none)
|
||||
{
|
||||
const future = main.parseReleaseTimestamp("2025-01-01T00:00:00Z") catch 0;
|
||||
var filtered = try main.filterNewReleases(allocator, &releases, future);
|
||||
defer {
|
||||
for (filtered.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
filtered.deinit();
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 0), filtered.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "XML parsing preserves timestamp precision" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const precise_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>precise/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/precise/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-06-15T14:30:45Z</updated>
|
||||
\\ <summary>Precise timestamp test</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var releases = try xml_parser.parseAtomFeed(allocator, precise_xml);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), releases.items.len);
|
||||
try std.testing.expectEqualStrings("2024-06-15T14:30:45Z", releases.items[0].published_at);
|
||||
|
||||
// Verify the timestamp can be parsed correctly
|
||||
const parsed_timestamp = main.parseReleaseTimestamp(releases.items[0].published_at) catch 0;
|
||||
try std.testing.expect(parsed_timestamp > 0);
|
||||
}
|
||||
|
||||
test "compareReleasesByDate with various timestamp formats" {
|
||||
const release_iso_early = Release{
|
||||
.repo_name = "test/iso-early",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/iso-early/releases/tag/v1.0.0",
|
||||
.description = "Early ISO format",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
const release_iso_late = Release{
|
||||
.repo_name = "test/iso-late",
|
||||
.tag_name = "v2.0.0",
|
||||
.published_at = "2024-12-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/iso-late/releases/tag/v2.0.0",
|
||||
.description = "Late ISO format",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
const release_invalid = Release{
|
||||
.repo_name = "test/invalid",
|
||||
.tag_name = "v3.0.0",
|
||||
.published_at = "invalid-date",
|
||||
.html_url = "https://github.com/test/invalid/releases/tag/v3.0.0",
|
||||
.description = "Invalid format",
|
||||
.provider = "github",
|
||||
};
|
||||
|
||||
// Later date should come before earlier date (more recent first)
|
||||
try std.testing.expect(main.compareReleasesByDate({}, release_iso_late, release_iso_early));
|
||||
try std.testing.expect(!main.compareReleasesByDate({}, release_iso_early, release_iso_late));
|
||||
|
||||
// Invalid timestamps should be treated as 0 and come last
|
||||
try std.testing.expect(main.compareReleasesByDate({}, release_iso_early, release_invalid));
|
||||
try std.testing.expect(main.compareReleasesByDate({}, release_iso_late, release_invalid));
|
||||
}
|
314
src/xml_parser.zig
Normal file
314
src/xml_parser.zig
Normal file
|
@ -0,0 +1,314 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
|
||||
const Release = @import("main.zig").Release;
|
||||
|
||||
pub const ParseError = error{
|
||||
InvalidXml,
|
||||
MalformedEntry,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn parseAtomFeed(allocator: Allocator, xml_content: []const u8) !ArrayList(Release) {
|
||||
var releases = ArrayList(Release).init(allocator);
|
||||
errdefer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
var entry_start: ?usize = null;
|
||||
var pos: usize = 0;
|
||||
|
||||
while (pos < xml_content.len) {
|
||||
// Find next entry
|
||||
if (std.mem.indexOf(u8, xml_content[pos..], "<entry>")) |entry_offset| {
|
||||
entry_start = pos + entry_offset;
|
||||
pos = entry_start.? + 7; // Move past "<entry>"
|
||||
|
||||
// Find the end of this entry
|
||||
if (std.mem.indexOf(u8, xml_content[pos..], "</entry>")) |end_offset| {
|
||||
const entry_end = pos + end_offset;
|
||||
const entry_content = xml_content[entry_start.? .. entry_end + 8]; // Include "</entry>"
|
||||
|
||||
if (parseEntry(allocator, entry_content)) |release| {
|
||||
try releases.append(release);
|
||||
} else |err| {
|
||||
std.debug.print("Warning: Failed to parse entry: {}\n", .{err});
|
||||
}
|
||||
|
||||
pos = entry_end + 8; // Move past "</entry>"
|
||||
} else {
|
||||
break; // No closing tag found
|
||||
}
|
||||
} else {
|
||||
break; // No more entries
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
fn parseEntry(allocator: Allocator, entry_xml: []const u8) !Release {
|
||||
var release = Release{
|
||||
.repo_name = try allocator.dupe(u8, ""),
|
||||
.tag_name = try allocator.dupe(u8, ""),
|
||||
.published_at = try allocator.dupe(u8, ""),
|
||||
.html_url = try allocator.dupe(u8, ""),
|
||||
.description = try allocator.dupe(u8, ""),
|
||||
.provider = try allocator.dupe(u8, ""),
|
||||
};
|
||||
errdefer release.deinit(allocator);
|
||||
|
||||
// Parse title to extract repo_name and tag_name
|
||||
if (extractTagContent(entry_xml, "title", allocator)) |title| {
|
||||
defer allocator.free(title);
|
||||
if (std.mem.lastIndexOf(u8, title, " - ")) |dash_pos| {
|
||||
allocator.free(release.repo_name);
|
||||
allocator.free(release.tag_name);
|
||||
release.repo_name = try allocator.dupe(u8, title[0..dash_pos]);
|
||||
release.tag_name = try allocator.dupe(u8, title[dash_pos + 3 ..]);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse link href attribute
|
||||
if (extractLinkHref(entry_xml, allocator)) |url| {
|
||||
allocator.free(release.html_url);
|
||||
release.html_url = url;
|
||||
}
|
||||
|
||||
// Parse updated timestamp
|
||||
if (extractTagContent(entry_xml, "updated", allocator)) |updated| {
|
||||
allocator.free(release.published_at);
|
||||
release.published_at = updated;
|
||||
}
|
||||
|
||||
// Parse summary (description)
|
||||
if (extractTagContent(entry_xml, "summary", allocator)) |summary| {
|
||||
allocator.free(release.description);
|
||||
release.description = summary;
|
||||
}
|
||||
|
||||
// Parse category term attribute (provider)
|
||||
if (extractCategoryTerm(entry_xml, allocator)) |provider| {
|
||||
allocator.free(release.provider);
|
||||
release.provider = provider;
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
fn extractTagContent(xml: []const u8, tag_name: []const u8, allocator: Allocator) ?[]u8 {
|
||||
const open_tag = std.fmt.allocPrint(allocator, "<{s}>", .{tag_name}) catch return null;
|
||||
defer allocator.free(open_tag);
|
||||
const close_tag = std.fmt.allocPrint(allocator, "</{s}>", .{tag_name}) catch return null;
|
||||
defer allocator.free(close_tag);
|
||||
|
||||
if (std.mem.indexOf(u8, xml, open_tag)) |start_pos| {
|
||||
const content_start = start_pos + open_tag.len;
|
||||
if (std.mem.indexOf(u8, xml[content_start..], close_tag)) |end_offset| {
|
||||
const content_end = content_start + end_offset;
|
||||
const content = xml[content_start..content_end];
|
||||
return unescapeXml(allocator, content) catch null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn extractLinkHref(xml: []const u8, allocator: Allocator) ?[]u8 {
|
||||
const pattern = "<link href=\"";
|
||||
if (std.mem.indexOf(u8, xml, pattern)) |start_pos| {
|
||||
const content_start = start_pos + pattern.len;
|
||||
if (std.mem.indexOf(u8, xml[content_start..], "\"")) |end_offset| {
|
||||
const content_end = content_start + end_offset;
|
||||
const href = xml[content_start..content_end];
|
||||
return allocator.dupe(u8, href) catch null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn extractCategoryTerm(xml: []const u8, allocator: Allocator) ?[]u8 {
|
||||
const pattern = "<category term=\"";
|
||||
if (std.mem.indexOf(u8, xml, pattern)) |start_pos| {
|
||||
const content_start = start_pos + pattern.len;
|
||||
if (std.mem.indexOf(u8, xml[content_start..], "\"")) |end_offset| {
|
||||
const content_end = content_start + end_offset;
|
||||
const term = xml[content_start..content_end];
|
||||
return allocator.dupe(u8, term) catch null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn unescapeXml(allocator: Allocator, input: []const u8) ![]u8 {
|
||||
var result = ArrayList(u8).init(allocator);
|
||||
defer result.deinit();
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < input.len) {
|
||||
if (input[i] == '&') {
|
||||
if (std.mem.startsWith(u8, input[i..], "<")) {
|
||||
try result.append('<');
|
||||
i += 4;
|
||||
} else if (std.mem.startsWith(u8, input[i..], ">")) {
|
||||
try result.append('>');
|
||||
i += 4;
|
||||
} else if (std.mem.startsWith(u8, input[i..], "&")) {
|
||||
try result.append('&');
|
||||
i += 5;
|
||||
} else if (std.mem.startsWith(u8, input[i..], """)) {
|
||||
try result.append('"');
|
||||
i += 6;
|
||||
} else if (std.mem.startsWith(u8, input[i..], "'")) {
|
||||
try result.append('\'');
|
||||
i += 6;
|
||||
} else {
|
||||
try result.append(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
try result.append(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toOwnedSlice();
|
||||
}
|
||||
|
||||
// Tests
|
||||
test "parse simple atom entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const entry_xml =
|
||||
\\<entry>
|
||||
\\ <title>test/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\ <id>https://github.com/test/repo/releases/tag/v1.0.0</id>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <author><n>github</n></author>
|
||||
\\ <summary>Test release</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
;
|
||||
|
||||
const release = try parseEntry(allocator, entry_xml);
|
||||
defer release.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqualStrings("test/repo", release.repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0", release.tag_name);
|
||||
try std.testing.expectEqualStrings("https://github.com/test/repo/releases/tag/v1.0.0", release.html_url);
|
||||
try std.testing.expectEqualStrings("2024-01-01T00:00:00Z", release.published_at);
|
||||
try std.testing.expectEqualStrings("Test release", release.description);
|
||||
try std.testing.expectEqualStrings("github", release.provider);
|
||||
}
|
||||
|
||||
test "parse atom entry with escaped characters" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const entry_xml =
|
||||
\\<entry>
|
||||
\\ <title>test/repo<script> - v1.0.0 & more</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\ <id>https://github.com/test/repo/releases/tag/v1.0.0</id>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <author><n>github</n></author>
|
||||
\\ <summary>Test "release" with <special> chars & symbols</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
;
|
||||
|
||||
const release = try parseEntry(allocator, entry_xml);
|
||||
defer release.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqualStrings("test/repo<script>", release.repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0 & more", release.tag_name);
|
||||
try std.testing.expectEqualStrings("Test \"release\" with <special> chars & symbols", release.description);
|
||||
}
|
||||
|
||||
test "parse full atom feed" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const atom_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<subtitle>New releases from starred repositories</subtitle>
|
||||
\\<link href="https://github.com" rel="alternate"/>
|
||||
\\<link href="https://example.com/releases.xml" rel="self"/>
|
||||
\\<id>https://example.com/releases</id>
|
||||
\\<updated>2024-01-01T00:00:00Z</updated>
|
||||
\\<entry>
|
||||
\\ <title>test/repo1 - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo1/releases/tag/v1.0.0"/>
|
||||
\\ <id>https://github.com/test/repo1/releases/tag/v1.0.0</id>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <author><n>github</n></author>
|
||||
\\ <summary>First release</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>test/repo2 - v2.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo2/releases/tag/v2.0.0"/>
|
||||
\\ <id>https://github.com/test/repo2/releases/tag/v2.0.0</id>
|
||||
\\ <updated>2024-01-02T00:00:00Z</updated>
|
||||
\\ <author><n>github</n></author>
|
||||
\\ <summary>Second release</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var releases = try parseAtomFeed(allocator, atom_xml);
|
||||
defer {
|
||||
for (releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
releases.deinit();
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), releases.items.len);
|
||||
|
||||
try std.testing.expectEqualStrings("test/repo1", releases.items[0].repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0", releases.items[0].tag_name);
|
||||
try std.testing.expectEqualStrings("First release", releases.items[0].description);
|
||||
|
||||
try std.testing.expectEqualStrings("test/repo2", releases.items[1].repo_name);
|
||||
try std.testing.expectEqualStrings("v2.0.0", releases.items[1].tag_name);
|
||||
try std.testing.expectEqualStrings("Second release", releases.items[1].description);
|
||||
}
|
||||
|
||||
test "XML unescaping" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const input = "Test <tag> & "quotes" & 'apostrophes'";
|
||||
const result = try unescapeXml(allocator, input);
|
||||
defer allocator.free(result);
|
||||
|
||||
const expected = "Test <tag> & \"quotes\" & 'apostrophes'";
|
||||
try std.testing.expectEqualStrings(expected, result);
|
||||
}
|
||||
|
||||
test "parse entry with missing fields" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const entry_xml =
|
||||
\\<entry>
|
||||
\\ <title>test/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\</entry>
|
||||
;
|
||||
|
||||
const release = try parseEntry(allocator, entry_xml);
|
||||
defer release.deinit(allocator);
|
||||
|
||||
try std.testing.expectEqualStrings("test/repo", release.repo_name);
|
||||
try std.testing.expectEqualStrings("v1.0.0", release.tag_name);
|
||||
try std.testing.expectEqualStrings("https://github.com/test/repo/releases/tag/v1.0.0", release.html_url);
|
||||
// Missing fields should be empty strings
|
||||
try std.testing.expectEqualStrings("", release.published_at);
|
||||
try std.testing.expectEqualStrings("", release.description);
|
||||
try std.testing.expectEqualStrings("", release.provider);
|
||||
}
|
285
src/xml_parser_tests.zig
Normal file
285
src/xml_parser_tests.zig
Normal file
|
@ -0,0 +1,285 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const ArrayList = std.ArrayList;
|
||||
|
||||
const xml_parser = @import("xml_parser.zig");
|
||||
const atom = @import("atom.zig");
|
||||
const Release = @import("main.zig").Release;
|
||||
|
||||
test "round trip: generate atom feed and parse it back" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
// Create test releases
|
||||
const original_releases = [_]Release{
|
||||
Release{
|
||||
.repo_name = "test/repo1",
|
||||
.tag_name = "v1.0.0",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo1/releases/tag/v1.0.0",
|
||||
.description = "First release",
|
||||
.provider = "github",
|
||||
},
|
||||
Release{
|
||||
.repo_name = "test/repo2",
|
||||
.tag_name = "v2.0.0",
|
||||
.published_at = "2024-01-02T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo2/releases/tag/v2.0.0",
|
||||
.description = "Second release",
|
||||
.provider = "github",
|
||||
},
|
||||
};
|
||||
|
||||
// Generate atom feed
|
||||
const atom_content = try atom.generateFeed(allocator, &original_releases);
|
||||
defer allocator.free(atom_content);
|
||||
|
||||
// Parse it back
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, atom_content);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
// Verify we got the same data back
|
||||
try testing.expectEqual(@as(usize, 2), parsed_releases.items.len);
|
||||
|
||||
try testing.expectEqualStrings("test/repo1", parsed_releases.items[0].repo_name);
|
||||
try testing.expectEqualStrings("v1.0.0", parsed_releases.items[0].tag_name);
|
||||
try testing.expectEqualStrings("2024-01-01T00:00:00Z", parsed_releases.items[0].published_at);
|
||||
try testing.expectEqualStrings("https://github.com/test/repo1/releases/tag/v1.0.0", parsed_releases.items[0].html_url);
|
||||
try testing.expectEqualStrings("First release", parsed_releases.items[0].description);
|
||||
try testing.expectEqualStrings("github", parsed_releases.items[0].provider);
|
||||
|
||||
try testing.expectEqualStrings("test/repo2", parsed_releases.items[1].repo_name);
|
||||
try testing.expectEqualStrings("v2.0.0", parsed_releases.items[1].tag_name);
|
||||
try testing.expectEqualStrings("2024-01-02T00:00:00Z", parsed_releases.items[1].published_at);
|
||||
try testing.expectEqualStrings("https://github.com/test/repo2/releases/tag/v2.0.0", parsed_releases.items[1].html_url);
|
||||
try testing.expectEqualStrings("Second release", parsed_releases.items[1].description);
|
||||
try testing.expectEqualStrings("github", parsed_releases.items[1].provider);
|
||||
}
|
||||
|
||||
test "parse atom feed with special characters" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
// Create releases with special characters
|
||||
const original_releases = [_]Release{
|
||||
Release{
|
||||
.repo_name = "test/repo<script>",
|
||||
.tag_name = "v1.0.0 & more",
|
||||
.published_at = "2024-01-01T00:00:00Z",
|
||||
.html_url = "https://github.com/test/repo/releases/tag/v1.0.0",
|
||||
.description = "Test \"release\" with <special> chars & symbols",
|
||||
.provider = "github",
|
||||
},
|
||||
};
|
||||
|
||||
// Generate atom feed (this should escape the characters)
|
||||
const atom_content = try atom.generateFeed(allocator, &original_releases);
|
||||
defer allocator.free(atom_content);
|
||||
|
||||
// Verify the XML contains escaped characters
|
||||
try testing.expect(std.mem.indexOf(u8, atom_content, "<script>") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, atom_content, "& more") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, atom_content, ""release"") != null);
|
||||
|
||||
// Parse it back (this should unescape the characters)
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, atom_content);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
// Verify the parsed data has the original unescaped characters
|
||||
try testing.expectEqual(@as(usize, 1), parsed_releases.items.len);
|
||||
try testing.expectEqualStrings("test/repo<script>", parsed_releases.items[0].repo_name);
|
||||
try testing.expectEqualStrings("v1.0.0 & more", parsed_releases.items[0].tag_name);
|
||||
try testing.expectEqualStrings("Test \"release\" with <special> chars & symbols", parsed_releases.items[0].description);
|
||||
}
|
||||
|
||||
test "parse malformed atom feed gracefully" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const malformed_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>test/repo1 - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo1/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <summary>Good entry</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>test/repo2 - v2.0.0</title>
|
||||
\\ <!-- Missing closing entry tag -->
|
||||
\\<entry>
|
||||
\\ <title>test/repo3 - v3.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo3/releases/tag/v3.0.0"/>
|
||||
\\ <updated>2024-01-03T00:00:00Z</updated>
|
||||
\\ <summary>Another good entry</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, malformed_xml);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
// Should parse the valid entries and skip the malformed one
|
||||
// Note: The malformed entry (repo2) will be parsed but will contain mixed content
|
||||
// The parser finds the first closing </entry> tag which belongs to repo3
|
||||
try testing.expectEqual(@as(usize, 2), parsed_releases.items.len);
|
||||
try testing.expectEqualStrings("test/repo1", parsed_releases.items[0].repo_name);
|
||||
try testing.expectEqualStrings("test/repo2", parsed_releases.items[1].repo_name); // This gets the first title found
|
||||
}
|
||||
|
||||
test "parse empty atom feed" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const empty_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<subtitle>New releases from starred repositories</subtitle>
|
||||
\\<link href="https://github.com" rel="alternate"/>
|
||||
\\<link href="https://example.com/releases.xml" rel="self"/>
|
||||
\\<id>https://example.com/releases</id>
|
||||
\\<updated>2024-01-01T00:00:00Z</updated>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, empty_xml);
|
||||
defer parsed_releases.deinit();
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), parsed_releases.items.len);
|
||||
}
|
||||
|
||||
test "parse atom feed with multiline summaries" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const multiline_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>test/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <summary>This is a multiline
|
||||
\\summary with line breaks
|
||||
\\and multiple paragraphs</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, multiline_xml);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), parsed_releases.items.len);
|
||||
const expected_summary = "This is a multiline\nsummary with line breaks\nand multiple paragraphs";
|
||||
try testing.expectEqualStrings(expected_summary, parsed_releases.items[0].description);
|
||||
}
|
||||
|
||||
test "parse atom feed with different providers" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const multi_provider_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<title>Repository Releases</title>
|
||||
\\<entry>
|
||||
\\ <title>github/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/github/repo/releases/tag/v1.0.0"/>
|
||||
\\ <updated>2024-01-01T00:00:00Z</updated>
|
||||
\\ <summary>GitHub release</summary>
|
||||
\\ <category term="github"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>gitlab/repo - v2.0.0</title>
|
||||
\\ <link href="https://gitlab.com/gitlab/repo/-/releases/v2.0.0"/>
|
||||
\\ <updated>2024-01-02T00:00:00Z</updated>
|
||||
\\ <summary>GitLab release</summary>
|
||||
\\ <category term="gitlab"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>codeberg/repo - v3.0.0</title>
|
||||
\\ <link href="https://codeberg.org/codeberg/repo/releases/tag/v3.0.0"/>
|
||||
\\ <updated>2024-01-03T00:00:00Z</updated>
|
||||
\\ <summary>Codeberg release</summary>
|
||||
\\ <category term="codeberg"/>
|
||||
\\</entry>
|
||||
\\<entry>
|
||||
\\ <title>~user/repo - v4.0.0</title>
|
||||
\\ <link href="https://git.sr.ht/~user/repo/refs/v4.0.0"/>
|
||||
\\ <updated>2024-01-04T00:00:00Z</updated>
|
||||
\\ <summary>SourceHut release</summary>
|
||||
\\ <category term="sourcehut"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, multi_provider_xml);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
try testing.expectEqual(@as(usize, 4), parsed_releases.items.len);
|
||||
|
||||
try testing.expectEqualStrings("github", parsed_releases.items[0].provider);
|
||||
try testing.expectEqualStrings("gitlab", parsed_releases.items[1].provider);
|
||||
try testing.expectEqualStrings("codeberg", parsed_releases.items[2].provider);
|
||||
try testing.expectEqualStrings("sourcehut", parsed_releases.items[3].provider);
|
||||
}
|
||||
|
||||
test "parse atom feed with missing optional fields" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const minimal_xml =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
\\<entry>
|
||||
\\ <title>test/repo - v1.0.0</title>
|
||||
\\ <link href="https://github.com/test/repo/releases/tag/v1.0.0"/>
|
||||
\\</entry>
|
||||
\\</feed>
|
||||
;
|
||||
|
||||
var parsed_releases = try xml_parser.parseAtomFeed(allocator, minimal_xml);
|
||||
defer {
|
||||
for (parsed_releases.items) |release| {
|
||||
release.deinit(allocator);
|
||||
}
|
||||
parsed_releases.deinit();
|
||||
}
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), parsed_releases.items.len);
|
||||
|
||||
const release = parsed_releases.items[0];
|
||||
try testing.expectEqualStrings("test/repo", release.repo_name);
|
||||
try testing.expectEqualStrings("v1.0.0", release.tag_name);
|
||||
try testing.expectEqualStrings("https://github.com/test/repo/releases/tag/v1.0.0", release.html_url);
|
||||
|
||||
// Missing fields should be empty strings
|
||||
try testing.expectEqualStrings("", release.published_at);
|
||||
try testing.expectEqualStrings("", release.description);
|
||||
try testing.expectEqualStrings("", release.provider);
|
||||
}
|
Loading…
Add table
Reference in a new issue