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

VolumeGraphics / havocompare / a399ab79e2aa86f9669021f39335a9b05257ab16-PR-38

pending completion
a399ab79e2aa86f9669021f39335a9b05257ab16-PR-38

Pull #38

github

web-flow
Merge ac8ddb1e0 into 97c747d62
Pull Request #38: draft: Json reporting

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

2761 of 3267 relevant lines covered (84.51%)

2763.17 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 {
3✔
111
        match self {
3✔
112
            DiffType::DifferentValueTypes {
113
                nominal,
1✔
114
                actual,
1✔
115
                position,
1✔
116
            } => {
1✔
117
                write!(
1✔
118
                    f,
1✔
119
                    "Line: {}, Col: {} -- Different value types -- Expected {}, Found {}",
1✔
120
                    position.row, position.col, nominal, actual
1✔
121
                )
1✔
122
                .unwrap_or_default();
1✔
123
            }
1✔
124
            DiffType::OutOfTolerance {
125
                actual,
1✔
126
                nominal,
1✔
127
                mode,
1✔
128
                position,
1✔
129
            } => {
1✔
130
                write!(
1✔
131
                    f,
1✔
132
                    "Line: {}, Col: {} -- Out of tolerance -- Expected {}, Found {}, Mode {}",
1✔
133
                    position.row, position.col, nominal, actual, mode
1✔
134
                )
1✔
135
                .unwrap_or_default();
1✔
136
            }
1✔
137
            DiffType::UnequalStrings {
138
                nominal,
1✔
139
                actual,
1✔
140
                position,
1✔
141
            } => {
1✔
142
                write!(
1✔
143
                    f,
1✔
144
                    "Line: {}, Col: {} -- Different strings -- Expected {}, Found {}",
1✔
145
                    position.row, position.col, nominal, actual
1✔
146
                )
1✔
147
                .unwrap_or_default();
1✔
148
            }
1✔
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(())
3✔
159
    }
3✔
160
}
161

162
#[derive(Copy, Clone, JsonSchema, Debug, Deserialize, Serialize, PartialEq)]
8✔
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 {
4✔
175
        match &self {
4✔
176
            Mode::Absolute(tolerance) => {
2✔
177
                write!(f, "Absolute (tol: {tolerance})").unwrap_or_default();
2✔
178
            }
2✔
179
            Mode::Relative(tolerance) => {
1✔
180
                write!(f, "Relative (tol: {tolerance})").unwrap_or_default();
1✔
181
            }
1✔
182
            Mode::Ignore => {
1✔
183
                write!(f, "Ignored").unwrap_or_default();
1✔
184
            }
1✔
185
        };
186
        Ok(())
4✔
187
    }
4✔
188
}
189

190
impl Mode {
191
    pub(crate) fn in_tolerance(&self, nominal: &Quantity, actual: &Quantity) -> bool {
22,740✔
192
        if nominal.value.is_nan() && actual.value.is_nan() {
22,740✔
193
            return true;
3✔
194
        }
22,737✔
195
        match self {
22,737✔
196
            Mode::Absolute(tolerance) => {
12,315✔
197
                let plain_diff = (nominal.value - actual.value).abs();
12,315✔
198
                let numerically = if plain_diff == 0.0 {
12,315✔
199
                    true
11,407✔
200
                } else if *tolerance == 0.0 {
908✔
201
                    false
×
202
                } else {
203
                    let diff = nominal.minimal_diff(actual);
908✔
204
                    diff <= *tolerance
908✔
205
                };
206

207
                let identical_units = nominal.unit == actual.unit;
12,315✔
208
                numerically && identical_units
12,315✔
209
            }
210
            Mode::Ignore => true,
1✔
211
            Mode::Relative(tolerance) => {
10,421✔
212
                let plain_diff = (nominal.value - actual.value).abs();
10,421✔
213
                let numerically = if plain_diff == 0.0 {
10,421✔
214
                    true
10,408✔
215
                } else if *tolerance == 0.0 {
13✔
216
                    false
×
217
                } else {
218
                    let diff = nominal.minimal_diff(actual);
13✔
219
                    let diff = (diff / nominal.value).abs();
13✔
220
                    diff <= *tolerance
13✔
221
                };
222
                let identical_units = nominal.unit == actual.unit;
10,421✔
223
                numerically && identical_units
10,421✔
224
            }
225
        }
226
    }
22,740✔
227
}
228

229
#[derive(JsonSchema, Deserialize, Serialize, Debug, Default, Clone)]
12✔
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)]
29✔
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 {
622✔
254
        self.decimal_separator.is_none() && self.field_delimiter.is_none()
622✔
255
    }
622✔
256

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

266
#[derive(Default, Clone)]
708✔
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) {
2✔
274
        self.header = Some("DELETED".to_string());
2✔
275
        let row_count = self.rows.len();
2✔
276
        self.rows = vec![Value::deleted(); row_count];
2✔
277
    }
