vt100/test/tests.c

fail line terminal_tests now_ms run_for press type type_at on_screen field wait_for sim_int dump enter_prescription finish paints_figure_a normal_treatment tyler slow_edit_is_safe yakima tyler_with_returns reset_then_reenter reset_and_verification run_job vt_run_tests

1/* Tests for the VT100 console: the terminal on its own, then the whole installation driven2 * by VT100 keystrokes against the real simulator. The scenarios run in parallel (about 40 s). */3#define _POSIX_C_SOURCE 200809L4#include <pthread.h>5#include <stdarg.h>6#include <stdio.h>7#include <stdlib.h>8#include <string.h>9#include <time.h>1011#include "session.h"1213typedef struct {14  const char *name;15  char failure[4096];16} result;1718static void fail(result *r, const char *fmt, ...) {19  if (r->failure[0]) return;20  va_list ap;21  va_start(ap, fmt);22  vsnprintf(r->failure, sizeof r->failure, fmt, ap);23  va_end(ap);24}2526#define CHECK(r, cond, ...)        \27  do {                             \28    if (!(cond)) fail(r, __VA_ARGS__); \29  } while (0)3031/* ---- the terminal on its own ---- */3233static void feed(vt100 *t, const char *s) { vt_receive_buf(t, (const uint8_t *)s, strlen(s)); }3435static const char *line(const vt100 *t, int row, char *buf) {36  char all[VT_ROWS * (VT_COLS + 1) + 1];37  vt_text(t, all);38  memcpy(buf, all + row * (VT_COLS + 1), VT_COLS);39  buf[VT_COLS] = 0;40  return buf;41}4243static void terminal_tests(result *r) {44  static vt100 t; /* big */45  char buf[VT_COLS + 1];46  uint8_t tx[64];47  int n;4849  vt_init(&t);50  feed(&t, "\033[3;5Hhello");51  CHECK(r, strncmp(line(&t, 2, buf) + 4, "hello", 5) == 0, "CUP: got [%s]", buf);52  CHECK(r, t.row == 2 && t.col == 9, "cursor after text at %d,%d", t.row, t.col);53  feed(&t, "\033[1;7H\033[K");54  CHECK(r, strncmp(line(&t, 2, buf) + 4, "hello", 5) == 0, "EL on another line");55  feed(&t, "\033[3;7H\033[K");56  CHECK(r, strncmp(line(&t, 2, buf) + 4, "he   ", 5) == 0, "EL 0: got [%s]", buf);57  feed(&t, "\033[2J");58  CHECK(r, strspn(line(&t, 2, buf), " ") == VT_COLS, "ED 2 left [%s]", buf);5960  /* the VT100 stays on the last column and wraps with the next character, if auto wrap is on */61  feed(&t, "\033[?7h\033[1;80Hab");62  CHECK(r, line(&t, 0, buf)[79] == 'a' && line(&t, 1, buf)[0] == 'b', "auto wrap");63  feed(&t, "\033[?7l\033[5;80Hab");64  CHECK(r, line(&t, 4, buf)[79] == 'b' && line(&t, 5, buf)[0] == ' ', "no auto wrap: overwrite");6566  /* scrolling region and index */67  feed(&t, "\033[2J\033[1;1Htop\033[24;1Hbottom\n");68  CHECK(r, strncmp(line(&t, 22, buf), "bottom", 6) == 0, "LF at the bottom scrolls");69  CHECK(r, line(&t, 0, buf)[0] == ' ', "top line scrolled away");7071  /* special graphics: 'q' is the horizontal line (ROM 0x12) */72  feed(&t, "\033[2J\033[H\033(0q\033(Bq");73  CHECK(r, t.cells[0][0].glyph == 0x12 && t.cells[0][1].glyph == 'q', "special graphics");7475  /* answers */76  feed(&t, "\033[5;10H\033[6n");77  n = vt_take_tx(&t, tx, sizeof tx);78  CHECK(r, n == 7 && memcmp(tx, "\033[5;10R", 7) == 0, "CPR answer");79  feed(&t, "\033[c");80  n = vt_take_tx(&t, tx, sizeof tx);81  CHECK(r, n == 7 && memcmp(tx, "\033[?1;2c", 7) == 0, "DA answer");8283  /* raster: '|' is ROM 0x10 on rows 0-2: dot 3, stretched into dot 4, from scan 1 */84  vt_init(&t);85  t.block_cursor = false;86  feed(&t, "|\033[5;1H");87  vt_render(&t, 1000.0);88  CHECK(r, t.raster[0][3] == 0, "scan 0 shows ROM row 15 (blank for '|')");89  CHECK(r, t.raster[1][2] == 0 && t.raster[1][3] == 2 && t.raster[1][4] == 2 && t.raster[1][5] == 0,90        "dot stretcher: got %d %d %d %d", t.raster[1][2], t.raster[1][3], t.raster[1][4], t.raster[1][5]);91  /* a horizontal line character fills its whole cell width (last dot replicated) and joins up */92  feed(&t, "\033[1;3H\033(0qq\033(B");93  vt_render(&t, 1000.0);94  int y = -1;95  for (int s = 0; s < 10; s++)96    if (t.raster[s][25]) y = s;97  CHECK(r, y >= 0, "graphics line not drawn");98  if (y >= 0)99    for (int x = 20; x < 40; x++) CHECK(r, t.raster[y][x] == 2, "line gap at dot %d", x);100  /* underline: the ninth scan */101  feed(&t, "\033[2;1H\033[4m \033[m");102  vt_render(&t, 1000.0);103  CHECK(r, t.raster[10 + 8][0] == 2 && t.raster[10 + 8][9] == 2 && t.raster[10 + 7][0] == 0, "underline scan");104  /* bold is the bright level */105  feed(&t, "\033[3;1H\033[1m|\033[m");106  vt_render(&t, 1000.0);107  CHECK(r, t.raster[21][3] == 3, "bold intensity %d", t.raster[21][3]);108109  /* keyboard */110  vt_init(&t);111  vt_key(&t, VK_UP, true, 0);112  vt_key(&t, VK_UP, false, 1);113  n = vt_take_tx(&t, tx, sizeof tx);114  CHECK(r, n == 3 && memcmp(tx, "\033[A", 3) == 0, "up arrow");115  feed(&t, "\033[?1h");116  vt_key(&t, VK_UP, true, 0);117  vt_key(&t, VK_UP, false, 1);118  n = vt_take_tx(&t, tx, sizeof tx);119  CHECK(r, n == 3 && memcmp(tx, "\033OA", 3) == 0, "up arrow in cursor key mode");120  vt_key(&t, VK_BACKSPACE, true, 0);121  vt_key(&t, VK_DELETE, true, 0);122  vt_key(&t, VK_LINEFEED, true, 0);123  vt_key(&t, VK_SHIFT, true, 0);124  vt_key(&t, 'x', true, 0);125  vt_key(&t, VK_SHIFT, false, 0);126  vt_key(&t, VK_CTRL, true, 0);127  vt_key(&t, 'c', true, 0);128  vt_key(&t, VK_CTRL, false, 0);129  n = vt_take_tx(&t, tx, sizeof tx);130  CHECK(r, n == 5 && memcmp(tx, "\b\x7f\nX\x03", 5) == 0, "BS DEL LF shift ctrl");131  CHECK(r, t.clicks == 7, "keyclicks: %d", t.clicks);132  /* auto repeat: half a second, then 30 a second; RETURN never repeats */133  vt_init(&t);134  vt_key(&t, 'a', true, 0);135  vt_tick(&t, 499);136  CHECK(r, t.tx_len == 1, "repeat too early");137  vt_tick(&t, 500);138  CHECK(r, t.tx_len == 2, "no repeat after 0.5 s");139  for (double ms = 500; ms <= 1500; ms += 16.7) vt_tick(&t, ms); /* called once a frame */140  CHECK(r, t.tx_len >= 31 && t.tx_len <= 33, "repeat rate: %d in 1.5 s", t.tx_len);141  vt_key(&t, 'a', false, 1500);142  vt_take_tx(&t, tx, sizeof tx);143  vt_take_tx(&t, tx, sizeof tx);144  vt_key(&t, VK_RETURN, true, 2000);145  vt_tick(&t, 4000);146  CHECK(r, t.tx_len == 1, "RETURN repeated");147  feed(&t, "\a");148  CHECK(r, t.bells == 1, "bell");149}150151/* ---- the whole installation ---- */152153static double now_ms(void) {154  struct timespec ts;155  clock_gettime(CLOCK_MONOTONIC, &ts);156  return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;157}158159static void run_for(session *s, double ms) {160  double end = now_ms() + ms;161  struct timespec pause = {0, 2 * 1000 * 1000};162  do {163    session_step(s, now_ms(), (long long)time(NULL));164    nanosleep(&pause, NULL);165  } while (now_ms() < end);166}167168static void press(session *s, int key) {169  session_key(s, key, true, now_ms());170  run_for(s, 15);171  session_key(s, key, false, now_ms());172  run_for(s, 15);173}174175/* types a string on the VT100 keyboard; \r is RETURN */176static void type(session *s, const char *str) {177  for (; *str; str++) {178    char ch = *str;179    if (ch == '\r') press(s, VK_RETURN);180    else if (ch >= 'A' && ch <= 'Z') {181      session_key(s, VK_SHIFT, true, now_ms());182      press(s, ch - 'A' + 'a');183      session_key(s, VK_SHIFT, false, now_ms());184    } else press(s, ch);185  }186}187188/* the same at a person's pace: a key every gap_ms */189static void type_at(session *s, const char *str, double gap_ms) {190  for (; *str; str++) {191    char one[2] = {*str, 0};192    type(s, one);193    run_for(s, gap_ms - 30);194  }195}196197static void screen(session *s, char *out) { vt_text(&s->term, out); }198199static bool on_screen(session *s, const char *what) {200  static __thread char buf[VT_ROWS * (VT_COLS + 1) + 1];201  screen(s, buf);202  return strstr(buf, what) != NULL;203}204205/* the text after a label on the screen, up to two spaces */206static void field(session *s, const char *label, char *out, size_t n) {207  char buf[VT_ROWS * (VT_COLS + 1) + 1];208  screen(s, buf);209  const char *p = strstr(buf, label);210  out[0] = 0;211  if (!p) return;212  p += strlen(label);213  while (*p == ' ') p++;214  size_t i = 0;215  while (p[i] && p[i] != '\n' && !(p[i] == ' ' && p[i + 1] == ' ') && i + 1 < n) {216    out[i] = p[i];217    i++;218  }219  out[i] = 0;220}221222static bool wait_for(session *s, const char *what, double timeout_ms) {223  double end = now_ms() + timeout_ms;224  while (now_ms() < end) {225    if (on_screen(s, what)) return true;226    run_for(s, 20);227  }228  return on_screen(s, what);229}230231static int sim_int(session *s, StateInfoRequest q) {232  char *p = request_state_info(s->machine, q);233  int v = atoi(p);234  free_state_info(p);235  return v;236}237238static void dump(session *s, result *r) {239  char buf[VT_ROWS * (VT_COLS + 1) + 1];240  screen(s, buf);241  size_t n = strlen(r->failure);242  snprintf(r->failure + n, sizeof r->failure - n, "\n--- screen ---\n%s", buf);243}244245/* fills in the whole prescription from the patient name down to the command line */246static void enter_prescription(session *s, char beam) {247  type(s, "TEST\r\r"); /* name, treatment mode FIX */248  char b[3] = {beam, '\r', 0};249  type(s, b);250  type(s, "\r");     /* energy: 25 */251  type(s, "200\r");  /* unit rate/minute */252  type(s, "202\r");  /* monitor units */253  type(s, "1\r");    /* time (min) */254  type(s, "\r\r\r\r\r\r"); /* "a carriage return to merely copy the treatment site data" */255}256257/* press P through the machine's nuisance pauses until the treatment ends */258static bool finish(session *s) {259  for (int i = 0; i < 10; i++) {260    double end = now_ms() + 3000;261    while (now_ms() < end && !on_screen(s, "TREAT PAUSE") && !on_screen(s, "TERMINATE") &&262           !on_screen(s, "TREAT SUSPEND"))263      run_for(s, 20);264    if (on_screen(s, "TERMINATE") || on_screen(s, "TREAT SUSPEND")) return true;265    if (!on_screen(s, "TREAT PAUSE")) return false;266    type(s, "P\r");267    run_for(s, 400);268  }269  return false;270}271272static void paints_figure_a(result *r) {273  static session s;274  session_init(&s, 9600, now_ms());275  double start = now_ms();276  CHECK(r, wait_for(&s, "COMMAND:", 5000), "screen never painted");277  double took = now_ms() - start;278  /* about a screenful at 960 characters a second */279  CHECK(r, took > 500, "painted too fast for 9600 baud: %.0f ms", took);280  const char *labels[] = {"PATIENT NAME  :", "TREATMENT MODE: FIX", "BEAM TYPE:", "ENERGY (KeV):",281                          "ACTUAL", "PRESCRIBED", "UNIT RATE/MINUTE", "MONITOR UNITS", "TIME (MIN)",282                          "GANTRY ROTATION (DEG)", "COLLIMATOR ROTATION (DEG)", "COLLIMATOR X (CM)",283                          "COLLIMATOR Y (CM)", "WEDGE NUMBER", "ACCESSORY NUMBER", "DATE  :", "TIME  :",284                          "OPR ID: T25V02-R03", "SYSTEM:", "TREAT :", "REASON: OPERATOR",285                          "OP.MODE: TREAT", "AUTO", "173777", "COMMAND:"};286  for (size_t i = 0; i < sizeof labels / sizeof *labels; i++)287    CHECK(r, on_screen(&s, labels[i]), "missing %s", labels[i]);288  char buf[VT_COLS + 1];289  CHECK(r, strncmp(line(&s.term, 23, buf) + 52, "COMMAND:", 8) == 0, "command line not at lower right");290  /* once a second the clock is rewritten and the cursor makes a trip down there and back */291  for (int i = 0; i < 100 && !(s.term.row == 1 && s.term.col == 18); i++) run_for(&s, 10);292  CHECK(r, s.term.row == 1 && s.term.col == 18, "cursor should wait on the patient name, at %d,%d", s.term.row,293        s.term.col);294  if (r->failure[0]) dump(&s, r);295}296297static void normal_treatment(result *r) {298  static session s;299  char buf[64];300  session_init(&s, 9600, now_ms());301  wait_for(&s, "COMMAND:", 5000);302  enter_prescription(&s, 'X');303  CHECK(r, on_screen(&s, "X-RAY"), "beam type not shown");304  for (int i = 0; i < 6; i++) CHECK(r, on_screen(&s, "VERIFIED"), "treatment site not verified");305  CHECK(r, wait_for(&s, "BEAM READY", 15000), "no BEAM READY");306  run_for(&s, 1000);307  CHECK(r, sim_int(&s, RequestPatientDose) == 0, "beam came on without B");308  type(&s, "B\r");309  CHECK(r, finish(&s), "treatment did not finish");310  field(&s, "MONITOR UNITS", buf, sizeof buf);311  CHECK(r, strcmp(buf, "202 202") == 0, "dose monitor should read the prescribed 202 MU: [%s]", buf);312  CHECK(r, sim_int(&s, RequestPatientDose) == 202, "patient dose %d", sim_int(&s, RequestPatientDose));313  if (r->failure[0]) dump(&s, r);314}315316/* East Texas Cancer Center: "she had typed 'x' (for X ray) when she had intended 'e' ... she317 * merely used the cursor up key to edit the mode entry ... she hit the return key several times318 * ... the terminal displayed 'beam ready' ... She hit the one-key command 'B'" */319static void tyler(result *r) {320  static session s;321  char buf[64];322  session_init(&s, 9600, now_ms());323  wait_for(&s, "COMMAND:", 5000);324  type(&s, "TEST\r\r");325  double entered = now_ms();326  type(&s, "x\r");327  type(&s, "\r200\r202\r1\r\r\r\r\r\r\r");328  /* past the first magnet (2 s), well inside the 8 s it takes to set them all */329  while (now_ms() < entered + 3000) run_for(&s, 20);330  for (int i = 0; i < 11; i++) press(&s, VK_UP);331  field(&s, "BEAM TYPE:", buf, sizeof buf);332  for (int i = 0; i < 30 && !(s.term.row == 2 && s.term.col == 42); i++) run_for(&s, 10);333  CHECK(r, s.term.row == 2 && s.term.col == 42, "cursor should be on the beam type, at %d,%d", s.term.row,334        s.term.col);335  type(&s, "e\r");336  type(&s, "\r\r\r\r\r\r\r\r\r\r");337  CHECK(r, now_ms() < entered + 7500, "the edit took too long for the race: %.0f ms", now_ms() - entered);338  CHECK(r, on_screen(&s, "BEAM TYPE: E") && on_screen(&s, "ELECTRON"), "the screen should show the edit");339  CHECK(r, wait_for(&s, "BEAM READY", 15000), "no BEAM READY");340  type(&s, "B\r");341  CHECK(r, wait_for(&s, "MALFUNCTION 54", 3000), "no Malfunction 54");342  CHECK(r, on_screen(&s, "TREAT PAUSE"), "should be a treatment pause");343  field(&s, "MONITOR UNITS", buf, sizeof buf);344  CHECK(r, strcmp(buf, "6 6") == 0, "dose monitor should read 6 MU: [%s]", buf);345  CHECK(r, sim_int(&s, RequestPatientDose) >= 16500, "no overdose: %d", sim_int(&s, RequestPatientDose));346  /* "She immediately took the normal action ... which was to hit the 'P' key" */347  type(&s, "P\r");348  run_for(&s, 500);349  CHECK(r, on_screen(&s, "MALFUNCTION 54"), "P should repeat it");350  CHECK(r, sim_int(&s, RequestPatientDose) >= 33000, "second overdose missing");351  if (r->failure[0]) dump(&s, r);352}353354/* The same edit, made slowly, is caught; the edit keys also execute B (the FDA's complaint) */355static void slow_edit_is_safe(result *r) {356  static session s;357  session_init(&s, 9600, now_ms());358  wait_for(&s, "COMMAND:", 5000);359  enter_prescription(&s, 'X');360  CHECK(r, wait_for(&s, "BEAM READY", 15000), "no BEAM READY");361  for (int i = 0; i < 11; i++) press(&s, VK_UP);362  type(&s, "e\r\r\r\r\r\r\r\r\r\r\r");363  run_for(&s, 500);364  CHECK(r, !on_screen(&s, "BEAM READY"), "an edit should take BEAM READY away");365  CHECK(r, wait_for(&s, "BEAM READY", 15000), "no BEAM READY after the edit");366  type(&s, "B");367  press(&s, VK_DOWN); /* "the normal edit keys (down arrow ...) will be interpreted as a CR" */368  CHECK(r, finish(&s), "treatment did not finish");369  CHECK(r, sim_int(&s, RequestPatientDose) == 202, "expected a normal 202, got %d",370        sim_int(&s, RequestPatientDose));371  if (r->failure[0]) dump(&s, r);372}373374/* Yakima: field light from the hand control, then "set" typed at the console at the moment375 * Class3 rolls over */376static void yakima(result *r) {377  static session s;378  char buf[64];379  session_init(&s, 9600, now_ms());380  wait_for(&s, "COMMAND:", 5000);381  enter_prescription(&s, 'X');382  session_hand(&s, HAND_FIELD_LIGHT);383  CHECK(r, wait_for(&s, "PRESS SET BUTTON", 15000), "no PRESS SET BUTTON");384  double end = now_ms() + 45000; /* Class3 counts ten a second, once set-up test starts */385  int c3 = 0;386  while (now_ms() < end && !((c3 = sim_int(&s, RequestClass3)) >= 244 && c3 <= 249)) run_for(&s, 10);387  CHECK(r, c3 >= 244 && c3 <= 249, "Class3 never came round");388  type(&s, "SET\r");389  CHECK(r, wait_for(&s, "BEAM READY", 5000), "no BEAM READY (Class3 was %d)", c3);390  type(&s, "B\r");391  CHECK(r, wait_for(&s, "FLATNESS", 3000), "no FLATNESS");392  field(&s, "MONITOR UNITS", buf, sizeof buf);393  CHECK(r, strcmp(buf, "0 0") == 0, "\"the console displayed no dose\": [%s]", buf);394  CHECK(r, sim_int(&s, RequestPatientDose) >= 4000, "no overdose");395  if (r->failure[0]) dump(&s, r);396}397398/* The page's recipe: nothing but RETURN and cursor up after the X, four keys a second */399static void tyler_with_returns(result *r) {400  static session s;401  session_init(&s, 9600, now_ms());402  wait_for(&s, "COMMAND:", 5000);403  type_at(&s, "TEST\r\r", 250);404  double entered = now_ms();405  type_at(&s, "x\r\r\r\r\r\r\r\r\r\r\r", 250); /* RETURN copies the plan and the room */406  CHECK(r, on_screen(&s, "202") && s.term.row == 23, "RETURNs should reach COMMAND with the plan filled in");407  for (int i = 0; i < 11; i++) {408    press(&s, VK_UP);409    run_for(&s, 220);410  }411  type_at(&s, "e\r", 250);412  CHECK(r, now_ms() < entered + 7500, "the recipe took too long: %.0f ms", now_ms() - entered);413  type_at(&s, "\r\r\r\r\r\r\r\r\r\r", 250); /* back down to COMMAND, no hurry now */414  CHECK(r, wait_for(&s, "BEAM READY", 15000), "no BEAM READY");415  type(&s, "B\r");416  CHECK(r, wait_for(&s, "MALFUNCTION 54", 3000), "no Malfunction 54");417  if (r->failure[0]) dump(&s, r);418}419420/* R while the magnets are being set, then the prescription again at once: the machine resets421 * when the magnets are done, and must keep what was typed since */422static void reset_then_reenter(result *r) {423  static session s;424  session_init(&s, 9600, now_ms());425  wait_for(&s, "COMMAND:", 5000);426  type(&s, "TEST\r\rx\r\r\r\r\r\r\r\r\r\r\r");427  run_for(&s, 500);428  type(&s, "R\r");429  type(&s, "\r\rx\r\r\r\r\r\r\r\r\r\r\r");430  CHECK(r, wait_for(&s, "BEAM READY", 25000), "stuck after R and re-entry");431  if (r->failure[0]) dump(&s, r);432}433434static void reset_and_verification(result *r) {435  static session s;436  char buf[64];437  session_init(&s, 0, now_ms());438  wait_for(&s, "COMMAND:", 5000);439  type(&s, "TEST\r\rx\r\r200\r202\r1\r");440  type(&s, "90\r"); /* gantry: the room is set to 0 */441  type(&s, "\r\r\r\r\r");442  char row[VT_COLS + 1];443  line(&s.term, 9, row);444  CHECK(r, strstr(row, "GANTRY") && strstr(row, "90") && !strstr(row, "VERIFIED"), "a mismatch must not verify: [%s]",445        row);446  line(&s.term, 10, row);447  CHECK(r, strstr(row, "VERIFIED"), "the copied row should verify: [%s]", row);448  CHECK(r, wait_for(&s, "SET-UP DONE", 15000), "machine should still get to set-up done");449  CHECK(r, !on_screen(&s, "BEAM READY"), "no BEAM READY while unverified");450  int bells = s.term.bells;451  type(&s, "B\r");452  run_for(&s, 300);453  CHECK(r, s.term.bells > bells, "B should ring the bell");454  CHECK(r, sim_int(&s, RequestPatientDose) == 0, "B fired while unverified");455  type(&s, "R\r");456  run_for(&s, 500);457  field(&s, "BEAM TYPE:", buf, sizeof buf);458  CHECK(r, buf[0] == 'E' && buf[1] == 'N', "R should clear the beam type, got [%s]", buf);459  CHECK(r, on_screen(&s, "PATIENT NAME  : TEST"), "R keeps the patient");460  CHECK(r, on_screen(&s, "DATA ENTRY"), "R should go back to data entry");461  if (r->failure[0]) dump(&s, r);462}463464typedef struct {465  void (*fn)(result *);466  result res;467} job;468469static void *run_job(void *p) {470  job *j = p;471  j->fn(&j->res);472  return NULL;473}474475int vt_run_tests(void) {476  job jobs[] = {477      {terminal_tests, {"terminal: control sequences, raster, keyboard", {0}}},478      {paints_figure_a, {"console paints Figure A at 9600 baud", {0}}},479      {normal_treatment, {"normal treatment: BEAM READY waits for B, prescribed MU delivered", {0}}},480      {tyler, {"Tyler: cursor-up edit within 8 s gives MALFUNCTION 54, 6 MU, overdose", {0}}},481      {slow_edit_is_safe, {"a slow edit is caught; B plus down arrow fires", {0}}},482      {yakima, {"Yakima: SET as Class3 rolls over gives FLATNESS, no dose shown", {0}}},483      {reset_and_verification, {"unverified rows block B; R clears the prescription", {0}}},484      {tyler_with_returns, {"Tyler with RETURNs only, at four keys a second", {0}}},485      {reset_then_reenter, {"R during the magnets, then re-entry at once, gets to BEAM READY", {0}}},486  };487  size_t n = sizeof jobs / sizeof *jobs;488  pthread_t th[16];489  for (size_t i = 0; i < n; i++) pthread_create(&th[i], NULL, run_job, &jobs[i]);490  int failures = 0;491  for (size_t i = 0; i < n; i++) {492    pthread_join(th[i], NULL);493    if (jobs[i].res.failure[0]) {494      failures++;495      printf("FAIL %s: %s\n", jobs[i].res.name, jobs[i].res.failure);496    } else {497      printf("ok   %s\n", jobs[i].res.name);498    }499  }500  fflush(stdout);501  return failures;502}