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

VolumeGraphics / havocompare / 4e21ebdce5b3b7f85cef483f4d8dc770a51305fa-PR-32

pending completion
4e21ebdce5b3b7f85cef483f4d8dc770a51305fa-PR-32

Pull #32

github

GitHub
Merge eacbeed0c into 09319bfb4
Pull Request #32: Compare csv header

51 of 51 new or added lines in 2 files covered. (100.0%)

2819 of 3065 relevant lines covered (91.97%)

2036.98 hits per line

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

95.38
/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("Problem creating csv report {0}")]
36
    /// Reporting could not be created
37
    ReportingFailed(#[from] report::Error),
38
    #[error("File access failed {0}")]
39
    /// File access failed
40
    FileAccessFailed(#[from] FatIOError),
41
    #[error("IoError occurred {0}")]
42
    /// Problem involving files or readers
43
    IoProblem(#[from] std::io::Error),
44

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

49
    #[error("A string literal was started but did never end")]
50
    /// A string literal was started but did never end
51
    UnterminatedLiteral,
52

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

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

62
#[derive(Clone, Copy, Debug)]
×
63
pub(crate) struct Position {
64
    pub row: usize,
65
    pub col: usize,
66
}
67

68
#[derive(Debug)]
×
69
pub(crate) enum DiffType {
70
    UnequalStrings {
71
        nominal: String,
72
        actual: String,
73
        position: Position,
74
    },
75
    OutOfTolerance {
76
        nominal: Quantity,
77
        actual: Quantity,
78
        mode: Mode,
79
        position: Position,
80
    },
81
    DifferentValueTypes {
82
        nominal: Value,
83
        actual: Value,
84
        position: Position,
85
    },
86
    UnequalHeader {
87
        nominal: String,
88
        actual: String,
89
    },
90
}
91

92
impl Display for DiffType {
93
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3✔
94
        match self {
3✔
95
            DiffType::DifferentValueTypes {
96
                nominal,
1✔
97
                actual,
1✔
98
                position,
1✔
99
            } => {
1✔
100
                write!(
1✔
101
                    f,
1✔
102
                    "Line: {}, Col: {} -- Different value types -- Expected {}, Found {}",
1✔
103
                    position.row, position.col, nominal, actual
1✔
104
                )
1✔
105
                .unwrap_or_default();
1✔
106
            }
1✔
107
            DiffType::OutOfTolerance {
108
                actual,
1✔
109
                nominal,
1✔
110
                mode,
1✔
111
                position,
1✔
112
            } => {
1✔
113
                write!(
1✔
114
                    f,
1✔
115
                    "Line: {}, Col: {} -- Out of tolerance -- Expected {}, Found {}, Mode {}",
1✔
116
                    position.row, position.col, nominal, actual, mode
1✔
117
                )
1✔
118
                .unwrap_or_default();
1✔
119
            }
1✔
120
            DiffType::UnequalStrings {
121
                nominal,
1✔
122
                actual,
1✔
123
                position,
1✔
124
            } => {
1✔
125
                write!(
1✔
126
                    f,
1✔
127
                    "Line: {}, Col: {} -- Different strings -- Expected {}, Found {}",
1✔
128
                    position.row, position.col, nominal, actual
1✔
129
                )
1✔
130
                .unwrap_or_default();
1✔
131
            }
1✔
132
            DiffType::UnequalHeader { nominal, actual } => {
×
133
                write!(
×
134
                    f,
×
135
                    "Different header strings -- Expected {}, Found {}",
×
136
                    nominal, actual
×
137
                )
×
138
                .unwrap_or_default();
×
139
            }
×
140
        };
141
        Ok(())
3✔
142
    }
3✔
143
}
144

145
#[derive(Copy, Clone, JsonSchema, Debug, Deserialize, Serialize, PartialEq)]
8✔
146
/// comparison mode for csv cells
147
pub enum Mode {
148
    /// `(a-b).abs() < threshold`
149
    Absolute(f64),
150
    /// `((a-b)/a).abs() < threshold`
151
    Relative(f64),
152
    /// always matches
153
    Ignore,
154
}
155

156
impl Display for Mode {
157
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4✔
158
        match &self {
4✔
159
            Mode::Absolute(tolerance) => {
2✔
160
                write!(f, "Absolute (tol: {tolerance})").unwrap_or_default();
2✔
161
            }
2✔
162
            Mode::Relative(tolerance) => {
1✔
163
                write!(f, "Relative (tol: {tolerance})").unwrap_or_default();
1✔
164
            }
1✔
165
            Mode::Ignore => {
1✔
166
                write!(f, "Ignored").unwrap_or_default();
1✔
167
            }
1✔
168
        };
169
        Ok(())
4✔
170
    }
4✔
171
}
172

173
impl Mode {
174
    pub(crate) fn in_tolerance(&self, nominal: &Quantity, actual: &Quantity) -> bool {
21,956✔
175
        if nominal.value.is_nan() && actual.value.is_nan() {
21,956✔
176
            return true;
3✔
177
        }
21,953✔
178
        match self {
21,953✔
179
            Mode::Absolute(tolerance) => {
11,923✔
180
                let plain_diff = (nominal.value - actual.value).abs();
11,923✔
181
                let numerically = if plain_diff == 0.0 {
11,923✔
182
                    true
11,015✔
183
                } else if *tolerance == 0.0 {
908✔
184
                    false
×
185
                } else {
186
                    let diff = nominal.minimal_diff(actual);
908✔
187
                    diff <= *tolerance
908✔
188
                };
189

190
                let identical_units = nominal.unit == actual.unit;
11,923✔
191
                numerically && identical_units
11,923✔
192
            }
193
            Mode::Ignore => true,
1✔
194
            Mode::Relative(tolerance) => {
10,029✔
195
                let plain_diff = (nominal.value - actual.value).abs();
10,029✔
196
                let numerically = if plain_diff == 0.0 {
10,029✔
197
                    true
10,016✔
198
                } else if *tolerance == 0.0 {
13✔
199
                    false
×
200
                } else {
201
                    let diff = nominal.minimal_diff(actual);
13✔
202
                    let diff = (diff / nominal.value).abs();
13✔
203
                    diff <= *tolerance
13✔
204
                };
205
                let identical_units = nominal.unit == actual.unit;
10,029✔
206
                numerically && identical_units
10,029✔
207
            }
208
        }
209
    }
21,956✔
210
}
211

212
#[derive(JsonSchema, Deserialize, Serialize, Debug, Default)]
12✔
213
/// Settings for the CSV comparison module
214
pub struct CSVCompareConfig {
215
    #[serde(flatten)]
216
    /// delimiters for the file parsing
217
    pub delimiters: Delimiters,
218
    /// How numerical values shall be compared, strings are always checked for identity
219
    pub comparison_modes: Vec<Mode>,
220
    /// Any field matching the given regex is excluded from comparison
221
    pub exclude_field_regex: Option<String>,
222
    /// Preprocessing done to the csv files before beginning the comparison
223
    pub preprocessing: Option<Vec<Preprocessor>>,
224
}
225

226
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
26✔
227
/// Delimiter configuration for file parsing
228
pub struct Delimiters {
229
    /// The delimiters of the csv fields (typically comma, semicolon or pipe)
230
    pub field_delimiter: Option<char>,
231
    /// The decimal separator for floating point numbers (typically dot or comma)
232
    pub decimal_separator: Option<char>,
233
}
234

235
impl Delimiters {
236
    pub(crate) fn is_empty(&self) -> bool {
317✔
237
        self.decimal_separator.is_none() && self.field_delimiter.is_none()
317✔
238
    }
317✔
239

240
    #[cfg(test)]
241
    pub fn autodetect() -> Delimiters {
2✔
242
        Delimiters {
2✔
243
            field_delimiter: None,
2✔
244
            decimal_separator: None,
2✔
245
        }
2✔
246
    }
2✔
247
}
248

249
#[derive(Default, Clone)]
423✔
250
pub(crate) struct Column {
251
    pub header: Option<String>,
252
    pub rows: Vec<Value>,
253
}
254

255
impl Column {
256
    pub fn delete_contents(&mut self) {
2✔
257
        self.header = Some("DELETED".to_string());
2✔
258
        let row_count = self.rows.len();
2✔
259
        self.rows = vec![Value::deleted(); row_count];
2✔
260
    }
2✔
261
}
262

263
pub(crate) struct Table {
264
    pub columns: Vec<Column>,
265
}
266

267
impl Table {
268
    pub(crate) fn from_reader<R: Read + Seek>(
101✔
269
        input: R,
101✔
270
        config: &Delimiters,
101✔
271
    ) -> Result<Table, Error> {
101✔
272
        let mut cols = Vec::new();
101✔
273
        let input = BufReader::new(input);
101✔
274
        let mut parser = if config.is_empty() {
101✔
275
            tokenizer::Parser::new_guess_format(input)?
101✔
276
        } else {
277
            tokenizer::Parser::new(input, config.clone()).ok_or(Error::FormatGuessingFailure)?
×
278
        };
279

280
        for (line_num, fields) in parser.parse_to_rows()?.enumerate() {
11,759✔
281
            if cols.is_empty() {
11,759✔
282
                cols.resize_with(fields.len(), Column::default);
101✔
283
            }
11,658✔
284
            if fields.len() != cols.len() {
11,759✔
285
                let message = format!("Error: Columns inconsistent! First row had {}, this row has {} (row:{line_num})", cols.len(), fields.len());
×
286
                error!("{}", message.as_str());
×
287
                return Err(Error::UnstableColumnCount(line_num));
×
288
            } else {
11,759✔
289
                fields
11,759✔
290
                    .into_iter()
11,759✔
291
                    .zip(cols.iter_mut())
11,759✔
292
                    .for_each(|(f, col)| col.rows.push(f));
25,621✔
293
            }
11,759✔
294
        }
295

296
        Ok(Table { columns: cols })
101✔
297
    }
101✔
298

299
    pub(crate) fn rows(&self) -> RowIterator {
597✔
300
        RowIterator {
597✔
301
            position: self.columns.iter().map(|c| c.rows.iter()).collect(),
2,287✔
302
        }
597✔
303
    }
597✔
304

305
    pub(crate) fn rows_mut(&mut self) -> RowIteratorMut {
4✔
306
        RowIteratorMut {
4✔
307
            position: self.columns.iter_mut().map(|c| c.rows.iter_mut()).collect(),
8✔
308
        }
4✔
309
    }
4✔
310
}
311

312
macro_rules! mk_next {
313
    ($pos: expr) => {{
314
        let row: Vec<_> = $pos.iter_mut().filter_map(|i| i.next()).collect();
315
        if row.is_empty() {
316
            None
317
        } else {
318
            Some(row)
319
        }
320
    }};
321
}
322

323
macro_rules! impl_ex_size_it {
324
    ($($t:ty),+) => {
325
        $(impl<'a> ExactSizeIterator for $t {
326
            fn len(&self) -> usize {
307✔
327
                self.position.first().unwrap().len()
307✔
328
            }
307✔
329
        })+
330
    };
331
}
332

