https://github.com/akkartik/mu/blob/master/082scenario_screen.cc

      1 //: Clean syntax to manipulate and check the screen in scenarios.
      2 //: Instructions 'assume-screen' and 'screen-should-contain' implicitly create
      3 //: a variable called 'screen' that is accessible to later instructions in the
      4 //: scenario. 'screen-should-contain' can check unicode characters in the fake
      5 //: screen
      6
      7 //: first make sure we don't mangle these instructions in other transforms
      8 :(before "End initialize_transform_rewrite_literal_string_to_text()")
      9 recipes_taking_literal_strings.insert("screen-should-contain");
     10 recipes_taking_literal_strings.insert("screen-should-contain-in-color");
     11
     12 :(scenarios run_mu_scenario)
     13 :(scenario screen_in_scenario)
     14 scenario screen-in-scenario [
     15   local-scope
     16   assume-screen 5/width, 3/height
     17   run [
     18     a:char <- copy 97/a
     19     screen:&:screen <- print screen:&:screen, a
     20   ]
     21   screen-should-contain [
     22   #  01234
     23     .a    .
     24     .     .
     25     .     .
     26   ]
     27 ]
     28 # checks are inside scenario
     29
     30 :(scenario screen_in_scenario_unicode)
     31 # screen-should-contain can check unicode characters in the fake screen
     32 scenario screen-in-scenario-unicode [
     33   local-scope
     34   assume-screen 5/width, 3/height
     35   run [
     36     lambda:char <- copy 955/greek-small-lambda
     37     screen:&:screen <- print screen:&:screen, lambda
     38     a:char <- copy 97/a
     39     screen:&:screen <- print screen:&:screen, a
     40   ]
     41   screen-should-contain [
     42   #  01234
     43     .λa   .
     44     .     .
     45     .     .
     46   ]
     47 ]
     48 # checks are inside scenario
     49
     50 :(scenario screen_in_scenario_color)
     51 scenario screen-in-scenario-color [
     52   local-scope
     53   assume-screen 5/width, 3/height
     54   run [
     55     lambda:char <- copy 955/greek-small-lambda
     56     screen:&:screen <- print screen:&:screen, lambda, 1/red
     57     a:char <- copy 97/a
     58     screen:&:screen <- print screen:&:screen, a, 7/white
     59   ]
     60   # screen-should-contain shows everything
     61   screen-should-contain [
     62   #  01234
     63     .λa   .
     64     .     .
     65     .     .
     66   ]
     67   # screen-should-contain-in-color filters out everything except the given
     68   # color, all you see is the 'a' in white.
     69   screen-should-contain-in-color 7/white, [
     70   #  01234
     71     . a   .
     72     .     .
     73     .     .
     74   ]
     75   # ..and the λ in red.
     76   screen-should-contain-in-color 1/red, [
     77   #  01234
     78     .λ    .
     79     .     .
     80     .     .
     81   ]
     82 ]
     83 # checks are inside scenario
     84
     85 :(scenario screen_in_scenario_error)
     86 % Scenario_testing_scenario = true;
     87 % Hide_errors = true;
     88 scenario screen-in-scenario-error [
     89   local-scope
     90   assume-screen 5/width, 3/height
     91   run [
     92     a:char <- copy 97/a
     93     screen:&:screen <- print screen:&:screen, a
     94   ]
     95   screen-should-contain [
     96   #  01234
     97     .b    .
     98     .     .
     99     .     .
    100   ]
    101 ]
    102 +error: F - screen-in-scenario-error: expected screen location (0, 0) to contain 98 ('b') instead of 97 ('a')
    103
    104 :(scenario screen_in_scenario_color_error)
    105 % Scenario_testing_scenario = true;
    106 % Hide_errors = true;
    107 # screen-should-contain can check unicode characters in the fake screen
    108 scenario screen-in-scenario-color-error [
    109   local-scope
    110   assume-screen 5/width, 3/height
    111   run [
    112     a:char <- copy 97/a
    113     screen:&:screen <- print screen:&:screen, a, 1/red
    114   ]
    115   screen-should-contain-in-color 2/green, [
    116   #  01234
    117     .a    .
    118     .     .
    119     .     .
    120   ]
    121 ]
    122 +error: F - screen-in-scenario-color-error: expected screen location (0, 0) to contain 'a' in color 2 instead of 1
    123
    124 :(scenarios run)
    125 :(scenario convert_names_does_not_fail_when_mixing_special_names_and_numeric_locations)
    126 % Scenario_testing_scenario = true;
    127 def main [
    128   screen:num <- copy 1:num
    129 ]
    130 -error: mixing variable names and numeric addresses in main
    131 $error: 0
    132 :(scenarios run_mu_scenario)
    133
    134 //: It's easier to implement assume-screen and other similar scenario-only
    135 //: primitives if they always write to a fixed location. So we'll assign a
    136 //: single fixed location for the per-scenario screen, keyboard, file system,
    137 //: etc. Carve space for these fixed locations out of the reserved-for-test
    138 //: locations.
    139
    140 :(before "End Globals")
    141 extern const int Max_variables_in_scenarios = Reserved_for_tests-100;
    142 int Next_predefined_global_for_scenarios = Max_variables_in_scenarios;
    143 :(before "End Reset")
    144 assert(Next_predefined_global_for_scenarios < Reserved_for_tests);
    145
    146 :(before "End Globals")
    147 // Scenario Globals.
    148 extern const int SCREEN = next_predefined_global_for_scenarios(/*size_of(address:screen)*/2);
    149 // End Scenario Globals.
    150 :(code)
    151 int next_predefined_global_for_scenarios(int size) {
    152   int result = Next_predefined_global_for_scenarios;
    153   Next_predefined_global_for_scenarios += size;
    154   return result;
    155 }
    156
    157 //: give 'screen' a fixed location in scenarios
    158 :(before "End Special Scenario Variable Names(r)")
    159 Name[r]["screen"] = SCREEN;
    160 //: make 'screen' always a raw location in scenarios
    161 :(before "End is_special_name Special-cases")
    162 if (s == "screen") return true;
    163
    164 :(before "End Rewrite Instruction(curr, recipe result)")
    165 // rewrite 'assume-screen width, height' to
    166 // 'screen:&:screen <- new-fake-screen width, height'
    167 if (curr.name == "assume-screen") {
    168   curr.name = "new-fake-screen";
    169   if (!curr.products.empty()) {
    170     raise << result.name << ": 'assume-screen' has no products\n" << end();
    171   }
    172   else if (!starts_with(result.name, "scenario_")) {
    173     raise << result.name << ": 'assume-screen' can't be called here, only in scenarios\n" << end();
    174   }
    175   else {
    176     assert(curr.products.empty());
    177     curr.products.push_back(reagent("screen:&:screen/raw"));
    178     curr.products.at(0).set_value(SCREEN);
    179   }
    180 }
    181
    182 :(scenario assume_screen_shows_up_in_errors)
    183 % Hide_errors = true;
    184 scenario assume-screen-shows-up-in-errors [
    185   assume-screen width, 5
    186 ]
    187 +error: assume-screen-shows-up-in-errors: missing type for 'width' in 'assume-screen width, 5'
    188
    189 //: screen-should-contain is a regular instruction
    190 :(before "End Primitive Recipe Declarations")
    191 SCREEN_SHOULD_CONTAIN,
    192 :(before "End Primitive Recipe Numbers")
    193 put(Recipe_ordinal, "screen-should-contain", SCREEN_SHOULD_CONTAIN);
    194 :(before "End Primitive Recipe Checks")
    195 case SCREEN_SHOULD_CONTAIN: {
    196   if (SIZE(inst.ingredients) != 1) {
    197     raise << maybe(get(Recipe, r).name) << "'screen-should-contain' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
    198     break;
    199   }
    200   if (!is_literal_text(inst.ingredients.at(0))) {
    201     raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain' should be a literal string, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    202     break;
    203   }
    204   break;
    205 }
    206 :(before "End Primitive Recipe Implementations")
    207 case SCREEN_SHOULD_CONTAIN: {
    208   if (!Passed) break;
    209   assert(scalar(ingredients.at(0)));
    210   check_screen(current_instruction().ingredients.at(0).name, -1);
    211   break;
    212 }
    213
    214 :(before "End Primitive Recipe Declarations")
    215 SCREEN_SHOULD_CONTAIN_IN_COLOR,
    216 :(before "End Primitive Recipe Numbers")
    217 put(Recipe_ordinal, "screen-should-contain-in-color", SCREEN_SHOULD_CONTAIN_IN_COLOR);
    218 :(before "End Primitive Recipe Checks")
    219 case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
    220   if (SIZE(inst.ingredients) != 2) {
    221     raise << maybe(get(Recipe, r).name) << "'screen-should-contain-in-color' requires exactly two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
    222     break;
    223   }
    224   if (!is_mu_number(inst.ingredients.at(0))) {
    225     raise << maybe(get(Recipe, r).name) << "first ingredient of 'screen-should-contain-in-color' should be a number (color code), but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    226     break;
    227   }
    228   if (!is_literal_text(inst.ingredients.at(1))) {
    229     raise << maybe(get(Recipe, r).name) << "second ingredient of 'screen-should-contain-in-color' should be a literal string, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
    230     break;
    231   }
    232   break;
    233 }
    234 :(before "End Primitive Recipe Implementations")
    235 case SCREEN_SHOULD_CONTAIN_IN_COLOR: {
    236   if (!Passed) break;
    237   assert(scalar(ingredients.at(0)));
    238   assert(scalar(ingredients.at(1)));
    239   check_screen(current_instruction().ingredients.at(1).name, ingredients.at(0).at(0));
    240   break;
    241 }
    242
    243 :(before "End Types")
    244 // scan an array of characters in a unicode-aware, bounds-checked manner
    245 struct raw_string_stream {
    246   int index;
    247   const int max;
    248   const char* buf;
    249
    250   raw_string_stream(const string&);
    251   uint32_t get();  // unicode codepoint
    252   uint32_t peek();  // unicode codepoint
    253   bool at_end() const;
    254   void skip_whitespace_and_comments();
    255 };
    256
    257 :(code)
    258 void check_screen(const string& expected_contents, const int color) {
    259   int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
    260   reagent screen("x:screen");  // just to ensure screen.type is reclaimed
    261   int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
    262   assert(screen_data_location >= 0);
    263 //?   cerr << "screen data is at location " << screen_data_location << '\n';
    264   int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
    265 //?   cerr << "screen data start is at " << screen_data_start << '\n';
    266   int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
    267 //?   cerr << "screen width is at location " << screen_width_location << '\n';
    268   int screen_width = get_or_insert(Memory, screen_width_location);
    269 //?   cerr << "screen width: " << screen_width << '\n';
    270   int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
    271 //?   cerr << "screen height is at location " << screen_height_location << '\n';
    272   int screen_height = get_or_insert(Memory, screen_height_location);
    273 //?   cerr << "screen height: " << screen_height << '\n';
    274   int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
    275 //?   cerr << "top of screen is at location " << top_index_location << '\n';
    276   int top_index = get_or_insert(Memory, top_index_location);
    277 //?   cerr << "top of screen is index " << top_index << '\n';
    278   raw_string_stream cursor(expected_contents);
    279   // todo: too-long expected_contents should fail
    280   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
    281     cursor.skip_whitespace_and_comments();
    282     if (cursor.at_end()) break;
    283     if (cursor.get() != '.') {
    284       raise << maybe(current_recipe_name()) << "each row of the expected screen should start with a '.'\n" << end();
    285       if (!Scenario_testing_scenario) Passed = false;
    286       return;
    287     }
    288     int addr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
    289     for (int column = 0;  column < screen_width;  ++column, addr+= /*size of screen-cell*/2) {
    290       const int cell_color_offset = 1;
    291       uint32_t curr = cursor.get();
    292       if (get_or_insert(Memory, addr) == 0 && isspace(curr)) continue;
    293       if (curr == ' ' && color != -1 && color != get_or_insert(Memory, addr+cell_color_offset)) {
    294         // filter out other colors
    295         continue;
    296       }
    297       if (get_or_insert(Memory, addr) != 0 && get_or_insert(Memory, addr) == curr) {
    298         if (color == -1 || color == get_or_insert(Memory, addr+cell_color_offset)) continue;
    299         // contents match but color is off
    300         if (!Hide_errors) cerr << '\n';
    301         raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain '" << unicode_character_at(addr) << "' in color " << color << " instead of " << no_scientific(get_or_insert(Memory, addr+cell_color_offset)) << "\n" << end();
    302         if (!Hide_errors) dump_screen();
    303         if (!Scenario_testing_scenario) Passed = false;
    304         return;
    305       }
    306
    307       // really a mismatch
    308       // can't print multi-byte unicode characters in errors just yet. not very useful for debugging anyway.
    309       char expected_pretty[10] = {0};
    310       if (curr < 256 && !iscntrl(curr)) {
    311         // " ('<curr>')"
    312         expected_pretty[0] = ' ', expected_pretty[1] = '(', expected_pretty[2] = '\'', expected_pretty[3] = static_cast<unsigned char>(curr), expected_pretty[4] = '\'', expected_pretty[5] = ')', expected_pretty[6] = '\0';
    313       }
    314       char actual_pretty[10] = {0};
    315       if (get_or_insert(Memory, addr) < 256 && !iscntrl(get_or_insert(Memory, addr))) {
    316         // " ('<curr>')"
    317         actual_pretty[0] = ' ', actual_pretty[1] = '(', actual_pretty[2] = '\'', actual_pretty[3] = static_cast<unsigned char>(get_or_insert(Memory, addr)), actual_pretty[4] = '\'', actual_pretty[5] = ')', actual_pretty[6] = '\0';
    318       }
    319
    320       ostringstream color_phrase;
    321       if (color != -1) color_phrase << " in color " << color;
    322       if (!Hide_errors) cerr << '\n';
    323       raise << "F - " << maybe(current_recipe_name()) << "expected screen location (" << row << ", " << column << ") to contain " << curr << expected_pretty << color_phrase.str() << " instead of " << no_scientific(get_or_insert(Memory, addr)) << actual_pretty << '\n' << end();
    324       if (!Hide_errors) dump_screen();
    325       if (!Scenario_testing_scenario) Passed = false;
    326       return;
    327     }
    328     if (cursor.get() != '.') {
    329       raise << maybe(current_recipe_name()) << "row " << row << " of the expected screen is too long\n" << end();
    330       if (!Scenario_testing_scenario) Passed = false;
    331       return;
    332     }
    333   }
    334   cursor.skip_whitespace_and_comments();
    335   if (!cursor.at_end()) {
    336     raise << maybe(current_recipe_name()) << "expected screen has too many rows\n" << end();
    337     Passed = false;
    338   }
    339 }
    340
    341 const char* unicode_character_at(int addr) {
    342   int unicode_code_point = static_cast<int>(get_or_insert(Memory, addr));
    343   return to_unicode(unicode_code_point);
    344 }
    345
    346 raw_string_stream::raw_string_stream(const string& backing) :index(0), max(SIZE(backing)), buf(backing.c_str()) {}
    347
    348 bool raw_string_stream::at_end() const {
    349   if (index >= max) return true;
    350   if (tb_utf8_char_length(buf[index]) > max-index) {
    351     raise << "unicode string seems corrupted at index "<< index << " character " << static_cast<int>(buf[index]) << '\n' << end();
    352     return true;
    353   }
    354   return false;
    355 }
    356
    357 uint32_t raw_string_stream::get() {
    358   assert(index < max);  // caller must check bounds before calling 'get'
    359   uint32_t result = 0;
    360   int length = tb_utf8_char_to_unicode(&result, &buf[index]);
    361   assert(length != TB_EOF);
    362   index += length;
    363   return result;
    364 }
    365
    366 uint32_t raw_string_stream::peek() {
    367   assert(index < max);  // caller must check bounds before calling 'get'
    368   uint32_t result = 0;
    369   int length = tb_utf8_char_to_unicode(&result, &buf[index]);
    370   assert(length != TB_EOF);
    371   return result;
    372 }
    373
    374 void raw_string_stream::skip_whitespace_and_comments() {
    375   while (!at_end()) {
    376     if (isspace(peek())) get();
    377     else if (peek() == '#') {
    378       // skip comment
    379       get();
    380       while (peek() != '\n') get();  // implicitly also handles CRLF
    381     }
    382     else break;
    383   }
    384 }
    385
    386 :(before "End Primitive Recipe Declarations")
    387 _DUMP_SCREEN,
    388 :(before "End Primitive Recipe Numbers")
    389 put(Recipe_ordinal, "$dump-screen", _DUMP_SCREEN);
    390 :(before "End Primitive Recipe Checks")
    391 case _DUMP_SCREEN: {
    392   break;
    393 }
    394 :(before "End Primitive Recipe Implementations")
    395 case _DUMP_SCREEN: {
    396   dump_screen();
    397   break;
    398 }
    399
    400 :(code)
    401 void dump_screen() {
    402   int screen_location = get_or_insert(Memory, SCREEN+/*skip address alloc id*/1) + /*skip payload alloc id*/1;
    403   reagent screen("x:screen");  // just to ensure screen.type is reclaimed
    404   int screen_data_location = find_element_location(screen_location, "data", screen.type, "check_screen");  // type: address:array:character
    405   assert(screen_data_location >= 0);
    406 //?   cerr << "screen data is at location " << screen_data_location << '\n';
    407   int screen_data_start = get_or_insert(Memory, screen_data_location+/*skip address alloc id*/1) + /*skip payload alloc id*/1;  // type: array:character
    408 //?   cerr << "screen data start is at " << screen_data_start << '\n';
    409   int screen_width_location = find_element_location(screen_location, "num-columns", screen.type, "check_screen");
    410 //?   cerr << "screen width is at location " << screen_width_location << '\n';
    411   int screen_width = get_or_insert(Memory, screen_width_location);
    412 //?   cerr << "screen width: " << screen_width << '\n';
    413   int screen_height_location = find_element_location(screen_location, "num-rows", screen.type, "check_screen");
    414 //?   cerr << "screen height is at location " << screen_height_location << '\n';
    415   int screen_height = get_or_insert(Memory, screen_height_location);
    416 //?   cerr << "screen height: " << screen_height << '\n';
    417   int top_index_location= find_element_location(screen_location, "top-idx", screen.type, "check_screen");
    418 //?   cerr << "top of screen is at location " << top_index_location << '\n';
    419   int top_index = get_or_insert(Memory, top_index_location);
    420 //?   cerr << "top of screen is index " << top_index << '\n';
    421   for (int i=0, row=top_index/screen_width;  i < screen_height;  ++i, row=(row+1)%screen_height) {
    422     cerr << '.';
    423     int curr = screen_data_start+/*length*/1+row*screen_width* /*size of screen-cell*/2;
    424     for (int col = 0;  col < screen_width;  ++col) {
    425       if (get_or_insert(Memory, curr))
    426         cerr << to_unicode(static_cast<uint32_t>(get_or_insert(Memory, curr)));
    427       else
    428         cerr << ' ';
    429       curr += /*size of screen-cell*/2;
    430     }
    431     cerr << ".\n";
    432   }
    433 }