1use super::{
11 CompletedRunStats, RecordedRunStatus, RunRecorder, RunStore, ShortestRunIdPrefix, StoreSizes,
12 StressCompletedRunStats, records_state_dir,
13 retention::{PruneResult, RecordRetentionPolicy},
14};
15use crate::{
16 errors::{RecordPruneError, RecordSetupError, RunStoreError},
17 record::{Styles, format::RerunInfo},
18 reporter::{
19 RunFinishedInfo,
20 events::{FinalRunStats, RunFinishedStats, StressFinalRunStats},
21 },
22};
23use bytesize::ByteSize;
24use camino::{Utf8Path, Utf8PathBuf};
25use chrono::{DateTime, FixedOffset};
26use owo_colors::OwoColorize;
27use quick_junit::ReportUuid;
28use semver::Version;
29use std::{collections::BTreeMap, fmt};
30
31#[derive(Clone, Debug)]
33pub struct RecordSessionConfig<'a> {
34 pub workspace_root: &'a Utf8Path,
36 pub run_id: ReportUuid,
38 pub nextest_version: Version,
40 pub started_at: DateTime<FixedOffset>,
42 pub cli_args: Vec<String>,
44 pub build_scope_args: Vec<String>,
49 pub env_vars: BTreeMap<String, String>,
51 pub max_output_size: ByteSize,
53 pub rerun_info: Option<RerunInfo>,
57}
58
59#[derive(Debug)]
61pub struct RecordSessionSetup {
62 pub session: RecordSession,
64 pub recorder: RunRecorder,
66}
67
68#[derive(Debug)]
72pub struct RecordSession {
73 state_dir: Utf8PathBuf,
74 run_id: ReportUuid,
75 run_id_unique_prefix: ShortestRunIdPrefix,
76}
77
78impl RecordSession {
79 pub fn setup(config: RecordSessionConfig<'_>) -> Result<RecordSessionSetup, RecordSetupError> {
88 let state_dir =
89 records_state_dir(config.workspace_root).map_err(RecordSetupError::StateDirNotFound)?;
90
91 let store = RunStore::new(&state_dir).map_err(RecordSetupError::StoreCreate)?;
92
93 let locked_store = store
94 .lock_exclusive()
95 .map_err(RecordSetupError::StoreLock)?;
96
97 let (mut recorder, run_id_unique_prefix) = locked_store
98 .create_run_recorder(
99 config.run_id,
100 config.nextest_version,
101 config.started_at,
102 config.cli_args,
103 config.build_scope_args,
104 config.env_vars,
105 config.max_output_size,
106 config.rerun_info.as_ref().map(|info| info.parent_run_id),
107 )
108 .map_err(RecordSetupError::RecorderCreate)?;
109
110 if let Some(rerun_info) = config.rerun_info {
112 recorder
113 .write_rerun_info(&rerun_info)
114 .map_err(RecordSetupError::RecorderCreate)?;
115 }
116
117 let session = RecordSession {
118 state_dir,
119 run_id: config.run_id,
120 run_id_unique_prefix,
121 };
122
123 Ok(RecordSessionSetup { session, recorder })
124 }
125
126 pub fn run_id(&self) -> ReportUuid {
128 self.run_id
129 }
130
131 pub fn run_id_unique_prefix(&self) -> &ShortestRunIdPrefix {
133 &self.run_id_unique_prefix
134 }
135
136 pub fn state_dir(&self) -> &Utf8Path {
138 &self.state_dir
139 }
140
141 pub fn finalize(
153 self,
154 recording_sizes: Option<StoreSizes>,
155 run_finished: Option<RunFinishedInfo>,
156 exit_code: i32,
157 policy: &RecordRetentionPolicy,
158 ) -> RecordFinalizeResult {
159 let mut result = RecordFinalizeResult::default();
160
161 let Some(sizes) = recording_sizes else {
163 return result;
164 };
165
166 let (status, duration_secs) = match run_finished {
168 Some(info) => (
169 convert_run_stats_to_status(info.stats, exit_code),
170 Some(info.elapsed.as_secs_f64()),
171 ),
172 None => (RecordedRunStatus::Incomplete, None),
174 };
175
176 let store = match RunStore::new(&self.state_dir) {
178 Ok(store) => store,
179 Err(err) => {
180 result
181 .warnings
182 .push(RecordFinalizeWarning::StoreOpenFailed(err));
183 return result;
184 }
185 };
186
187 let mut locked_store = match store.lock_exclusive() {
188 Ok(locked) => locked,
189 Err(err) => {
190 result
191 .warnings
192 .push(RecordFinalizeWarning::StoreLockFailed(err));
193 return result;
194 }
195 };
196
197 match locked_store.complete_run(self.run_id, sizes, status, duration_secs) {
199 Ok(true) => {}
200 Ok(false) => {
201 result
203 .warnings
204 .push(RecordFinalizeWarning::RunNotFoundDuringComplete(
205 self.run_id,
206 ));
207 }
208 Err(err) => {
209 result
210 .warnings
211 .push(RecordFinalizeWarning::MetadataPersistFailed(err));
212 }
213 }
214 match locked_store.prune_if_needed(policy) {
218 Ok(Some(mut prune_result)) => {
219 for error in prune_result.errors.drain(..) {
221 result
222 .warnings
223 .push(RecordFinalizeWarning::PruneError(error));
224 }
225 result.prune_result = Some(prune_result);
226 }
227 Ok(None) => {
228 }
230 Err(err) => {
231 result
232 .warnings
233 .push(RecordFinalizeWarning::PruneFailed(err));
234 }
235 }
236
237 result
238 }
239}
240
241fn convert_run_stats_to_status(stats: RunFinishedStats, exit_code: i32) -> RecordedRunStatus {
243 match stats {
244 RunFinishedStats::Single(run_stats) => {
245 let completed_stats = CompletedRunStats {
246 initial_run_count: run_stats.initial_run_count,
247 passed: run_stats.passed,
248 failed: run_stats.failed_count(),
249 exit_code,
250 };
251
252 match run_stats.summarize_final() {
254 FinalRunStats::Success
255 | FinalRunStats::NoTestsRun
256 | FinalRunStats::Failed { .. } => RecordedRunStatus::Completed(completed_stats),
257 FinalRunStats::Cancelled { .. } => RecordedRunStatus::Cancelled(completed_stats),
258 }
259 }
260 RunFinishedStats::Stress(stress_stats) => {
261 let stress_completed_stats = StressCompletedRunStats {
262 initial_iteration_count: stress_stats.completed.total,
263 success_count: stress_stats.success_count,
264 failed_count: stress_stats.failed_count,
265 exit_code,
266 };
267
268 match stress_stats.summarize_final() {
270 StressFinalRunStats::Success
271 | StressFinalRunStats::NoTestsRun
272 | StressFinalRunStats::Failed => {
273 RecordedRunStatus::StressCompleted(stress_completed_stats)
274 }
275 StressFinalRunStats::Cancelled => {
276 RecordedRunStatus::StressCancelled(stress_completed_stats)
277 }
278 }
279 }
280 }
281}
282
283#[derive(Debug, Default)]
285pub struct RecordFinalizeResult {
286 pub warnings: Vec<RecordFinalizeWarning>,
288 pub prune_result: Option<PruneResult>,
290}
291
292impl RecordFinalizeResult {
293 pub fn log(&self, styles: &Styles) {
295 for warning in &self.warnings {
296 tracing::warn!("{warning}");
297 }
298
299 if let Some(prune_result) = &self.prune_result
300 && (prune_result.deleted_count > 0 || prune_result.orphans_deleted > 0)
301 {
302 tracing::info!(
303 "{}(hint: {} to replay runs)",
304 prune_result.display(styles),
305 "cargo nextest replay".style(styles.count),
306 );
307 }
308 }
309}
310
311#[derive(Debug)]
313pub enum RecordFinalizeWarning {
314 StoreOpenFailed(RunStoreError),
316 StoreLockFailed(RunStoreError),
318 MetadataPersistFailed(RunStoreError),
320 RunNotFoundDuringComplete(ReportUuid),
325 PruneFailed(RunStoreError),
327 PruneError(RecordPruneError),
329}
330
331impl fmt::Display for RecordFinalizeWarning {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 match self {
334 Self::StoreOpenFailed(err) => {
335 write!(f, "recording completed but failed to open run store: {err}")
336 }
337 Self::StoreLockFailed(err) => {
338 write!(f, "recording completed but failed to lock run store: {err}")
339 }
340 Self::MetadataPersistFailed(err) => {
341 write!(
342 f,
343 "recording completed but failed to persist run metadata: {err}"
344 )
345 }
346 Self::RunNotFoundDuringComplete(run_id) => {
347 write!(
348 f,
349 "recording completed but run {run_id} was not found in store \
350 (may have been pruned during execution)"
351 )
352 }
353 Self::PruneFailed(err) => write!(f, "error during prune: {err}"),
354 Self::PruneError(msg) => write!(f, "error during prune: {msg}"),
355 }
356 }
357}