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