wpt_interop/
lib.rs

1pub mod results_cache;
2
3use serde_derive::Deserialize;
4use std::collections::{BTreeMap, BTreeSet};
5use std::default::Default;
6use std::fmt::Display;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, Error>;
10pub type RunScores = BTreeMap<String, Vec<u64>>;
11pub type InteropScore = BTreeMap<String, u64>;
12pub type ExpectedFailureScores = BTreeMap<String, Vec<(u64, u64)>>;
13
14#[derive(Error, Debug)]
15pub enum Error {
16    #[error(transparent)]
17    Git(#[from] git2::Error),
18    #[error(transparent)]
19    Serde(#[from] serde_json::Error),
20    #[error("{0}")]
21    String(String),
22}
23
24#[derive(Debug, Deserialize)]
25pub struct Results {
26    pub status: TestStatus,
27    #[serde(default)]
28    pub subtests: Vec<SubtestResult>,
29    #[serde(default)]
30    pub expected: Option<TestStatus>,
31}
32
33#[derive(Debug, Deserialize)]
34pub struct SubtestResult {
35    pub name: String,
36    pub status: SubtestStatus,
37    #[serde(default)]
38    pub expected: Option<SubtestStatus>,
39}
40
41#[derive(Deserialize, PartialEq, Eq, Clone, Debug, Copy, Hash)]
42#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
43pub enum TestStatus {
44    Pass,
45    Fail,
46    Ok,
47    Error,
48    Timeout,
49    Crash,
50    Assert,
51    PreconditionFailed,
52    Skip,
53}
54
55impl TryFrom<&str> for TestStatus {
56    type Error = Error;
57
58    fn try_from(value: &str) -> Result<TestStatus> {
59        match value {
60            "PASS" => Ok(TestStatus::Pass),
61            "FAIL" => Ok(TestStatus::Fail),
62            "OK" => Ok(TestStatus::Ok),
63            "ERROR" => Ok(TestStatus::Error),
64            "TIMEOUT" => Ok(TestStatus::Timeout),
65            "CRASH" => Ok(TestStatus::Crash),
66            "ASSERT" => Ok(TestStatus::Assert),
67            "PRECONDITION_FAILED" => Ok(TestStatus::PreconditionFailed),
68            "SKIP" => Ok(TestStatus::Skip),
69            x => Err(Error::String(format!("Unrecognised test status {}", x))),
70        }
71    }
72}
73
74impl Display for TestStatus {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(
77            f,
78            "{}",
79            match self {
80                TestStatus::Pass => "PASS",
81                TestStatus::Fail => "FAIL",
82                TestStatus::Ok => "OK",
83                TestStatus::Error => "ERROR",
84                TestStatus::Timeout => "TIMEOUT",
85                TestStatus::Crash => "CRASH",
86                TestStatus::Assert => "ASSERT",
87                TestStatus::PreconditionFailed => "PRECONDITION_FAILED",
88                TestStatus::Skip => "SKIP",
89            }
90        )
91    }
92}
93
94#[derive(Deserialize, PartialEq, Eq, Clone, Debug, Copy, Hash)]
95#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
96pub enum SubtestStatus {
97    Pass,
98    Fail,
99    Error,
100    Timeout,
101    Assert,
102    PreconditionFailed,
103    Notrun,
104    Skip,
105}
106
107impl TryFrom<&str> for SubtestStatus {
108    type Error = Error;
109
110    fn try_from(value: &str) -> Result<SubtestStatus> {
111        match value {
112            "PASS" => Ok(SubtestStatus::Pass),
113            "FAIL" => Ok(SubtestStatus::Fail),
114            "ERROR" => Ok(SubtestStatus::Error),
115            "TIMEOUT" => Ok(SubtestStatus::Timeout),
116            "ASSERT" => Ok(SubtestStatus::Assert),
117            "PRECONDITION_FAILED" => Ok(SubtestStatus::PreconditionFailed),
118            "NOTRUN" => Ok(SubtestStatus::Notrun),
119            "SKIP" => Ok(SubtestStatus::Skip),
120            x => Err(Error::String(format!("Unrecognised subtest status {}", x))),
121        }
122    }
123}
124
125impl Display for SubtestStatus {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(
128            f,
129            "{}",
130            match self {
131                SubtestStatus::Pass => "PASS",
132                SubtestStatus::Fail => "FAIL",
133                SubtestStatus::Error => "ERROR",
134                SubtestStatus::Timeout => "TIMEOUT",
135                SubtestStatus::Assert => "ASSERT",
136                SubtestStatus::PreconditionFailed => "PRECONDITION_FAILED",
137                SubtestStatus::Notrun => "NOTRUN",
138                SubtestStatus::Skip => "SKIP",
139            }
140        )
141    }
142}
143
144#[derive(Debug, Default)]
145struct TestScore {
146    passes: u64,
147    total: u64,
148}
149
150impl TestScore {
151    fn new(passes: u64, total: u64) -> TestScore {
152        TestScore { passes, total }
153    }
154}
155
156#[derive(Debug, Default)]
157struct RunScore {
158    category_scores: Vec<f64>,
159    category_expected_failures: Vec<f64>,
160    unexpected_not_ok: BTreeSet<String>,
161}
162
163impl RunScore {
164    fn new(size: usize) -> RunScore {
165        RunScore {
166            category_scores: vec![0.; size],
167            category_expected_failures: vec![0.; size],
168            ..Default::default()
169        }
170    }
171}
172
173fn score_run<'a>(
174    run: impl Iterator<Item = (&'a str, &'a Results)>,
175    num_categories: usize,
176    categories_by_test: &BTreeMap<&'a str, Vec<usize>>,
177    expected_not_ok: &BTreeSet<String>,
178    test_scores_by_category: &mut [BTreeMap<&'a str, Vec<TestScore>>],
179) -> RunScore {
180    let mut run_score = RunScore::new(num_categories);
181    for (test_id, test_results) in run {
182        if let Some(categories) = categories_by_test.get(test_id) {
183            if test_results.status != TestStatus::Ok && !expected_not_ok.contains(test_id) {
184                run_score.unexpected_not_ok.insert(test_id.into());
185            }
186
187            let (test_passes, expected_failures, test_total) = if !test_results.subtests.is_empty()
188            {
189                let (test_passes, expected_failures) = test_results
190                    .subtests
191                    .iter()
192                    .map(|subtest| {
193                        if (subtest.status) == SubtestStatus::Pass {
194                            (1, 0)
195                        } else {
196                            (
197                                0,
198                                if (test_results.expected.is_some()
199                                    && test_results.expected != Some(TestStatus::Ok)
200                                    && test_results.expected != Some(TestStatus::Pass))
201                                    || (subtest.expected.is_some()
202                                        && subtest.expected != Some(SubtestStatus::Pass))
203                                {
204                                    1
205                                } else {
206                                    0
207                                },
208                            )
209                        }
210                    })
211                    .fold((0, 0), |acc, elem| (acc.0 + elem.0, acc.1 + elem.1));
212                (
213                    test_passes,
214                    expected_failures,
215                    test_results.subtests.len() as u32,
216                )
217            } else {
218                let (is_pass, expected_failure) = if test_results.status == TestStatus::Pass {
219                    (1, 0)
220                } else {
221                    (
222                        0,
223                        if test_results.expected.is_some()
224                            && test_results.expected != Some(TestStatus::Ok)
225                            && test_results.expected != Some(TestStatus::Pass)
226                        {
227                            1
228                        } else {
229                            0
230                        },
231                    )
232                };
233                (is_pass, expected_failure, 1)
234            };
235            for category_idx in categories {
236                let test_scores = &mut test_scores_by_category[*category_idx];
237                let pass_count = test_scores.entry(test_id).or_insert_with(Vec::new);
238                pass_count.push(TestScore::new(test_passes, test_total as u64));
239
240                run_score.category_scores[*category_idx] += test_passes as f64 / test_total as f64;
241                run_score.category_expected_failures[*category_idx] +=
242                    expected_failures as f64 / test_total as f64;
243            }
244        }
245    }
246    run_score
247}
248
249fn interop_score<'a>(
250    test_scores: impl Iterator<Item = &'a Vec<TestScore>>,
251    num_runs: usize,
252) -> u64 {
253    let mut interop_score = 0;
254    let mut num_test_scores = 0;
255    for test_score in test_scores {
256        num_test_scores += 1;
257        if test_score.len() != num_runs {
258            continue;
259        }
260        let min_score = test_score
261            .iter()
262            .map(|score| (1000. * score.passes as f64 / score.total as f64).trunc() as u64)
263            .min()
264            .unwrap_or(0);
265        interop_score += min_score
266    }
267    (interop_score as f64 / num_test_scores as f64).trunc() as u64
268}
269
270/// Compute the Interop scores for a set of web-platform-tests runs
271///
272/// * `runs` - One element for each run, containing a mapping from test id to test results.
273/// * `tests_by_category` - Mapping from category to the set of test ids in that category
274/// * `expected_not_ok` - Set of tests which are known to have non-OK statuses
275///
276/// Returns a tuple of
277/// (Mapping from category to score per run,
278///  Mapping of category to interop score for all runs,
279///  Mapping of category to expected failure score for each run)
280pub fn score_runs<'a>(
281    runs: impl Iterator<Item = &'a BTreeMap<String, Results>>,
282    tests_by_category: &BTreeMap<String, BTreeSet<String>>,
283    expected_not_ok: &BTreeSet<String>,
284) -> (RunScores, InteropScore, ExpectedFailureScores) {
285    let mut unexpected_not_ok = BTreeSet::new();
286
287    // Instead of passing round per-category maps, use a vector with categories at a fixed index
288    let num_categories = tests_by_category.len();
289    let mut categories = Vec::with_capacity(num_categories);
290    let mut test_count_by_category = Vec::with_capacity(num_categories);
291    let mut test_scores_by_category = Vec::with_capacity(num_categories);
292
293    let mut categories_by_test = BTreeMap::new();
294
295    let mut scores_by_category = BTreeMap::new();
296    let mut interop_by_category = BTreeMap::new();
297    let mut expected_failures_by_category = BTreeMap::new();
298
299    for (cat_idx, (category, tests)) in tests_by_category.iter().enumerate() {
300        categories.push(category);
301        test_count_by_category.push(tests.len());
302        test_scores_by_category.push(BTreeMap::new());
303
304        for test_id in tests {
305            categories_by_test
306                .entry(test_id.as_ref())
307                .or_insert_with(Vec::new)
308                .push(cat_idx)
309        }
310        scores_by_category.insert(category.clone(), Vec::with_capacity(runs.size_hint().0));
311        expected_failures_by_category
312            .insert(category.clone(), Vec::with_capacity(runs.size_hint().0));
313        interop_by_category.insert(category.clone(), 0);
314    }
315
316    let mut run_count = 0;
317    for run in runs {
318        run_count += 1;
319        let run_score = score_run(
320            run.iter()
321                .map(|(test_id, results)| (test_id.as_ref(), results)),
322            num_categories,
323            &categories_by_test,
324            expected_not_ok,
325            &mut test_scores_by_category,
326        );
327        for (idx, name) in categories.iter().enumerate() {
328            scores_by_category
329                .get_mut(*name)
330                .expect("Missing category")
331                .push(
332                    (1000. * run_score.category_scores[idx] / test_count_by_category[idx] as f64)
333                        .trunc() as u64,
334                );
335            expected_failures_by_category
336                .get_mut(*name)
337                .expect("Missing category")
338                .push((
339                    (1000. * run_score.category_expected_failures[idx]
340                        / test_count_by_category[idx] as f64)
341                        .trunc() as u64,
342                    (1000.
343                        * (run_score.category_scores[idx]
344                            / (test_count_by_category[idx] as f64
345                                - run_score.category_expected_failures[idx])))
346                        .trunc() as u64,
347                ));
348        }
349        unexpected_not_ok.extend(run_score.unexpected_not_ok)
350    }
351    for (idx, name) in categories.iter().enumerate() {
352        let scores = &test_scores_by_category[idx];
353        interop_by_category.insert((*name).clone(), interop_score(scores.values(), run_count));
354    }
355    (
356        scores_by_category,
357        interop_by_category,
358        expected_failures_by_category,
359    )
360}