cogno/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use crate::error::CognoError;
4use crate::report::model::{
5    is_a_not_assertion, is_passed_assertion, AssertionDef, AssertionType, TestDef,
6};
7use crate::report::{Reporter, RawReporter};
8use crate::spec::{load_spec_modifier, AssertionModifier, SpecModifier};
9pub use assert::*;
10pub use cogno_attr::*;
11pub use proc::*;
12use itertools::Itertools;
13use std::collections::HashSet;
14use std::fmt::Debug;
15#[cfg(feature = "console")]
16use crate::report::ConsoleReporter;
17
18pub extern crate tracing;
19pub extern crate tracing_subscriber;
20
21mod error;
22mod report;
23mod spec;
24mod assert;
25mod proc;
26
27/// Used by the test harness. Not for direct use.
28///
29/// Holds test state by recording assertions and test metadata. It drives the test reporter as
30/// assertions arrive.
31#[derive(Debug)]
32pub struct TestController {
33    tests: Vec<TestDef>,
34    specs: HashSet<String>,
35    modifiers: Vec<SpecModifier>,
36    reporter: Box<dyn Reporter>,
37}
38
39impl TestController {
40    pub fn new() -> Result<Self, CognoError> {
41        let specs = load_specs();
42        let modifiers = load_modifiers()?;
43
44        Ok(TestController {
45            tests: Vec::new(),
46            specs,
47            modifiers,
48            reporter: create_reporter(),
49        })
50    }
51
52    pub fn is_spec_enabled(&self, spec: &str) -> bool {
53        self.specs.contains(spec)
54    }
55
56    pub fn register(&mut self, name: &str, spec_id: &str) {
57        self.tests.push(TestDef {
58            name: name.to_string(),
59            spec_id: spec_id.to_string(),
60            panic_info: None,
61            completed: false,
62            assertions: Vec::new(),
63        });
64    }
65
66    pub fn set_panic_info(&mut self, info: String) {
67        if self.tests.is_empty() {
68            return;
69        }
70
71        let current_test = self.tests.last_mut().unwrap();
72        current_test.panic_info = Some(info);
73        self.reporter.report(current_test);
74    }
75
76    pub fn complete(&mut self) {
77        let current_test = self.tests.last_mut().unwrap();
78        current_test.completed = true;
79        self.reporter.report(current_test);
80    }
81
82    pub fn finalize(&self) -> Result<(), CognoError> {
83        self.reporter.finalize()
84    }
85
86    pub fn must_eq<T: PartialEq + Debug>(
87        &mut self,
88        id: &str,
89        expected: T,
90        actual: T,
91    ) -> Result<(), CognoError> {
92        self.append_assert(id, AssertionType::Must, expected, actual)
93    }
94
95    pub fn must_not_eq<T: PartialEq + Debug>(
96        &mut self,
97        id: &str,
98        expected: T,
99        actual: T,
100    ) -> Result<(), CognoError> {
101        self.append_assert(id, AssertionType::MustNot, expected, actual)
102    }
103
104    pub fn should_eq<T: PartialEq + Debug>(
105        &mut self,
106        id: &str,
107        expected: T,
108        actual: T,
109    ) -> Result<(), CognoError> {
110        self.append_assert(id, AssertionType::Should, expected, actual)
111    }
112
113    pub fn should_not_eq<T: PartialEq + Debug>(
114        &mut self,
115        id: &str,
116        expected: T,
117        actual: T,
118    ) -> Result<(), CognoError> {
119        self.append_assert(id, AssertionType::ShouldNot, expected, actual)
120    }
121
122    pub fn may_eq<T: PartialEq + Debug>(
123        &mut self,
124        id: &str,
125        expected: T,
126        actual: T,
127    ) -> Result<(), CognoError> {
128        self.append_assert(id, AssertionType::May, expected, actual)
129    }
130
131    fn append_assert<T: PartialEq + Debug>(
132        &mut self,
133        id: &str,
134        kind: AssertionType,
135        expected: T,
136        actual: T,
137    ) -> Result<(), CognoError> {
138        let result = expected == actual;
139
140        let error_message = if is_passed_assertion(&kind, result) {
141            None
142        } else if is_a_not_assertion(&kind) {
143            Some(format!("got [{:?}]", actual))
144        } else {
145            Some(format!("expected [{:?}] but was [{:?}]", expected, actual))
146        };
147
148        let kind = self.assertion_or_override(id.to_string(), kind)?;
149
150        let def = AssertionDef {
151            id: id.to_string(),
152            kind,
153            result,
154            error_message,
155        };
156
157        self.tests.last_mut().unwrap().assertions.push(def);
158
159        Ok(())
160    }
161
162    fn assertion_or_override(
163        &self,
164        assertion_id: String,
165        original_assertion_type: AssertionType,
166    ) -> Result<AssertionType, CognoError> {
167        let current_test = self.tests.last().unwrap();
168
169        let matched_assertions: HashSet<&AssertionModifier> = self
170            .modifiers
171            .iter()
172            .filter(|sm| sm.spec_id == current_test.spec_id)
173            .flat_map(|sm| {
174                sm.test_modifiers
175                    .iter()
176                    .filter(|tm| tm.test_id == current_test.name)
177                    .flat_map(|tm| {
178                        tm.assertion_modifiers
179                            .iter()
180                            .filter(|am| am.assertion_id == assertion_id)
181                    })
182            })
183            .collect();
184
185        if matched_assertions.len() > 1 {
186            return Err(CognoError::ConflictingModifiers(format!(
187                "{:?}",
188                matched_assertions
189            )));
190        }
191
192        Ok(match matched_assertions.iter().collect_vec().first() {
193            Some(ma) => ma.assertion_type.clone(),
194            None => original_assertion_type,
195        })
196    }
197}
198
199fn create_reporter() -> Box<dyn Reporter> {
200    match std::env::var("COGNO_REPORTER")
201        .unwrap_or("".to_string())
202        .as_str()
203    {
204        "console" => {
205            #[cfg(not(feature = "console"))]
206            panic!("Reporter is not enabled, requires feature 'console'");
207            #[cfg(feature = "console")]
208            Box::new(ConsoleReporter::new())
209        }
210        "raw" => Box::new(RawReporter::new()),
211        #[cfg(not(feature = "console"))]
212        _ => Box::new(RawReporter::new()),
213        #[cfg(feature = "console")]
214        _ => Box::new(ConsoleReporter::new()),
215    }
216}
217
218fn load_specs() -> HashSet<String> {
219    std::env::var("COGNO_SPECS")
220        .unwrap_or(String::new())
221        .split(",")
222        .map(|s| s.to_string())
223        .collect()
224}
225
226fn load_modifiers() -> Result<Vec<SpecModifier>, CognoError> {
227    let mut modifiers = Vec::new();
228    for f in std::env::var("COGNO_MODIFIERS")
229        .unwrap_or("".to_string())
230        .split(",")
231    {
232        if f.is_empty() {
233            continue;
234        }
235
236        modifiers.extend(load_spec_modifier(f)?.spec_modifiers);
237    }
238
239    Ok(modifiers)
240}