333
impl_ex_size_it!(RowIteratorMut<'_>, RowIterator<'_>);
334

335
pub(crate) struct RowIteratorMut<'a> {
336
    position: Vec<IterMut<'a, Value>>,
337
}
338

339
impl<'a> Iterator for RowIteratorMut<'a> {
340
    type Item = Vec<&'a mut Value>;
341
    fn next(&mut self) -> Option<Self::Item> {
524✔
342
        mk_next!(self.position)
1,048✔
343
    }
524✔
344
}
345

346
pub(crate) struct RowIterator<'a> {
347
    position: Vec<Iter<'a, Value>>,
348
}
349

350
impl<'a> Iterator for RowIterator<'a> {
351
    type Item = Vec<&'a Value>;
352
    fn next(&mut self) -> Option<Self::Item> {
10,940✔
353
        mk_next!(self.position)
28,612✔
354
    }
10,940✔
355
}
356

357
pub(crate) fn compare_tables(
152✔
358
    nominal: &Table,
152✔
359
    actual: &Table,
152✔
360
    config: &CSVCompareConfig,
152✔
361
) -> Result<Vec<DiffType>, Error> {
152✔
362
    if nominal.rows().len() != actual.rows().len() {
152✔
363
        return Err(Error::UnequalRowCount(
×
364
            nominal.rows().len(),
×
365
            actual.rows().len(),
×
366
        ));
×
367
    }
152✔
368

152✔
369
    let mut diffs = Vec::new();
152✔
370
    for (col, (col_nom, col_act)) in nominal
585✔
371
        .columns
152✔
372
        .iter()
152✔
373
        .zip(actual.columns.iter())
152✔
374
        .enumerate()
152✔
375
    {
376
        if let (Some(nom_header), Some(act_header)) = (&col_nom.header, &col_act.header) {
585✔
377
            if nom_header != act_header {
101✔
378
                diffs.extend(vec![DiffType::UnequalHeader {
3✔
379
                    nominal: nom_header.to_owned(),
3✔
380
                    actual: act_header.to_owned(),
3✔
381
                }]);
3✔
382
            }
98✔
383
        }
484✔
384

385
        for (row, (val_nom, val_act)) in col_nom.rows.iter().zip(col_act.rows.iter()).enumerate() {
17,260✔
386
            let position = Position { row, col };
17,260✔
387
            let diffs_field = compare_values(val_nom, val_act, config, position)?;
17,260✔
388
            diffs.extend(diffs_field);
17,260✔
389
        }
390
    }
391
    Ok(diffs)
152✔
392
}
152✔
393

