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 = compiled
122 .find_matches(&tc.input)
123 .is_ok_and(|m| !m.is_empty());
124 results.push(TestResult {
125 input: tc.input.clone(),
126 should_match: tc.should_match,
127 did_match,
128 });
129 }
130 Ok(results)
131 }
132}
133
134use crate::ansi::{BOLD, GREEN, RED, RESET};
135
136pub fn print_test_results(path: &str, pattern: &str, results: &[TestResult], color: bool) -> bool {
138 let total = results.len();
139 let passed = results.iter().filter(|r| r.passed()).count();
140 let failed = total - passed;
141
142 if color {
143 println!("{BOLD}Testing:{RESET} {path}");
144 println!("{BOLD}Pattern:{RESET} {pattern}");
145 } else {
146 println!("Testing: {path}");
147 println!("Pattern: {pattern}");
148 }
149 println!();
150
151 for (i, r) in results.iter().enumerate() {
152 let status = if r.passed() {
153 if color {
154 format!("{GREEN}PASS{RESET}")
155 } else {
156 "PASS".to_string()
157 }
158 } else if color {
159 format!("{RED}FAIL{RESET}")
160 } else {
161 "FAIL".to_string()
162 };
163 let expect = if r.should_match { "match" } else { "no match" };
164 let got = if r.did_match { "matched" } else { "no match" };
165 println!(
166 " {status} [{:>2}] {:?} (expect {expect}, got {got})",
167 i + 1,
168 r.input
169 );
170 }
171
172 println!();
173 if failed == 0 {
174 if color {
175 println!("{GREEN}{BOLD}{passed}/{total} passed{RESET}");
176 } else {
177 println!("{passed}/{total} passed");
178 }
179 } else if color {
180 println!("{RED}{BOLD}{failed}/{total} failed{RESET}");
181 } else {
182 println!("{failed}/{total} failed");
183 }
184
185 failed == 0
186}