• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

VolumeGraphics / havocompare / 9e3a76cf10ae976e96a583ab94578b9bf5ee1429

pending completion
9e3a76cf10ae976e96a583ab94578b9bf5ee1429

push

github

web-flow
Merge pull request #38 from VolumeGraphics/json_reporting

Add Json reporting, decouple reporting html from comparison logic

479 of 479 new or added lines in 12 files covered. (100.0%)

2776 of 3272 relevant lines covered (84.84%)

5518.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.36
/src/csv/mod.rs
1
use crate::report;
2
mod preprocessing;
3
mod tokenizer;
4
mod value;
5

6
pub use preprocessing::Preprocessor;
7
use value::Quantity;
8
use value::Value;
9

10
use rayon::prelude::*;
11
use regex::Regex;
12
use schemars_derive::JsonSchema;
13
use serde::{Deserialize, Serialize};
14
use std::fmt::{Debug, Display, Formatter};
15
use std::fs::File;
16
use std::io::{BufReader, Read, Seek};
17
use std::path::Path;
18
use std::slice::{Iter, IterMut};
19
use thiserror::Error;
20
use tracing::error;
21
use vg_errortools::{fat_io_wrap_std, FatIOError};
22

23
#[derive(Error, Debug)]
×
24
/// Possible errors during csv parsing
25
pub enum Error {
26
    #[error("Unexpected Value found {0} - {1}")]
27
    /// Value type was different than expected
28
    UnexpectedValue(Value, String),
29
    #[error("Tried accessing empty field")]
30
    /// Tried to access a non-existing field
31
    InvalidAccess(String),
32
    #[error("Failed to compile regex {0}")]
33
    /// Regex compilation failed
34
    RegexCompilationFailed(#[from] regex::Error),
35
    #[error("File access failed {0}")]
36
    /// File access failed
37
    FileAccessFailed(#[from] FatIOError),
38
    #[error("IoError occurred {0}")]
39
    /// Problem involving files or readers
40
    IoProblem(#[from] std::io::Error),
41

42
    #[error("Format guessing failed")]
43
    /// Failure to guess field delimiters - decimal separator guessing is optional
44
    FormatGuessingFailure,
45

46
    #[error("A string literal was started but did never end")]
47
    /// A string literal was started but did never end
48
    UnterminatedLiteral,
49

50
    #[error("CSV format invalid: first row has a different column number then row {0}")]
51
    /// The embedded row number had a different column count than the first
52
    UnstableColumnCount(usize),
53

54
    #[error("The files compared have different row count. Nominal: {0}, and Actual: {1}")]
55
    /// Files being compared have different row numbers
56
    UnequalRowCount(usize, usize),
57
}
58

59
/// A position inside a table
60
#[derive(Clone, Copy, Debug, Serialize)]
×
61
pub struct Position {
62
    /// row number, starting with zero
63
    pub row: usize,
64
    /// column number, starting with zero
65
    pub col: usize,
66
}
67

68
#[derive(Debug, Serialize, Clone)]
×
69
/// Difference of a table entry
70
pub enum DiffType {
71
    /// Both entries were strings, but had different contents
72
    UnequalStrings {
73
        /// nominal string
74
        nominal: String,
75
        /// actual string
76
        actual: String,
77
        /// position
78
        position: Position,
79
    },
80
    /// Both entries were [`Quantity`]s but exceeded tolerances
81
    OutOfTolerance {
82
        /// nominal
83
        nominal: Quantity,
84
        /// actual
85
        actual: Quantity,
86
        /// compare mode that was exceeded
87
        mode: Mode,
88
        /// position in table
89
        position: Position,
90
    },
91
    /// both fields had different value types
92
    DifferentValueTypes {
93
        /// nominal
94
        nominal: Value,
95
        /// actual
96
        actual: Value,
97
        /// position
98
        position: Position,
99
    },
100
    /// Both fields were headers but with different contents
101
    UnequalHeader {
102
        /// nominal
103
        nominal: String,
104
        /// actual
105
        actual: String,
106
    },
107
}
108

109
impl Display for DiffType {
110
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
6✔
111
        match self {
6✔
112
            DiffType::DifferentValueTypes {
113
                nominal,
2✔
114
                actual,
2✔
115
                position,
2✔
116
            } => {
2✔
117
                write!(
2✔
118
                    f,
2✔
119
                    "Line: {}, Col: {} -- Different value types -- Expected {}, Found {}",
2✔
120
                    position.row, position.col, nominal, actual
2✔
121
                )
2✔
122
                .unwrap_or_default();
2✔
123
            }
2✔
124
            DiffType::OutOfTolerance {
125
                actual,
2✔
126
                nominal,
2✔
127
                mode,
2✔
128
                position,
2✔
129
            } => {
2✔
130
                write!(
2✔
131
                    f,
2✔
132
                    "Line: {}, Col: {} -- Out of tolerance -- Expected {}, Found {}, Mode {}",
2✔
133
                    position.row, position.col, nominal, actual, mode
2✔
134
                )
2✔
135
                .unwrap_or_default();
2✔
136
            }
2✔
137
            DiffType::UnequalStrings {
138
                nominal,
2✔
139
                actual,
2✔
140
                position,
2✔
141
            } => {
2✔
142
                write!(
2✔
143
                    f,
2✔
144
                    "Line: {}, Col: {} -- Different strings -- Expected {}, Found {}",
2✔
145
                    position.row, position.col, nominal, actual
2✔
146
                )
2✔
147
                .unwrap_or_default();
2✔
148
            }
2✔
149
            DiffType::UnequalHeader { nominal, actual } => {
×
150
                write!(
×
151
                    f,
×
152
                    "Different header strings -- Expected {}, Found {}",
×
153
                    nominal, actual
×
154
                )
×
155
                .unwrap_or_default();
×
156
            }
×
157
        };
158
        Ok(())
6✔
159
    }
6✔
160
}
161

162
#[derive(Copy, Clone, JsonSchema, Debug, Deserialize, Serialize, PartialEq)]
16✔
163
/// comparison mode for csv cells
164
pub enum Mode {
165
    /// `(a-b).abs() < threshold`
166
    Absolute(f64),
167
    /// `((a-b)/a).abs() < threshold`
168
    Relative(f64),
169
    /// always matches
170
    Ignore,
171
}
172

173
impl Display for Mode {
174
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
8✔
175
        match &self {
8✔
176
            Mode::Absolute(tolerance) => {
4✔
177
                write!(f, "Absolute (tol: {tolerance})").unwrap_or_default();
4✔
178
            }
4✔
179
            Mode::Relative(tolerance) => {
2✔
180
                write!(f, "Relative (tol: {tolerance})").unwrap_or_default();
2✔
181
            }
2✔
182
            Mode::Ignore => {
2✔
183
                write!(f, "Ignored").unwrap_or_default();
2✔
184
            }
2✔
185
        };
186
        Ok(())
8✔
187
    }
8✔
188
}
189

190
impl Mode {
191
    pub(crate) fn in_tolerance(&self, nominal: &Quantity, actual: &Quantity) -> bool {
45,480✔
192
        if nominal.value.is_nan() && actual.value.is_nan() {
45,480✔
193
            return true;
6✔
194
        }
45,474✔
195
        match self {
45,474✔
196
            Mode::Absolute(tolerance) => {
24,630✔
197
                let plain_diff = (nominal.value - actual.value).abs();
24,630✔
198
                let numerically = if plain_diff == 0.0 {
24,630✔
199
                    true
22,814✔
200
                } else if *tolerance == 0.0 {
1,816✔
201
                    false
×
202
                } else {
203
                    let diff = nominal.minimal_diff(actual);
1,816✔
204
                    diff <= *tolerance
1,816✔
205
                };
206

207
                let identical_units = nominal.unit == actual.unit;
24,630✔
208
                numerically && identical_units
24,630✔
209
            }
210
            Mode::Ignore => true,
2✔
211
            Mode::Relative(tolerance) => {
20,842✔
212
                let plain_diff = (nominal.value - actual.value).abs();
20,842✔
213
                let numerically = if plain_diff == 0.0 {
20,842✔
214
                    true
20,816✔
215
                } else if *tolerance == 0.0 {
26✔
216
                    false
×
217
                } else {
218
                    let diff = nominal.minimal_diff(actual);
26✔
219
                    let diff = (diff / nominal.value).abs();
26✔
220
                    diff <= *tolerance
26✔
221
                };
222
                let identical_units = nominal.unit == actual.unit;
20,842✔
223
                numerically && identical_units
20,842✔
224
            }
225
        }
226
    }
45,480✔
227
}
228

229
#[derive(JsonSchema, Deserialize, Serialize, Debug, Default, Clone)]
24✔
230
/// Settings for the CSV comparison module
231
pub struct CSVCompareConfig {
232
    #[serde(flatten)]
233
    /// delimiters for the file parsing
234
    pub delimiters: Delimiters,
235
    /// How numerical values shall be compared, strings are always checked for identity
236
    pub comparison_modes: Vec<Mode>,
237
    /// Any field matching the given regex is excluded from comparison
238
    pub exclude_field_regex: Option<String>,
239
    /// Preprocessing done to the csv files before beginning the comparison
240
    pub preprocessing: Option<Vec<Preprocessor>>,
241
}
242

243
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
58✔
244
/// Delimiter configuration for file parsing
245
pub struct Delimiters {
246
    /// The delimiters of the csv fields (typically comma, semicolon or pipe)
247
    pub field_delimiter: Option<char>,
248
    /// The decimal separator for floating point numbers (typically dot or comma)
249
    pub decimal_separator: Option<char>,
250
}
251

252
impl Delimiters {
253
    pub(crate) fn is_empty(&self) -> bool {
1,244✔
254
        self.decimal_separator.is_none() && self.field_delimiter.is_none()
1,244✔
255
    }
1,244✔
256

257
    #[cfg(test)]
258
    pub fn autodetect() -> Delimiters {
4✔
259
        Delimiters {
4✔
260
            field_delimiter: None,
4✔
261
            decimal_separator: None,
4✔
262
        }
4✔
263
    }
4✔
264
}
265

266
#[derive(Default, Clone)]
1,416✔
267
pub(crate) struct Column {
268
    pub header: Option<String>,
269
    pub rows: Vec<Value>,
270
}
271

272
impl Column {
273
    pub fn delete_contents(&mut self) {
4✔
274
        self.header = Some("DELETED".to_string());
4✔
275
        let row_count = self.rows.len();
4✔
276
        self.rows = vec![Value::deleted(); row_count];
4✔
277
    }
4✔
278
}
279

280
pub(crate) struct Table {
281
    pub columns: Vec<Column>,
282
}
283

284
impl Table {
285
    pub(crate) fn from_reader<R: Read + Seek>(
356✔
286
        input: R,
356✔
287
        config: &Delimiters,
356✔
288
    ) -> Result<Table, Error> {
356✔
289
        let mut cols = Vec::new();
356✔
290
        let input = BufReader::new(input);
356✔
291
        let mut parser = if config.is_empty() {
356✔
292
            tokenizer::Parser::new_guess_format(input)?
356✔
293
        } else {
294
            tokenizer::Parser::new(input, config.clone()).ok_or(Error::FormatGuessingFailure)?
×
295
        };
296

297
        for (line_num, fields) in parser.parse_to_rows()?.enumerate() {
28,934✔
298
            if cols.is_empty() {
28,934✔
299
                cols.resize_with(fields.len(), Column::default);
356✔
300
            }
28,578✔
301
            if fields.len() != cols.len() {
28,934✔
302
                let message = format!("Error: Columns inconsistent! First row had {}, this row has {} (row:{line_num})", cols.len(), fields.len());
×
303
                error!("{}", message.as_str());
×
304
                return Err(Error::UnstableColumnCount(line_num));
×
305
            } else {
28,934✔
306
                fields
28,934✔
307
                    .into_iter()
28,934✔
308
                    .zip(cols.iter_mut())
28,934✔
309
                    .for_each(|(f, col)| col.rows.push(f));
65,328✔
310
            }
28,934✔
311
        }
312

313
        Ok(Table { columns: cols })
356✔
314
    }
356✔
315

316
    pub(crate) fn rows(&self) -> RowIterator {
1,226✔
317
        RowIterator {
1,226✔
318
            position: self.columns.iter().map(|c| c.rows.iter()).collect(),
4,638✔
319
        }
1,226✔
320
    }
1,226✔
321

322
    pub(crate) fn rows_mut(&mut self) -> RowIteratorMut {
8✔
323
        RowIteratorMut {
8✔
324
            position: self.columns.iter_mut().map(|c| c.rows.iter_mut()).collect(),
16✔
325
        }
8✔
326
    }
8✔
327
}
328

329
macro_rules! mk_next {
330
    ($pos: expr) => {{
331
        let row: Vec<_> = $pos.iter_mut().filter_map(|i| i.next()).collect();
332
        if row.is_empty() {
333
            None
334
        } else {
335
            Some(row)
336
        }
337
    }};
338
}
339

340
macro_rules! impl_ex_size_it {
341
    ($($t:ty),+) => {
342
        $(impl<'a> ExactSizeIterator for $t {
343
            fn len(&self) -> usize {
630✔
344
                self.position.first().unwrap().len()
630✔
345
            }
630✔
346
        })+
347
    };
348
}
349