394
fn both_quantity<'a>(
395
    actual: &'a Value,
396
    nominal: &'a Value,
397
) -> Option<(&'a Quantity, &'a Quantity)> {
398
    if let Some(actual) = actual.get_quantity() {
17,260✔
399
        if let Some(nominal) = nominal.get_quantity() {
13,118✔
400
            return Some((actual, nominal));
13,114✔
401
        }
4✔
402
    }
4,142✔
403
    None
4,146✔
404
}
17,260✔
405

406
fn both_string(actual: &Value, nominal: &Value) -> Option<(String, String)> {
407
    if let Some(actual) = actual.get_string() {
4,146✔
408
        if let Some(nominal) = nominal.get_string() {
4,142✔
409
            return Some((actual, nominal));
4,142✔
410
        }
×
411
    }
4✔
412
    None
4✔
413
}
4,146✔
414

415
fn compare_values(
416
    nominal: &Value,
417
    actual: &Value,
418
    config: &CSVCompareConfig,
419
    position: Position,
420
) -> Result<Vec<DiffType>, Error> {
421
    // float quantity compare
422
    if let Some((actual_float, nominal_float)) = both_quantity(actual, nominal) {
17,260✔
423
        Ok(config
13,114✔
424
            .comparison_modes
13,114✔
425
            .iter()
13,114✔
426
            .filter_map(|cm| {
22,090✔
427
                if !cm.in_tolerance(nominal_float, actual_float) {
20,042✔
428
                    Some(DiffType::OutOfTolerance {
7✔
429
                        nominal: nominal_float.clone(),
7✔
430
                        actual: actual_float.clone(),
7✔
431
                        mode: *cm,
7✔
432
                        position,
7✔
433
                    })
7✔
434
                } else {
435
                    None
20,035✔
436
                }
437
            })
22,090✔
438
            .collect())
13,114✔
439
    } else if let Some((actual_string, nominal_string)) = both_string(actual, nominal) {
4,146✔
440
        if let Some(exclude_regex) = config.exclude_field_regex.as_deref() {
4,142✔
441
            let regex = Regex::new(exclude_regex)?;
4,032✔
442
            if regex.is_match(nominal_string.as_str()) {
4,032✔
443
                return Ok(Vec::new());
4✔
444
            }
4,028✔
445
        }
110✔
446
        if nominal_string != actual_string {
4,138✔
447
            Ok(vec![DiffType::UnequalStrings {
×
448
                position,
×
449
                nominal: nominal_string,
×
450
                actual: actual_string,
×
451
            }])
×
452
        } else {
453
            Ok(Vec::new())
4,138✔
454
        }
455
    } else {
456
        Ok(vec![DiffType::DifferentValueTypes {
4✔
457
            actual: actual.clone(),
4✔
458
            nominal: nominal.clone(),
4✔
459
            position,
4✔
460
        }])
4✔
461
    }
462
}
17,260✔
463

