• 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

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)]
13✔
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> {
19✔
26
        let exclusion_list: Option<Result<Vec<_>, regex::Error>> = self
19✔
27
            .ignore_lines
19✔
28
            .as_ref()
19✔
29
            .map(|v| v.iter().map(|exc| Regex::new(exc)).collect());
19✔
30
        let exclusion_list = match exclusion_list {
19✔
31
            Some(r) => r?,
6✔
32
            None => Vec::new(),
13✔
33
        };
34
        Ok(exclusion_list)
19✔
35
    }
19✔
36
}
37

38
impl Default for HTMLCompareConfig {
39
    fn default() -> Self {
4✔
40
        HTMLCompareConfig {
4✔
41
            threshold: 1.0,
4✔
42
            ignore_lines: None,
4✔
43
        }
4✔
44
    }
4✔
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>>(
7✔
59
    nominal_path: P,
7✔
60
    actual_path: P,
7✔
61
    config: &HTMLCompareConfig,
7✔
62
) -> Result<Difference, Error> {
7✔
63
    let actual = BufReader::new(fat_io_wrap_std(actual_path.as_ref(), &File::open)?);
7✔
64
    let nominal = BufReader::new(fat_io_wrap_std(nominal_path.as_ref(), &File::open)?);
7✔
65

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

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

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

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

94
#[cfg(test)]
95
mod test {
96
    use super::*;
97
    use test_log::test;
98
    #[test]
2✔
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]
2✔
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]
2✔
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]
2✔
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