350
impl_ex_size_it!(RowIteratorMut<'_>, RowIterator<'_>);
351

352
pub(crate) struct RowIteratorMut<'a> {
353
    position: Vec<IterMut<'a, Value>>,
354
}
355

356
impl<'a> Iterator for RowIteratorMut<'a> {
357
    type Item = Vec<&'a mut Value>;
358
    fn next(&mut self) -> Option<Self::Item> {
1,048✔
359
        mk_next!(self.position)
2,096✔
360
    }
1,048✔
361
}
362

363
pub(crate) struct RowIterator<'a> {
364
    position: Vec<Iter<'a, Value>>,
365
}
366

367
impl<'a> Iterator for RowIterator<'a> {
368
    type Item = Vec<&'a Value>;
369
    fn next(&mut self) -> Option<Self::Item> {
22,688✔
370
        mk_next!(self.position)
58,840✔
371
    }
22,688✔
372
}
373

374
pub(crate) fn compare_tables(
312✔
375
    nominal: &Table,
312✔
376
    actual: &Table,
312✔
377
    config: &CSVCompareConfig,
312✔
378
) -> Result<Vec<DiffType>, Error> {
312✔
379
    if nominal.rows().len() != actual.rows().len() {
312✔
380
        return Err(Error::UnequalRowCount(
×
381
            nominal.rows().len(),
×
382
            actual.rows().len(),
×
383
        ));
×
384
    }
312✔
385

312✔
386
    let mut diffs = Vec::new();
312✔
387
    for (col, (col_nom, col_act)) in nominal
1,186✔
388
        .columns
312✔
389
        .iter()
312✔
390
        .zip(actual.columns.iter())
312✔
391
        .enumerate()
312✔
392
    {
393
        if let (Some(nom_header), Some(act_header)) = (&col_nom.header, &col_act.header) {
1,186✔
394
            if nom_header != act_header {
202✔
395
                diffs.extend(vec![DiffType::UnequalHeader {
6✔
396
                    nominal: nom_header.to_owned(),
6✔
397
                    actual: act_header.to_owned(),
6✔
398
                }]);
6✔
399
            }
196✔
400
        }
984✔
401

402
        for (row, (val_nom, val_act)) in col_nom.rows.iter().zip(col_act.rows.iter()).enumerate() {
35,320✔
403
            let position = Position { row, col };
35,320✔
404
            let diffs_field = compare_values(val_nom, val_act, config, position)?;
35,320✔
405
            diffs.extend(diffs_field);
35,320✔
406
        }
407
    }
408
    Ok(diffs)
312✔
409
}
312✔
410