464
fn get_diffs_readers<R: Read + Seek + Send>(
42✔
465
    nominal: R,
42✔
466
    actual: R,
42✔
467
    config: &CSVCompareConfig,
42✔
468
) -> Result<(Table, Table, Vec<DiffType>), Error> {
42✔
469
    let tables: Result<Vec<Table>, Error> = [nominal, actual]
42✔
470
        .into_par_iter()
42✔
471
        .map(|r| Table::from_reader(r, &config.delimiters))
84✔
472
        .collect();
42✔
473
    let mut tables = tables?;
42✔
474
    if let (Some(mut actual), Some(mut nominal)) = (tables.pop(), tables.pop()) {
42✔
475
        if let Some(preprocessors) = config.preprocessing.as_ref() {
42✔
476
            for preprocessor in preprocessors.iter() {
9✔
477
                preprocessor.process(&mut nominal)?;
9✔
478
                preprocessor.process(&mut actual)?;
9✔
479
            }
480
        }
33✔
481
        let comparison_result = compare_tables(&nominal, &actual, config)?;
42✔
482
        Ok((nominal, actual, comparison_result))
42✔
483
    } else {
484
        Err(Error::UnterminatedLiteral)
×
485
    }
486
}
42✔
487

488
pub(crate) fn compare_paths(
37✔
489
    nominal: impl AsRef<Path>,
37✔
490
    actual: impl AsRef<Path>,
37✔
491
    config: &CSVCompareConfig,
37✔
492
) -> Result<report::FileCompareResult, Error> {
37✔
493
    let nominal_file = fat_io_wrap_std(nominal.as_ref(), &File::open)?;
37✔
494
    let actual_file = fat_io_wrap_std(actual.as_ref(), &File::open)?;
36✔
495

496
    let (nominal_table, actual_table, results) =
36✔
497
        get_diffs_readers(&nominal_file, &actual_file, config)?;
36✔
498
    results.iter().for_each(|error| {
36✔
499
        error!("{}", &error);
×
500
    });
36✔
501

36✔
502
    Ok(report::write_csv_detail(
36✔
503
        nominal_table,
36✔
504
        actual_table,
36✔
505
        nominal.as_ref(),
36✔
506
        actual.as_ref(),
36✔
507
        results.as_slice(),
36✔
508
    )?)
36✔
509
}
37✔
510

