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 #[clap(short, long)]
25 verbose: bool,
26 #[command(subcommand)]
27 command: Commands,
28}
29
30#[derive(Subcommand)]
31enum Commands {
32 Initialize {
37 path: PathBuf,
39 },
40 Check {
45 path: PathBuf,
50 },
51 Update {
57 path: PathBuf,
62 },
63 All {
67 path: PathBuf,
69 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 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 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 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 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}