411
fn both_quantity<'a>(
412
    actual: &'a Value,
413
    nominal: &'a Value,
414
) -> Option<(&'a Quantity, &'a Quantity)> {
415
    if let Some(actual) = actual.get_quantity() {
35,320✔
416
        if let Some(nominal) = nominal.get_quantity() {
27,020✔
417
            return Some((actual, nominal));
27,012✔
418
        }
8✔
419
    }
8,300✔
420
    None
8,308✔
421
}
35,320✔
422

423
fn both_string(actual: &Value, nominal: &Value) -> Option<(String, String)> {
424
    if let Some(actual) = actual.get_string() {
8,308✔
425
        if let Some(nominal) = nominal.get_string() {
8,300✔
426
            return Some((actual, nominal));
8,300✔
427
        }
×
428
    }
8✔
429
    None
8✔
430
}
8,308✔
431

432
fn compare_values(
433
    nominal: &Value,
434
    actual: &Value,
435
    config: &CSVCompareConfig,
436
    position: Position,
437
) -> Result<Vec<DiffType>, Error> {
438
    // float quantity compare
439
    if let Some((actual_float, nominal_float)) = both_quantity(actual, nominal) {
35,320✔
440
        Ok(config
27,012✔
441
            .comparison_modes
27,012✔
442
            .iter()
27,012✔
443
            .filter_map(|cm| {
45,748✔
444
                if !cm.in_tolerance(nominal_float, actual_float) {
41,652✔
445
                    Some(DiffType::OutOfTolerance {
14✔
446
                        nominal: nominal_float.clone(),
14✔
447
                        actual: actual_float.clone(),
14✔
448
                        mode: *cm,
14✔
449
                        position,
14✔
450
                    })
14✔
451
                } else {
452
                    None
41,638✔
453
                }
454
            })
45,748✔
455
            .collect())
27,012✔
456
    } else if let Some((actual_string, nominal_string)) = both_string(actual, nominal) {
8,308✔
457
        if let Some(exclude_regex) = config.exclude_field_regex.as_deref() {
8,300✔
458
            let regex = Regex::new(exclude_regex)?;
8,080✔
459
            if regex.is_match(nominal_string.as_str()) {
8,080✔
460
                return Ok(Vec::new());
8✔
461
            }
8,072✔
462
        }
220✔
463
        if nominal_string != actual_string {
8,292✔
464
            Ok(vec![DiffType::UnequalStrings {
×
465
                position,
×
466
                nominal: nominal_string,
×
467
                actual: actual_string,
×
468
            }])
×
469
        } else {
470
            Ok(Vec::new())
8,292✔
471
        }
472
    } else {
473
        Ok(vec![DiffType::DifferentValueTypes {
8✔
474
            actual: actual.clone(),
8✔
475
            nominal: nominal.clone(),
8✔
476
            position,
8✔
477
        }])
8✔
478
    }
479
}
35,320✔
480

