1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::app::App;
6use crate::engine::{self, EngineFlags, EngineKind};
7
8#[derive(Serialize, Deserialize)]
9pub struct Workspace {
10 pub pattern: String,
11 pub test_string: String,
12 pub replacement: String,
13 pub engine: String,
14 pub case_insensitive: bool,
15 pub multiline: bool,
16 pub dotall: bool,
17 pub unicode: bool,
18 pub extended: bool,
19 pub show_whitespace: bool,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub tests: Vec<TestCase>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TestCase {
26 pub input: String,
27 pub should_match: bool,
28}
29
30#[derive(Debug)]
31pub struct TestResult {
32 pub input: String,
33 pub should_match: bool,
34 pub did_match: bool,
35}
36
37impl TestResult {
38 pub fn passed(&self) -> bool {
39 self.did_match == self.should_match
40 }
41}
42
43impl Workspace {
44 fn engine_kind(&self) -> EngineKind {
45 match self.engine.as_str() {
46 "fancy" => EngineKind::FancyRegex,
47 #[cfg(feature = "pcre2-engine")]
48 "pcre2" => EngineKind::Pcre2,
49 _ => EngineKind::RustRegex,
50 }
51 }
52
53 fn flags(&self) -> EngineFlags {
54 EngineFlags {
55 case_insensitive: self.case_insensitive,
56 multi_line: self.multiline,
57 dot_matches_newline: self.dotall,
58 unicode: self.unicode,
59 extended: self.extended,
60 }
61 }
62
63 pub fn from_app(app: &App) -> Self {
64 let engine = match app.engine_kind {
65 EngineKind::RustRegex => "rust",
66 EngineKind::FancyRegex => "fancy",
67 #[cfg(feature = "pcre2-engine")]
68 EngineKind::Pcre2 => "pcre2",
69 };
70 Self {
71 pattern: app.regex_editor.content().to_string(),
72 test_string: app.test_editor.content().to_string(),
73 replacement: app.replace_editor.content().to_string(),
74 engine: engine.to_string(),
75 case_insensitive: app.flags.case_insensitive,
76 multiline: app.flags.multi_line,
77 dotall: app.flags.dot_matches_newline,
78 unicode: app.flags.unicode,
79 extended: app.flags.extended,
80 show_whitespace: app.show_whitespace,
81 tests: Vec::new(),
82 }
83 }
84
85 pub fn apply(&self, app: &mut App) {
86 let engine_kind = self.engine_kind();
87 if app.engine_kind != engine_kind {
88 app.engine_kind = engine_kind;
89 app.switch_engine_to(engine_kind);
90 }
91 app.flags = self.flags();
92 app.show_whitespace = self.show_whitespace;
93 app.set_test_string(&self.test_string);
94 if !self.replacement.is_empty() {
95 app.set_replacement(&self.replacement);
96 }
97 app.set_pattern(&self.pattern);
98 }
99
100 pub fn save(&self, path: &Path) -> anyhow::Result<()> {
101 let content = toml::to_string_pretty(self)?;
102 std::fs::write(path, content)?;
103 Ok(())
104 }
105
106 pub fn load(path: &Path) -> anyhow::Result<Self> {
107 let content = std::fs::read_to_string(path)?;
108 let ws: Self = toml::from_str(&content)?;
109 Ok(ws)
110 }
111
112 pub fn run_tests(&self) -> anyhow::Result<Vec<TestResult>> {
114 let eng = engine::create_engine(self.engine_kind());
115 let compiled = eng
116 .compile(&self.pattern, &self.flags())
117 .map_err(|e| anyhow::anyhow!("{e}"))?;
118
119 let mut results = Vec::with_capacity(self.tests.len());
120 for tc in &self.tests {
121 let did_match = match compiled.find_matches(&tc.input) {
122 Ok(m) => !m.is_empty(),
123 Err(_) => false,
124 };
125 results.push(TestResult {
126 input: tc.input.clone(),
127 should_match: tc.should_match,
128 did_match,
129 });
130 }
131 Ok(results)
132 }
133}
134
135use crate::ansi::{BOLD, GREEN, RED, RESET};
136
137pub fn print_test_results(path: &str, pattern: &str, results: &[TestResult], color: bool) -> bool {
139 let total = results.len();
140 let passed = results.iter().filter(|r| r.passed()).count();
141 let failed = total - passed;
142
143 if color {
144 println!("{BOLD}Testing:{RESET} {path}");
145 println!("{BOLD}Pattern:{RESET} {pattern}");
146 } else {
147 println!("Testing: {path}");
148 println!("Pattern: {pattern}");
149 }
150 println!();
151
152 for (i, r) in results.iter().enumerate() {
153 let status = if r.passed() {
154 if color {
155 format!("{GREEN}PASS{RESET}")
156 } else {
157 "PASS".to_string()
158 }
159 } else if color {
160 format!("{RED}FAIL{RESET}")
161 } else {
162 "FAIL".to_string()
163 };
164 let expect = if r.should_match { "match" } else { "no match" };
165 let got = if r.did_match { "matched" } else { "no match" };
166 println!(
167 " {status} [{:>2}] {:?} (expect {expect}, got {got})",
168 i + 1,
169 r.input
170 );
171 }
172
173 println!();
174 if failed == 0 {
175 if color {
176 println!("{GREEN}{BOLD}{passed}/{total} passed{RESET}");
177 } else {
178 println!("{passed}/{total} passed");
179 }
180 } else if color {
181 println!("{RED}{BOLD}{failed}/{total} failed{RESET}");
182 } else {
183 println!("{failed}/{total} failed");
184 }
185
186 failed == 0
187}