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}