shiplog_render_json/
lib.rs1use anyhow::{Context, Result};
2use shiplog_schema::coverage::CoverageManifest;
3use shiplog_schema::event::EventEnvelope;
4use std::io::Write;
5use std::path::Path;
6
7pub fn write_events_jsonl(path: &Path, events: &[EventEnvelope]) -> Result<()> {
14 let mut f = std::fs::File::create(path).with_context(|| format!("create {path:?}"))?;
15 for ev in events {
16 let line = serde_json::to_string(ev).context("serialize event")?;
17 f.write_all(line.as_bytes())?;
18 f.write_all(b"\n")?;
19 }
20 Ok(())
21}
22
23pub fn write_coverage_manifest(path: &Path, cov: &CoverageManifest) -> Result<()> {
24 let text = serde_json::to_string_pretty(cov).context("serialize coverage")?;
25 std::fs::write(path, text).with_context(|| format!("write {path:?}"))?;
26 Ok(())
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use chrono::{NaiveDate, TimeZone, Utc};
33 use shiplog_ids::EventId;
34 use shiplog_ids::RunId;
35 use shiplog_schema::coverage::{Completeness, CoverageManifest, TimeWindow};
36 use shiplog_schema::event::*;
37
38 fn pr_event(repo: &str, number: u64, title: &str) -> EventEnvelope {
39 EventEnvelope {
40 id: EventId::from_parts(["github", "pr", repo, &number.to_string()]),
41 kind: EventKind::PullRequest,
42 occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
43 actor: Actor {
44 login: "user".into(),
45 id: None,
46 },
47 repo: RepoRef {
48 full_name: repo.to_string(),
49 html_url: Some(format!("https://github.com/{repo}")),
50 visibility: RepoVisibility::Unknown,
51 },
52 payload: EventPayload::PullRequest(PullRequestEvent {
53 number,
54 title: title.to_string(),
55 state: PullRequestState::Merged,
56 created_at: Utc.timestamp_opt(0, 0).unwrap(),
57 merged_at: Some(Utc.timestamp_opt(0, 0).unwrap()),
58 additions: Some(1),
59 deletions: Some(0),
60 changed_files: Some(1),
61 touched_paths_hint: vec![],
62 window: Some(TimeWindow {
63 since: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
64 until: NaiveDate::from_ymd_opt(2025, 2, 1).unwrap(),
65 }),
66 }),
67 tags: vec![],
68 links: vec![Link {
69 label: "pr".into(),
70 url: format!("https://github.com/{repo}/pull/{number}"),
71 }],
72 source: SourceRef {
73 system: SourceSystem::Github,
74 url: Some("https://api.github.com/...".into()),
75 opaque_id: None,
76 },
77 }
78 }
79
80 fn test_coverage() -> CoverageManifest {
81 CoverageManifest {
82 run_id: RunId("test_run".into()),
83 generated_at: Utc.timestamp_opt(0, 0).unwrap(),
84 user: "tester".into(),
85 window: TimeWindow {
86 since: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
87 until: NaiveDate::from_ymd_opt(2025, 2, 1).unwrap(),
88 },
89 mode: "merged".into(),
90 sources: vec!["github".into()],
91 slices: vec![],
92 warnings: vec![],
93 completeness: Completeness::Complete,
94 }
95 }
96
97 #[test]
98 fn jsonl_round_trip() {
99 let dir = tempfile::tempdir().unwrap();
100 let path = dir.path().join("events.jsonl");
101
102 let events = vec![
103 pr_event("acme/foo", 1, "First PR"),
104 pr_event("acme/foo", 2, "Second PR"),
105 ];
106
107 write_events_jsonl(&path, &events).unwrap();
108
109 let text = std::fs::read_to_string(&path).unwrap();
110 for (i, line) in text.lines().enumerate() {
111 let ev: EventEnvelope = serde_json::from_str(line)
112 .unwrap_or_else(|e| panic!("line {i} failed to parse: {e}"));
113 assert_eq!(ev.id, events[i].id);
114 }
115 }
116
117 #[test]
118 fn coverage_manifest_round_trip() {
119 let dir = tempfile::tempdir().unwrap();
120 let path = dir.path().join("coverage.manifest.json");
121
122 let cov = test_coverage();
123 write_coverage_manifest(&path, &cov).unwrap();
124
125 let text = std::fs::read_to_string(&path).unwrap();
126 let loaded: CoverageManifest = serde_json::from_str(&text).unwrap();
127 assert_eq!(loaded.run_id, cov.run_id);
128 assert_eq!(loaded.user, cov.user);
129 assert_eq!(loaded.completeness, cov.completeness);
130 }
131
132 #[test]
133 fn empty_events_produces_empty_file() {
134 let dir = tempfile::tempdir().unwrap();
135 let path = dir.path().join("events.jsonl");
136
137 write_events_jsonl(&path, &[]).unwrap();
138
139 let text = std::fs::read_to_string(&path).unwrap();
140 assert!(text.is_empty());
141 }
142
143 #[test]
144 fn multiple_events_one_per_line() {
145 let dir = tempfile::tempdir().unwrap();
146 let path = dir.path().join("events.jsonl");
147
148 let n = 5;
149 let events: Vec<_> = (1..=n)
150 .map(|i| pr_event("acme/foo", i, &format!("PR {i}")))
151 .collect();
152
153 write_events_jsonl(&path, &events).unwrap();
154
155 let text = std::fs::read_to_string(&path).unwrap();
156 let lines: Vec<_> = text.lines().collect();
157 assert_eq!(lines.len(), n as usize);
158 }
159}