mu/050scenario.cc

787 lines
24 KiB
C++
Raw Normal View History

//: Mu scenarios. This will get long, but these are the tests we want to
//: support in this layer.
2015-04-20 17:14:56 +00:00
//: We avoid raw numeric locations in Mu -- except in scenarios, where they're
//: handy to check the values of specific variables
:(scenarios run_mu_scenario)
:(scenario scenario_block)
scenario foo [
run [
2016-09-17 17:28:25 +00:00
1:num <- copy 13
]
memory-should-contain [
1 <- 13
]
]
# checks are inside scenario
2015-04-20 17:14:56 +00:00
:(scenario scenario_multiple_blocks)
2015-04-20 17:14:56 +00:00
scenario foo [
run [
2016-09-17 17:28:25 +00:00
1:num <- copy 13
]
memory-should-contain [
1 <- 13
]
run [
2016-09-17 17:28:25 +00:00
2:num <- copy 13
2015-04-20 17:14:56 +00:00
]
memory-should-contain [
1 <- 13
2 <- 13
2015-04-20 17:14:56 +00:00
]
]
2016-07-11 04:47:24 +00:00
# checks are inside scenario
2015-04-20 17:14:56 +00:00
:(scenario scenario_check_memory_and_trace)
scenario foo [
run [
2016-09-17 17:28:25 +00:00
1:num <- copy 13
trace 1, [a], [a b c]
]
memory-should-contain [
1 <- 13
]
trace-should-contain [
a: a b c
]
trace-should-not-contain [
a: x y z
]
]
2016-07-11 04:47:24 +00:00
# checks are inside scenario
//:: Core data structure
:(before "End Types")
struct scenario {
string name;
string to_run;
};
:(before "End Globals")
vector<scenario> Scenarios;
//:: Parse the 'scenario' form.
//: Simply store the text of the scenario.
:(before "End Command Handlers")
else if (command == "scenario") {
Scenarios.push_back(parse_scenario(in));
}
else if (command == "pending-scenario") {
// for temporary use only
parse_scenario(in); // discard
}
:(code)
scenario parse_scenario(istream& in) {
scenario result;
result.name = next_word(in);
if (result.name.empty()) {
assert(!has_data(in));
raise << "incomplete scenario at end of file\n" << end();
return result;
}
skip_whitespace_and_comments(in);
2016-05-05 15:13:45 +00:00
if (in.peek() != '[') {
raise << "Expected '[' after scenario '" << result.name << "'\n" << end();
2016-05-05 15:13:45 +00:00
exit(0);
}
// scenarios are take special 'code' strings so we need to ignore brackets
// inside comments
2015-06-14 23:24:37 +00:00
result.to_run = slurp_quoted(in);
// delete [] delimiters
2016-09-12 01:07:29 +00:00
assert(starts_with(result.to_run, "["));
2015-06-14 23:24:37 +00:00
result.to_run.erase(0, 1);
assert(result.to_run.at(SIZE(result.to_run)-1) == ']');
2015-06-14 23:24:37 +00:00
result.to_run.erase(SIZE(result.to_run)-1);
return result;
}
:(scenario read_scenario_with_bracket_in_comment)
scenario foo [
# ']' in comment
2016-09-17 17:28:25 +00:00
1:num <- copy 0
]
+run: {1: "number"} <- copy {0: "literal"}
:(scenario read_scenario_with_bracket_in_comment_in_nested_string)
scenario foo [
1:text <- new [# not a comment]
]
+run: {1: ("address" "array" "character")} <- new {"# not a comment": "literal-string"}
//:: Run scenarios when we run 'mu test'.
//: Treat the text of the scenario as a regular series of instructions.
:(before "End Globals")
int Num_core_mu_scenarios = 0;
:(after "Check For .mu Files")
Num_core_mu_scenarios = SIZE(Scenarios);
:(before "End Tests")
2016-02-25 16:24:14 +00:00
Hide_missing_default_space_errors = false;
if (Num_core_mu_scenarios) {
time(&t);
cerr << "Mu tests: " << ctime(&t);
2016-10-20 05:10:35 +00:00
for (int i = 0; i < Num_core_mu_scenarios; ++i) {
2016-08-14 12:45:30 +00:00
//? cerr << '\n' << i << ": " << Scenarios.at(i).name;
run_mu_scenario(Scenarios.at(i));
if (Passed) cerr << ".";
}
cerr << "\n";
}
run_app_scenarios:
if (Num_core_mu_scenarios != SIZE(Scenarios)) {
time(&t);
cerr << "App tests: " << ctime(&t);
2016-10-20 05:10:35 +00:00
for (int i = Num_core_mu_scenarios; i < SIZE(Scenarios); ++i) {
2016-08-14 12:45:30 +00:00
//? cerr << '\n' << i << ": " << Scenarios.at(i).name;
run_mu_scenario(Scenarios.at(i));
if (Passed) cerr << ".";
}
cerr << "\n";
}
//: For faster debugging, support running tests for just the Mu app(s) we are
//: loading.
:(before "End Globals")
bool Test_only_app = false;
:(before "End Commandline Options(*arg)")
else if (is_equal(*arg, "--test-only-app")) {
Test_only_app = true;
}
:(after "End Test Run Initialization")
if (Test_only_app && Num_core_mu_scenarios < SIZE(Scenarios)) {
goto run_app_scenarios;
}
2015-05-12 16:20:19 +00:00
//: Convenience: run a single named scenario.
:(after "Test Runs")
2016-10-20 05:10:35 +00:00
for (int i = 0; i < SIZE(Scenarios); ++i) {
if (Scenarios.at(i).name == argv[argc-1]) {
run_mu_scenario(Scenarios.at(i));
if (Passed) cerr << ".\n";
return 0;
2015-05-12 16:20:19 +00:00
}
}
:(before "End Globals")
// this isn't a constant, just a global of type const*
const scenario* Current_scenario = NULL;
:(code)
void run_mu_scenario(const scenario& s) {
Current_scenario = &s;
bool not_already_inside_test = !Trace_stream;
//? cerr << s.name << '\n';
if (not_already_inside_test) {
Trace_stream = new trace_stream;
setup();
}
vector<recipe_ordinal> tmp = load("recipe scenario_"+s.name+" [ "+s.to_run+" ]");
2016-05-27 16:50:39 +00:00
mark_autogenerated(tmp.at(0));
bind_special_scenario_names(tmp.at(0));
transform_all();
run(tmp.front());
// End Mu Test Teardown
if (!Hide_errors && trace_count("error") > 0 && !Scenario_testing_scenario)
Passed = false;
if (not_already_inside_test && Trace_stream) {
teardown();
if (Save_trace) {
ofstream fout("last_trace");
fout << Trace_stream->readable_contents("");
fout.close();
}
delete Trace_stream;
Trace_stream = NULL;
}
Current_scenario = NULL;
}
2016-05-27 16:50:39 +00:00
//: Some variables for fake resources always get special /raw addresses in scenarios.
// Should contain everything passed by is_special_name but failed by is_disqualified.
void bind_special_scenario_names(recipe_ordinal r) {
// Special Scenario Variable Names(r)
// End Special Scenario Variable Names(r)
}
2016-08-25 17:11:04 +00:00
:(before "Done Placing Ingredient(ingredient, inst, caller)")
maybe_make_raw(ingredient, caller);
:(before "Done Placing Product(product, inst, caller)")
maybe_make_raw(product, caller);
2016-05-27 16:50:39 +00:00
:(code)
void maybe_make_raw(reagent& r, const recipe& caller) {
if (!is_special_name(r.name)) return;
if (starts_with(caller.name, "scenario_"))
r.properties.push_back(pair<string, string_tree*>("raw", NULL));
// End maybe_make_raw
}
//: Test.
:(before "End is_special_name Cases")
if (s == "__maybe_make_raw_test__") return true;
:(before "End Special Scenario Variable Names(r)")
//: ugly: we only need this for this one test, but need to define it for all time
Name[r]["__maybe_make_raw_test__"] = Reserved_for_tests-1;
:(code)
void test_maybe_make_raw() {
// check that scenarios can use local-scope and special variables together
vector<recipe_ordinal> tmp = load(
"def scenario_foo [\n"
" local-scope\n"
2016-09-17 17:28:25 +00:00
" __maybe_make_raw_test__:num <- copy 34\n"
2016-05-27 16:50:39 +00:00
"]\n");
mark_autogenerated(tmp.at(0));
bind_special_scenario_names(tmp.at(0));
transform_all();
run(tmp.at(0));
2016-09-03 00:57:43 +00:00
CHECK_EQ(trace_count("error"), 0);
2016-05-27 16:50:39 +00:00
}
//: Watch out for redefinitions of scenario routines. We should never ever be
//: doing that, regardless of anything else.
:(scenarios run)
:(scenario forbid_redefining_scenario_even_if_forced)
2016-02-25 15:58:09 +00:00
% Hide_errors = true;
% Disable_redefine_checks = true;
def scenario-foo [
2016-09-17 17:28:25 +00:00
1:num <- copy 34
]
def scenario-foo [
2016-09-17 17:28:25 +00:00
1:num <- copy 35
]
2016-02-25 15:58:09 +00:00
+error: redefining recipe scenario-foo
:(after "bool should_check_for_redefine(const string& recipe_name)")
2015-08-24 21:19:52 +00:00
if (recipe_name.find("scenario-") == 0) return true;
//:: The special instructions we want to support inside scenarios.
//: In a compiler for the mu VM these will require more work.
//: 'run' is a purely lexical convenience to separate the code actually being
//: tested from any setup or teardown
:(scenario run)
def main [
run [
2016-09-17 17:28:25 +00:00
1:num <- copy 13
]
]
+mem: storing 13 in location 1
:(before "End Rewrite Instruction(curr, recipe result)")
if (curr.name == "run") {
// Just inline all instructions inside the run block in the containing
// recipe. 'run' is basically a comment; pretend it doesn't exist.
istringstream in2("[\n"+curr.ingredients.at(0).name+"\n]\n");
slurp_body(in2, result);
curr.clear();
2015-10-02 00:30:14 +00:00
}
:(scenario run_multiple)
def main [
run [
2016-09-17 17:28:25 +00:00
1:num <- copy 13
]
run [
2016-09-17 17:28:25 +00:00
2:num <- copy 13
]
]
+mem: storing 13 in location 1
+mem: storing 13 in location 2
//: 'memory-should-contain' raises errors if specific locations aren't as expected
//: Also includes some special support for checking strings.
:(before "End Globals")
bool Scenario_testing_scenario = false;
:(before "End Setup")
Scenario_testing_scenario = false;
:(scenario memory_check)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
memory-should-contain [
1 <- 13
]
]
+run: checking location 1
+error: expected location '1' to contain 13 but saw 0
:(before "End Primitive Recipe Declarations")
MEMORY_SHOULD_CONTAIN,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "memory-should-contain", MEMORY_SHOULD_CONTAIN);
2015-10-02 00:30:14 +00:00
:(before "End Primitive Recipe Checks")
case MEMORY_SHOULD_CONTAIN: {
break;
}
:(before "End Primitive Recipe Implementations")
case MEMORY_SHOULD_CONTAIN: {
2015-05-28 01:32:07 +00:00
if (!Passed) break;
check_memory(current_instruction().ingredients.at(0).name);
break;
}
:(code)
void check_memory(const string& s) {
istringstream in(s);
in >> std::noskipws;
set<int> locations_checked;
while (true) {
skip_whitespace_and_comments(in);
if (!has_data(in)) break;
string lhs = next_word(in);
if (lhs.empty()) {
assert(!has_data(in));
raise << "incomplete 'memory-should-contain' block at end of file (0)\n" << end();
return;
}
2015-05-17 04:24:21 +00:00
if (!is_integer(lhs)) {
check_type(lhs, in);
continue;
}
int address = to_integer(lhs);
skip_whitespace_and_comments(in);
string _assign; in >> _assign; assert(_assign == "<-");
skip_whitespace_and_comments(in);
2016-02-25 05:04:11 +00:00
string rhs = next_word(in);
if (rhs.empty()) {
assert(!has_data(in));
raise << "incomplete 'memory-should-contain' block at end of file (1)\n" << end();
return;
}
2016-02-25 05:04:11 +00:00
if (!is_integer(rhs) && !is_noninteger(rhs)) {
if (Current_scenario && !Scenario_testing_scenario)
// genuine test in a mu file
raise << "\nF - " << Current_scenario->name << ": location '" << address << "' can't contain non-number " << rhs << "\n" << end();
2016-02-25 05:04:11 +00:00
else
// just testing scenario support
raise << "location '" << address << "' can't contain non-number " << rhs << '\n' << end();
if (!Scenario_testing_scenario) Passed = false;
2016-02-25 05:04:11 +00:00
return;
}
double value = to_double(rhs);
if (contains_key(locations_checked, address))
raise << "duplicate expectation for location '" << address << "'\n" << end();
2015-10-29 19:09:23 +00:00
trace(9999, "run") << "checking location " << address << end();
if (get_or_insert(Memory, address) != value) {
if (Current_scenario && !Scenario_testing_scenario) {
2015-05-26 23:15:07 +00:00
// genuine test in a mu file
raise << "\nF - " << Current_scenario->name << ": expected location '" << address << "' to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end();
2015-05-26 23:15:07 +00:00
}
else {
// just testing scenario support
raise << "expected location '" << address << "' to contain " << no_scientific(value) << " but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end();
2015-05-26 23:15:07 +00:00
}
if (!Scenario_testing_scenario) Passed = false;
return;
}
locations_checked.insert(address);
}
}
void check_type(const string& lhs, istream& in) {
reagent x(lhs);
3309 Rip out everything to fix one failing unit test (commit 3290; type abbreviations). This commit does several things at once that I couldn't come up with a clean way to unpack: A. It moves to a new representation for type trees without changing the actual definition of the `type_tree` struct. B. It adds unit tests for our type metadata precomputation, so that errors there show up early and in a simpler setting rather than dying when we try to load Mu code. C. It fixes a bug, guarding against infinite loops when precomputing metadata for recursive shape-shifting containers. To do this it uses a dumb way of comparing type_trees, comparing their string representations instead. That is likely incredibly inefficient. Perhaps due to C, this commit has made Mu incredibly slow. Running all tests for the core and the edit/ app now takes 6.5 minutes rather than 3.5 minutes. == more notes and details I've been struggling for the past week now to back out of a bad design decision, a premature optimization from the early days: storing atoms directly in the 'value' slot of a cons cell rather than creating a special 'atom' cons cell and storing it on the 'left' slot. In other words, if a cons cell looks like this: o / | \ left val right ..then the type_tree (a b c) used to look like this (before this commit): o | \ a o | \ b o | \ c null ..rather than like this 'classic' approach to s-expressions which never mixes val and right (which is what we now have): o / \ o o | / \ a o o | / \ b o null | c The old approach made several operations more complicated, most recently the act of replacing a (possibly atom/leaf) sub-tree with another. That was the final straw that got me to realize the contortions I was going through to save a few type_tree nodes (cons cells). Switching to the new approach was hard partly because I've been using the old approach for so long and type_tree manipulations had pervaded everything. Another issue I ran into was the realization that my layers were not cleanly separated. Key parts of early layers (precomputing type metadata) existed purely for far later ones (shape-shifting types). Layers I got repeatedly stuck at: 1. the transform for precomputing type sizes (layer 30) 2. type-checks on merge instructions (layer 31) 3. the transform for precomputing address offsets in types (layer 36) 4. replace operations in supporting shape-shifting recipes (layer 55) After much thrashing I finally noticed that it wasn't the entirety of these layers that was giving me trouble, but just the type metadata precomputation, which had bugs that weren't manifesting until 30 layers later. Or, worse, when loading .mu files before any tests had had a chance to run. A common failure mode was running into types at run time that I hadn't precomputed metadata for at transform time. Digging into these bugs got me to realize that what I had before wasn't really very good, but a half-assed heuristic approach that did a whole lot of extra work precomputing metadata for utterly meaningless types like `((address number) 3)` which just happened to be part of a larger type like `(array (address number) 3)`. So, I redid it all. I switched the representation of types (because the old representation made unit tests difficult to retrofit) and added unit tests to the metadata precomputation. I also made layer 30 only do the minimal metadata precomputation it needs for the concepts introduced until then. In the process, I also made the precomputation more correct than before, and added hooks in the right place so that I could augment the logic when I introduced shape-shifting containers. == lessons learned There's several levels of hygiene when it comes to layers: 1. Every layer introduces precisely what it needs and in the simplest way possible. If I was building an app until just that layer, nothing would seem over-engineered. 2. Some layers are fore-shadowing features in future layers. Sometimes this is ok. For example, layer 10 foreshadows containers and arrays and so on without actually supporting them. That is a net win because it lets me lay out the core of Mu's data structures out in one place. But if the fore-shadowing gets too complex things get nasty. Not least because it can be hard to write unit tests for features before you provide the plumbing to visualize and manipulate them. 3. A layer is introducing features that are tested only in later layers. 4. A layer is introducing features with tests that are invalidated in later layers. (This I knew from early on to be an obviously horrendous idea.) Summary: avoid Level 2 (foreshadowing layers) as much as possible. Tolerate it indefinitely for small things where the code stays simple over time, but become strict again when things start to get more complex. Level 3 is mostly a net lose, but sometimes it can be expedient (a real case of the usually grossly over-applied term "technical debt"), and it's better than the conventional baseline of no layers and no scenarios. Just clean it up as soon as possible. Definitely avoid layer 4 at any time. == minor lessons Avoid unit tests for trivial things, write scenarios in context as much as possible. But within those margins unit tests are fine. Just introduce them before any scenarios (commit 3297). Reorganizing layers can be easy. Just merge layers for starters! Punt on resplitting them in some new way until you've gotten them to work. This is the wisdom of Refactoring: small steps. What made it hard was not wanting to merge *everything* between layer 30 and 55. The eventual insight was realizing I just need to move those two full-strength transforms and nothing else.
2016-09-10 01:32:52 +00:00
if (is_mu_array(x.type) && is_mu_character(x.type->right)) {
2015-05-17 04:24:21 +00:00
x.set_value(to_integer(x.name));
skip_whitespace_and_comments(in);
string _assign = next_word(in);
if (_assign.empty()) {
assert(!has_data(in));
raise << "incomplete 'memory-should-contain' block at end of file (2)\n" << end();
return;
}
assert(_assign == "<-");
skip_whitespace_and_comments(in);
string literal = next_word(in);
if (literal.empty()) {
assert(!has_data(in));
raise << "incomplete 'memory-should-contain' block at end of file (3)\n" << end();
return;
}
int address = x.value;
// exclude quoting brackets
assert(*literal.begin() == '['); literal.erase(literal.begin());
assert(*--literal.end() == ']'); literal.erase(--literal.end());
check_string(address, literal);
return;
}
2016-02-25 05:04:11 +00:00
// End Scenario Type Cases
raise << "don't know how to check memory for '" << lhs << "'\n" << end();
}
void check_string(int address, const string& literal) {
2015-10-29 19:09:23 +00:00
trace(9999, "run") << "checking string length at " << address << end();
if (get_or_insert(Memory, address) != SIZE(literal)) {
if (Current_scenario && !Scenario_testing_scenario)
2016-09-17 06:52:15 +00:00
raise << "\nF - " << Current_scenario->name << ": expected location '" << address << "' to contain length " << SIZE(literal) << " of string [" << literal << "] but saw " << no_scientific(get_or_insert(Memory, address)) << " (" << read_mu_text(address-/*fake refcount*/1) << ")\n" << end();
2015-05-23 19:30:58 +00:00
else
raise << "expected location '" << address << "' to contain length " << SIZE(literal) << " of string [" << literal << "] but saw " << no_scientific(get_or_insert(Memory, address)) << '\n' << end();
if (!Scenario_testing_scenario) Passed = false;
2015-05-23 19:30:58 +00:00
return;
}
++address; // now skip length
2016-10-20 05:10:35 +00:00
for (int i = 0; i < SIZE(literal); ++i) {
2015-10-29 19:09:23 +00:00
trace(9999, "run") << "checking location " << address+i << end();
if (get_or_insert(Memory, address+i) != literal.at(i)) {
if (Current_scenario && !Scenario_testing_scenario) {
2015-05-26 23:15:07 +00:00
// genuine test in a mu file
2016-02-26 21:04:55 +00:00
raise << "\nF - " << Current_scenario->name << ": expected location " << (address+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, address+i)) << " ('" << static_cast<char>(get_or_insert(Memory, address+i)) << "')\n" << end();
2015-05-26 23:15:07 +00:00
}
else {
// just testing scenario support
2016-02-26 21:04:55 +00:00
raise << "expected location " << (address+i) << " to contain " << literal.at(i) << " but saw " << no_scientific(get_or_insert(Memory, address+i)) << '\n' << end();
2015-05-26 23:15:07 +00:00
}
if (!Scenario_testing_scenario) Passed = false;
2015-05-23 19:30:58 +00:00
return;
}
}
}
:(scenario memory_check_multiple)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
memory-should-contain [
1 <- 0
1 <- 0
]
]
+error: duplicate expectation for location '1'
:(scenario memory_check_string_length)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
2016-09-17 17:28:25 +00:00
1:num <- copy 3
2:num <- copy 97 # 'a'
3:num <- copy 98 # 'b'
4:num <- copy 99 # 'c'
memory-should-contain [
1:array:character <- [ab]
]
]
+error: expected location '1' to contain length 2 of string [ab] but saw 3
:(scenario memory_check_string)
def main [
2016-09-17 17:28:25 +00:00
1:num <- copy 3
2:num <- copy 97 # 'a'
3:num <- copy 98 # 'b'
4:num <- copy 99 # 'c'
memory-should-contain [
1:array:character <- [abc]
]
]
+run: checking string length at 1
+run: checking location 2
+run: checking location 3
+run: checking location 4
2016-02-25 05:04:11 +00:00
:(scenario memory_invalid_string_check)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
2016-02-25 05:04:11 +00:00
memory-should-contain [
1 <- [abc]
]
]
+error: location '1' can't contain non-number [abc]
2016-02-25 05:04:11 +00:00
:(scenario memory_check_with_comment)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
2016-02-25 05:04:11 +00:00
memory-should-contain [
1 <- 34 # comment
]
]
-error: location 1 can't contain non-number 34 # comment
# but there'll be an error signalled by memory-should-contain
2016-02-25 05:04:11 +00:00
//: 'trace-should-contain' is like the '+' lines in our scenarios so far
// Like runs of contiguous '+' lines, order is important. The trace checks
// that the lines are present *and* in the specified sequence. (There can be
// other lines in between.)
:(scenario trace_check_fails)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
trace-should-contain [
a: b
a: d
]
]
2016-07-20 20:06:28 +00:00
+error: missing [b] in trace with label 'a'
:(before "End Primitive Recipe Declarations")
TRACE_SHOULD_CONTAIN,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "trace-should-contain", TRACE_SHOULD_CONTAIN);
2015-10-02 00:30:14 +00:00
:(before "End Primitive Recipe Checks")
case TRACE_SHOULD_CONTAIN: {
break;
}
:(before "End Primitive Recipe Implementations")
case TRACE_SHOULD_CONTAIN: {
2015-05-28 01:32:07 +00:00
if (!Passed) break;
check_trace(current_instruction().ingredients.at(0).name);
break;
}
:(code)
// simplified version of check_trace_contents() that emits errors rather
// than just printing to stderr
void check_trace(const string& expected) {
Trace_stream->newline();
2015-05-22 01:10:17 +00:00
vector<trace_line> expected_lines = parse_trace(expected);
if (expected_lines.empty()) return;
int curr_expected_line = 0;
2016-10-20 05:10:35 +00:00
for (vector<trace_line>::iterator p = Trace_stream->past_lines.begin(); p != Trace_stream->past_lines.end(); ++p) {
2015-05-22 01:10:17 +00:00
if (expected_lines.at(curr_expected_line).label != p->label) continue;
if (expected_lines.at(curr_expected_line).contents != trim(p->contents)) continue;
// match
++curr_expected_line;
if (curr_expected_line == SIZE(expected_lines)) return;
}
if (Current_scenario && !Scenario_testing_scenario)
raise << "\nF - " << Current_scenario->name << ": missing [" << expected_lines.at(curr_expected_line).contents << "] "
<< "in trace with label '" << expected_lines.at(curr_expected_line).label << "'\n" << end();
else
raise << "missing [" << expected_lines.at(curr_expected_line).contents << "] "
<< "in trace with label '" << expected_lines.at(curr_expected_line).label << "'\n" << end();
if (!Hide_errors)
DUMP(expected_lines.at(curr_expected_line).label);
if (!Scenario_testing_scenario) Passed = false;
2015-04-21 05:20:39 +00:00
}
2015-05-22 01:10:17 +00:00
vector<trace_line> parse_trace(const string& expected) {
vector<string> buf = split(expected, "\n");
2015-05-22 01:10:17 +00:00
vector<trace_line> result;
2016-10-20 05:10:35 +00:00
for (int i = 0; i < SIZE(buf); ++i) {
buf.at(i) = trim(buf.at(i));
if (buf.at(i).empty()) continue;
int delim = buf.at(i).find(": ");
2016-09-27 21:45:51 +00:00
if (delim == -1) {
raise << Current_scenario->name << ": lines in 'trace-should-contain' should be of the form <label>: <contents>. Both parts are required.\n" << end();
result.clear();
return result;
}
2015-05-22 01:10:17 +00:00
result.push_back(trace_line(trim(buf.at(i).substr(0, delim)), trim(buf.at(i).substr(delim+2))));
}
return result;
}
:(scenario trace_check_fails_in_nonfirst_line)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
run [
trace 1, [a], [b]
]
trace-should-contain [
a: b
a: d
]
]
2016-07-20 20:06:28 +00:00
+error: missing [d] in trace with label 'a'
:(scenario trace_check_passes_silently)
% Scenario_testing_scenario = true;
def main [
run [
trace 1, [a], [b]
]
trace-should-contain [
a: b
]
]
2016-07-20 20:06:28 +00:00
-error: missing [b] in trace with label 'a'
$error: 0
//: 'trace-should-not-contain' is like the '-' lines in our scenarios so far
//: Each trace line is separately checked for absense. Order is *not*
//: important, so you can't say things like "B should not exist after A."
:(scenario trace_negative_check_fails)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
run [
trace 1, [a], [b]
]
trace-should-not-contain [
a: b
]
]
2016-07-20 20:06:28 +00:00
+error: unexpected [b] in trace with label 'a'
:(before "End Primitive Recipe Declarations")
TRACE_SHOULD_NOT_CONTAIN,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "trace-should-not-contain", TRACE_SHOULD_NOT_CONTAIN);
2015-10-02 00:30:14 +00:00
:(before "End Primitive Recipe Checks")
case TRACE_SHOULD_NOT_CONTAIN: {
break;
}
:(before "End Primitive Recipe Implementations")
case TRACE_SHOULD_NOT_CONTAIN: {
2015-05-28 01:32:07 +00:00
if (!Passed) break;
check_trace_missing(current_instruction().ingredients.at(0).name);
break;
}
:(code)
// simplified version of check_trace_contents() that emits errors rather
// than just printing to stderr
bool check_trace_missing(const string& in) {
Trace_stream->newline();
2015-05-22 01:10:17 +00:00
vector<trace_line> lines = parse_trace(in);
2016-10-20 05:10:35 +00:00
for (int i = 0; i < SIZE(lines); ++i) {
2015-05-22 01:10:17 +00:00
if (trace_count(lines.at(i).label, lines.at(i).contents) != 0) {
2016-07-20 20:06:28 +00:00
raise << "unexpected [" << lines.at(i).contents << "] in trace with label '" << lines.at(i).label << "'\n" << end();
if (!Scenario_testing_scenario) Passed = false;
return false;
}
}
return true;
}
:(scenario trace_negative_check_passes_silently)
% Scenario_testing_scenario = true;
def main [
trace-should-not-contain [
a: b
]
]
2016-07-20 20:06:28 +00:00
-error: unexpected [b] in trace with label 'a'
$error: 0
:(scenario trace_negative_check_fails_on_any_unexpected_line)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
run [
trace 1, [a], [d]
]
trace-should-not-contain [
a: b
a: d
]
]
2016-07-20 20:06:28 +00:00
+error: unexpected [d] in trace with label 'a'
:(scenario trace_count_check)
def main [
run [
trace 1, [a], [foo]
]
check-trace-count-for-label 1, [a]
]
2016-07-11 04:47:24 +00:00
# checks are inside scenario
:(before "End Primitive Recipe Declarations")
CHECK_TRACE_COUNT_FOR_LABEL,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "check-trace-count-for-label", CHECK_TRACE_COUNT_FOR_LABEL);
2015-10-02 00:30:14 +00:00
:(before "End Primitive Recipe Checks")
case CHECK_TRACE_COUNT_FOR_LABEL: {
2015-10-02 00:30:14 +00:00
if (SIZE(inst.ingredients) != 2) {
raise << maybe(get(Recipe, r).name) << "'check-trace-count-for-label' requires exactly two ingredients, but got '" << inst.original_string << "'\n" << end();
break;
}
if (!is_mu_number(inst.ingredients.at(0))) {
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();
break;
}
2016-09-17 06:52:15 +00:00
if (!is_literal_text(inst.ingredients.at(1))) {
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();
break;
}
2015-10-02 00:30:14 +00:00
break;
}
:(before "End Primitive Recipe Implementations")
case CHECK_TRACE_COUNT_FOR_LABEL: {
if (!Passed) break;
int expected_count = ingredients.at(0).at(0);
string label = current_instruction().ingredients.at(1).name;
int count = trace_count(label);
if (count != expected_count) {
if (Current_scenario && !Scenario_testing_scenario) {
// genuine test in a mu file
2016-07-20 20:06:28 +00:00
raise << "\nF - " << Current_scenario->name << ": " << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label '" << label << "' in trace: " << end();
DUMP(label);
}
else {
// just testing scenario support
2016-07-20 20:06:28 +00:00
raise << maybe(current_recipe_name()) << "expected " << expected_count << " lines in trace with label '" << label << "' in trace\n" << end();
}
2016-07-22 21:13:25 +00:00
if (!Scenario_testing_scenario) Passed = false;
}
break;
}
:(scenario trace_count_check_2)
% Scenario_testing_scenario = true;
% Hide_errors = true;
def main [
run [
trace 1, [a], [foo]
]
check-trace-count-for-label 2, [a]
]
2016-07-20 20:06:28 +00:00
+error: main: expected 2 lines in trace with label 'a' in trace
2015-07-28 22:22:58 +00:00
//: Minor detail: ignore 'system' calls in scenarios, since anything we do
//: with them is by definition impossible to test through mu.
:(after "case _SYSTEM:")
if (Current_scenario) break;
2016-05-27 16:50:39 +00:00
//:: Warn if people use '_' manually in function names. They're reserved for internal use.
:(scenario recipe_name_with_underscore)
% Hide_errors = true;
def foo_bar [
]
+error: foo_bar: don't create recipes with '_' in the name
:(before "End recipe Fields")
bool is_autogenerated;
:(before "End recipe Constructor")
is_autogenerated = false;
:(code)
void mark_autogenerated(recipe_ordinal r) {
get(Recipe, r).is_autogenerated = true;
}
:(after "void transform_all()")
2016-10-20 05:10:35 +00:00
for (map<recipe_ordinal, recipe>::iterator p = Recipe.begin(); p != Recipe.end(); ++p) {
2016-05-27 16:50:39 +00:00
const recipe& r = p->second;
if (r.name.find('_') == string::npos) continue;
if (r.is_autogenerated) continue; // created by previous call to transform_all()
raise << r.name << ": don't create recipes with '_' in the name\n" << end();
}
//:: Helpers
:(code)
// just for the scenarios running scenarios in C++ layers
void run_mu_scenario(const string& form) {
istringstream in(form);
in >> std::noskipws;
skip_whitespace_and_comments(in);
string _scenario = next_word(in);
if (_scenario.empty()) {
assert(!has_data(in));
raise << "no scenario in string passed into run_mu_scenario()\n" << end();
return;
}
assert(_scenario == "scenario");
scenario s = parse_scenario(in);
run_mu_scenario(s);
}