511
#[cfg(test)]
512
mod tests {
513
    use super::*;
514
    use crate::csv::DiffType::{
515
        DifferentValueTypes, OutOfTolerance, UnequalHeader, UnequalStrings,
516
    };
517
    use crate::csv::Preprocessor::ExtractHeaders;
518
    use std::io::Cursor;
519

520
    const NOMINAL: &str = "nominal";
521
    const ACTUAL: &str = "actual";
522
    const POS_COL: usize = 1337;
523
    const POS_ROW: usize = 667;
524

525
    fn mk_position() -> Position {
3✔
526
        Position {
3✔
527
            col: POS_COL,
3✔
528
            row: POS_ROW,
3✔
529
        }
3✔
530
    }
3✔
531

532
    #[test]
1✔
533
    fn diff_types_readable_string() {
1✔
534
        let string_unequal = UnequalStrings {
1✔
535
            nominal: NOMINAL.to_string(),
1✔
536
            actual: ACTUAL.to_string(),
1✔
537
            position: mk_position(),
1✔
538
        };
1✔
539
        let msg = format!("{string_unequal}");
1✔
540
        assert!(msg.contains(NOMINAL));
1✔
541
        assert!(msg.contains(ACTUAL));
1✔
542
        assert!(msg.contains(format!("{POS_COL}").as_str()));
1✔
543
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
1✔
544
    }
1✔
545

546
    #[test]
1✔
547
    fn diff_types_readable_out_of_tolerance() {
1✔
548
        let string_unequal = OutOfTolerance {
1✔
549
            nominal: Quantity {
1✔
550
                value: 10.0,
1✔
551
                unit: Some("mm".to_owned()),
1✔
552
            },
1✔
553
            actual: Quantity {
1✔
554
                value: 12.0,
1✔
555
                unit: Some("um".to_owned()),
1✔
556
            },
1✔
557
            mode: Mode::Absolute(11.0),
1✔
558
            position: mk_position(),
1✔
559
        };
1✔
560
        let msg = format!("{string_unequal}");
1✔
561
        assert!(msg.contains("10 mm"));
1✔
562
        assert!(msg.contains("11"));
1✔
563
        assert!(msg.contains("12 um"));
1✔
564
        assert!(msg.contains("Absolute"));
1✔
565
        assert!(msg.contains(format!("{POS_COL}").as_str()));
1✔
566
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
1✔
567
    }
1✔
568

569
    #[test]
1✔
570
    fn diff_types_readable_different_value_types() {
1✔
571
        let string_unequal = DifferentValueTypes {
1✔
572
            nominal: Value::from_str("10.0 mm", &None),
1✔
573
            actual: Value::from_str(ACTUAL, &None),
1✔
574
            position: mk_position(),
1✔
575
        };
1✔
576
        let msg = format!("{string_unequal}");
1✔
577
        assert!(msg.contains("10 mm"));
1✔
578
        assert!(msg.contains(ACTUAL));
1✔
579
        assert!(msg.contains(format!("{POS_COL}").as_str()));
1✔
580
        assert!(msg.contains(format!("{POS_ROW}").as_str()));
1✔
581
    }
1✔
582

583
    #[test]
1✔
584
    fn table_cols_reading_correct() {
1✔
585
        let table = Table::from_reader(
1✔
586
            File::open("tests/csv/data/Annotations.csv").unwrap(),
1✔
587
            &Delimiters::default(),
1✔
588
        )
1✔
589
        .unwrap();
1✔
590
        assert_eq!(table.columns.len(), 13);
1✔
591
    }
1✔
592

593
    #[test]
1✔
594
    fn table_rows_reading_correct() {
1✔
595
        let table = Table::from_reader(
1✔
596
            File::open("tests/csv/data/Annotations.csv").unwrap(),
1✔
597
            &Delimiters::default(),
1✔
598
        )
1✔
599
        .unwrap();
1✔
600
        assert_eq!(table.rows().len(), 6);
1✔
601
    }
1✔
602

603
    #[test]
1✔
604
    fn identity_comparison_is_empty() {
1✔
605
        let config = CSVCompareConfig {
1✔
606
            exclude_field_regex: None,
1✔
607
            comparison_modes: vec![Mode::Absolute(0.0), Mode::Relative(0.0)],
1✔
608
            delimiters: Delimiters::default(),
1✔
609
            preprocessing: None,
1✔
610
        };
1✔
611

1✔
612
        let actual = File::open("tests/csv/data/Annotations.csv").unwrap();
1✔
613
        let nominal = File::open("tests/csv/data/Annotations.csv").unwrap();
1✔
614

1✔
615
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
1✔
616
        assert!(diff.is_empty());
1✔
617
    }
1✔
618

619
    #[test]
1✔
620
    fn diffs_on_table_level() {
1✔
621
        let config = CSVCompareConfig {
1✔
622
            preprocessing: None,
1✔
623
            exclude_field_regex: Some(r"Surface".to_owned()),
1✔
624
            comparison_modes: vec![],
1✔
625
            delimiters: Delimiters::default(),
1✔
626
        };
1✔
627

1✔
628
        let actual = Table::from_reader(
1✔
629
            File::open("tests/csv/data/DeviationHistogram.csv").unwrap(),
1✔
630
            &config.delimiters,
1✔
631
        )
1✔
632
        .unwrap();
1✔
633
        let nominal = Table::from_reader(
1✔
634
            File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap(),
1✔
635
            &config.delimiters,
1✔
636
        )
1✔
637
        .unwrap();
1✔
638

1✔
639
        let diff = compare_tables(&nominal, &actual, &config).unwrap();
1✔
640
        assert_eq!(diff.len(), 1);
1✔
641
        let first_diff = diff.first().unwrap();
1✔
642
        if let DifferentValueTypes {
643
            nominal,
1✔
644
            actual,
1✔
645
            position,
1✔
646
        } = first_diff
1✔
647
        {
648
            assert_eq!(nominal.get_string().unwrap(), "different_type_here");
1✔
649
            assert_eq!(actual.get_quantity().unwrap().value, 0.00204398);
1✔
650
            assert_eq!(position.col, 1);
1✔
651
            assert_eq!(position.row, 12);
1✔
652
        } else {
653
            unreachable!();
×
654
        }
655
    }
1✔
656

657
    #[test]
1✔
658
    fn header_diffs_on_table_level() {
1✔
659
        let config = CSVCompareConfig {
1✔
660
            preprocessing: Some(vec![ExtractHeaders]),
1✔
661
            exclude_field_regex: None,
1✔
662
            comparison_modes: vec![],
1✔
663
            delimiters: Delimiters::default(),
1✔
664
        };
1✔
665

1✔
666
        let mut actual = Table::from_reader(
1✔
667
            File::open("tests/csv/data/Annotations.csv").unwrap(),
1✔
668
            &config.delimiters,
1✔
669
        )
1✔
670
        .unwrap();
1✔
671

1✔
672
        ExtractHeaders.process(&mut actual).unwrap();
1✔
673

1✔
674
        let mut nominal = Table::from_reader(
1✔
675
            File::open("tests/csv/data/Annotations_diff.csv").unwrap(),
1✔
676
            &config.delimiters,
1✔
677
        )
1✔
678
        .unwrap();
1✔
679

1✔
680
        ExtractHeaders.process(&mut nominal).unwrap();
1✔
681

1✔
682
        let diff = compare_tables(&nominal, &actual, &config).unwrap();
1✔
683
        assert_eq!(diff.len(), 3);
1✔
684

685
        let first_diff = diff.first().unwrap();
1✔
686
        if let UnequalHeader { nominal, actual } = first_diff {
1✔
687
            assert_eq!(nominal, "Position x [mm]");
1✔
688
            assert_eq!(actual, "Pos. x [mm]");
1✔
689
        } else {
690
            unreachable!();
×
691
        }
692
    }
1✔
693

694
    #[test]
1✔
695
    fn different_type_search_only() {
1✔
696
        let config = CSVCompareConfig {
1✔
697
            preprocessing: None,
1✔
698
            exclude_field_regex: Some(r"Surface".to_owned()),
1✔
699
            comparison_modes: vec![],
1✔
700
            delimiters: Delimiters::default(),
1✔
701
        };
1✔
702

1✔
703
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
1✔
704
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
1✔
705

1✔
706
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
1✔
707
        assert_eq!(diff.len(), 1);
1✔
708
        let first_diff = diff.first().unwrap();
1✔
709
        if let DifferentValueTypes {
710
            nominal,
1✔
711
            actual,
1✔
712
            position,
1✔
713
        } = first_diff
1✔
714
        {
715
            assert_eq!(nominal.get_string().unwrap(), "different_type_here");
1✔
716
            assert_eq!(actual.get_quantity().unwrap().value, 0.00204398);
1✔
717
            assert_eq!(position.col, 1);
1✔
718
            assert_eq!(position.row, 12);
1✔
719
        }
×
720
    }
1✔
721

722
    #[test]
1✔
723
    fn numerics_test_absolute() {
1✔
724
        let config = CSVCompareConfig {
1✔
725
            preprocessing: None,
1✔
726
            exclude_field_regex: Some(r"Surface".to_owned()),
1✔
727
            comparison_modes: vec![Mode::Absolute(0.5)],
1✔
728
            delimiters: Delimiters::default(),
1✔
729
        };
1✔
730

1✔
731
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
1✔
732
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
1✔
733

1✔
734
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
1✔
735
        // the different value type is still there, but we have 2 diffs over 0.5
1✔
736
        assert_eq!(diff.len(), 3);
1✔
737
    }
1✔
738

739
    #[test]
1✔
740
    fn mode_formatting() {
1✔
741
        let abs = Mode::Absolute(0.1);
1✔
742
        let msg = format!("{abs}");
1✔
743
        assert!(msg.contains("0.1"));
1✔
744
        assert!(msg.contains("Absolute"));
1✔
745

746
        let abs = Mode::Relative(0.1);
1✔
747
        let msg = format!("{abs}");
1✔
748
        assert!(msg.contains("0.1"));
1✔
749
        assert!(msg.contains("Relative"));
1✔
750

751
        let abs = Mode::Ignore;
1✔
752
        let msg = format!("{abs}");
1✔
753
        assert!(msg.contains("Ignored"));
1✔
754
    }
1✔
755

756
    #[test]
1✔
757
    fn different_formattings() {
1✔
758
        let config = CSVCompareConfig {
1✔
759
            preprocessing: None,
1✔
760
            exclude_field_regex: None,
1✔
761
            comparison_modes: vec![Mode::Absolute(0.5)],
1✔
762
            delimiters: Delimiters::autodetect(),
1✔
763
        };
1✔
764

1✔
765
        let actual = File::open(
1✔
766
            "tests/integ/data/display_of_status_message_in_cm_tables/actual/Volume1.csv",
1✔
767
        )
1✔
768
        .unwrap();
1✔
769
        let nominal = File::open(
1✔
770
            "tests/integ/data/display_of_status_message_in_cm_tables/expected/Volume1.csv",
1✔
771
        )
1✔
772
        .unwrap();
1✔
773

1✔
774
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
1✔
775
        // the different value type is still there, but we have 2 diffs over 0.5
1✔
776
        assert_eq!(diff.len(), 0);
1✔
777
    }
1✔
778

779
    #[test]
1✔
780
    fn numerics_test_relative() {
1✔
781
        let config = CSVCompareConfig {
1✔
782
            preprocessing: None,
1✔
783
            exclude_field_regex: Some(r"Surface".to_owned()),
1✔
784
            comparison_modes: vec![Mode::Relative(0.1)],
1✔
785
            delimiters: Delimiters::default(),
1✔
786
        };
1✔
787

1✔
788
        let actual = File::open("tests/csv/data/DeviationHistogram.csv").unwrap();
1✔
789
        let nominal = File::open("tests/csv/data/DeviationHistogram_diff.csv").unwrap();
1✔
790

1✔
791
        let (_, _, diff) = get_diffs_readers(nominal, actual, &config).unwrap();
1✔
792
        // the different value type is still there, but we have 5 rel diffs over 0.1
1✔
793
        assert_eq!(diff.len(), 6);
1✔
794
    }
1✔
795

796
    #[test]
1✔
797
    fn string_value_parsing_works() {
1✔
798
        let pairs = [
1✔
799
            ("0.6", Quantity::new(0.6, None)),
1✔
800
            ("0.6 in", Quantity::new(0.6, Some("in"))),
1✔
801
            ("inf", Quantity::new(f64::INFINITY, None)),
1✔
802
            ("-0.6", Quantity::new(-0.6, None)),
1✔
803
            ("-0.6 mm", Quantity::new(-0.6, Some("mm"))),
1✔
804
        ];
1✔
805
        pairs.into_iter().for_each(|(string, quantity)| {
5✔
806
            assert_eq!(Value::from_str(string, &None), Value::Quantity(quantity));
5✔
807
        });
5✔
808

1✔
809
        let nan_value = Value::from_str("nan mm", &None);
1✔
810
        let nan_value = nan_value.get_quantity().unwrap();
1✔
811
        assert!(nan_value.value.is_nan());
1✔
812
        assert_eq!(nan_value.unit, Some("mm".to_string()));
1✔
813
    }
1✔
814

815
    #[test]
1✔
816
    fn basic_compare_modes_test_absolute() {
1✔
817
        let abs_mode = Mode::Absolute(1.0);
1✔
818
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(1.0, None)));
1✔
819
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(-1.0, None)));
1✔
820
        assert!(abs_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(0.0, None)));
