A fork of mtelver's day10 project
1(** Read run data from day10's log directory *)
2
3let list_runs ~log_dir =
4 let runs_dir = Filename.concat log_dir "runs" in
5 if Sys.file_exists runs_dir && Sys.is_directory runs_dir then
6 Sys.readdir runs_dir
7 |> Array.to_list
8 |> List.filter (fun name ->
9 let path = Filename.concat runs_dir name in
10 Sys.is_directory path)
11 |> List.sort (fun a b -> String.compare b a) (* Descending *)
12 else
13 []
14
15let get_latest_run_id ~log_dir =
16 let latest = Filename.concat log_dir "latest" in
17 if Sys.file_exists latest then
18 try
19 let target = Unix.readlink latest in
20 (* Target is like "runs/2026-02-04-120000" *)
21 Some (Filename.basename target)
22 with Unix.Unix_error _ -> None
23 else
24 None
25
26(** Get the most recent run, including runs in progress.
27 This scans the runs/ directory directly rather than relying on the
28 'latest' symlink which is only created when a run completes. *)
29let get_most_recent_run_id ~log_dir =
30 match list_runs ~log_dir with
31 | [] -> None
32 | most_recent :: _ -> Some most_recent
33
34let read_summary ~log_dir ~run_id =
35 let path = Filename.concat log_dir
36 (Filename.concat "runs" (Filename.concat run_id "summary.json")) in
37 if Sys.file_exists path then
38 try
39 let content = In_channel.with_open_text path In_channel.input_all in
40 let json = Yojson.Safe.from_string content in
41 let open Yojson.Safe.Util in
42 let failures =
43 json |> member "failures" |> to_list
44 |> List.map (fun f ->
45 (f |> member "package" |> to_string,
46 f |> member "error" |> to_string))
47 in
48 Some {
49 Day10_lib.Run_log.run_id = json |> member "run_id" |> to_string;
50 start_time = json |> member "start_time" |> to_string;
51 end_time = json |> member "end_time" |> to_string;
52 duration_seconds = json |> member "duration_seconds" |> to_float;
53 targets_requested = json |> member "targets_requested" |> to_int;
54 solutions_found = json |> member "solutions_found" |> to_int;
55 build_success = json |> member "build_success" |> to_int;
56 build_failed = json |> member "build_failed" |> to_int;
57 doc_success = json |> member "doc_success" |> to_int;
58 doc_failed = json |> member "doc_failed" |> to_int;
59 doc_skipped = json |> member "doc_skipped" |> to_int;
60 failures;
61 }
62 with _ -> None
63 else
64 None
65
66let read_log_file path =
67 if Sys.file_exists path then
68 try Some (In_channel.with_open_text path In_channel.input_all)
69 with _ -> None
70 else
71 None
72
73let read_build_log ~log_dir ~run_id ~package =
74 let path = Filename.concat log_dir
75 (Filename.concat "runs"
76 (Filename.concat run_id
77 (Filename.concat "build" (package ^ ".log")))) in
78 read_log_file path
79
80let read_doc_log ~log_dir ~run_id ~package =
81 let path = Filename.concat log_dir
82 (Filename.concat "runs"
83 (Filename.concat run_id
84 (Filename.concat "docs" (package ^ ".log")))) in
85 read_log_file path
86
87let list_logs_in_dir dir =
88 if Sys.file_exists dir && Sys.is_directory dir then
89 Sys.readdir dir
90 |> Array.to_list
91 |> List.filter (fun name -> Filename.check_suffix name ".log")
92 |> List.map (fun name -> Filename.chop_suffix name ".log")
93 |> List.sort String.compare
94 else
95 []
96
97let list_build_logs ~log_dir ~run_id =
98 let dir = Filename.concat log_dir
99 (Filename.concat "runs" (Filename.concat run_id "build")) in
100 list_logs_in_dir dir
101
102let list_doc_logs ~log_dir ~run_id =
103 let dir = Filename.concat log_dir
104 (Filename.concat "runs" (Filename.concat run_id "docs")) in
105 list_logs_in_dir dir
106
107let has_build_log ~log_dir ~run_id ~package =
108 let path = Filename.concat log_dir
109 (Filename.concat "runs"
110 (Filename.concat run_id
111 (Filename.concat "build" (package ^ ".log")))) in
112 Sys.file_exists path
113
114let has_doc_log ~log_dir ~run_id ~package =
115 let path = Filename.concat log_dir
116 (Filename.concat "runs"
117 (Filename.concat run_id
118 (Filename.concat "docs" (package ^ ".log")))) in
119 Sys.file_exists path
120
121let is_run_in_progress ~log_dir ~run_id =
122 let summary_path = Filename.concat log_dir
123 (Filename.concat "runs" (Filename.concat run_id "summary.json")) in
124 (* If no summary.json exists, the run is likely still in progress *)
125 not (Sys.file_exists summary_path)
126
127let get_package_status_from_summary ~log_dir ~run_id ~package =
128 match read_summary ~log_dir ~run_id with
129 | None -> None
130 | Some summary ->
131 (* Check if package is in the failures list *)
132 match List.find_opt (fun (pkg, _) -> pkg = package) summary.failures with
133 | Some (_, error) -> Some (`Failed error)
134 | None ->
135 (* Not in failures - check if logs exist to confirm it was processed *)
136 if has_build_log ~log_dir ~run_id ~package ||
137 has_doc_log ~log_dir ~run_id ~package then
138 Some `Success
139 else
140 Some `Not_in_run