481
fn get_diffs_readers<R: Read + Seek + Send>(
86✔
482
    nominal: R,
86✔
483
    actual: R,
86✔
484
    config: &CSVCompareConfig,
86✔
485
) -> Result<(Table, Table, Vec<DiffType>), Error> {
86✔
486
    let tables: Result<Vec<Table>, Error> = [nominal, actual]
86✔
487
        .into_par_iter()
86✔
488
        .map(|r| Table::from_reader(r, &config.delimiters))
172✔
489
        .collect();
86✔
490
    let mut tables = tables?;
86✔
491
    if let (Some(mut actual), Some(mut nominal)) = (tables.pop(), tables.pop()) {
86✔
492
        if let Some(preprocessors) = config.preprocessing.as_ref() {
86✔
493
            for preprocessor in preprocessors.iter() {
18✔
494
                preprocessor.process(&mut nominal)?;
18✔
495
                preprocessor.process(&mut actual)?;
18✔
496
            }
497
        }
68✔
498
        let comparison_result = compare_tables(&nominal, &actual, config)?;
86✔
499
        Ok((nominal, actual, comparison_result))
86✔
500
    } else {
501
        Err(Error::UnterminatedLiteral)
×
502
    }
503
}
86✔
504

505
pub(crate) fn compare_paths(
76✔
506
    nominal: impl AsRef<Path>,
76✔
507
    actual: impl AsRef<Path>,
76✔
508
    config: &CSVCompareConfig,
76✔
509
) -> Result<report::Difference, Error> {
76✔
510
    let nominal_file = fat_io_wrap_std(nominal.as_ref(), &File::open)?;
76✔
511
    let actual_file = fat_io_wrap_std(actual.as_ref(), &File::open)?;
74✔
512

513
    let (_, _, results) = get_diffs_readers(&nominal_file, &actual_file, config)?;
74✔
514
    results.iter().for_each(|error| {
74✔
515
        error!("{}", &error);
×
516
    });
74✔
517
    let is_error = !results.is_empty();
74✔
518
    let mut result = report::Difference::new_for_file(nominal.as_ref(), actual.as_ref());
74✔
519
    result.is_error = is_error;
74✔
520
    result.detail = results.into_iter().map(report::DiffDetail::CSV).collect();
74✔
521
    Ok(result)
74✔
522
}
76✔
523