1✔
821
        assert!(abs_mode.in_tolerance(&Quantity::new(-1.0, None), &Quantity::new(0.0, None)));
1✔
822
        assert!(abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(0.0, None)));
1✔
823

824
        assert!(!abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(1.01, None)));
1✔
825
        assert!(!abs_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(-1.01, None)));
1✔
826
        assert!(!abs_mode.in_tolerance(&Quantity::new(1.01, None), &Quantity::new(0.0, None)));
1✔
827
        assert!(!abs_mode.in_tolerance(&Quantity::new(-1.01, None), &Quantity::new(0.0, None)));
1✔
828
    }
1✔
829

830
    #[test]
1✔
831
    fn basic_compare_modes_test_relative() {
1✔
832
        let rel_mode = Mode::Relative(1.0);
1✔
833
        assert!(rel_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(2.0, None)));
1✔
834
        assert!(rel_mode.in_tolerance(&Quantity::new(2.0, None), &Quantity::new(4.0, None)));
1✔
835
        assert!(rel_mode.in_tolerance(&Quantity::new(-1.0, None), &Quantity::new(-2.0, None)));
1✔
836
        assert!(rel_mode.in_tolerance(&Quantity::new(-2.0, None), &Quantity::new(-4.0, None)));
1✔
837
        assert!(rel_mode.in_tolerance(&Quantity::new(0.0, None), &Quantity::new(0.0, None)));
