ocptv/output/run.rs
1// (c) Meta Platforms, Inc. and affiliates.
2//
3// Use of this source code is governed by an MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT.
6
7use std::collections::BTreeMap;
8use std::env;
9use std::future::Future;
10use std::sync::{
11 atomic::{self, Ordering},
12 Arc,
13};
14
15use delegate::delegate;
16
17use crate::output as tv;
18use crate::spec;
19use tv::step::TestStep;
20use tv::{config, dut, emitter, error, log};
21
22use super::trait_ext::MapExt;
23
24/// The outcome of a TestRun.
25/// It's returned when the scope method of the [`TestRun`] object is used.
26pub struct TestRunOutcome {
27 /// Reports the execution status of the test
28 pub status: spec::TestStatus,
29 /// Reports the result of the test
30 pub result: spec::TestResult,
31}
32
33/// The main diag test run.
34///
35/// This object describes a single run instance of the diag, and therefore drives the test session.
36pub struct TestRun {
37 name: String,
38 version: String,
39 parameters: BTreeMap<String, tv::Value>,
40 command_line: String,
41 metadata: BTreeMap<String, tv::Value>,
42
43 emitter: Arc<emitter::JsonEmitter>,
44}
45
46impl TestRun {
47 /// Creates a new [`TestRun`] object.
48 ///
49 /// # Examples
50 ///
51 /// ```rust
52 /// # use ocptv::output::*;
53 /// let run = TestRun::new("diagnostic_name", "1.0");
54 /// ```
55 pub fn new(name: &str, version: &str) -> TestRun {
56 TestRunBuilder::new(name, version).build()
57 }
58
59 /// Creates a new [`TestRunBuilder`] object.
60 ///
61 /// # Examples
62 ///
63 /// ```rust
64 /// # use ocptv::output::*;
65 /// let builder = TestRun::builder("run_name", "1.0");
66 /// ```
67 pub fn builder(name: &str, version: &str) -> TestRunBuilder {
68 TestRunBuilder::new(name, version)
69 }
70
71 /// Starts the test run.
72 ///
73 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart>
74 ///
75 /// # Examples
76 ///
77 /// ```rust
78 /// # tokio_test::block_on(async {
79 /// # use ocptv::output::*;
80 /// let run = TestRun::new("diagnostic_name", "1.0");
81 /// let dut = DutInfo::builder("my_dut").build();
82 /// run.start(dut).await?;
83 ///
84 /// # Ok::<(), OcptvError>(())
85 /// # });
86 /// ```
87 pub async fn start(self, dut: dut::DutInfo) -> Result<StartedTestRun, tv::OcptvError> {
88 let start = spec::RootImpl::TestRunArtifact(spec::TestRunArtifact {
89 artifact: spec::TestRunArtifactImpl::TestRunStart(spec::TestRunStart {
90 name: self.name.clone(),
91 version: self.version.clone(),
92 command_line: self.command_line.clone(),
93 parameters: self.parameters.clone(),
94 metadata: self.metadata.option(),
95 dut_info: dut.to_spec(),
96 }),
97 });
98
99 self.emitter.emit(&start).await?;
100
101 Ok(StartedTestRun::new(self))
102 }
103
104 /// Builds a scope in the [`TestRun`] object, taking care of starting and
105 /// ending it. View [`TestRun::start`] and [`StartedTestRun::end`] methods.
106 /// After the scope is constructed, additional objects may be added to it.
107 /// This is the preferred usage for the [`TestRun`], since it guarantees
108 /// all the messages are emitted between the start and end messages, the order
109 /// is respected and no messages is lost.
110 ///
111 /// # Examples
112 ///
113 /// ```rust
114 /// # tokio_test::block_on(async {
115 /// # use futures::FutureExt;
116 /// # use ocptv::output::*;
117 /// let run = TestRun::new("diagnostic_name", "1.0");
118 /// let dut = DutInfo::builder("my_dut").build();
119 /// run.scope(dut, |r| {
120 /// async move {
121 /// r.add_log(LogSeverity::Info, "First message").await?;
122 /// Ok(TestRunOutcome {
123 /// status: TestStatus::Complete,
124 /// result: TestResult::Pass,
125 /// })
126 /// }.boxed()
127 /// }).await?;
128 ///
129 /// # Ok::<(), OcptvError>(())
130 /// # });
131 /// ```
132 pub async fn scope<F, R>(self, dut: dut::DutInfo, func: F) -> Result<(), tv::OcptvError>
133 where
134 R: Future<Output = Result<TestRunOutcome, tv::OcptvError>> + Send + 'static,
135 F: FnOnce(ScopedTestRun) -> R,
136 {
137 let run = Arc::new(self.start(dut).await?);
138 let outcome = func(ScopedTestRun {
139 run: Arc::clone(&run),
140 })
141 .await?;
142 run.end_impl(outcome.status, outcome.result).await?;
143
144 Ok(())
145 }
146
147 /// Emits a Error message.
148 ///
149 /// This operation is useful in such cases when there is an error before starting the test.
150 /// (eg. failing to discover a DUT).
151 ///
152 /// See: [`StartedTestRun::add_error`] for details and examples.
153 pub async fn add_error(&self, symptom: &str) -> Result<(), tv::OcptvError> {
154 let error = error::Error::builder(symptom).build();
155
156 self.add_error_detail(error).await?;
157 Ok(())
158 }
159
160 /// Emits a Error message.
161 ///
162 /// This operation is useful in such cases when there is an error before starting the test.
163 /// (eg. failing to discover a DUT).
164 ///
165 /// See: [`StartedTestRun::add_error_msg`] for details and examples.
166 pub async fn add_error_msg(&self, symptom: &str, msg: &str) -> Result<(), tv::OcptvError> {
167 let error = error::Error::builder(symptom).message(msg).build();
168
169 self.add_error_detail(error).await?;
170 Ok(())
171 }
172
173 /// Emits a Error message.
174 ///
175 /// This operation is useful in such cases when there is an error before starting the test.
176 /// (eg. failing to discover a DUT).
177 ///
178 /// See: [`StartedTestRun::add_error_detail`] for details and examples.
179 pub async fn add_error_detail(&self, error: error::Error) -> Result<(), tv::OcptvError> {
180 let artifact = spec::TestRunArtifact {
181 artifact: spec::TestRunArtifactImpl::Error(error.to_artifact()),
182 };
183 self.emitter
184 .emit(&spec::RootImpl::TestRunArtifact(artifact))
185 .await?;
186
187 Ok(())
188 }
189}
190
191/// Builder for the [`TestRun`] object.
192#[derive(Default)]
193pub struct TestRunBuilder {
194 name: String,
195 version: String,
196 parameters: BTreeMap<String, tv::Value>,
197 command_line: String,
198
199 config: Option<config::Config>,
200 metadata: BTreeMap<String, tv::Value>,
201}
202
203impl TestRunBuilder {
204 fn new(name: &str, version: &str) -> Self {
205 Self {
206 name: name.to_string(),
207 version: version.to_string(),
208 parameters: BTreeMap::new(),
209 command_line: env::args().collect::<Vec<_>>()[1..].join(" "),
210 ..Default::default()
211 }
212 }
213
214 /// Adds a user defined parameter to the future [`TestRun`] object.
215 ///
216 /// # Examples
217 ///
218 /// ```rust
219 /// # use ocptv::output::*;
220 /// let run = TestRun::builder("run_name", "1.0")
221 /// .add_parameter("param1", "value1")
222 /// .build();
223 /// ```
224 pub fn add_parameter<V: Into<tv::Value>>(mut self, key: &str, value: V) -> Self {
225 self.parameters.insert(key.to_string(), value.into());
226 self
227 }
228
229 /// Adds the command line used to run the test session to the future
230 /// [`TestRun`] object.
231 ///
232 /// # Examples
233 ///
234 /// ```rust
235 /// # use ocptv::output::*;
236 /// let run = TestRun::builder("run_name", "1.0")
237 /// .command_line("my_diag --arg value")
238 /// .build();
239 /// ```
240 pub fn command_line(mut self, cmd: &str) -> Self {
241 self.command_line = cmd.to_string();
242 self
243 }
244
245 /// Adds the configuration for the test session to the future [`TestRun`] object
246 ///
247 /// # Examples
248 ///
249 /// ```rust
250 /// # use ocptv::output::*;
251 /// let run = TestRun::builder("run_name", "1.0")
252 /// .config(Config::builder().build())
253 /// .build();
254 /// ```
255 pub fn config(mut self, value: config::Config) -> Self {
256 self.config = Some(value);
257 self
258 }
259
260 /// Adds user defined metadata to the future [`TestRun`] object
261 ///
262 /// # Examples
263 ///
264 /// ```rust
265 /// # use ocptv::output::*;
266 ///
267 /// let run = TestRun::builder("run_name", "1.0")
268 /// .add_metadata("meta1", "value1")
269 /// .build();
270 /// ```
271 pub fn add_metadata<V: Into<tv::Value>>(mut self, key: &str, value: V) -> Self {
272 self.metadata.insert(key.to_string(), value.into());
273 self
274 }
275
276 pub fn build(self) -> TestRun {
277 let config = self.config.unwrap_or(config::Config::builder().build());
278 let emitter = emitter::JsonEmitter::new(config.timestamp_provider, config.writer);
279
280 TestRun {
281 name: self.name,
282 version: self.version,
283 parameters: self.parameters,
284 command_line: self.command_line,
285 metadata: self.metadata,
286
287 emitter: Arc::new(emitter),
288 }
289 }
290}
291
292/// A test run that was started.
293///
294/// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunstart>
295pub struct StartedTestRun {
296 run: TestRun,
297
298 step_seqno: atomic::AtomicU64,
299}
300
301impl StartedTestRun {
302 fn new(run: TestRun) -> StartedTestRun {
303 StartedTestRun {
304 run,
305 step_seqno: atomic::AtomicU64::new(0),
306 }
307 }
308
309 // note: keep the self-consuming method for crate api, but use this one internally,
310 // since `StartedTestRun::end` only needs to take ownership for syntactic reasons
311 async fn end_impl(
312 &self,
313 status: spec::TestStatus,
314 result: spec::TestResult,
315 ) -> Result<(), tv::OcptvError> {
316 let end = spec::RootImpl::TestRunArtifact(spec::TestRunArtifact {
317 artifact: spec::TestRunArtifactImpl::TestRunEnd(spec::TestRunEnd { status, result }),
318 });
319
320 self.run.emitter.emit(&end).await?;
321 Ok(())
322 }
323
324 /// Ends the test run.
325 ///
326 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#testrunend>
327 ///
328 /// # Examples
329 ///
330 /// ```rust
331 /// # tokio_test::block_on(async {
332 /// # use ocptv::output::*;
333 /// let dut = DutInfo::builder("my_dut").build();
334 /// let run = TestRun::new("diagnostic_name", "1.0").start(dut).await?;
335 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
336 ///
337 /// # Ok::<(), OcptvError>(())
338 /// # });
339 /// ```
340 pub async fn end(
341 self,
342 status: spec::TestStatus,
343 result: spec::TestResult,
344 ) -> Result<(), tv::OcptvError> {
345 self.end_impl(status, result).await
346 }
347
348 /// Emits a Log message.
349 /// This method accepts a [`tv::LogSeverity`] to define the severity
350 /// and a [`String`] for the message.
351 ///
352 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log>
353 ///
354 /// # Examples
355 ///
356 /// ```rust
357 /// # tokio_test::block_on(async {
358 /// # use ocptv::output::*;
359 /// let dut = DutInfo::builder("my_dut").build();
360 /// let run = TestRun::new("diagnostic_name", "1.0").start(dut).await?;
361 /// run.add_log(
362 /// LogSeverity::Info,
363 /// "This is a log message with INFO severity",
364 /// ).await?;
365 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
366 ///
367 /// # Ok::<(), OcptvError>(())
368 /// # });
369 /// ```
370 pub async fn add_log(
371 &self,
372 severity: spec::LogSeverity,
373 msg: &str,
374 ) -> Result<(), tv::OcptvError> {
375 let log = log::Log::builder(msg).severity(severity).build();
376
377 let artifact = spec::TestRunArtifact {
378 artifact: spec::TestRunArtifactImpl::Log(log.to_artifact()),
379 };
380 self.run
381 .emitter
382 .emit(&spec::RootImpl::TestRunArtifact(artifact))
383 .await?;
384
385 Ok(())
386 }
387
388 /// Emits a Log message.
389 /// This method accepts a [`tv::Log`] object.
390 ///
391 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#log>
392 ///
393 /// # Examples
394 ///
395 /// ```rust
396 /// # tokio_test::block_on(async {
397 /// # use ocptv::output::*;
398 /// let dut = DutInfo::builder("my_dut").build();
399 /// let run = TestRun::new("diagnostic_name", "1.0").start(dut).await?;
400 /// run.add_log_detail(
401 /// Log::builder("This is a log message with INFO severity")
402 /// .severity(LogSeverity::Info)
403 /// .source("file", 1)
404 /// .build(),
405 /// ).await?;
406 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
407 ///
408 /// # Ok::<(), OcptvError>(())
409 /// # });
410 /// ```
411 pub async fn add_log_detail(&self, log: log::Log) -> Result<(), tv::OcptvError> {
412 let artifact = spec::TestRunArtifact {
413 artifact: spec::TestRunArtifactImpl::Log(log.to_artifact()),
414 };
415 self.run
416 .emitter
417 .emit(&spec::RootImpl::TestRunArtifact(artifact))
418 .await?;
419
420 Ok(())
421 }
422
423 /// Emits a Error message.
424 /// This method accepts a [`String`] to define the symptom.
425 ///
426 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error>
427 ///
428 /// # Examples
429 ///
430 /// ```rust
431 /// # tokio_test::block_on(async {
432 /// # use ocptv::output::*;
433 /// let dut = DutInfo::builder("my_dut").build();
434 /// let run = TestRun::new("diagnostic_name", "1.0").start(dut).await?;
435 /// run.add_error("symptom").await?;
436 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
437 ///
438 /// # Ok::<(), OcptvError>(())
439 /// # });
440 /// ```
441 pub async fn add_error(&self, symptom: &str) -> Result<(), tv::OcptvError> {
442 let error = error::Error::builder(symptom).build();
443
444 self.add_error_detail(error).await?;
445 Ok(())
446 }
447
448 /// Emits a Error message.
449 /// This method accepts a [`String`] to define the symptom and
450 /// another [`String`] as error message.
451 ///
452 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error>
453 ///
454 /// # Examples
455 ///
456 /// ```rust
457 /// # tokio_test::block_on(async {
458 /// # use ocptv::output::*;
459 /// let dut = DutInfo::builder("my_dut").build();
460 /// let run = TestRun::new("diagnostic_name", "1.0").start(dut).await?;
461 /// run.add_error_msg("symptom", "error messasge").await?;
462 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
463 ///
464 /// # Ok::<(), OcptvError>(())
465 /// # });
466 /// ```
467 pub async fn add_error_msg(&self, symptom: &str, msg: &str) -> Result<(), tv::OcptvError> {
468 let error = error::Error::builder(symptom).message(msg).build();
469
470 self.add_error_detail(error).await?;
471 Ok(())
472 }
473
474 /// Emits a Error message.
475 /// This method accepts an [`tv::Error`] object.
476 ///
477 /// ref: <https://github.com/opencomputeproject/ocp-diag-core/tree/main/json_spec#error>
478 ///
479 /// # Examples
480 ///
481 /// ```rust
482 /// # tokio_test::block_on(async {
483 /// # use ocptv::output::*;
484 /// let mut dut = DutInfo::new("my_dut");
485 /// let sw_info = dut.add_software_info(SoftwareInfo::builder("name").build());
486 /// let run = TestRun::builder("diagnostic_name", "1.0").build().start(dut).await?;
487 ///
488 /// run.add_error_detail(
489 /// Error::builder("symptom")
490 /// .message("Error message")
491 /// .source("file", 1)
492 /// .add_software_info(&sw_info)
493 /// .build(),
494 /// ).await?;
495 ///
496 /// run.end(TestStatus::Complete, TestResult::Pass).await?;
497 ///
498 /// # Ok::<(), OcptvError>(())
499 /// # });
500 /// ```
501 pub async fn add_error_detail(&self, error: error::Error) -> Result<(), tv::OcptvError> {
502 let artifact = spec::TestRunArtifact {
503 artifact: spec::TestRunArtifactImpl::Error(error.to_artifact()),
504 };
505 self.run
506 .emitter
507 .emit(&spec::RootImpl::TestRunArtifact(artifact))
508 .await?;
509
510 Ok(())
511 }
512
513 /// Create a new step for this test run.
514 /// TODO: docs + example
515 pub fn add_step(&self, name: &str) -> TestStep {
516 let step_id = format!("step{}", self.step_seqno.fetch_add(1, Ordering::AcqRel));
517 TestStep::new(&step_id, name, Arc::clone(&self.run.emitter))
518 }
519}
520
521/// TODO: docs
522pub struct ScopedTestRun {
523 run: Arc<StartedTestRun>,
524}
525
526impl ScopedTestRun {
527 delegate! {
528 to self.run {
529 pub async fn add_log(&self, severity: spec::LogSeverity, msg: &str) -> Result<(), tv::OcptvError>;
530 pub async fn add_log_detail(&self, log: log::Log) -> Result<(), tv::OcptvError>;
531
532 pub async fn add_error(&self, symptom: &str) -> Result<(), tv::OcptvError>;
533 pub async fn add_error_msg(&self, symptom: &str, msg: &str) -> Result<(), tv::OcptvError>;
534 pub async fn add_error_detail(&self, error: error::Error) -> Result<(), tv::OcptvError>;
535
536 pub fn add_step(&self, name: &str) -> TestStep;
537 }
538 }
539}