524
#[cfg(test)]
525
mod tests {
526
    use super::*;
527
    use crate::csv::DiffType::{
528
        DifferentValueTypes, OutOfTolerance, UnequalHeader, UnequalStrings,
529
    };
530
    use crate::csv::Preprocessor::ExtractHeaders;
531
    use std::io::Cursor;
532

533
    const NOMINAL: &str = "nominal";
534
    const ACTUAL: &str = "actual";
535
    const POS_COL: usize = 1337;
536
    const POS_ROW: usize = 667;
537

538
    fn mk_position() -> Position {
6✔
539
        Position {
6✔
540
            col: POS_COL,
6✔
541
            row: POS_ROW,
6✔
542
        }
6✔
543
    }
6✔
544

545
    #[test]
2✔
546
    fn diff_types_readable_string() {
2✔
547
        let string_unequal = UnequalStrings {
2✔
548
            nominal: NOMINAL.to_string(),
2✔
549
            actual: ACTUAL.to_string(),
2✔
550
            position: mk_position(),
2✔
551
        };
2✔
552
        let msg = format!("{string_unequal}");
2✔
553
        assert!(msg.contains(NOMINAL));
2✔
554
        assert!(msg.contains(ACTUAL));
2✔
555
        assert!(msg.contains(format!("{POS_COL}").as_str()));
2✔
556
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
2✔
557
    }
2✔
558

559
    #[test]
2✔
560
    fn diff_types_readable_out_of_tolerance() {
2✔
561
        let string_unequal = OutOfTolerance {
2✔
562
            nominal: Quantity {
2✔
563
                value: 10.0,
2✔
564
                unit: Some("mm".to_owned()),
2✔
565
            },
2✔
566
            actual: Quantity {
2✔
567
                value: 12.0,
2✔
568
                unit: Some("um".to_owned()),
2✔
569
            },
2✔
570
            mode: Mode::Absolute(11.0),
2✔
571
            position: mk_position(),
2✔
572
        };
2✔
573
        let msg = format!("{string_unequal}");
2✔
574
        assert!(msg.contains("10 mm"));
2✔
575
        assert!(msg.contains("11"));
2✔
576
        assert!(msg.contains("12 um"));
2✔
577
        assert!(msg.contains("Absolute"));
2✔
578
        assert!(msg.contains(format!("{POS_COL}").as_str()));
2✔
579
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
2✔
580
    }
2✔
581

582
    #[test]
2✔
583
    fn diff_types_readable_different_value_types() {
2✔
584
        let string_unequal = DifferentValueTypes {
2✔
585
            nominal: Value::from_str("10.0 mm", &None),
2✔
586
            actual: Value::from_str(ACTUAL, &None),
2✔
587
            position: mk_position(),
2✔
588
        };
2✔
589
        let msg = format!("{string_unequal}");
2✔
590
        assert!(msg.contains("10 mm"));
2✔
591
        assert!(msg.contains(ACTUAL));
2✔
592
        assert!(msg.contains(format!("{POS_COL}").as_str()));
2✔
593
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
2✔
594
    }
2✔
595

596
    #[test]
2✔
597
    fn table_cols_reading_correct() {
2✔
598
        let table = Table::from_reader(
2✔
599
            File::open("tests/csv/data/Annotations.csv").unwrap(),
2✔
600
            &Delimiters::default(),
2✔
601
        )
2✔
602
        .unwrap();
2✔
603
        assert_eq!(table.columns.len(), 13);
2✔
604
    }
2✔
605

606
    #[test]
2✔
607
    fn table_rows_reading_correct() {
2✔
608
        let table = Table::from_reader(
2✔
609
            File::open("tests/csv/data/Annotations.csv").unwrap(),
2✔
610
            &Delimiters::default(),
2✔
611
        )
2✔
612
        .unwrap();
2✔
613
        assert_eq!(table.rows().len(), 6);
2✔
614
    }
2✔
615

616
    #[test]
2✔
617
    fn identity_comparison_is_empty() {
2✔
618
        let config = CSVCompareConfig {
2✔
619
            exclude_field_regex: None,
2✔
620
            comparison_modes: vec![Mode::Absolute(0.0), Mode::Relative(0.0)],
2✔
621
            delimiters: Delimiters::default(),
2✔
622
            preprocessing: None,
2✔
623
        };
2✔
624

2✔
625
        let actual = File::open("tests/csv/data/Annotations.csv").unwrap();
2✔
626
        let nominal = File::open("tests/csv/data/Annotations.csv").unwrap();
2✔
627

2✔
628
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
2✔
629
        assert!(diff.is_empty());
2✔
630
    }
2✔
631

632
    #[test]
2✔
633
    fn diffs_on_table_level() {
2✔
634
        let config = CSVCompareConfig {
2✔
635
            preprocessing: None,
2✔
636
            exclude_field_regex: Some(r"Surface".to_owned()),
2✔
637
            comparison_modes: vec![],
2✔
638
            delimiters: Delimiters::default(),
2✔
639
        };
2✔
640

2✔
641
        let actual = Table::from_reader(
2✔
642
            File::open("tests/csv/data/DeviationHistogram.csv").unwrap(),
2✔
643
            &config.delimiters,
2✔
644
        )
2✔
645
        .unwrap();
2✔
646
        let nominal = Table::from_reader(
2✔
647
            File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap(),
2✔
648
            &config.delimiters,
2✔
649
        )
2✔
650
        .unwrap();
2✔
651

2✔
652
        let diff = compare_tables(&nominal, &actual, &config).unwrap();
2✔
653
        assert_eq!(diff.len(), 1);
2✔
654
        let first_diff = diff.first().unwrap();
2✔
655
        if let DifferentValueTypes {
656
            nominal,
2✔
657
            actual,
2✔
658
            position,
2✔
659
        } = first_diff
2✔
660
        {
661
            assert_eq!(nominal.get_string().unwrap(), "different_type_here");
2✔
662
            assert_eq!(actual.get_quantity().unwrap().value, 0.00204398);
2✔
663
            assert_eq!(position.col, 1);
2✔
664
            assert_eq!(position.row, 12);
2✔
665
        } else {
666
            unreachable!();
×
667
        }
668
    }
2✔
669

670
    #[test]
2✔
671
    fn header_diffs_on_table_level() {
2✔
672
        let config = CSVCompareConfig {
2✔
673
            preprocessing: Some(vec![ExtractHeaders]),
2✔
674
            exclude_field_regex: None,
2✔
675
            comparison_modes: vec![],
2✔
676
            delimiters: Delimiters::default(),
2✔
677
        };
2✔
678

2✔
679
        let mut actual = Table::from_reader(
2✔
680
            File::open("tests/csv/data/Annotations.csv").unwrap(),
2✔
681
            &config.delimiters,
2✔
682
        )
2✔
683
        .unwrap();
2✔
684

2✔
685
        ExtractHeaders.process(&mut actual).unwrap();
2✔
686

2✔
687
        let mut nominal = Table::from_reader(
2✔
688
            File::open("tests/csv/data/Annotations_diff.csv").unwrap(),
2✔
689
            &config.delimiters,
2✔
690
        )
2✔
691
        .unwrap();
2✔
692

2✔
693
        ExtractHeaders.process(&mut nominal).unwrap();
2✔
694

2✔
695
        let diff = compare_tables(&nominal, &actual, &config).unwrap();
2✔
696
        assert_eq!(diff.len(), 3);
2✔
697

698
        let first_diff = diff.first().unwrap();
2✔
699
        if let UnequalHeader { nominal, actual } = first_diff {
2✔
700
            assert_eq!(nominal, "Position x [mm]");
2✔
701
            assert_eq!(actual, "Pos. x [mm]");
2✔
702
        } else {
703
            unreachable!();
×
704
        }
705
    }
2✔
706

707
    #[test]
2✔
708
    fn different_type_search_only() {
2✔
709
        let config = CSVCompareConfig {
2✔
710
            preprocessing: None,
2✔
711
            exclude_field_regex: Some(r"Surface".to_owned()),
2✔
712
            comparison_modes: vec![],
2✔
713
            delimiters: Delimiters::default(),
2✔
714
        };
2✔
715

2✔
716
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
2✔
717
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
2✔
718

2✔
719
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
2✔
720
        assert_eq!(diff.len(), 1);
2✔
721
        let first_diff = diff.first().unwrap();
2✔
722
        if let DifferentValueTypes {
723
            nominal,
2✔
724
            actual,
2✔
725
            position,
2✔
726
        } = first_diff
2✔
727
        {
728
            assert_eq!(nominal.get_string().unwrap(), "different_type_here");
2✔
729
            assert_eq!(actual.get_quantity().unwrap().value, 0.00204398);
2✔
730
            assert_eq!(position.col, 1);
2✔
731
            assert_eq!(position.row, 12);
2✔
732
        }
×
733
    }
2✔
734

735
    #[test]
2✔
736
    fn numerics_test_absolute() {
2✔
737
        let config = CSVCompareConfig {
2✔
738
            preprocessing: None,
2✔
739
            exclude_field_regex: Some(r"Surface".to_owned()),
2✔
740
            comparison_modes: vec![Mode::Absolute(0.5)],
2✔
741
            delimiters: Delimiters::default(),
2✔
742
        };
2✔
743

2✔
744
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
2✔
745
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
2✔
746

2✔
747
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
2✔
748
        // the different value type is still there, but we have 2 diffs over 0.5
2✔
749
        assert_eq!(diff.len(), 3);
2✔
750
    }
2✔
751

752
    #[test]
2✔
753
    fn mode_formatting() {
2✔
754
        let abs = Mode::Absolute(0.1);
2✔
755
        let msg = format!("{abs}");
2✔
756
        assert!(msg.contains("0.1"));
2✔
757
        assert!(msg.contains("Absolute"));
2✔
758

759
        let abs = Mode::Relative(0.1);
2✔
760
        let msg = format!("{abs}");
2✔
761
        assert!(msg.contains("0.1"));
2✔
762
        assert!(msg.contains("Relative"));
2✔
763

764
        let abs = Mode::Ignore;
2✔
765
        let msg = format!("{abs}");
2✔
766
        assert!(msg.contains("Ignored"));
2✔
767
    }
2✔
768

769
    #[test]
2✔
770
    fn different_formattings() {
2✔
771
        let config = CSVCompareConfig {
2✔
772
            preprocessing: None,
2✔
773
            exclude_field_regex: None,
2✔
774
            comparison_modes: vec![Mode::Absolute(0.5)],
2✔
775
            delimiters: Delimiters::autodetect(),
2✔
776
        };
2✔
777

2✔
778
        let actual = File::open(
2✔
779
            "tests/integ/data/display_of_status_message_in_cm_tables/actual/Volume1.csv",
2✔
780
        )
2✔
781
        .unwrap();
2✔
782
        let nominal = File::open(
2✔
783
            "tests/integ/data/display_of_status_message_in_cm_tables/expected/Volume1.csv",
2✔
784
        )
2✔
785
        .unwrap();
2✔
786

2✔
787
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
2✔
788
        // the different value type is still there, but we have 2 diffs over 0.5
2✔
789
        assert_eq!(diff.len(), 0);
2✔
790
    }
2✔
791

792
    #[test]
2✔
793
    fn numerics_test_relative() {
2✔
794
        let config = CSVCompareConfig {
2✔
795
            preprocessing: None,
2✔
796
            exclude_field_regex: Some(r"Surface".to_owned()),
2✔
797
            comparison_modes: vec![Mode::Relative(0.1)],
2✔
798
            delimiters: Delimiters::default(),
2✔
799
        };
2✔
800

2✔
801
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
2✔
802
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
2✔
803

2✔
804
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
2✔
805
        // the different value type is still there, but we have 5 rel diffs over 0.1
2✔
806
        assert_eq!(diff.len(), 6);
2✔
807
    }
2✔
808

809
    #[test]
2✔
810
    fn string_value_parsing_works() {
2✔
811
        let pairs = [
2✔
812
            ("0.6", Quantity::new(0.6, None)),
2✔
813
            ("0.6 in", Quantity::new(0.6, Some("in"))),
2✔
814
            ("inf", Quantity::new(f64::INFINITY, None)),
2✔
815
            ("-0.6", Quantity::new(-0.6, None)),
2✔
816
            ("-0.6 mm", Quantity::new(-0.6, Some("mm"))),
2✔
817
        ];
2✔
818
        pairs.into_iter().for_each(|(string, quantity)| {
10✔
819
            assert_eq!(Value::from_str(string, &None), Value::Quantity(quantity));
10✔
820
        });
10✔
821

2✔
822
        let nan_value = Value::from_str("nan mm", &None);
2✔
823
        let nan_value = nan_value.get_quantity().unwrap();
2✔
824
        assert!(nan_value.value.is_nan());
2✔
825
        assert_eq!(nan_value.unit, Some("mm".to_string()));
2✔
826
    }
2✔
827

828
    #[test]
2✔
829
    fn basic_compare_modes_test_absolute() {
2✔
830
        let abs_mode = Mode::Absolute(1.0);
2✔
831
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(1.0, None)));
2✔
832
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(-1.0, None)));
2✔
833
        assert!(abs_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(0.0, None)));
