1 //: Mu scenarios. This will get long, but these are the tests we want to
  2 //: support in this layer.
  3 
  4 //: We avoid raw numeric locations in Mu -- except in scenarios, where they're
  5 //: handy to check the values of specific variables
  6 :(scenarios run_mu_scenario)
  7 :(scenario scenario_block)
  8 scenario foo [
  9   run [
 10   ¦ 1:num <- copy 13
 11   ]
 12   memory-should-contain [
 13   ¦ 1 <- 13
 14   ]
 15 ]
 16 # checks are inside scenario
 17 
 18 :(scenario scenario_multiple_blocks)
 19 scenario foo [
 20   run [
 21   ¦ 1:num <- copy 13
 22   ]
 23   memory-should-contain [
 24   ¦ 1 <- 13
 25   ]
 26   run [
 27   ¦ 2:num <- copy 13
 28   ]
 29   memory-should-contain [
 30   ¦ 1 <- 13
 31   ¦ 2 <- 13
 32   ]
 33 ]
 34 # checks are inside scenario
 35 
 36 :(scenario scenario_check_memory_and_trace)
 37 scenario foo [
 38   run [
 39   ¦ 1:num <- copy 13
 40   ¦ trace 1, [a], [a b c]
 41   ]
 42   memory-should-contain [
 43   ¦ 1 <- 13
 44   ]
 45   trace-should-contain [
 46   ¦ a: a b c
 47   ]
 48   trace-should-not-contain [
 49   ¦ a: x y z
 50   ]
 51 ]
 52 # checks are inside scenario
 53 
 54 //:: Core data structure
 55 
 56 :(before "End Types")
 57 struct scenario {
 58   string name;
 59   string to_run;
 60   void clear() {
 61   ¦ name.clear();
 62   ¦ to_run.clear();
 63   }
 64 };
 65 
 66 :(before "End Globals")
 67 vector<scenario> Scenarios, Scenarios_snapshot;
 68 set<string> Scenario_names, Scenario_names_snapshot;
 69 :(before "End save_snapshots")
 70 Scenarios_snapshot = Scenarios;
 71 Scenario_names_snapshot = Scenario_names;
 72 :(before "End restore_snapshots")
 73 Scenarios = Scenarios_snapshot;
 74 Scenario_names = Scenario_names_snapshot;
 75 
 76 //:: Parse the 'scenario' form.
 77 //: Simply store the text of the scenario.
 78 
 79 :(before "End Command Handlers")
 80 else if (command == "scenario") {
 81   scenario result = parse_scenario(in);
 82   if (!result.name.empty())
 83   ¦ Scenarios.push_back(result);
 84 }
 85 else if (command == "pending-scenario") {
 86   // for temporary use only
 87   parse_scenario(in);  // discard
 88 }
 89 
 90 :(code)
 91 scenario parse_scenario(istream& in) {
 92   scenario result;
 93   result.name = next_word(in);
 94   if (contains_key(Scenario_names, result.name))
 95   ¦ raise << "duplicate scenario name: '" << result.name << "'\n" << end();
 96   Scenario_names.insert(result.name);
 97   if (result.name.empty()) {
 98   ¦ assert(!has_data(in));
 99   ¦ raise << "incomplete scenario at end of file\n" << end();
100   ¦ return result;
101   }
102   skip_whitespace_and_comments(in);
103   if (in.peek() != '[') {
104   ¦ raise << "Expected '[' after scenario '" << result.name << "'\n" << end();
105   ¦ exit(0);
106   }
107   // scenarios are take special 'code' strings so we need to ignore brackets
108   // inside comments
109   result.to_run = slurp_quoted(in);
110   // delete [] delimiters
111   if (!starts_with(result.to_run, "[")) {
112   ¦ raise << "scenario " << result.name << " should start with '['\n" << end();
113   ¦ result.clear();
114   ¦ return result;
115   }
116   result.to_run.erase(0, 1);
117   if (result.to_run.at(SIZE(result.to_run)-1) != ']') {
118   ¦ raise << "scenario " << result.name << " has an unbalanced '['\n" << end();
119   ¦ result.clear();
120   ¦ return result;
121   }
122   result.to_run.erase(SIZE(result.to_run)-1);
123   return result;
124 }
125 
126 :(scenario read_scenario_with_bracket_in_comment)
127 scenario foo [
128   # ']' in comment
129   1:num <- copy 0
130 ]
131 +run: {1: "number"} <- copy {0: "literal"}
132 
133 :(scenario read_scenario_with_bracket_in_comment_in_nested_string)
134 scenario foo [
135   1:text <- new [# not a comment]
136 ]
137 +run: {1: ("address" "array" "character")} <- new {"# not a comment": "literal-string"}
138 
139 :(scenarios run)
140 :(scenario duplicate_scenarios)
141 % Hide_errors = true;
142 scenario foo [
143   1:num <- copy 0
144 ]
145 scenario foo [
146   2:num <- copy 0
147 ]
148 +error: duplicate scenario name: 'foo'
149 
150 //:: Run scenarios when we run './mu test'.
151 //: Treat the text of the scenario as a regular series of instructions.
152 
153 :(before "End Globals")
154 int Num_core_mu_scenarios = 0;
155 :(after "Check For .mu Files")
156 Num_core_mu_scenarios = SIZE(Scenarios);
157 :(before "End Tests")
158 Hide_missing_default_space_errors = false;
159 if (Num_core_mu_scenarios > 0) {
160   time(&t);
161   cerr << "Mu tests: " << ctime(&t);
162   for (int i = 0;  i < Num_core_mu_scenarios;  ++i) {
163 //?     cerr << '\n' << i << ": " << Scenarios.at(i).name;
164   ¦ run_mu_scenario(Scenarios.at(i));
165   ¦ if (Passed) cerr << ".";
166   ¦ else ++Num_failures;
167   }
168   cerr << "\n";
169 }
170 run_app_scenarios:
171 if (Num_core_mu_scenarios != SIZE(Scenarios)) {
172   time(&t);
173   cerr << "App tests: " << ctime(&t);
174   for (int i = Num_core_mu_scenarios;  i < SIZE(Scenarios);  ++i) {
175 //?     cerr << '\n' << i << ": " << Scenarios.at(i).name;
176   ¦ run_mu_scenario(Scenarios.at(i));
177   ¦ if (Passed) cerr << ".";
178   ¦ else ++Num_failures;
179   }
180   cerr << "\n";
181 }
182 
183 //: For faster debugging, support running tests for just the Mu app(s) we are
184 //: loading.
185 :(before "End Globals")
186 bool Test_only_app = false;
187 :(before "End Commandline Options(*arg)")
188 else if (is_equal(*arg, "--test-only-app")) {
189   Test_only_app = true;
190 }
191 :(after "End Test Run Initialization")
192 if (Test_only_app && Num_core_mu_scenarios < SIZE(Scenarios)) {
193   goto run_app_scenarios;
194 }
195 
196 //: Convenience: run a single named scenario.
197 :(after "Test Runs")
198 for (int i = 0;  i < SIZE(Scenarios);  ++i) {
199   if (Scenarios.at(i).name == argv[argc-1]) {
200   ¦ if (Start_tracing) {
201   ¦ ¦ Trace_stream = new trace_stream;
202   ¦ ¦ Save_trace = true;
203   ¦ }
204   ¦ run_mu_scenario(Scenarios.at(i));
205   ¦ if (Passed) cerr << ".\n";
206   ¦ return 0;
207   }
208 }
209 
210 :(before "End Globals")
211 // this isn't a constant, just a global of type const*
212 const scenario* Current_scenario = NULL;
213 :(code)
214 void run_mu_scenario(const scenario& s) {
215   Current_scenario = &s;
216   bool not_already_inside_test = !Trace_stream;
217 //?   cerr << s.name << '\n';
218   if (not_already_inside_test) {
219   ¦ Trace_stream = new trace_stream;
220   ¦ setup();
221   }
222   vector<recipe_ordinal> tmp = load("recipe scenario_"+s.name+" [ "+s.to_run+" ]");
223   mark_autogenerated(tmp.at(0));
224   bind_special_scenario_names(tmp.at(0));
225   transform_all();
226   if (!trace_contains_errors())
227   ¦ run(tmp.front());
228   // End Mu Test Teardown
229   if (!Hide_errors && trace_contains_errors() && !Scenario_testing_scenario)
230   ¦ Passed = false;
231   if (not_already_inside_test && Trace_stream) {
232   ¦ teardown();
233   ¦ if (Save_trace) {
234   ¦ ¦ ofstream fout("last_trace");
235   ¦ ¦ fout << Trace_stream->readable_contents("");
236   ¦ ¦ fout.close();
237   ¦ }
238   ¦ delete Trace_stream;
239   ¦ Trace_stream = NULL;
240   }
241   Current_scenario = NULL;
242 }
243 
244 //: Permit numeric locations to be accessed in scenarios.
245 :(before "End check_default_space Special-cases")
246 // user code should never create recipes with underscores in their names
247 if (starts_with(caller.name, "scenario_")) return;  // skip Mu scenarios which will use raw memory locations
248 if (starts_with(caller.name, "run_")) return;  // skip calls to 'run', which should be in scenarios and will also use raw memory locations
249 
250 :(before "End maybe(recipe_name) Special-cases")
251 if (starts_with(recipe_name, "scenario_"))
252   return recipe_name.substr(strlen("scenario_")) + ": ";
253 
254 //: Some variables for fake resources always get special /raw addresses in scenarios.
255 
256 :(code)
257 // Should contain everything passed by is_special_name but failed by is_disqualified.
258 void bind_special_scenario_names(const recipe_ordinal r) {
259   // Special Scenario Variable Names(r)
260   // End Special Scenario Variable Names(r)
261 }
262 :(before "Done Placing Ingredient(ingredient, inst, caller)")
263 maybe_make_raw(ingredient, caller);
264 :(before "Done Placing Product(product, inst, caller)")
265 maybe_make_raw(product, caller);
266 :(code)
267 void maybe_make_raw(reagent& r, const recipe& caller) {
268   if (!is_special_name(r.name)) return;
269   if (starts_with(caller.name, "scenario_"))
270   ¦ r.properties.push_back(pair<string, string_tree*>("raw", NULL));
271   // End maybe_make_raw
272 }
273 
274 //: Test.
275 :(before "End is_special_name Special-cases")
276 if (s == "__maybe_make_raw_test__") return true;
277 :(before "End Special Scenario Variable Names(r)")
278 //: ugly: we only need this for this one test, but need to define it for all time
279 Name[r]["__maybe_make_raw_test__"] = Reserved_for_tests-1;
280 :(code)
281 void test_maybe_make_raw() {
282   // check that scenarios can use local-scope and special variables together
283   vector<recipe_ordinal> tmp = load(
284   ¦ ¦ "def scenario_foo [\n"
285   ¦ ¦ "  local-scope\n"
286   ¦ ¦ "  __maybe_make_raw_test__:num <- copy 34\n"
287   ¦ ¦ "]\n");
288   mark_autogenerated(tmp.at(0));
289   bind_special_scenario_names(tmp.at(0));
290   transform_all();
291   run(tmp.at(0));
292   CHECK_TRACE_DOESNT_CONTAIN_ERRORS();
293 }
294 
295 //: Watch out for redefinitions of scenario routines. We should never ever be
296 //: doing that, regardless of anything else.
297 :(scenario forbid_redefining_scenario_even_if_forced)
298 % Hide_errors = true;
299 % Disable_redefine_checks = true;
300 def scenario-foo [
301   1:num <- copy 34
302 ]
303 def scenario-foo [
304   1:num <- copy 35
305 ]
306 +error: redefining recipe scenario-foo
307 
308 :(scenario scenario_containing_parse_error)
309 % Hide_errors = true;
310 scenario foo [
311   memory-should-contain [
312   ¦ 1 <- 0
313   # missing ']'
314 ]
315 # no crash
316 
317 :(scenario scenario_containing_transform_error)
318 % Hide_errors = true;
319 def main [
320   local-scope
321   add x, 1
322 ]
323 # no crash
324 
325 :(after "bool should_check_for_redefine(const string& recipe_name)")
326   if (recipe_name.find("scenario-") == 0) return true;
327 
328 //:: The special instructions we want to support inside scenarios.
329 //: These are easy to support in an interpreter, but will require more work
330 //: when we eventually build a compiler.
331 
332 //: 'run' is a purely lexical convenience to separate the code actually being
333 //: tested from any setup or teardown
334 
335 :(scenario run)
336 def main [
337   run [
338   ¦ 1:num <- copy 13
339   ]
340 ]
341 +mem: storing 13 in location 1
342 
343 :(before "End Rewrite Instruction(curr, recipe result)")
344 if (curr.name == "run") {
345   // Just inline all instructions inside the run block in the containing
346   // recipe. 'run' is basically a comment; pretend it doesn't exist.
347   istringstream in2("[\n"+curr.ingredients.at(0).name+"\n]\n");
348   slurp_body(in2, result);
349   curr.clear();
350 }
351 
352 :(scenario run_multiple)
353 def main [
354   run [
355   ¦ 1:num <- copy 13
356   ]
357   run [
358   ¦ 2:num <- copy 13
359   ]
360 ]
361 +mem: storing 13 in location 1
362 +mem: storing 13 in location 2
363 
364 //: 'memory-should-contain' raises errors if specific locations aren't as expected
365 //: Also includes some special support for checking Mu texts.
366 
367 :(before "End Globals")
368 bool Scenario_testing_scenario = false;
369 :(before "End Setup")
370 Scenario_testing_scenario = false;
371 
372 :(scenario memory_check)
373 % Scenario_testing_scenario = true;
374 % Hide_errors = true;
375 def main [
376   memory-should-contain [
377   ¦ 1 <- 13
378   ]
379 ]
380 +run: checking location 1
381 +error: F - main: expected location '1' to contain 13 but saw 0
382 
383 :(before "End Primitive Recipe Declarations")
384 MEMORY_SHOULD_CONTAIN,
385 :(before "End Primitive Recipe Numbers")
386 put(Recipe_ordinal, "memory-should-contain", MEMORY_SHOULD_CONTAIN);
387 :(before "End Primitive Recipe Checks")
388 case MEMORY_SHOULD_CONTAIN: {
389   break;
390 }
391 :(before "End Primitive Recipe Implementations")
392 case MEMORY_SHOULD_CONTAIN: {
393   if (!Passed) break;
394   check_memory(current_instruction().ingredients.at(0).name);
395   break;
396 }
397 
398 :(code)
399 void check_memory(const string& s) {
400   istringstream in(s);
401   in >> std::noskipws;
402   set<int> locations_checked;
403   while (true) {
404   ¦ skip_whitespace_and_comments(in);
405   ¦ if (!has_data(in)) break;
406   ¦ string lhs = next_word(in);
407   ¦ if (lhs.empty()) {
408   ¦ ¦ assert(!has_data(in));
409   ¦ ¦ raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (0)\n" << end();
410   ¦ ¦ return;
411   ¦ }
412   ¦ if (!is_integer(lhs)) {
413   ¦ ¦ check_type(lhs, in);
414   ¦ ¦ continue;
415   ¦ }
416   ¦ int address = to_integer(lhs);
417   ¦ skip_whitespace_and_comments(in);
418   ¦ string _assign;  in >> _assign;  assert(_assign == "<-");
419   ¦ skip_whitespace_and_comments(in);
420   ¦ string rhs = next_word(in);
421   ¦ if (rhs.empty()) {
422   ¦ ¦ assert(!has_data(in));
423   ¦ ¦ raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (1)\n" << end();
424   ¦ ¦ return;
425   ¦ }
426   ¦ if (!is_integer(rhs) && !is_noninteger(rhs)) {
427   ¦ ¦ if (!Hide_errors) cerr << '\n';
428   ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "location '" << address << "' can't contain non-number " << rhs << '\n' << end();
429   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
430   ¦ ¦ return;
431   ¦ }
432   ¦ double value = to_double(rhs);
433   ¦ if (contains_key(locations_checked, address))
434   ¦ ¦ raise << maybe(current_recipe_name()) << "duplicate expectation for location '" << address << "'\n" << end();
435   ¦ trace(9999, "run") << "checking location " << address << end();
436   ¦ if (get_or_insert(Memory, address) != value) {
437   ¦ ¦ if (!Hide_errors) cerr << '\n';
438   ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "expected location '" << address << "' to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end();
439   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
440   ¦ ¦ return;
441   ¦ }
442   ¦ locations_checked.insert(address);
443   }
444 }
445 
446 void check_type(const string& lhs, istream& in) {
447   reagent x(lhs);
448   if (is_mu_array(x.type) && is_mu_character(array_element(x.type))) {
449   ¦ x.set_value(to_integer(x.name));
450   ¦ skip_whitespace_and_comments(in);
451   ¦ string _assign = next_word(in);
452   ¦ if (_assign.empty()) {
453   ¦ ¦ assert(!has_data(in));
454   ¦ ¦ raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (2)\n" << end();
455   ¦ ¦ return;
456   ¦ }
457   ¦ assert(_assign == "<-");
458   ¦ skip_whitespace_and_comments(in);
459   ¦ string literal = next_word(in);
460   ¦ if (literal.empty()) {
461   ¦ ¦ assert(!has_data(in));
462   ¦ ¦ raise << maybe(current_recipe_name()) << "incomplete 'memory-should-contain' block at end of file (3)\n" << end();
463   ¦ ¦ return;
464   ¦ }
465   ¦ int address = x.value;
466   ¦ // exclude quoting brackets
467   ¦ if (*literal.begin() != '[') {
468   ¦ ¦ raise << maybe(current_recipe_name()) << "array:character types inside 'memory-should-contain' can only be compared with text literals surrounded by [], not '" << literal << "'\n" << end();
469   ¦ ¦ return;
470   ¦ }
471   ¦ literal.erase(literal.begin());
472   ¦ assert(*--literal.end() == ']');  literal.erase(--literal.end());
473   ¦ check_mu_text(address, literal);
474   ¦ return;
475   }
476   // End Scenario Type Special-cases
477   raise << "don't know how to check memory for '" << lhs << "'\n" << end();
478 }
479 
480 void check_mu_text(int start, const string& literal) {
481   trace(9999, "run") << "checking text length at " << start << end();
482   int array_length = static_cast<int>(get_or_insert(Memory, start));
483   if (array_length != SIZE(literal)) {
484   ¦ if (!Hide_errors) cerr << '\n';
485   ¦ raise << "F - " << maybe(current_recipe_name()) << "expected location '" << start << "' to contain length " << SIZE(literal) << " of text [" << literal << "] but saw " << array_length << " (for text [" << read_mu_characters(start+/*skip length*/1, array_length) << "])\n" << end();
486   ¦ if (!Scenario_testing_scenario) Passed = false;
487   ¦ return;
488   }
489   int curr = start+1;  // now skip length
490   for (int i = 0;  i < SIZE(literal);  ++i) {
491   ¦ trace(9999, "run") << "checking location " << curr+i << end();
492   ¦ if (get_or_insert(Memory, curr+i) != literal.at(i)) {
493   ¦ ¦ if (!Hide_errors) cerr << '\n';
494   ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "expected location " << (curr+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, curr+i)) << '\n' << end();
495   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
496   ¦ ¦ return;
497   ¦ }
498   }
499 }
500 
501 :(scenario memory_check_multiple)
502 % Scenario_testing_scenario = true;
503 % Hide_errors = true;
504 def main [
505   memory-should-contain [
506   ¦ 1 <- 0
507   ¦ 1 <- 0
508   ]
509 ]
510 +error: main: duplicate expectation for location '1'
511 
512 :(scenario memory_check_mu_text_length)
513 % Scenario_testing_scenario = true;
514 % Hide_errors = true;
515 def main [
516   1:num <- copy 3
517   2:num <- copy 97  # 'a'
518   3:num <- copy 98  # 'b'
519   4:num <- copy 99  # 'c'
520   memory-should-contain [
521   ¦ 1:array:character <- [ab]
522   ]
523 ]
524 +error: F - main: expected location '1' to contain length 2 of text [ab] but saw 3 (for text [abc])
525 
526 :(scenario memory_check_mu_text)
527 def main [
528   1:num <- copy 3
529   2:num <- copy 97  # 'a'
530   3:num <- copy 98  # 'b'
531   4:num <- copy 99  # 'c'
532   memory-should-contain [
533   ¦ 1:array:character <- [abc]
534   ]
535 ]
536 +run: checking text length at 1
537 +run: checking location 2
538 +run: checking location 3
539 +run: checking location 4
540 
541 :(scenario memory_invalid_string_check)
542 % Scenario_testing_scenario = true;
543 % Hide_errors = true;
544 def main [
545   memory-should-contain [
546   ¦ 1 <- [abc]
547   ]
548 ]
549 +error: F - main: location '1' can't contain non-number [abc]
550 
551 :(scenario memory_invalid_string_check2)
552 % Hide_errors = true;
553 def main [
554   1:num <- copy 3
555   2:num <- copy 97  # 'a'
556   3:num <- copy 98  # 'b'
557   4:num <- copy 99  # 'c'
558   memory-should-contain [
559   ¦ 1:array:character <- 0
560   ]
561 ]
562 +error: main: array:character types inside 'memory-should-contain' can only be compared with text literals surrounded by [], not '0'
563 
564 :(scenario memory_check_with_comment)
565 % Scenario_testing_scenario = true;
566 % Hide_errors = true;
567 def main [
568   memory-should-contain [
569   ¦ 1 <- 34  # comment
570   ]
571 ]
572 -error: location 1 can't contain non-number 34  # comment
573 # but there'll be an error signalled by memory-should-contain
574 
575 //: 'trace-should-contain' is like the '+' lines in our scenarios so far
576 // Like runs of contiguous '+' lines, order is important. The trace checks
577 // that the lines are present *and* in the specified sequence. (There can be
578 // other lines in between.)
579 
580 :(scenario trace_check_fails)
581 % Scenario_testing_scenario = true;
582 % Hide_errors = true;
583 def main [
584   trace-should-contain [
585   ¦ a: b
586   ¦ a: d
587   ]
588 ]
589 +error: F - main: missing [b] in trace with label 'a'
590 
591 :(before "End Primitive Recipe Declarations")
592 TRACE_SHOULD_CONTAIN,
593 :(before "End Primitive Recipe Numbers")
594 put(Recipe_ordinal, "trace-should-contain", TRACE_SHOULD_CONTAIN);
595 :(before "End Primitive Recipe Checks")
596 case TRACE_SHOULD_CONTAIN: {
597   break;
598 }
599 :(before "End Primitive Recipe Implementations")
600 case TRACE_SHOULD_CONTAIN: {
601   if (!Passed) break;
602   check_trace(current_instruction().ingredients.at(0).name);
603   break;
604 }
605 
606 :(code)
607 // simplified version of check_trace_contents() that emits errors rather
608 // than just printing to stderr
609 void check_trace(const string& expected) {
610   Trace_stream->newline();
611   vector<trace_line> expected_lines = parse_trace(expected);
612   if (expected_lines.empty()) return;
613   int curr_expected_line = 0;
614   for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin();  p != Trace_stream->past_lines.end();  ++p) {
615   ¦ if (expected_lines.at(curr_expected_line).label != p->label) continue;
616   ¦ if (expected_lines.at(curr_expected_line).contents != trim(p->contents)) continue;
617   ¦ // match
618   ¦ ++curr_expected_line;
619   ¦ if (curr_expected_line == SIZE(expected_lines)) return;
620   }
621   if (!Hide_errors) cerr << '\n';
622   raise << "F - " << maybe(current_recipe_name()) << "missing [" << expected_lines.at(curr_expected_line).contents << "] "
623   ¦ ¦ ¦ << "in trace with label '" << expected_lines.at(curr_expected_line).label << "'\n" << end();
624   if (!Hide_errors)
625   ¦ DUMP(expected_lines.at(curr_expected_line).label);
626   if (!Scenario_testing_scenario) Passed = false;
627 }
628 
629 vector<trace_line> parse_trace(const string& expected) {
630   vector<string> buf = split(expected, "\n");
631   vector<trace_line> result;
632   for (int i = 0;  i < SIZE(buf);  ++i) {
633   ¦ buf.at(i) = trim(buf.at(i));
634   ¦ if (buf.at(i).empty()) continue;
635   ¦ int delim = buf.at(i).find(": ");
636   ¦ if (delim == -1) {
637   ¦ ¦ raise << maybe(current_recipe_name()) << "lines in 'trace-should-contain' should be of the form <label>: <contents>. Both parts are required.\n" << end();
638   ¦ ¦ result.clear();
639   ¦ ¦ return result;
640   ¦ }
641   ¦ result.push_back(trace_line(trim(buf.at(i).substr(0, delim)),  trim(buf.at(i).substr(delim+2))));
642   }
643   return result;
644 }
645 
646 :(scenario trace_check_fails_in_nonfirst_line)
647 % Scenario_testing_scenario = true;
648 % Hide_errors = true;
649 def main [
650   run [
651   ¦ trace 1, [a], [b]
652   ]
653   trace-should-contain [
654   ¦ a: b
655   ¦ a: d
656   ]
657 ]
658 +error: F - main: missing [d] in trace with label 'a'
659 
660 :(scenario trace_check_passes_silently)
661 % Scenario_testing_scenario = true;
662 def main [
663   run [
664   ¦ trace 1, [a], [b]
665   ]
666   trace-should-contain [
667   ¦ a: b
668   ]
669 ]
670 -error: missing [b] in trace with label 'a'
671 $error: 0
672 
673 //: 'trace-should-not-contain' is like the '-' lines in our scenarios so far
674 //: Each trace line is separately checked for absense. Order is *not*
675 //: important, so you can't say things like "B should not exist after A."
676 
677 :(scenario trace_negative_check_fails)
678 % Scenario_testing_scenario = true;
679 % Hide_errors = true;
680 def main [
681   run [
682   ¦ trace 1, [a], [b]
683   ]
684   trace-should-not-contain [
685   ¦ a: b
686   ]
687 ]
688 +error: F - main: unexpected [b] in trace with label 'a'
689 
690 :(before "End Primitive Recipe Declarations")
691 TRACE_SHOULD_NOT_CONTAIN,
692 :(before "End Primitive Recipe Numbers")
693 put(Recipe_ordinal, "trace-should-not-contain", TRACE_SHOULD_NOT_CONTAIN);
694 :(before "End Primitive Recipe Checks")
695 case TRACE_SHOULD_NOT_CONTAIN: {
696   break;
697 }
698 :(before "End Primitive Recipe Implementations")
699 case TRACE_SHOULD_NOT_CONTAIN: {
700   if (!Passed) break;
701   check_trace_missing(current_instruction().ingredients.at(0).name);
702   break;
703 }
704 
705 :(code)
706 // simplified version of check_trace_contents() that emits errors rather
707 // than just printing to stderr
708 bool check_trace_missing(const string& in) {
709   Trace_stream->newline();
710   vector<trace_line> lines = parse_trace(in);
711   for (int i = 0;  i < SIZE(lines);  ++i) {
712   ¦ if (trace_count(lines.at(i).label, lines.at(i).contents) != 0) {
713   ¦ ¦ raise << "F - " << maybe(current_recipe_name()) << "unexpected [" << lines.at(i).contents << "] in trace with label '" << lines.at(i).label << "'\n" << end();
714   ¦ ¦ if (!Scenario_testing_scenario) Passed = false;
715   ¦ ¦ return false;
716   ¦ }
717   }
718   return true;
719 }
720 
721 :(scenario trace_negative_check_passes_silently)
722 % Scenario_testing_scenario = true;
723 def main [
724   trace-should-not-contain [
725   ¦ a: b
726   ]
727 ]
728 -error: unexpected [b] in trace with label 'a'
729 $error: 0
730 
731 :(scenario trace_negative_check_fails_on_any_unexpected_line)
732 % Scenario_testing_scenario = true;
733 % Hide_errors = true;
734 def main [
735   run [
736   ¦ trace 1, [a], [d]
737   ]
738   trace-should-not-contain [
739   ¦ a: b
740   ¦ a: d
741   ]
742 ]
743 +error: F - main: unexpected [d] in trace with label 'a'
744 
745 :(scenario trace_count_check)
746 def main [
747   run [
748   ¦ trace 1, [a], [foo]
749   ]
750   check-trace-count-for-label 1, [a]
751 ]
752 # checks are inside scenario
753 
754 :(before "End Primitive Recipe Declarations")
755 CHECK_TRACE_COUNT_FOR_LABEL,
756 :(before "End Primitive Recipe Numbers")
757 put(Recipe_ordinal, "check-trace-count-for-label", CHECK_TRACE_COUNT_FOR_LABEL);
758 :(before "End Primitive Recipe Checks")
759 case CHECK_TRACE_COUNT_FOR_LABEL: {
760   if (SIZE(inst.ingredients) != 2) {
761   ¦ raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
762   ¦ break;
763   }
764   if (!is_mu_number(inst.ingredients.at(0))) {
765   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
766   ¦ break;
767   }
768   if (!is_literal_text(inst.ingredients.at(1))) {
769   ¦ raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
770   ¦ break;
771   }
772   break;
773 }
774 :(before "End Primitive Recipe Implementations")
775 case CHECK_TRACE_COUNT_FOR_LABEL: {
776   if (!Passed) break;
777   int expected_count = ingredients.at(0).at(0);
778   string label = current_instruction().ingredients.at(1).name;
779   int count = trace_count(label);
780   if (count != expected_count) {
781   ¦ if (!Hide_errors) cerr << '\n';
782   ¦ raise << "F - " << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
783   ¦ if (!Hide_errors) DUMP(label);
784   ¦ if (!Scenario_testing_scenario) Passed = false;
785   }
786   break;
787 }
788 
789 :(before "End Primitive Recipe Declarations")
790 CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN,
791 :(before "End Primitive Recipe Numbers")
792 put(Recipe_ordinal, "check-trace-count-for-label-greater-than", CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN);
793 :(before "End Primitive Recipe Checks")
794 case CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN: {
795   if (SIZE(inst.ingredients) != 2) {
796   ¦ raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
797   ¦ break;
798   }
799   if (!is_mu_number(inst.ingredients.at(0))) {
800   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
801   ¦ break;
802   }
803   if (!is_literal_text(inst.ingredients.at(1))) {
804   ¦ raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
805   ¦ break;
806   }
807   break;
808 }
809 :(before "End Primitive Recipe Implementations")
810 case CHECK_TRACE_COUNT_FOR_LABEL_GREATER_THAN: {
811   if (!Passed) break;
812   int expected_count = ingredients.at(0).at(0);
813   string label = current_instruction().ingredients.at(1).name;
814   int count = trace_count(label);
815   if (count <= expected_count) {
816   ¦ if (!Hide_errors) cerr << '\n';
817   ¦ raise << maybe(current_recipe_name()) << "expected more than " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
818   ¦ if (!Hide_errors) {
819   ¦ ¦ cerr << "trace contents:\n";
820   ¦ ¦ DUMP(label);
821   ¦ }
822   ¦ if (!Scenario_testing_scenario) Passed = false;
823   }
824   break;
825 }
826 
827 :(before "End Primitive Recipe Declarations")
828 CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN,
829 :(before "End Primitive Recipe Numbers")
830 put(Recipe_ordinal, "check-trace-count-for-label-lesser-than", CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN);
831 :(before "End Primitive Recipe Checks")
832 case CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN: {
833   if (SIZE(inst.ingredients) != 2) {
834   ¦ raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
835   ¦ break;
836   }
837   if (!is_mu_number(inst.ingredients.at(0))) {
838   ¦ raise << maybe(get(Recipe, r).name) << "first ingredient of 'check-trace-count-for-label' should be a number (count), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
839   ¦ break;
840   }
841   if (!is_literal_text(inst.ingredients.at(1))) {
842   ¦ raise << maybe(get(Recipe, r).name) << "second ingredient of 'check-trace-count-for-label' should be a literal string (label), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
843   ¦ break;
844   }
845   break;
846 }
847 :(before "End Primitive Recipe Implementations")
848 case CHECK_TRACE_COUNT_FOR_LABEL_LESSER_THAN: {
849   if (!Passed) break;
850   int expected_count = ingredients.at(0).at(0);
851   string label = current_instruction().ingredients.at(1).name;
852   int count = trace_count(label);
853   if (count >= expected_count) {
854   ¦ if (!Hide_errors) cerr << '\n';
855   ¦ raise << "F - " << maybe(current_recipe_name()) << "expected less than " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
856   ¦ if (!Hide_errors) {
857   ¦ ¦ cerr << "trace contents:\n";
858   ¦ ¦ DUMP(label);
859   ¦ }
860   ¦ if (!Scenario_testing_scenario) Passed = false;
861   }
862   break;
863 }
864 
865 :(scenario trace_count_check_2)
866 % Scenario_testing_scenario = true;
867 % Hide_errors = true;
868 def main [
869   run [
870   ¦ trace 1, [a], [foo]
871   ]
872   check-trace-count-for-label 2, [a]
873 ]
874 +error: F - main: expected 2 lines in trace with label 'a' in trace
875 
876 //: Minor detail: ignore 'system' calls in scenarios, since anything we do
877 //: with them is by definition impossible to test through Mu.
878 :(after "case _SYSTEM:")
879   if (Current_scenario) break;
880 
881 //:: Warn if people use '_' manually in function names. They're reserved for internal use.
882 
883 :(scenario recipe_name_with_underscore)
884 % Hide_errors = true;
885 def foo_bar [
886 ]
887 +error: foo_bar: don't create recipes with '_' in the name
888 
889 :(before "End recipe Fields")
890 bool is_autogenerated;
891 :(before "End recipe Constructor")
892 is_autogenerated = false;
893 :(code)
894 void mark_autogenerated(recipe_ordinal r) {
895   get(Recipe, r).is_autogenerated = true;
896 }
897 
898 :(after "void transform_all()")
899   for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin();  p != Recipe.end();  ++p) {
900   ¦ const recipe& r = p->second;
901   ¦ if (r.name.find('_') == string::npos) continue;
902   ¦ if (r.is_autogenerated) continue;  // created by previous call to transform_all()
903   ¦ raise << r.name << ": don't create recipes with '_' in the name\n" << end();
904   }
905 
906 //:: Helpers
907 
908 :(code)
909 // just for the scenarios running scenarios in C++ layers
910 void run_mu_scenario(const string& form) {
911   Scenario_names.clear();
912   istringstream in(form);
913   in >> std::noskipws;
914   skip_whitespace_and_comments(in);
915   string _scenario = next_word(in);
916   if (_scenario.empty()) {
917   ¦ assert(!has_data(in));
918   ¦ raise << "no scenario in string passed into run_mu_scenario()\n" << end();
919   ¦ return;
920   }
921   assert(_scenario == "scenario");
922   scenario s = parse_scenario(in);
923   run_mu_scenario(s);
924 }