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#[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}