1✔
838

839
        assert!(!rel_mode.in_tolerance(&Quantity::new(1.0, None), &Quantity::new(2.01, None)));
1✔
840
        assert!(!rel_mode.in_tolerance(&Quantity::new(2.0, None), &Quantity::new(4.01, None)));
1✔
841
    }
1✔
842

843
    #[test]
1✔
844
    fn check_same_numbers_different_missmatch() {
1✔
845
        let rel_mode = Mode::Relative(1.0);
1✔
846
        assert!(!rel_mode.in_tolerance(
1✔
847
            &Quantity::new(2.0, Some("mm")),
1✔
848
            &Quantity::new(2.0, Some("m"))
1✔
849
        ));
1✔
850
    }
1✔
851

852
    #[test]
1✔
853
    fn basic_compare_modes_test_ignored() {
1✔
854
        let abs_mode = Mode::Ignore;
1✔
855
        assert!(abs_mode.in_tolerance(
1✔
856
            &Quantity::new(1.0, None),
1✔
857
            &Quantity::new(f64::INFINITY, None)
1✔
858
        ));
1✔
859
    }
1✔
860

861
    #[test]
1✔
862
    fn nan_is_nan() {
1✔
863
        let nan = f64::NAN;
1✔
864
        let nominal = Quantity {
1✔
865
            value: nan,
1✔
866
            unit: None,
1✔
867
        };
1✔
868
        let actual = Quantity {
1✔
869
            value: nan,
1✔
870
            unit: None,
1✔
871
        };
1✔
872

1✔
873
        assert!(Mode::Relative(1.0).in_tolerance(&nominal, &actual));
1✔
874
        assert!(Mode::Absolute(1.0).in_tolerance(&nominal, &actual));
1✔
875
        assert!(Mode::Ignore.in_tolerance(&nominal, &actual))
1✔
876
    }