2✔
834
        assert!(abs_mode.in_tolerance(&Quantity::new(-1.0, None), &Quantity::new(0.0, None)));
2✔
835
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(0.0, None)));
2✔
836

837
        assert!(!abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(1.01, None)));
2✔
838
        assert!(!abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(-1.01, None)));
2✔
839
        assert!(!abs_mode.in_tolerance(&Quantity::new(1.01, None), &Quantity::new(0.0, None)));
2✔
840
        assert!(!abs_mode.in_tolerance(&Quantity::new(-1.01, None), &Quantity::new(0.0, None)));
2✔
841
    }
2✔
842

843
    #[test]
2✔
844
    fn basic_compare_modes_test_relative() {
2✔
845
        let rel_mode = Mode::Relative(1.0);
2✔
846
        assert!(rel_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(2.0, None)));
2✔
847
        assert!(rel_mode.in_tolerance(&Quantity::new(2.0, None), &Quantity::new(4.0, None)));
2✔
848
        assert!(rel_mode.in_tolerance(&Quantity::new(-1.0, None), &Quantity::new(-2.0, None)));
2✔
849
        assert!(rel_mode.in_tolerance(&Quantity::new(-2.0, None), &Quantity::new(-4.0, None)));
2✔
850
        assert!(rel_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(0.0, None)));
