https://github.com/akkartik/mu/blob/master/050scenario.cc

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