1✔
877

878
    #[test]
1✔
879
    fn bom_is_trimmed() {
1✔
880
        let str_with_bom = "\u{feff}Hallo\n\r";
1✔
881
        let str_no_bom = "Hallo\n";
1✔
882
        let cfg = CSVCompareConfig {
1✔
883
            preprocessing: None,
1✔
884
            delimiters: Delimiters::default(),
1✔
885
            exclude_field_regex: None,
1✔
886
            comparison_modes: vec![Mode::Absolute(0.0)],
1✔
887
        };
1✔
888
        let (_, _, res) =
1✔
889
            get_diffs_readers(Cursor::new(str_with_bom), Cursor::new(str_no_bom), &cfg).unwrap();
1✔
890
        assert!(res.is_empty());
1✔
891
    }
1✔
892

893
    fn mk_test_table() -> Table {
2✔
894
        let col = Column {
2✔
895
            rows: vec![
2✔
896
                Value::from_str("0.0", &None),
2✔
897
                Value::from_str("1.0", &None),
2✔
898
                Value::from_str("2.0", &None),
2✔
899
            ],
2✔
900
            header: None,
2✔
901
        };
2✔
902

2✔
903
        let col_two = col.clone();
2✔
904
        Table {
2✔
905
            columns: vec![col, col_two],
2✔
906
        }
2✔
907
    }
2✔
908

909
    #[test]
1✔
910
    fn row_iterator() {
1✔
911
        let table = mk_test_table();
1✔
912
        let mut row_iterator = table.rows();
1✔
913
        assert_eq!(row_iterator.len(), 3);
1✔
914
        let first_row = row_iterator.next().unwrap();
1✔
915
        assert!(first_row
1✔
916
            .iter()
1✔
917
            .all(|v| **v == Value::from_str("0.0", &None)));
2✔
918
        for row in row_iterator {
3✔
919
            assert_eq!(row.len(), 2);
2✔
920
        }
921
    }
1✔
922

923
    #[test]
1✔
924
    fn row_iterator_mut() {
1✔
925
        let mut table = mk_test_table();
1✔
926
        let mut row_iterator = table.rows_mut();
1✔
927
        assert_eq!(row_iterator.len(), 3);
1✔
928
        let first_row = row_iterator.next().unwrap();
1✔
929
        assert!(first_row
1✔
930
            .iter()
1✔
931
            .all(|v| **v == Value::from_str("0.0", &None)));
2✔
932
        for row in row_iterator {
3✔
933
            assert_eq!(row.len(), 2);
2✔
934
        }
935
        let row_iterator = table.rows_mut();
1✔
936
        for mut row in row_iterator {
4✔
937
            assert_eq!(row.len(), 2);
3✔
938
            row.iter_mut()
3✔
939
                .for_each(|v| **v = Value::from_str("4.0", &None));
6✔
940
        }
941
        let mut row_iterator = table.rows();
1✔
942
        assert!(row_iterator.all(|r| r.iter().all(|v| **v == Value::from_str("4.0", &None))));
6✔
943
    }
1✔
944

945
    #[test]
1✔
946
    fn loading_non_existing_folder_fails() {
1✔
947
        let conf = CSVCompareConfig {
1✔
948
            comparison_modes: vec![],
1✔
949
            delimiters: Delimiters::default(),
1✔
950
            exclude_field_regex: None,
1✔
951
            preprocessing: None,
1✔
952
        };
1✔
953
        let result = compare_paths("non_existing", "also_non_existing", &conf);
1✔
954
        assert!(matches!(result.unwrap_err(), Error::FileAccessFailed(_)));
1✔
955
    }
1✔
956

957
    #[test]
1✔
958
    fn table_with_newlines_consistent_col_lengths() {
1✔
959
        let table = Table::from_reader(
1✔
960
            File::open("tests/csv/data/defects.csv").unwrap(),
1✔
961
            &Delimiters::autodetect(),
1✔
962
        )
1✔
963
        .unwrap();
1✔
964
        for col in table.columns.iter() {
30✔
965
            assert_eq!(col.rows.len(), table.columns.first().unwrap().rows.len());
30✔
966
        }
967
    }
1✔
968

969
    #[test]
1✔
970
    fn test_float_diff_precision() {
1✔
971
        let magic_first = 0.03914;
1✔
972
        let magic_second = 0.03913;
1✔
973
        let tolerance = 0.00001;
1✔
974
        let tolerance_f64 = 0.00001;
1✔
975

1✔
976
        let single_diff: f32 = magic_first - magic_second;
1✔
977
        assert!(single_diff > tolerance);
1✔
978

979
        let quantity1 = Quantity::new(0.03914, None);
1✔
980
        let quantity2 = Quantity::new(0.03913, None);
1✔
981
        let modes = [
1✔
982
            Mode::Absolute(tolerance_f64),
1✔
983
            Mode::Relative(tolerance_f64 / 0.03914),
1✔
984
        ];
1✔
985
        for mode in modes {
3✔
986
            assert!(mode.in_tolerance(&quantity1, &quantity2));
2✔
987
        }
988
    }
1✔
989
}
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