use std::fs;
use std::path::Path;
use std::str::FromStr;
use anyhow::Error;
use fxhash::{FxHashMap, FxHashSet};
use crate::language::Language;
use crate::outcomes::{CatalogOutcomes, TestSetOutcomes};
use crate::testcase::TestCase;
use crate::testset::TestSet;
pub(crate) trait TestFilter<L: Language> {
fn is_included(&self, test_set: &TestSet<L>, test_case: &TestCase<L>) -> bool;
}
pub(crate) struct IncludeAllFilter {}
impl IncludeAllFilter {
pub(crate) fn new() -> Self {
Self {}
}
}
impl<L: Language> TestFilter<L> for IncludeAllFilter {
fn is_included(&self, _test_set: &TestSet<L>, _test_case: &TestCase<L>) -> bool {
true
}
}
pub(crate) struct NameFilter {
name_filter: Option<String>,
}
impl NameFilter {
pub(crate) fn new(name_filter: Option<String>) -> Self {
Self { name_filter }
}
}
impl<L: Language> TestFilter<L> for NameFilter {
fn is_included(&self, _test_set: &TestSet<L>, test_case: &TestCase<L>) -> bool {
if let Some(name_filter) = &self.name_filter {
test_case.name.contains(name_filter)
} else {
true
}
}
}
pub(crate) struct ExcludedNamesFilter {
names: FxHashMap<String, FxHashSet<String>>,
comments: FxHashMap<String, FxHashMap<String, String>>,
}
impl<L: Language> TestFilter<L> for ExcludedNamesFilter {
fn is_included(&self, test_set: &TestSet<L>, test_case: &TestCase<L>) -> bool {
let test_set_name = &test_set.name;
let test_case_name = &test_case.name;
if let Some(excluded_names) = self.names.get(test_set_name) {
!excluded_names.contains(test_case_name)
} else {
true
}
}
}
#[derive(Debug)]
pub(crate) enum UpdateResult {
NoChange,
Shrank,
NotSubset,
}
impl ExcludedNamesFilter {
pub(crate) fn new() -> Self {
Self {
names: FxHashMap::default(),
comments: FxHashMap::default(),
}
}
pub(crate) fn load_from_file(filter_path: &Path) -> Result<Self, Error> {
if filter_path.exists() {
let filter_data = fs::read_to_string(filter_path)?;
filter_data.parse()
} else {
Ok(ExcludedNamesFilter::new())
}
}
pub(crate) fn from_outcomes(catalog_outcomes: &CatalogOutcomes) -> Self {
let mut filter = Self::new();
filter.initialize_with_catalog_outcomes(catalog_outcomes);
filter
}
pub(crate) fn initialize_with_test_set_outcomes(
&mut self,
test_set_outcomes: &TestSetOutcomes,
) {
let failing_names: FxHashSet<String> =
test_set_outcomes.failing_names().into_iter().collect();
self.names
.insert(test_set_outcomes.test_set_name.clone(), failing_names);
}
pub(crate) fn update_with_test_set_outcomes(
&mut self,
test_set_outcomes: &TestSetOutcomes,
) -> UpdateResult {
let failing_names: FxHashSet<String> =
test_set_outcomes.failing_names().into_iter().collect();
let old_names = self.names.remove(&test_set_outcomes.test_set_name);
let old_names = old_names.unwrap_or_default();
if old_names.is_empty() {
self.names
.insert(test_set_outcomes.test_set_name.clone(), old_names);
if failing_names.is_empty() {
return UpdateResult::NoChange;
} else {
return UpdateResult::NotSubset;
}
}
if !failing_names.is_subset(&old_names) {
self.names
.insert(test_set_outcomes.test_set_name.clone(), old_names);
return UpdateResult::NotSubset;
}
self.names
.insert(test_set_outcomes.test_set_name.clone(), failing_names);
UpdateResult::Shrank
}
pub(crate) fn initialize_with_catalog_outcomes(&mut self, catalog_outcomes: &CatalogOutcomes) {
for test_set_outcomes in catalog_outcomes.outcomes.iter() {
self.initialize_with_test_set_outcomes(test_set_outcomes);
}
}
pub(crate) fn update_with_catalog_outcomes(
&mut self,
catalog_outcomes: &CatalogOutcomes,
) -> Vec<UpdateResult> {
let mut update_results = Vec::new();
for test_set_outcomes in catalog_outcomes.outcomes.iter() {
update_results.push(self.update_with_test_set_outcomes(test_set_outcomes));
}
update_results
}
}
impl FromStr for ExcludedNamesFilter {
type Err = Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let mut names = FxHashMap::default();
let mut comments = FxHashMap::default();
let mut test_set_name: Option<String> = None;
let mut test_case_names = FxHashSet::default();
let mut test_case_comments = FxHashMap::default();
for line in source.lines() {
let line = line.trim();
if line.starts_with('=') {
if let Some(test_set_name) = test_set_name {
names.insert(test_set_name.clone(), test_case_names);
comments.insert(test_set_name, test_case_comments);
}
test_set_name = Some(line.strip_prefix('=').unwrap().trim().to_string());
test_case_names = FxHashSet::default();
test_case_comments = FxHashMap::default();
} else if !line.is_empty() {
let mut parts = line.split('#');
let test_case_name = parts.next().unwrap();
let test_case_name = test_case_name.trim();
let comment = parts.next().map(|s| s.trim().to_string());
test_case_names.insert(test_case_name.to_string());
if let Some(comment) = comment {
test_case_comments.insert(test_case_name.to_string(), comment);
}
}
}
if let Some(test_set_name) = test_set_name {
names.insert(test_set_name.clone(), test_case_names);
comments.insert(test_set_name, test_case_comments);
}
Ok(Self { names, comments })
}
}
impl std::fmt::Display for ExcludedNamesFilter {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
let mut sorted_names = self.names.keys().collect::<Vec<_>>();
sorted_names.sort();
for test_set_name in sorted_names {
let excluded_names = self.names.get(test_set_name).unwrap();
let comments = self.comments.get(test_set_name);
fmt.write_str(&format!("= {}\n", test_set_name))?;
let mut sorted_excluded_names = excluded_names.iter().collect::<Vec<_>>();
sorted_excluded_names.sort();
for excluded_name in sorted_excluded_names {
fmt.write_str(excluded_name)?;
if let Some(comments) = comments {
let comment = comments.get(excluded_name);
if let Some(comment) = comment {
let comment = comment.trim();
if !comment.is_empty() {
fmt.write_str(&format!(" # {}", comment))?;
}
}
}
fmt.write_str("\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::testcase::TestOutcome;
use super::*;
#[test]
fn test_parse() {
let source = r#"
= test_set_1
test_case_1
test_case_2 # with a comment
= test_set_2
test_case_3
"#;
let filter: ExcludedNamesFilter = source.parse().unwrap();
assert_eq!(filter.names.len(), 2);
assert!(filter.names.contains_key("test_set_1"));
assert!(filter.names.contains_key("test_set_2"));
assert!(filter
.names
.get("test_set_1")
.unwrap()
.contains("test_case_1"));
assert!(filter
.names
.get("test_set_1")
.unwrap()
.contains("test_case_2"));
assert!(filter
.names
.get("test_set_2")
.unwrap()
.contains("test_case_3"));
assert_eq!(filter.comments.len(), 2);
assert!(filter.comments.contains_key("test_set_1"));
assert!(filter.comments.contains_key("test_set_2"));
assert_eq!(
filter
.comments
.get("test_set_1")
.unwrap()
.get("test_case_2")
.unwrap(),
"with a comment"
);
}
#[test]
fn test_parse_serialize() {
let source = r#"
= test_set_1
test_case_1
test_case_2 # with a comment
= test_set_2
test_case_3
"#
.trim_start();
let filter: ExcludedNamesFilter = source.parse().unwrap();
let serialized = filter.to_string();
assert_eq!(source, serialized);
}
#[test]
fn test_update_test_set() {
let mut filter = ExcludedNamesFilter::new();
let mut outcomes = TestSetOutcomes::new("test_set_1");
outcomes.add_outcome("test_case_1", TestOutcome::Unsupported);
outcomes.add_outcome("test_case_2", TestOutcome::Unsupported);
filter.initialize_with_test_set_outcomes(&outcomes);
let mut outcomes = TestSetOutcomes::new("test_set_1");
outcomes.add_outcome("test_case_1", TestOutcome::Passed);
outcomes.add_outcome("test_case_2", TestOutcome::Unsupported);
let r = filter.update_with_test_set_outcomes(&outcomes);
assert!(matches!(r, UpdateResult::Shrank));
let serialized = filter.to_string();
let expected = r#"= test_set_1
test_case_2
"#;
assert_eq!(serialized, expected.trim_start());
}
#[test]
fn test_update_test_set_not_subset() {
let mut filter = ExcludedNamesFilter::new();
let mut outcomes = TestSetOutcomes::new("test_set_1");
outcomes.add_outcome("test_case_1", TestOutcome::Passed);
outcomes.add_outcome("test_case_2", TestOutcome::Unsupported);
filter.initialize_with_test_set_outcomes(&outcomes);
let mut outcomes = TestSetOutcomes::new("test_set_1");
outcomes.add_outcome("test_case_1", TestOutcome::Unsupported);
outcomes.add_outcome("test_case_2", TestOutcome::Unsupported);
let r = filter.update_with_test_set_outcomes(&outcomes);
assert!(matches!(r, UpdateResult::NotSubset));
let serialized = filter.to_string();
let expected = r#"= test_set_1
test_case_2
"#;
assert_eq!(serialized, expected.trim_start());
}
}