2✔
851

852
        assert!(!rel_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(2.01, None)));
2✔
853
        assert!(!rel_mode.in_tolerance(&Quantity::new(2.0, None), &Quantity::new(4.01, None)));
2✔
854
    }
2✔
855

856
    #[test]
2✔
857
    fn check_same_numbers_different_missmatch() {
2✔
858
        let rel_mode = Mode::Relative(1.0);
2✔
859
        assert!(!rel_mode.in_tolerance(
2✔
860
            &Quantity::new(2.0, Some("mm")),
2✔
861
            &Quantity::new(2.0, Some("m"))
2✔
862
        ));
2✔
863
    }
2✔
864

865
    #[test]
2✔
866
    fn basic_compare_modes_test_ignored() {
2✔
867
        let abs_mode = Mode::Ignore;
2✔
868
        assert!(abs_mode.in_tolerance(
2✔
869
            &Quantity::new(1.0, None),
2✔
870
            &Quantity::new(f64::INFINITY, None)
2✔
871
        ));
2✔
872
    }
2✔
873

874
    #[test]
2✔
875
    fn nan_is_nan() {
2✔
876
        let nan = f64::NAN;
2✔
877
        let nominal = Quantity {
2✔
878
            value: nan,
2✔
879
            unit: None,
2✔
880
        };
2✔
881
        let actual = Quantity {
2✔
882
            value: nan,
2✔
883
            unit: None,
2✔
884
        };
2✔
885

2✔
886
        assert!(Mode::Relative(1.0).in_tolerance(&nominal, &actual));
2✔
887
        assert!(Mode::Absolute(1.0).in_tolerance(&nominal, &actual));
2✔
888
        assert!(Mode::Ignore.in_tolerance(&nominal, &actual))
2✔
889
    }
