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}