Skip to main content

gwr_track/
test_helpers.rs

1// Copyright (c) 2020 Graphcore Ltd. All rights reserved.
2
3//! This module provides helper functions for testing logging output
4//!
5//! The aim of this module is to provide commonly-used functions that enable the
6//! testing of the output that should appear from logging macros.
7//!
8//! *Note:* all tests should be run in a [serial](https://docs.rs/serial_test) manner because
9//! the logger involves shared global state that will otherwise give
10//! unpredictable results.
11
12use std::cell::RefCell;
13use std::fs;
14use std::io::BufWriter;
15use std::path::Path;
16use std::rc::Rc;
17
18use regex::Regex;
19
20use crate::entity::Capacity;
21use crate::tracker::aka::AlternativeNames;
22use crate::tracker::{CapnProtoTracker, EntityManager};
23use crate::{Id, Track, Tracker, Writer};
24
25/// A tracker that keeps track events.
26pub struct TestTracker {
27    events: RefCell<Vec<String>>,
28
29    unique_id: RefCell<u64>,
30    level: log::Level,
31}
32
33impl TestTracker {
34    /// Create a new [`Tracker`] for the tests.
35    ///
36    /// This keeps the track events in memory for checking later.
37    #[must_use]
38    pub fn new(initial_id: u64, level: log::Level) -> Self {
39        Self {
40            events: RefCell::new(Vec::new()),
41            unique_id: RefCell::new(initial_id),
42            level,
43        }
44    }
45
46    fn add_event(&self, event: String) {
47        println!("{event}");
48        let mut events = self.events.borrow_mut();
49        events.push(event);
50    }
51
52    /// Return a snapshot of the events recorded so far.
53    #[must_use]
54    pub fn events(&self) -> Vec<String> {
55        self.events.borrow().clone()
56    }
57}
58
59impl Track for TestTracker {
60    fn unique_id(&self) -> Id {
61        let mut guard = self.unique_id.borrow_mut();
62        let id = *guard;
63        *guard += 1;
64        Id(id)
65    }
66
67    fn enabled_level(&self, _id: Id) -> log::Level {
68        self.level
69    }
70
71    fn monitoring_window_size_for(&self, _id: Id) -> Option<u64> {
72        None
73    }
74
75    fn add_entity(
76        &self,
77        _id: Id,
78        _entity_name: &str,
79        _alternative_names: AlternativeNames,
80    ) -> log::Level {
81        self.level
82    }
83
84    fn enter(&self, id: Id, item: Id) {
85        self.add_event(format!("{id}: {item} entered"));
86    }
87
88    fn exit(&self, id: Id, item: Id) {
89        self.add_event(format!("{id}: {item} exited"));
90    }
91
92    fn value(&self, id: Id, value: f64) {
93        self.add_event(format!("{id}: {value}"));
94    }
95
96    fn begin_activity(&self, activity: Id, lane: Id, name: &str) {
97        self.add_event(format!("{activity}: activity begin {name} on lane {lane}"));
98    }
99
100    fn end_activity(&self, activity: Id) {
101        self.add_event(format!("{activity}: activity end"));
102    }
103
104    fn add_to_group(&self, activity: Id, group_id: Id) {
105        self.add_event(format!("{activity}: added to group {group_id}"));
106    }
107
108    fn remove_from_group(&self, activity: Id, group_id: Id) {
109        self.add_event(format!("{activity}: removed from group {group_id}"));
110    }
111
112    fn create_entity(&self, created_by: Id, id: Id, name: &str) {
113        self.add_event(format!("{created_by}: created entity {id}, {name}"));
114    }
115
116    fn create_monitor(&self, created_by: Id, id: Id, name: &str) {
117        self.add_event(format!("{created_by}: created monitor {id}, {name}"));
118    }
119
120    fn create_lane(&self, created_by: Id, id: Id, name: &str) {
121        self.add_event(format!("{created_by}: created lane {id}, {name}"));
122    }
123
124    fn create_group(&self, created_by: Id, id: Id, name: &str) {
125        self.add_event(format!("{created_by}: created group {id}, {name}"));
126    }
127
128    fn create_object(
129        &self,
130        created_by: Id,
131        id: Id,
132        size: usize,
133        units: &str,
134        req_type: u8,
135        details: &str,
136    ) {
137        self.add_event(format!(
138            "{created_by}: created object {id}, {req_type}, {size}, {units}, {details}"
139        ));
140    }
141
142    fn capacity(&self, id: Id, capacity: Capacity) {
143        self.add_event(format!(
144            "{id}: capacity {} {}",
145            capacity.value, capacity.units
146        ));
147    }
148
149    fn destroy(&self, destroyed_by: Id, id: Id) {
150        self.add_event(format!("{destroyed_by}: destroyed {id}"));
151    }
152
153    fn connect(&self, connect_from: Id, connect_to: Id) {
154        self.add_event(format!("{connect_from}: connect to {connect_to}"));
155    }
156
157    fn log(&self, id: Id, level: log::Level, msg: std::fmt::Arguments) {
158        self.add_event(format!("{id}:{level}: {msg}"));
159    }
160
161    fn time(&self, set_by: Id, time_ns: f64) {
162        self.add_event(format!("{set_by}: set time {time_ns:.1}ns"));
163    }
164
165    fn shutdown(&self) {
166        // Do nothing
167    }
168}
169
170/// Initialise the logging system for tests
171///
172/// Install the logger that will capture all _log_ messages. This is done by
173/// setting the default logging level to Trace and installing a logger that
174/// records all _log_ messages to a global string.
175///
176/// *Note*: this is called `test_init` because macros are exported at the root
177/// of the crate.
178///
179/// # Arguments
180///
181/// * `start_id` - The ID value to be set as the starting value
182///
183/// # Examples
184///
185/// ```
186/// use gwr_track::test_helpers;
187/// use serial_test::serial;
188///
189/// # /* Need to comment this out so that it is actually built/tested by the infrastructure
190/// #[test]
191/// # */
192/// fn smoke() {
193///     let (test_tracker, tracker) = gwr_track::test_init!(10);
194///     let top = gwr_track::entity::toplevel(&tracker, "top");
195///     test_helpers::check_and_clear(&test_tracker, &["10: top created"]);
196/// }
197/// ```
198#[macro_export]
199macro_rules! test_init {
200    ($start_id:expr) => {{
201        let test_tracker = std::rc::Rc::new($crate::test_helpers::TestTracker::new(
202            $start_id,
203            $crate::log::Level::Trace,
204        ));
205        let tracker: $crate::Tracker = test_tracker.clone();
206        (test_tracker, tracker)
207    }};
208}
209
210/// Check and clear the _trace_ and _log_ output
211///
212/// This function asserts that the logging output lines seen since the start or
213/// the last time this function was called are expected. The
214/// [test_init](../../gwr_track/macro.test_init.html) must have been called
215/// before this function can be used.
216///
217/// It then also clears both the _trace_ and _log_ output recorded so far.
218///
219/// # Arguments
220///
221/// * `tracker`  - A reference to the [`TestTracker`] being used in the test.
222///   This will have been keeping track of the trace and log events seen since
223///   it was created or last cleared.
224/// * `expected` - An array of expected regular expressions that the logging
225///   output will be matched against.
226///
227/// # Examples
228///
229/// ```
230/// use gwr_track::test_helpers;
231/// use serial_test::serial;
232///
233/// # /* Need to comment this out so that it is actually built/tested by the infrastructure
234/// #[test]
235/// # */
236/// fn smoke() {
237///     let (test_tracker, tracker) = gwr_track::test_init!(20);
238///     let top = gwr_track::entity::toplevel(&tracker, "top");
239///     let id = gwr_track::create_id!(top);
240///     test_helpers::check_and_clear(&test_tracker, &["20: top created"]);
241/// }
242/// ```
243pub fn check_and_clear(tracker: &TestTracker, expected: &[&str]) {
244    let mut log_contents_ref = tracker.events.borrow_mut();
245
246    println!("Checking {:?} matches {:?}", expected, *log_contents_ref);
247
248    // Check that there are the same number of strings produced as expected
249    let num_strings = expected.len();
250    assert_eq!(num_strings, log_contents_ref.len());
251
252    for i in 0..num_strings {
253        let log_expect = expected[i];
254        let re = Regex::new(log_expect).unwrap();
255        let actual = &(*log_contents_ref[i]);
256        println!("Checking {i}: {log_expect:?} matches {actual:?}");
257        assert!(re.is_match(actual));
258    }
259
260    log_contents_ref.clear();
261}
262
263/// Create a tracker for tests
264#[must_use]
265pub fn create_tracker(full_filepath: &str) -> Tracker {
266    // Place all trace files in one folder
267    const FOLDER: &str = "traces";
268
269    // Create that folder if it doesn't exist yet
270    fs::create_dir_all(FOLDER).unwrap();
271
272    let filename_only = Path::new(full_filepath)
273        .file_stem()
274        .and_then(|s| s.to_str())
275        .unwrap();
276
277    let bin_writer: Writer = Box::new(BufWriter::new(
278        fs::File::create(format!("{FOLDER}/{filename_only}.bin")).unwrap(),
279    ));
280
281    let default_log_level = log::Level::Trace;
282    let entity_manger = EntityManager::new(default_log_level);
283    let tracker: Tracker = Rc::new(CapnProtoTracker::new(entity_manger, bin_writer));
284    tracker
285}