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::{
18 Styles,
19 format::{RecordedBuildScope, RerunInfo},
20 },
21 reporter::{
22 RunFinishedInfo,
23 events::{FinalRunStats, RunFinishedStats, StressFinalRunStats},
24 },
25};
26use bytesize::ByteSize;
27use camino::{Utf8Path, Utf8PathBuf};
28use chrono::{DateTime, FixedOffset};
29use owo_colors::OwoColorize;
30use quick_junit::ReportUuid;
31use semver::Version;
32use std::{collections::BTreeMap, fmt};
33
34#[derive(Clone, Debug)]
36pub struct RecordSessionConfig<'a> {
37 pub workspace_root: &'a Utf8Path,
39 pub run_id: ReportUuid,
41 pub nextest_version: Version,
43 pub started_at: DateTime<FixedOffset>,
45 pub cli_args: Vec<String>,
47 pub build_scope: RecordedBuildScope,
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.env_vars,
104 config.max_output_size,
105 config.rerun_info.as_ref().map(|info| info.parent_run_id),
106 )
107 .map_err(RecordSetupError::RecorderCreate)?;
108
109 recorder
110 .write_build_scope(&config.build_scope)
111 .map_err(RecordSetupError::RecorderCreate)?;
112
113 if let Some(rerun_info) = config.rerun_info {
115 recorder
116 .write_rerun_info(&rerun_info)
117 .map_err(RecordSetupError::RecorderCreate)?;
118 }
119
120 let session = RecordSession {
121 state_dir,
122 run_id: config.run_id,
123 run_id_unique_prefix,
124 };
125
126 Ok(RecordSessionSetup { session, recorder })
127 }
128
129 pub fn run_id(&self) -> ReportUuid {
131 self.run_id
132 }
133
134 pub fn run_id_unique_prefix(&self) -> &ShortestRunIdPrefix {
136 &self.run_id_unique_prefix
137 }
138
139 pub fn state_dir(&self) -> &Utf8Path {
141 &self.state_dir
142 }
143
144 pub fn finalize(
156 self,
157 recording_sizes: Option<StoreSizes>,
158 run_finished: Option<RunFinishedInfo>,
159 exit_code: i32,
160 policy: &RecordRetentionPolicy,
161 ) -> RecordFinalizeResult {
162 let mut result = RecordFinalizeResult::default();
163
164 let Some(sizes) = recording_sizes else {
166 return result;
167 };
168
169 let (status, duration_secs) = match run_finished {
171 Some(info) => (
172 convert_run_stats_to_status(info.stats, exit_code),
173 Some(info.elapsed.as_secs_f64()),
174 ),
175 None => (RecordedRunStatus::Incomplete, None),
177 };
178
179 let store = match RunStore::new(&self.state_dir) {
181 Ok(store) => store,
182 Err(err) => {
183 result
184 .warnings
185 .push(RecordFinalizeWarning::StoreOpenFailed(err));
186 return result;
187 }
188 };
189
190 let mut locked_store = match store.lock_exclusive() {
191 Ok(locked) => locked,
192 Err(err) => {
193 result
194 .warnings
195 .push(RecordFinalizeWarning::StoreLockFailed(err));
196 return result;
197 }
198 };
199
200 match locked_store.complete_run(self.run_id, sizes, status, duration_secs) {
202 Ok(true) => {}
203 Ok(false) => {
204 result
206 .warnings
207 .push(RecordFinalizeWarning::RunNotFoundDuringComplete(
208 self.run_id,
209 ));
210 }
211 Err(err) => {
212 result
213 .warnings
214 .push(RecordFinalizeWarning::MetadataPersistFailed(err));
215 }
216 }
217 match locked_store.prune_if_needed(policy) {
221 Ok(Some(mut prune_result)) => {
222 for error in prune_result.errors.drain(..) {
224 result
225 .warnings
226 .push(RecordFinalizeWarning::PruneError(error));
227 }
228 result.prune_result = Some(prune_result);
229 }
230 Ok(None) => {
231 }
233 Err(err) => {
234 result
235 .warnings
236 .push(RecordFinalizeWarning::PruneFailed(err));
237 }
238 }
239
240 result
241 }
242}
243
244fn convert_run_stats_to_status(stats: RunFinishedStats, exit_code: i32) -> RecordedRunStatus {
246 match stats {
247 RunFinishedStats::Single(run_stats) => {
248 let completed_stats = CompletedRunStats {
249 initial_run_count: run_stats.initial_run_count,
250 passed: run_stats.passed,
251 failed: run_stats.failed_count(),
252 exit_code,
253 };
254
255 match run_stats.summarize_final() {
257 FinalRunStats::Success
258 | FinalRunStats::NoTestsRun
259 | FinalRunStats::Failed { .. } => RecordedRunStatus::Completed(completed_stats),
260 FinalRunStats::Cancelled { .. } => RecordedRunStatus::Cancelled(completed_stats),
261 }
262 }
263 RunFinishedStats::Stress(stress_stats) => {
264 let stress_completed_stats = StressCompletedRunStats {
265 initial_iteration_count: stress_stats.completed.total,
266 success_count: stress_stats.success_count,
267 failed_count: stress_stats.failed_count,
268 exit_code,
269 };
270
271 match stress_stats.summarize_final() {
273 StressFinalRunStats::Success
274 | StressFinalRunStats::NoTestsRun
275 | StressFinalRunStats::Failed => {
276 RecordedRunStatus::StressCompleted(stress_completed_stats)
277 }
278 StressFinalRunStats::Cancelled => {
279 RecordedRunStatus::StressCancelled(stress_completed_stats)
280 }
281 }
282 }
283 }
284}
285
286#[derive(Debug, Default)]
288pub struct RecordFinalizeResult {
289 pub warnings: Vec<RecordFinalizeWarning>,
291 pub prune_result: Option<PruneResult>,
293}
294
295impl RecordFinalizeResult {
296 pub fn log(&self, styles: &Styles) {
298 for warning in &self.warnings {
299 tracing::warn!("{warning}");
300 }
301
302 if let Some(prune_result) = &self.prune_result
303 && (prune_result.deleted_count > 0 || prune_result.orphans_deleted > 0)
304 {
305 tracing::info!(
306 "{}(hint: {} to replay runs)",
307 prune_result.display(styles),
308 "cargo nextest replay".style(styles.count),
309 );
310 }
311 }
312}
313
314#[derive(Debug)]
316pub enum RecordFinalizeWarning {
317 StoreOpenFailed(RunStoreError),
319 StoreLockFailed(RunStoreError),
321 MetadataPersistFailed(RunStoreError),
323 RunNotFoundDuringComplete(ReportUuid),
328 PruneFailed(RunStoreError),
330 PruneError(RecordPruneError),
332}
333
334impl fmt::Display for RecordFinalizeWarning {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 match self {
337 Self::StoreOpenFailed(err) => {
338 write!(f, "recording completed but failed to open run store: {err}")
339 }
340 Self::StoreLockFailed(err) => {
341 write!(f, "recording completed but failed to lock run store: {err}")
342 }
343 Self::MetadataPersistFailed(err) => {
344 write!(
345 f,
346 "recording completed but failed to persist run metadata: {err}"
347 )
348 }
349 Self::RunNotFoundDuringComplete(run_id) => {
350 write!(
351 f,
352 "recording completed but run {run_id} was not found in store \
353 (may have been pruned during execution)"
354 )
355 }
356 Self::PruneFailed(err) => write!(f, "error during prune: {err}"),
357 Self::PruneError(msg) => write!(f, "error during prune: {msg}"),
358 }
359 }
360}