xee_testrunner/
cli.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::Result;
5use clap::{Parser, Subcommand};
6
7use xee_xpath::Documents;
8use xee_xpath_load::PathLoadable;
9
10use crate::catalog::{Catalog, LoadContext};
11use crate::filter::{ExcludedNamesFilter, IncludeAllFilter, NameFilter, TestFilter};
12use crate::language::{Language, XPathLanguage, XsltLanguage};
13use crate::outcomes::{CatalogOutcomes, Outcomes, TestSetOutcomes};
14use crate::paths::{paths, Mode, PathInfo};
15use crate::runcontext::RunContext;
16use crate::testset::TestSet;
17
18#[derive(Parser)]
19#[command(author, version, about, long_about = None)]
20struct Cli {
21    /// Verbose mode
22    #[clap(short, long)]
23    verbose: bool,
24    #[command(subcommand)]
25    command: Commands,
26}
27
28#[derive(Subcommand)]
29enum Commands {
30    /// Initialize the filter.
31    ///
32    /// Runs all tests, and saves the test outcomes to a `filters` file in the
33    /// directory of the `catalog.xml`.
34    Initialize {
35        /// A path to a qttests directory or individual test file
36        path: PathBuf,
37    },
38    /// Check with filters engaged.
39    ///
40    /// Runs tests that are not excluded by the `filters` file. This can be
41    /// used to check for regressions after making changes.
42    Check {
43        /// A path to a qttests directory or individual test file.
44        ///
45        /// If individual test file, it runs only the tests in that file,
46        /// otherwise it runs the tests in the `catalog.xml`.
47        path: PathBuf,
48    },
49    /// Update the filters.
50    ///
51    /// This runs all the tests on the path, and then updates the `filters`
52    /// file if more tests pass in this run. If not, the filters file is not
53    /// updated, so that you can't accidentally introduce a regression.
54    Update {
55        /// A path to a qttests directory or individual test file.
56        ///
57        /// If individual test file, it updates only the tests in that file,
58        /// otherwise it updates tests in the `catalog.xml`.
59        path: PathBuf,
60    },
61    /// Run all tests.
62    ///
63    /// Do not use filters, simply run all the tests indicated by path.
64    All {
65        /// A path to a qttests directory or individual test file.
66        path: PathBuf,
67        /// Name filter, only test-cases that contain this name are found.
68        name_filter: Option<String>,
69    },
70}
71
72impl Commands {
73    fn path(&self) -> &Path {
74        match self {
75            Commands::Initialize { path } => path,
76            Commands::Check { path } => path,
77            Commands::Update { path } => path,
78            Commands::All { path, .. } => path,
79        }
80    }
81}
82
83pub fn cli() -> Result<()> {
84    let cli = Cli::parse();
85
86    let path = cli.command.path();
87    let path_info = paths(path)?;
88
89    match path_info.mode {
90        Mode::XPath => cli_run::<XPathLanguage>(cli, path_info),
91        Mode::Xslt => cli_run::<XsltLanguage>(cli, path_info),
92    }
93}
94
95fn cli_run<L: Language>(cli: Cli, path_info: PathInfo) -> Result<()> {
96    let mut documents = Documents::new();
97    let run_context = RunContext::new(&mut documents, L::known_dependencies(), cli.verbose);
98    let mut runner = Runner::<L>::new(run_context, path_info);
99    match cli.command {
100        Commands::Initialize { .. } => runner.initialize(),
101        Commands::Check { .. } => runner.check(),
102        Commands::Update { .. } => runner.update(),
103        Commands::All { name_filter, .. } => runner.all(name_filter),
104    }
105}
106
107struct Runner<'a, L: Language> {
108    run_context: RunContext<'a>,
109    path_info: PathInfo,
110    _l: std::marker::PhantomData<L>,
111}
112
113impl<'a, L: Language> Runner<'a, L> {
114    fn new(run_context: RunContext<'a>, path_info: PathInfo) -> Self {
115        Self {
116            run_context,
117            path_info,
118            _l: std::marker::PhantomData,
119        }
120    }
121
122    fn check(&mut self) -> Result<()> {
123        if !self.path_info.filter_path.exists() {
124            // we cannot check if we don't have a filter file yet
125            println!("Cannot check without filter file");
126            return Ok(());
127        }
128
129        let catalog = self.load_catalog()?;
130
131        let test_filter = self.load_check_test_filter()?;
132        if self.path_info.whole_catalog() {
133            let outcomes = self.catalog_outcomes(&catalog, &test_filter)?;
134            println!("{}", outcomes.display());
135            if outcomes.has_failures() {
136                // ensure we have process status 1
137                return Err(anyhow::anyhow!("Failures found"));
138            }
139        } else {
140            let outcomes = self.test_set_outcomes(&catalog, &test_filter)?;
141            println!("{}", outcomes.display());
142            if outcomes.has_failures() {
143                // ensure we have process status 1
144                return Err(anyhow::anyhow!("Failures found"));
145            }
146        }
147        Ok(())
148    }
149
150    fn all(&mut self, name_filter: Option<String>) -> Result<()> {
151        let catalog = self.load_catalog()?;
152
153        let test_filter = NameFilter::new(name_filter);
154
155        if self.path_info.whole_catalog() {
156            let outcomes = self.catalog_outcomes(&catalog, &test_filter)?;
157            println!("{}", outcomes.display());
158        } else {
159            let outcomes = self.test_set_outcomes(&catalog, &test_filter)?;
160            println!("{}", outcomes.display());
161        }
162        Ok(())
163    }
164
165    fn update(&mut self) -> Result<()> {
166        if !self.path_info.filter_path.exists() {
167            // we cannot update if we don't have a filter file yet
168            println!("Cannot update without filter file");
169            return Ok(());
170        }
171        let catalog = self.load_catalog()?;
172        let test_filter = IncludeAllFilter::new();
173        let mut update_filter = ExcludedNamesFilter::load_from_file(&self.path_info.filter_path)?;
174        if self.path_info.whole_catalog() {
175            let outcomes = self.catalog_outcomes(&catalog, &test_filter)?;
176            update_filter.update_with_catalog_outcomes(&outcomes);
177            println!("{}", outcomes.display());
178        } else {
179            let outcomes = self.test_set_outcomes(&catalog, &test_filter)?;
180            update_filter.update_with_test_set_outcomes(&outcomes);
181            println!("{}", outcomes.display());
182        }
183
184        let filter_data = update_filter.to_string();
185        fs::write(&self.path_info.filter_path, filter_data)?;
186        Ok(())
187    }
188
189    fn initialize(&mut self) -> Result<()> {
190        if self.path_info.filter_path.exists() {
191            println!("Cannot reinitialize filters. Use update or delete filters file first");
192            return Ok(());
193        }
194
195        let catalog = self.load_catalog()?;
196
197        let test_filter = IncludeAllFilter::new();
198
199        let outcomes = self.catalog_outcomes(&catalog, &test_filter)?;
200
201        let test_filter = ExcludedNamesFilter::from_outcomes(&outcomes);
202        let filter_data = test_filter.to_string();
203        fs::write(&self.path_info.filter_path, filter_data)?;
204        Ok(())
205    }
206
207    fn load_catalog(&mut self) -> Result<Catalog<L>> {
208        let context = LoadContext::new::<L>(self.path_info.catalog_path.clone());
209        Catalog::load_from_file(&context)
210    }
211
212    fn load_test_set(&mut self) -> Result<TestSet<L>> {
213        let context = LoadContext::new::<L>(self.path_info.test_file().clone());
214        TestSet::load_from_file(&context)
215    }
216
217    fn load_check_test_filter(&self) -> Result<impl TestFilter<L>> {
218        ExcludedNamesFilter::load_from_file(&self.path_info.filter_path)
219    }
220
221    fn catalog_outcomes(
222        &mut self,
223        catalog: &Catalog<L>,
224        test_filter: &impl TestFilter<L>,
225    ) -> Result<CatalogOutcomes> {
226        let mut out = std::io::stdout();
227        let renderer = self.run_context.renderer();
228        catalog.run(
229            &mut self.run_context,
230            test_filter,
231            &mut out,
232            renderer.as_ref(),
233        )
234    }
235
236    fn test_set_outcomes(
237        &mut self,
238        catalog: &Catalog<L>,
239        test_filter: &impl TestFilter<L>,
240    ) -> Result<TestSetOutcomes> {
241        let mut out = std::io::stdout();
242        let renderer = self.run_context.renderer();
243        let test_set = self.load_test_set()?;
244        test_set.run(
245            &mut self.run_context,
246            catalog,
247            test_filter,
248            &mut out,
249            renderer.as_ref(),
250        )
251    }
252}