1use crate::check_args;
26use crate::molt_ok;
27use crate::types::ContextID;
28use crate::Interp;
29use crate::MoltResult;
30use crate::ResultCode;
31use crate::Value;
32use std::env;
33use std::fs;
34use std::path::PathBuf;
35
36pub fn test_harness(interp: &mut Interp, args: &[String]) -> Result<(), ()> {
70 println!("Molt {} -- Test Harness", env!("CARGO_PKG_VERSION"));
72
73 if args.is_empty() {
75 eprintln!("missing test script");
76 return Err(());
77 }
78
79 let path = PathBuf::from(&args[0]);
80
81 let context_id = interp.save_context(TestContext::new());
83
84 interp.add_context_command("test", test_cmd, context_id);
86
87 match fs::read_to_string(&args[0]) {
89 Ok(script) => {
90 if let Some(parent) = path.parent() {
91 let _ = env::set_current_dir(parent);
92 }
93
94 if let Err(exception) = interp.eval(&script) {
95 if exception.code() == ResultCode::Error {
96 eprintln!("{}", exception.value());
97 return Err(());
98 } else {
99 eprintln!("Unexpected eval return: {:?}", exception);
100 return Err(());
101 }
102 }
103 }
104 Err(e) => {
105 println!("{}", e);
106 return Err(());
107 }
108 }
109
110 let ctx = interp.context::<TestContext>(context_id);
112 println!(
113 "\n{} tests, {} passed, {} failed, {} errors",
114 ctx.num_tests, ctx.num_passed, ctx.num_failed, ctx.num_errors
115 );
116
117 if ctx.num_failed + ctx.num_errors == 0 {
118 Ok(())
119 } else {
120 Err(())
121 }
122}
123
124struct TestContext {
125 num_tests: usize,
126 num_passed: usize,
127 num_failed: usize,
128 num_errors: usize,
129}
130
131impl TestContext {
132 fn new() -> Self {
133 Self {
134 num_tests: 0,
135 num_passed: 0,
136 num_failed: 0,
137 num_errors: 0,
138 }
139 }
140}
141
142#[derive(Eq, PartialEq, Debug)]
143enum Code {
144 Ok,
145 Error,
146}
147
148impl std::fmt::Display for Code {
149 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150 match self {
151 Code::Ok => write!(f, "-ok"),
152 Code::Error => write!(f, "-error"),
153 }
154 }
155}
156
157#[derive(Debug)]
158struct TestInfo {
159 name: String,
160 description: String,
161 setup: String,
162 body: String,
163 cleanup: String,
164 code: Code,
165 expect: String,
166}
167
168impl TestInfo {
169 fn new(name: &str, description: &str) -> Self {
170 Self {
171 name: name.into(),
172 description: description.into(),
173 setup: String::new(),
174 body: String::new(),
175 cleanup: String::new(),
176 code: Code::Ok,
177 expect: String::new(),
178 }
179 }
180
181 fn print_failure(&self, got_code: &str, received: &str) {
182 println!("\n*** FAILED {} {}", self.name, self.description);
183 println!("Expected {} <{}>", self.code.to_string(), self.expect);
184 println!("Received {} <{}>", got_code, received);
185 }
186
187 fn print_error(&self, result: &MoltResult) {
188 println!("\n*** ERROR {} {}", self.name, self.description);
189 println!("Expected {} <{}>", self.code.to_string(), self.expect);
190
191 match result {
192 Ok(val) => println!("Received -ok <{}>", val),
193 Err(exception) => match exception.code() {
194 ResultCode::Error => println!("Received -error <{}>", exception.value()),
195 ResultCode::Return => println!("Received -return <{}>", exception.value()),
196 ResultCode::Break => println!("Received -break <>"),
197 ResultCode::Continue => println!("Received -continue <>"),
198 _ => unimplemented!(),
199 },
200 }
201 }
202
203 fn print_helper_error(&self, part: &str, msg: &str) {
204 println!(
205 "\n*** ERROR (in {}) {} {}",
206 part, self.name, self.description
207 );
208 println!(" {}", msg);
209 }
210}
211
212fn test_cmd(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
221 check_args(1, argv, 4, 0, "name description args...")?;
223
224 let arg = argv[3].as_str();
226 if arg.starts_with('-') {
227 fancy_test(interp, context_id, argv)
228 } else {
229 simple_test(interp, context_id, argv)
230 }
231}
232
233fn simple_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
235 check_args(1, argv, 6, 6, "name description script -ok|-error result")?;
236
237 let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
239 info.body = argv[3].to_string();
240 info.expect = argv[5].to_string();
241
242 let code = argv[4].as_str();
243
244 info.code = if code == "-ok" {
245 Code::Ok
246 } else if code == "-error" {
247 Code::Error
248 } else {
249 incr_errors(interp, context_id);
250 info.print_helper_error("test command", &format!("invalid option: \"{}\"", code));
251
252 return molt_ok!();
253 };
254
255 run_test(interp, context_id, &info);
257 molt_ok!()
258}
259
260fn fancy_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
262 check_args(
263 1,
264 argv,
265 4,
266 0,
267 "name description option value ?option value...?",
268 )?;
269
270 let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
272 let mut iter = argv[3..].iter();
273 loop {
274 let opt = iter.next();
275 if opt.is_none() {
276 break;
277 }
278 let opt = opt.unwrap().as_str();
279
280 let val = iter.next();
281 if val.is_none() {
282 incr_errors(interp, context_id);
283 info.print_helper_error("test command", &format!("missing value for {}", opt));
284 return molt_ok!();
285 }
286 let val = val.unwrap().as_str();
287
288 match opt {
289 "-setup" => info.setup = val.to_string(),
290 "-body" => info.body = val.to_string(),
291 "-cleanup" => info.cleanup = val.to_string(),
292 "-ok" => {
293 info.code = Code::Ok;
294 info.expect = val.to_string();
295 }
296 "-error" => {
297 info.code = Code::Error;
298 info.expect = val.to_string();
299 }
300 _ => {
301 incr_errors(interp, context_id);
302 info.print_helper_error("test command", &format!("invalid option: \"{}\"", val));
303 return molt_ok!();
304 }
305 }
306 }
307
308 run_test(interp, context_id, &info);
310 molt_ok!()
311}
312
313fn run_test(interp: &mut Interp, context_id: ContextID, info: &TestInfo) {
315 interp.push_scope();
317
318 if let Err(exception) = interp.eval(&info.setup) {
322 if exception.code() == ResultCode::Error {
323 info.print_helper_error("-setup", exception.value().as_str());
324 }
325 }
326 let body = Value::from(&info.body);
332 let result = interp.eval_value(&body);
333
334 if let Err(exception) = interp.eval(&info.cleanup) {
336 if exception.code() == ResultCode::Error {
337 info.print_helper_error("-cleanup", exception.value().as_str());
338 }
339 }
340 interp.pop_scope();
346
347 let ctx = interp.context::<TestContext>(context_id);
349 ctx.num_tests += 1;
350
351 match &result {
352 Ok(out) => {
353 if info.code == Code::Ok {
354 if *out == Value::from(&info.expect) {
355 ctx.num_passed += 1;
356 } else {
357 ctx.num_failed += 1;
358 info.print_failure("-ok", &out.to_string());
359 }
360 return;
361 }
362 }
363 Err(exception) => {
364 if info.code == Code::Error {
365 if exception.value() == Value::from(&info.expect) {
366 ctx.num_passed += 1;
367 } else {
368 ctx.num_failed += 1;
369 info.print_failure("-error", exception.value().as_str());
370 }
371 return;
372 }
373 }
374 }
375 ctx.num_errors += 1;
376 info.print_error(&result);
377}
378
379fn incr_errors(interp: &mut Interp, context_id: ContextID) {
381 interp.context::<TestContext>(context_id).num_errors += 1;
382}