• 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

96.43
/src/html.rs
1
use crate::report;
2
use crate::report::{DiffDetail, Difference};
3
use regex::Regex;
4
use schemars_derive::JsonSchema;
5
use serde::{Deserialize, Serialize};
6
use std::fs::File;
7
use std::io::{BufRead, BufReader};
8
use std::path::Path;
9
use strsim::normalized_damerau_levenshtein;
10
use thiserror::Error;
11
use tracing::error;
12
use vg_errortools::fat_io_wrap_std;
13
use vg_errortools::FatIOError;
14

15
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
26✔
16
/// Plain text comparison config, also used for PDF
17
pub struct HTMLCompareConfig {
18
    /// Normalized Damerau-Levenshtein distance, 0.0 = bad, 1.0 = identity
19
    pub threshold: f64,
20
    /// Lines matching any of the given regex will be excluded from comparison
21
    pub ignore_lines: Option<Vec<String>>,
22
}
23

24
impl HTMLCompareConfig {
25
    pub(crate) fn get_ignore_list(&self) -> Result<Vec<Regex>, regex::Error> {
38✔
26
        let exclusion_list: Option<Result<Vec<_>, regex::Error>> = self
38✔
27
            .ignore_lines
38✔
28
            .as_ref()
38✔
29
            .map(|v| v.iter().map(|exc| Regex::new(exc)).collect());
38✔
30
        let exclusion_list = match exclusion_list {
38✔
31
            Some(r) => r?,
12✔
32
            None => Vec::new(),
26✔
33
        };
34
        Ok(exclusion_list)
38✔
35
    }
38✔
36
}
37

38
impl Default for HTMLCompareConfig {
39
    fn default() -> Self {
8✔
40
        HTMLCompareConfig {
8✔
41
            threshold: 1.0,
8✔
42
            ignore_lines: None,
8✔
43
        }
8✔
44
    }
8✔
45
}
46

47
#[derive(Debug, Error)]
×
48
/// Errors during html / plain text checking
×
49
pub enum Error {
50
    #[error("Failed to compile regex {0}")]
51
    RegexCompilationFailure(#[from] regex::Error),
52
    #[error("Problem creating hash report {0}")]
53
    ReportingProblem(#[from] report::Error),
54
    #[error("File access failed {0}")]
55
    FileAccessFailure(#[from] FatIOError),
56
}
57

58
pub fn compare_files<P: AsRef<Path>>(
14✔
59
    nominal_path: P,
14✔
60
    actual_path: P,
14✔
61
    config: &HTMLCompareConfig,
14✔
62
) -> Result<Difference, Error> {
14✔
63
    let actual = BufReader::new(fat_io_wrap_std(actual_path.as_ref(), &File::open)?);
14✔
64
    let nominal = BufReader::new(fat_io_wrap_std(nominal_path.as_ref(), &File::open)?);
14✔
65

66
    let exclusion_list = config.get_ignore_list()?;
14✔
67
    let mut difference = Difference::new_for_file(nominal_path, actual_path);
14✔
68
    actual
14✔
69
        .lines()
14✔
70
        .enumerate()
14✔
71
        .filter_map(|l| l.1.ok().map(|a| (l.0, a)))
420✔
72
        .zip(nominal.lines().map_while(Result::ok))
14✔
73
        .filter(|((_, a), n)|
14✔
74
            exclusion_list.iter().all(|exc| !exc.is_match(a)) && exclusion_list.iter().all(|exc| !exc.is_match(n))
822✔
75
        )
14✔
76
        .for_each(|((l, a), n)| {
402✔
77
            let distance = normalized_damerau_levenshtein(a.as_str(),n.as_str());
402✔
78
            if  distance < config.threshold {
402✔
79

80
                let error =  format!(
2✔
81
                    "Mismatch in HTML-file in line {}. Expected: '{}' found '{}' (diff: {}, threshold: {})",
2✔
82
                    l, n, a, distance, config.threshold
2✔
83
                );
2✔
84

2✔
85
                error!("{}" , &error);
2✔
86
                difference.push_detail(DiffDetail::Text {actual: a, nominal: n, score: distance, line: l});
2✔
87
                difference.error();
2✔
88
            }
400✔
89
        });
402✔
90

14✔
91
    Ok(difference)
14✔
92
}
14✔
93

94
#[cfg(test)]
95
mod test {
96
    use super::*;
97
    use test_log::test;
98
    #[test]
4✔
99
    fn test_identity() {
100
        assert!(
101
            !compare_files(
102
                "tests/html/test.html",
103
                "tests/html/test.html",
104
                &HTMLCompareConfig::default(),
105
            )
106
            .unwrap()
107
            .is_error
108
        );
109
    }
110

111
    #[test]
4✔
112
    fn test_modified() {
113
        let actual = "tests/html/test.html";
114
        let nominal = "tests/html/html_changed.html";
115

116
        let result = compare_files(actual, nominal, &HTMLCompareConfig::default()).unwrap();
117

118
        assert!(result.is_error);
119
    }
120

121
    #[test]
4✔
122
    fn test_allow_modified_threshold() {
123
        assert!(
124
            !compare_files(
125
                "tests/html/test.html",
126
                "tests/html/html_changed.html",
127
                &HTMLCompareConfig {
128
                    threshold: 0.9,
129
                    ignore_lines: None
130
                },
131
            )
132
            .unwrap()
133
            .is_error
134
        );
135
    }
136

137
    #[test]
4✔
138
    fn test_ignore_lines_regex() {
139
        assert!(
140
            !compare_files(
141
                "tests/html/test.html",
142
                "tests/html/html_changed.html",
143
                &HTMLCompareConfig {
144
                    threshold: 1.0,
145
                    ignore_lines: Some(vec!["stylesheet".to_owned()])
146
                },
147
            )
148
            .unwrap()
149
            .is_error
150
        );
151
    }
152
}
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