2✔
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>(
178✔
286
        input: R,
178✔
287
        config: &Delimiters,
178✔
288
    ) -> Result<Table, Error> {
178✔
289
        let mut cols = Vec::new();
178✔
290
        let input = BufReader::new(input);
178✔
291
        let mut parser = if config.is_empty() {
178✔
292
            tokenizer::Parser::new_guess_format(input)?
178✔
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() {
14,467✔
298
            if cols.is_empty() {
14,467✔
299
                cols.resize_with(fields.len(), Column::default);
178✔
300
            }
14,289✔
301
            if fields.len() != cols.len() {
14,467✔
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 {
14,467✔
306
                fields
14,467✔
307
                    .into_iter()
14,467✔
308
                    .zip(cols.iter_mut())
14,467✔
309
                    .for_each(|(f, col)| col.rows.push(f));
32,664✔
310
            }
14,467✔
311
        }
312

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

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

322
    pub(crate) fn rows_mut(&mut self) -> RowIteratorMut {
4✔
323
        RowIteratorMut {
4✔
324
            position: self.columns.iter_mut().map(|c| c.rows.iter_mut()).collect(),
8✔
325
        }
4✔
326
    }
4✔
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 {
315✔
344
                self.position.first().unwrap().len()
315✔
345
            }
315✔
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> {
524✔
359
        mk_next!(self.position)
1,048✔
360
    }
524✔
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> {
11,416✔
370
        mk_next!(self.position)
29,596✔
371
    }
11,416✔
372
}
373

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

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

402
        for (row, (val_nom, val_act)) in col_nom.rows.iter().zip(col_act.rows.iter()).enumerate() {
17,660✔
403
            let position = Position { row, col };
17,660✔
404
            let diffs_field = compare_values(val_nom, val_act, config, position)?;
17,660✔
405
            diffs.extend(diffs_field);
17,660✔
406
        }
407
    }
408
    Ok(diffs)
156✔
409
}
156✔
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() {
17,660✔
416
        if let Some(nominal) = nominal.get_quantity() {
13,510✔
417
            return Some((actual, nominal));
13,506✔
418
        }
4✔
419
    }
4,150✔
420
    None
4,154✔
421
}
17,660✔
422

423
fn both_string(actual: &Value, nominal: &Value) -> Option<(String, String)> {
424
    if let Some(actual) = actual.get_string() {
4,154✔
425
        if let Some(nominal) = nominal.get_string() {
4,150✔
426
            return Some((actual, nominal));
4,150✔
427
        }
×
428
    }
4✔
429
    None
4✔
430
}
4,154✔
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) {
17,660✔
440
        Ok(config
13,506✔
441
            .comparison_modes
13,506✔
442
            .iter()
13,506✔
443
            .filter_map(|cm| {
22,874✔
444
                if !cm.in_tolerance(nominal_float, actual_float) {
20,826✔
445
                    Some(DiffType::OutOfTolerance {
7✔
446
                        nominal: nominal_float.clone(),
7✔
447
                        actual: actual_float.clone(),
7✔
448
                        mode: *cm,
7✔
449
                        position,
7✔
450
                    })
7✔
451
                } else {
452
                    None
20,819✔
453
                }
454
            })
22,874✔
455
            .collect())
13,506✔
456
    } else if let Some((actual_string, nominal_string)) = both_string(actual, nominal) {
4,154✔
457
        if let Some(exclude_regex) = config.exclude_field_regex.as_deref() {
4,150✔
458
            let regex = Regex::new(exclude_regex)?;
4,040✔
459
            if regex.is_match(nominal_string.as_str()) {
4,040✔
460
                return Ok(Vec::new());
4✔
461
            }
4,036✔
462
        }
110✔
463
        if nominal_string != actual_string {
4,146✔
464
            Ok(vec![DiffType::UnequalStrings {
×
465
                position,
×
466
                nominal: nominal_string,
×
467
                actual: actual_string,
×
468
            }])
×
469
        } else {
470
            Ok(Vec::new())
4,146✔
471
        }
472
    } else {
473
        Ok(vec![DiffType::DifferentValueTypes {
4✔
474
            actual: actual.clone(),
4✔
475
            nominal: nominal.clone(),
4✔
476
            position,
4✔
477
        }])
4✔
478
    }
479
}
17,660✔
480

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

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

513
    let (_, _, results) = get_diffs_readers(&nominal_file, &actual_file, config)?;
37✔
514
    results.iter().for_each(|error| {
37✔
515
        error!("{}", &error);
×
516
    });
37✔
517
    let is_error = !results.is_empty();
37✔
518
    let mut result = report::Difference::new_for_file(nominal.as_ref(), actual.as_ref());
37✔
519
    result.is_error = is_error;
37✔
520
    result.detail = results.into_iter().map(report::DiffDetail::CSV).collect();
37✔
521
    Ok(result)
37✔
522
}
38✔
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 {
3✔
539
        Position {
3✔
540
            col: POS_COL,
3✔
541
            row: POS_ROW,
3✔
542
        }
3✔
543
    }
3✔
544

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

992
        let quantity1 = Quantity::new(0.03914, None);
1✔
993
        let quantity2 = Quantity::new(0.03913, None);
1✔
994
        let modes = [
1✔
995
            Mode::Absolute(tolerance_f64),
1✔
996
            Mode::Relative(tolerance_f64 / 0.03914),
1✔
997
        ];
1✔
998
        for mode in modes {
3✔
999
            assert!(mode.in_tolerance(&quantity1, &quantity2));
2✔
1000
        }
1001
    }
1✔
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