2✔
890

891
    #[test]
2✔
892
    fn bom_is_trimmed() {
2✔
893
        let str_with_bom = "\u{feff}Hallo\n\r";
2✔
894
        let str_no_bom = "Hallo\n";
2✔
895
        let cfg = CSVCompareConfig {
2✔
896
            preprocessing: None,
2✔
897
            delimiters: Delimiters::default(),
2✔
898
            exclude_field_regex: None,
2✔
899
            comparison_modes: vec![Mode::Absolute(0.0)],
2✔
900
        };
2✔
901
        let (_, _, res) =
2✔
902
            get_diffs_readers(Cursor::new(str_with_bom), Cursor::new(str_no_bom), &cfg).unwrap();
2✔
903
        assert!(res.is_empty());
2✔
904
    }
2✔
905

906
    fn mk_test_table() -> Table {
4✔
907
        let col = Column {
4✔
908
            rows: vec![
4✔
909
                Value::from_str("0.0", &None),
4✔
910
                Value::from_str("1.0", &None),
4✔
911
                Value::from_str("2.0", &None),
4✔
912
            ],
4✔
913
            header: None,
4✔
914
        };
4✔
915

4✔
916
        let col_two = col.clone();
4✔
917
        Table {
4✔
918
            columns: vec![col, col_two],
4✔
919
        }
4✔
920
    }
4✔
921

922
    #[test]
2✔
923
    fn row_iterator() {
2✔
924
        let table = mk_test_table();
2✔
925
        let mut row_iterator = table.rows();
2✔
926
        assert_eq!(row_iterator.len(), 3);
2✔
927
        let first_row = row_iterator.next().unwrap();
2✔
928
        assert!(first_row
2✔
929
            .iter()
2✔
930
            .all(|v| **v == Value::from_str("0.0", &None)));
4✔
931
        for row in row_iterator {
6✔
932
            assert_eq!(row.len(), 2);
4✔
933
        }
934
    }
2✔
935

936
    #[test]
2✔
937
    fn row_iterator_mut() {
2✔
938
        let mut table = mk_test_table();
2✔
939
        let mut row_iterator = table.rows_mut();
2✔
940
        assert_eq!(row_iterator.len(), 3);
2✔
941
        let first_row = row_iterator.next().unwrap();
2✔
942
        assert!(first_row
2✔
943
            .iter()
2✔
944
            .all(|v| **v == Value::from_str("0.0", &None)));
4✔
945
        for row in row_iterator {
6✔
946
            assert_eq!(row.len(), 2);
4✔
947
        }
948
        let row_iterator = table.rows_mut();
2✔
949
        for mut row in row_iterator {
8✔
950
            assert_eq!(row.len(), 2);
6✔
951
            row.iter_mut()
6✔
952
                .for_each(|v| **v = Value::from_str("4.0", &None));
12✔
953
        }
954
        let mut row_iterator = table.rows();
2✔
955
        assert!(row_iterator.all(|r| r.iter().all(|v| **v == Value::from_str("4.0", &None))));
12✔
956
    }
2✔
957

958
    #[test]
2✔
959
    fn loading_non_existing_folder_fails() {
2✔
960
        let conf = CSVCompareConfig {
2✔
961
            comparison_modes: vec![],
2✔
962
            delimiters: Delimiters::default(),
2✔
963
            exclude_field_regex: None,
2✔
964
            preprocessing: None,
2✔
965
        };
2✔
966
        let result = compare_paths("non_existing", "also_non_existing", &conf);
2✔
967
        assert!(matches!(result.unwrap_err(), Error::FileAccessFailed(_)));
2✔
968
    }
2✔
969

970
    #[test]
2✔
971
    fn table_with_newlines_consistent_col_lengths() {
2✔
972
        let table = Table::from_reader(
2✔
973
            File::open("tests/csv/data/defects.csv").unwrap(),
2✔
974
            &Delimiters::autodetect(),
2✔
975
        )
2✔
976
        .unwrap();
2✔
977
        for col in table.columns.iter() {
60✔
978
            assert_eq!(col.rows.len(), table.columns.first().unwrap().rows.len());
60✔
979
        }
980
    }
2✔
981

982
    #[test]
2✔
983
    fn test_float_diff_precision() {
2✔
984
        let magic_first = 0.03914;
2✔
985
        let magic_second = 0.03913;
2✔
986
        let tolerance = 0.00001;
2✔
987
        let tolerance_f64 = 0.00001;
2✔
988

2✔
989
        let single_diff: f32 = magic_first - magic_second;
2✔
990
        assert!(single_diff > tolerance);
2✔
991

992
        let quantity1 = Quantity::new(0.03914, None);
2✔
993
        let quantity2 = Quantity::new(0.03913, None);
2✔
994
        let modes = [
2✔
995
            Mode::Absolute(tolerance_f64),
2✔
996
            Mode::Relative(tolerance_f64 / 0.03914),
2✔
997
        ];
2✔
998
        for mode in modes {
6✔
999
            assert!(mode.in_tolerance(&quantity1, &quantity2));
4✔
1000
        }
1001
    }
2✔
1002
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc