*** /dev/null --- b/src/test/isolation/expected/fk-contention.out *************** *** 0 **** --- 1,16 ---- + Parsed test spec with 2 sessions + + starting permutation: ins com upd + step ins: INSERT INTO bar VALUES (42); + step com: COMMIT; + step upd: UPDATE foo SET b = 'Hello World'; + + starting permutation: ins upd com + step ins: INSERT INTO bar VALUES (42); + step upd: UPDATE foo SET b = 'Hello World'; + step com: COMMIT; + + starting permutation: upd ins com + step upd: UPDATE foo SET b = 'Hello World'; + step ins: INSERT INTO bar VALUES (42); + step com: COMMIT; *** /dev/null --- b/src/test/isolation/expected/fk-deadlock.out *************** *** 0 **** --- 1,63 ---- + Parsed test spec with 2 sessions + + starting permutation: s1i s1u s1c s2i s2u s2c + step s1i: INSERT INTO child VALUES (1, 1); + step s1u: UPDATE parent SET aux = 'bar'; + step s1c: COMMIT; + step s2i: INSERT INTO child VALUES (2, 1); + step s2u: UPDATE parent SET aux = 'baz'; + step s2c: COMMIT; + + starting permutation: s1i s1u s2i s1c s2u s2c + step s1i: INSERT INTO child VALUES (1, 1); + step s1u: UPDATE parent SET aux = 'bar'; + step s2i: INSERT INTO child VALUES (2, 1); + step s1c: COMMIT; + step s2i: <... completed> + step s2u: UPDATE parent SET aux = 'baz'; + step s2c: COMMIT; + + starting permutation: s1i s2i s1u s2u s1c s2c + step s1i: INSERT INTO child VALUES (1, 1); + step s2i: INSERT INTO child VALUES (2, 1); + step s1u: UPDATE parent SET aux = 'bar'; + step s2u: UPDATE parent SET aux = 'baz'; + step s1c: COMMIT; + step s2u: <... completed> + step s2c: COMMIT; + + starting permutation: s1i s2i s2u s1u s2c s1c + step s1i: INSERT INTO child VALUES (1, 1); + step s2i: INSERT INTO child VALUES (2, 1); + step s2u: UPDATE parent SET aux = 'baz'; + step s1u: UPDATE parent SET aux = 'bar'; + step s2c: COMMIT; + step s1u: <... completed> + step s1c: COMMIT; + + starting permutation: s2i s1i s1u s2u s1c s2c + step s2i: INSERT INTO child VALUES (2, 1); + step s1i: INSERT INTO child VALUES (1, 1); + step s1u: UPDATE parent SET aux = 'bar'; + step s2u: UPDATE parent SET aux = 'baz'; + step s1c: COMMIT; + step s2u: <... completed> + step s2c: COMMIT; + + starting permutation: s2i s1i s2u s1u s2c s1c + step s2i: INSERT INTO child VALUES (2, 1); + step s1i: INSERT INTO child VALUES (1, 1); + step s2u: UPDATE parent SET aux = 'baz'; + step s1u: UPDATE parent SET aux = 'bar'; + step s2c: COMMIT; + step s1u: <... completed> + step s1c: COMMIT; + + starting permutation: s2i s2u s1i s2c s1u s1c + step s2i: INSERT INTO child VALUES (2, 1); + step s2u: UPDATE parent SET aux = 'baz'; + step s1i: INSERT INTO child VALUES (1, 1); + step s2c: COMMIT; + step s1i: <... completed> + step s1u: UPDATE parent SET aux = 'bar'; + step s1c: COMMIT; *** /dev/null --- b/src/test/isolation/expected/fk-deadlock2.out *************** *** 0 **** --- 1,106 ---- + Parsed test spec with 2 sessions + + starting permutation: s1u1 s1u2 s1c s2u1 s2u2 s2c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1c: COMMIT; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2c: COMMIT; + + starting permutation: s1u1 s1u2 s2u1 s1c s2u2 s2c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1c: COMMIT; + step s2u1: <... completed> + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2c: COMMIT; + + starting permutation: s1u1 s2u1 s1u2 s2u2 s1c s2c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: <... completed> + ERROR: deadlock detected + step s1c: COMMIT; + step s2c: COMMIT; + + starting permutation: s1u1 s2u1 s1u2 s2u2 s2c s1c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: <... completed> + ERROR: deadlock detected + step s2c: COMMIT; + step s1c: COMMIT; + + starting permutation: s1u1 s2u1 s2u2 s1u2 s1c s2c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: <... completed> + ERROR: deadlock detected + step s1c: COMMIT; + step s2c: COMMIT; + + starting permutation: s1u1 s2u1 s2u2 s1u2 s2c s1c + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: <... completed> + ERROR: deadlock detected + step s2c: COMMIT; + step s1c: COMMIT; + + starting permutation: s2u1 s1u1 s1u2 s2u2 s1c s2c + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: <... completed> + ERROR: deadlock detected + step s1c: COMMIT; + step s2c: COMMIT; + + starting permutation: s2u1 s1u1 s1u2 s2u2 s2c s1c + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: <... completed> + ERROR: deadlock detected + step s2c: COMMIT; + step s1c: COMMIT; + + starting permutation: s2u1 s1u1 s2u2 s1u2 s1c s2c + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: <... completed> + ERROR: deadlock detected + step s1c: COMMIT; + step s2c: COMMIT; + + starting permutation: s2u1 s1u1 s2u2 s1u2 s2c s1c + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: <... completed> + ERROR: deadlock detected + step s2c: COMMIT; + step s1c: COMMIT; + + starting permutation: s2u1 s2u2 s1u1 s2c s1u2 s1c + step s2u1: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s2u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1u1: UPDATE A SET Col1 = 1 WHERE AID = 1; + step s2c: COMMIT; + step s1u2: UPDATE B SET Col2 = 1 WHERE BID = 2; + step s1c: COMMIT; *** a/src/test/isolation/isolation_schedule --- b/src/test/isolation/isolation_schedule *************** *** 9,11 **** test: ri-trigger --- 9,14 ---- test: partial-index test: two-ids test: multiple-row-versions + test: fk-contention + test: fk-deadlock + test: fk-deadlock2 *** a/src/test/isolation/isolationtester.c --- b/src/test/isolation/isolationtester.c *************** *** 9,23 **** #include #endif #include #include #include #include - #include "libpq-fe.h" #include "isolationtester.h" static PGconn **conns = NULL; static int nconns = 0; static void run_all_permutations(TestSpec *testspec); --- 9,36 ---- #include #endif + #include + #include #include #include #include #include + #ifdef HAVE_SYS_SELECT_H + #include + #endif + + #include "libpq-fe.h" #include "isolationtester.h" + #define PREP_WAITING "isolationtester_waiting" + + /* + * conns[0] is the global setup, teardown, and watchdog connection. Additional + * connections represent spec-defined sessions. + */ static PGconn **conns = NULL; + static const char **backend_ids = NULL; static int nconns = 0; static void run_all_permutations(TestSpec *testspec); *************** *** 25,30 **** static void run_all_permutations_recurse(TestSpec *testspec, int nsteps, Step ** --- 38,47 ---- static void run_named_permutations(TestSpec *testspec); static void run_permutation(TestSpec *testspec, int nsteps, Step **steps); + #define STEP_NONBLOCK 0x1 /* return 0 as soon as cmd waits for a lock */ + #define STEP_RETRY 0x2 /* this is a retry of a previously-waiting cmd */ + static int try_complete_step(Step *step, int flags); + static int step_qsort_cmp(const void *a, const void *b); static int step_bsearch_cmp(const void *a, const void *b); *************** *** 46,51 **** main(int argc, char **argv) --- 63,69 ---- const char *conninfo; TestSpec *testspec; int i; + PGresult *res; /* * If the user supplies a parameter on the command line, use it as the *************** *** 63,75 **** main(int argc, char **argv) testspec = &parseresult; printf("Parsed test spec with %d sessions\n", testspec->nsessions); ! /* Establish connections to the database, one for each session */ ! nconns = testspec->nsessions; conns = calloc(nconns, sizeof(PGconn *)); ! for (i = 0; i < testspec->nsessions; i++) { - PGresult *res; - conns[i] = PQconnectdb(conninfo); if (PQstatus(conns[i]) != CONNECTION_OK) { --- 81,95 ---- testspec = &parseresult; printf("Parsed test spec with %d sessions\n", testspec->nsessions); ! /* ! * Establish connections to the database, one for each session and an extra ! * for lock wait detection and global work. ! */ ! nconns = 1 + testspec->nsessions; conns = calloc(nconns, sizeof(PGconn *)); ! backend_ids = calloc(nconns, sizeof(*backend_ids)); ! for (i = 0; i < nconns; i++) { conns[i] = PQconnectdb(conninfo); if (PQstatus(conns[i]) != CONNECTION_OK) { *************** *** 89,94 **** main(int argc, char **argv) --- 109,136 ---- exit_nicely(); } PQclear(res); + + /* Get the backend ID for lock wait checking. */ + res = PQexec(conns[i], "SELECT i FROM pg_stat_get_backend_idset() t(i) " + "WHERE pg_stat_get_backend_pid(i) = pg_backend_pid()"); + if (PQresultStatus(res) == PGRES_TUPLES_OK) + { + if (PQntuples(res) == 1 && PQnfields(res) == 1) + backend_ids[i] = strdup(PQgetvalue(res, 0, 0)); + else + { + fprintf(stderr, "backend id query returned %d rows and %d columns, expected 1 row and 1 column", + PQntuples(res), PQnfields(res)); + exit_nicely(); + } + } + else + { + fprintf(stderr, "backend id query failed: %s", + PQerrorMessage(conns[i])); + exit_nicely(); + } + PQclear(res); } /* Set the session index fields in steps. */ *************** *** 100,105 **** main(int argc, char **argv) --- 142,157 ---- session->steps[stepindex]->session = i; } + res = PQprepare(conns[0], PREP_WAITING, + "SELECT 1 WHERE pg_stat_get_backend_waiting($1)", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "prepare of lock wait query failed: %s", + PQerrorMessage(conns[0])); + exit_nicely(); + } + PQclear(res); + /* * Run the permutations specified in the spec, or all if none were * explicitly specified. *************** *** 254,259 **** run_permutation(TestSpec *testspec, int nsteps, Step **steps) --- 306,312 ---- { PGresult *res; int i; + Step *waiting = NULL; printf("\nstarting permutation:"); for (i = 0; i < nsteps; i++) *************** *** 277,288 **** run_permutation(TestSpec *testspec, int nsteps, Step **steps) { if (testspec->sessions[i]->setupsql) { ! res = PQexec(conns[i], testspec->sessions[i]->setupsql); if (PQresultStatus(res) != PGRES_COMMAND_OK) { fprintf(stderr, "setup of session %s failed: %s", testspec->sessions[i]->name, ! PQerrorMessage(conns[0])); exit_nicely(); } PQclear(res); --- 330,341 ---- { if (testspec->sessions[i]->setupsql) { ! res = PQexec(conns[i+1], testspec->sessions[i]->setupsql); if (PQresultStatus(res) != PGRES_COMMAND_OK) { fprintf(stderr, "setup of session %s failed: %s", testspec->sessions[i]->name, ! PQerrorMessage(conns[i+1])); exit_nicely(); } PQclear(res); *************** *** 293,334 **** run_permutation(TestSpec *testspec, int nsteps, Step **steps) for (i = 0; i < nsteps; i++) { Step *step = steps[i]; - printf("step %s: %s\n", step->name, step->sql); - res = PQexec(conns[step->session], step->sql); ! switch(PQresultStatus(res)) { ! case PGRES_COMMAND_OK: ! break; ! ! case PGRES_TUPLES_OK: ! printResultSet(res); ! break; ! case PGRES_FATAL_ERROR: ! /* Detail may contain xid values, so just show primary. */ ! printf("%s: %s\n", PQresultErrorField(res, PG_DIAG_SEVERITY), ! PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY)); ! break; ! default: ! printf("unexpected result status: %s\n", ! PQresStatus(PQresultStatus(res))); } ! PQclear(res); } /* Perform per-session teardown */ for (i = 0; i < testspec->nsessions; i++) { if (testspec->sessions[i]->teardownsql) { ! res = PQexec(conns[i], testspec->sessions[i]->teardownsql); if (PQresultStatus(res) != PGRES_COMMAND_OK) { fprintf(stderr, "teardown of session %s failed: %s", testspec->sessions[i]->name, ! PQerrorMessage(conns[0])); /* don't exit on teardown failure */ } PQclear(res); --- 346,387 ---- for (i = 0; i < nsteps; i++) { Step *step = steps[i]; ! if (!PQsendQuery(conns[1 + step->session], step->sql)) { ! fprintf(stdout, "failed to send query: %s\n", ! PQerrorMessage(conns[1 + step->session])); ! exit_nicely(); ! } ! if (waiting != NULL) ! { ! /* Some other step is already waiting: just block. */ ! try_complete_step(step, 0); ! /* See if this step unblocked the waiting step. */ ! if (try_complete_step(waiting, STEP_NONBLOCK | STEP_RETRY)) ! waiting = NULL; } ! else if (!try_complete_step(step, STEP_NONBLOCK)) ! waiting = step; } + /* Finish any waiting query. */ + if (waiting != NULL) + try_complete_step(waiting, STEP_RETRY); + /* Perform per-session teardown */ for (i = 0; i < testspec->nsessions; i++) { if (testspec->sessions[i]->teardownsql) { ! res = PQexec(conns[i+1], testspec->sessions[i]->teardownsql); if (PQresultStatus(res) != PGRES_COMMAND_OK) { fprintf(stderr, "teardown of session %s failed: %s", testspec->sessions[i]->name, ! PQerrorMessage(conns[i+1])); /* don't exit on teardown failure */ } PQclear(res); *************** *** 350,355 **** run_permutation(TestSpec *testspec, int nsteps, Step **steps) --- 403,507 ---- } } + /* + * Our caller already sent the query associated with this step. Wait for it to + * either complete or (only when given the STEP_NONBLOCK flag) to block while + * waiting for a lock. We assume that any lock wait will persist until we have + * executed additional steps in the permutation. This is not fully robust -- a + * concurrent autovacuum could briefly take a lock with which we conflict. The + * risk may be low enough to discount. + * + * When calling this function on behalf of a given step for a second or later + * time, pass the STEP_RETRY flag. This only affects the messages printed. + * + * If the STEP_NONBLOCK flag was specified and the query is waiting to acquire a + * lock, returns 0. Otherwise, returns 1. + */ + static int + try_complete_step(Step *step, int flags) + { + PGconn *conn = conns[1 + step->session]; + fd_set read_set; + struct timeval timeout; + int sock = PQsocket(conn); + int ret; + PGresult *res; + + FD_ZERO(&read_set); + + while (flags & STEP_NONBLOCK && PQisBusy(conn)) + { + FD_SET(sock, &read_set); + timeout.tv_sec = 0; + timeout.tv_usec = 10000; /* Check for lock waits every 10ms. */ + + ret = select(sock + 1, &read_set, NULL, NULL, &timeout); + if (ret < 0) /* error in select() */ + { + fprintf(stderr, "select failed: %s\n", strerror(errno)); + exit_nicely(); + } + else if (ret == 0) /* select() timeout: check for lock wait */ + { + int ntuples; + + res = PQexecPrepared(conns[0], PREP_WAITING, 1, + &backend_ids[step->session + 1], + NULL, NULL, 0); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + fprintf(stderr, "lock wait query failed: %s", + PQerrorMessage(conn)); + exit_nicely(); + } + ntuples = PQntuples(res); + PQclear(res); + + if (ntuples >= 1) /* waiting to acquire a lock */ + { + if (!(flags & STEP_RETRY)) + printf("step %s: %s \n", + step->name, step->sql); + return 0; + } + /* else, not waiting: give it more time */ + } + else if (!PQconsumeInput(conn)) /* select(): data available */ + { + fprintf(stderr, "PQconsumeInput failed: %s", PQerrorMessage(conn)); + exit_nicely(); + } + } + + if (flags & STEP_RETRY) + printf("step %s: <... completed>\n", step->name); + else + printf("step %s: %s\n", step->name, step->sql); + + while ((res = PQgetResult(conn))) + { + switch (PQresultStatus(res)) + { + case PGRES_COMMAND_OK: + break; + case PGRES_TUPLES_OK: + printResultSet(res); + break; + case PGRES_FATAL_ERROR: + /* Detail may contain xid values, so just show primary. */ + printf("%s: %s\n", PQresultErrorField(res, PG_DIAG_SEVERITY), + PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY)); + break; + default: + printf("unexpected result status: %s\n", + PQresStatus(PQresultStatus(res))); + } + PQclear(res); + } + + return 1; + } + static void printResultSet(PGresult *res) { *** /dev/null --- b/src/test/isolation/specs/fk-contention.spec *************** *** 0 **** --- 1,19 ---- + setup + { + CREATE TABLE foo (a int PRIMARY KEY, b text); + CREATE TABLE bar (a int NOT NULL REFERENCES foo); + INSERT INTO foo VALUES (42); + } + + teardown + { + DROP TABLE foo, bar; + } + + session "s1" + setup { BEGIN; } + step "ins" { INSERT INTO bar VALUES (42); } + step "com" { COMMIT; } + + session "s2" + step "upd" { UPDATE foo SET b = 'Hello World'; } *** /dev/null --- b/src/test/isolation/specs/fk-deadlock.spec *************** *** 0 **** --- 1,54 ---- + setup + { + CREATE TABLE parent ( + parent_key int PRIMARY KEY, + aux text NOT NULL + ); + + CREATE TABLE child ( + child_key int PRIMARY KEY, + parent_key int NOT NULL REFERENCES parent + ); + + INSERT INTO parent VALUES (1, 'foo'); + } + + teardown + { + DROP TABLE parent, child; + } + + session "s1" + setup { BEGIN; } + step "s1i" { INSERT INTO child VALUES (1, 1); } + step "s1u" { UPDATE parent SET aux = 'bar'; } + step "s1c" { COMMIT; } + + session "s2" + setup { BEGIN; } + step "s2i" { INSERT INTO child VALUES (2, 1); } + step "s2u" { UPDATE parent SET aux = 'baz'; } + step "s2c" { COMMIT; } + + ## Most theoretical permutations require that a blocked session execute a + ## command, making them impossible in practice. + permutation "s1i" "s1u" "s1c" "s2i" "s2u" "s2c" + permutation "s1i" "s1u" "s2i" "s1c" "s2u" "s2c" + #permutation "s1i" "s1u" "s2i" "s2u" "s1c" "s2c" + #permutation "s1i" "s1u" "s2i" "s2u" "s2c" "s1c" + #permutation "s1i" "s2i" "s1u" "s1c" "s2u" "s2c" + permutation "s1i" "s2i" "s1u" "s2u" "s1c" "s2c" + #permutation "s1i" "s2i" "s1u" "s2u" "s2c" "s1c" + #permutation "s1i" "s2i" "s2u" "s1u" "s1c" "s2c" + permutation "s1i" "s2i" "s2u" "s1u" "s2c" "s1c" + #permutation "s1i" "s2i" "s2u" "s2c" "s1u" "s1c" + #permutation "s2i" "s1i" "s1u" "s1c" "s2u" "s2c" + permutation "s2i" "s1i" "s1u" "s2u" "s1c" "s2c" + #permutation "s2i" "s1i" "s1u" "s2u" "s2c" "s1c" + #permutation "s2i" "s1i" "s2u" "s1u" "s1c" "s2c" + permutation "s2i" "s1i" "s2u" "s1u" "s2c" "s1c" + #permutation "s2i" "s1i" "s2u" "s2c" "s1u" "s1c" + #permutation "s2i" "s2u" "s1i" "s1u" "s1c" "s2c" + #permutation "s2i" "s2u" "s1i" "s1u" "s2c" "s1c" + permutation "s2i" "s2u" "s1i" "s2c" "s1u" "s1c" + #permutation "s2i" "s2u" "s2c" "s1i" "s1u" "s1c" *** /dev/null --- b/src/test/isolation/specs/fk-deadlock2.spec *************** *** 0 **** --- 1,59 ---- + setup + { + CREATE TABLE A ( + AID integer not null, + Col1 integer, + PRIMARY KEY (AID) + ); + + CREATE TABLE B ( + BID integer not null, + AID integer not null, + Col2 integer, + PRIMARY KEY (BID), + FOREIGN KEY (AID) REFERENCES A(AID) + ); + + INSERT INTO A (AID) VALUES (1); + INSERT INTO B (BID,AID) VALUES (2,1); + } + + teardown + { + DROP TABLE a, b; + } + + session "s1" + setup { BEGIN; } + step "s1u1" { UPDATE A SET Col1 = 1 WHERE AID = 1; } + step "s1u2" { UPDATE B SET Col2 = 1 WHERE BID = 2; } + step "s1c" { COMMIT; } + + session "s2" + setup { BEGIN; } + step "s2u1" { UPDATE B SET Col2 = 1 WHERE BID = 2; } + step "s2u2" { UPDATE B SET Col2 = 1 WHERE BID = 2; } + step "s2c" { COMMIT; } + + ## Many theoretical permutations require that a blocked session execute a + ## command, making them impossible in practice. + permutation "s1u1" "s1u2" "s1c" "s2u1" "s2u2" "s2c" + permutation "s1u1" "s1u2" "s2u1" "s1c" "s2u2" "s2c" + #permutation "s1u1" "s1u2" "s2u1" "s2u2" "s1c" "s2c" + #permutation "s1u1" "s1u2" "s2u1" "s2u2" "s2c" "s1c" + #permutation "s1u1" "s2u1" "s1u2" "s1c" "s2u2" "s2c" + permutation "s1u1" "s2u1" "s1u2" "s2u2" "s1c" "s2c" + permutation "s1u1" "s2u1" "s1u2" "s2u2" "s2c" "s1c" + permutation "s1u1" "s2u1" "s2u2" "s1u2" "s1c" "s2c" + permutation "s1u1" "s2u1" "s2u2" "s1u2" "s2c" "s1c" + #permutation "s1u1" "s2u1" "s2u2" "s2c" "s1u2" "s1c" + #permutation "s2u1" "s1u1" "s1u2" "s1c" "s2u2" "s2c" + permutation "s2u1" "s1u1" "s1u2" "s2u2" "s1c" "s2c" + permutation "s2u1" "s1u1" "s1u2" "s2u2" "s2c" "s1c" + permutation "s2u1" "s1u1" "s2u2" "s1u2" "s1c" "s2c" + permutation "s2u1" "s1u1" "s2u2" "s1u2" "s2c" "s1c" + #permutation "s2u1" "s1u1" "s2u2" "s2c" "s1u2" "s1c" + #permutation "s2u1" "s2u2" "s1u1" "s1u2" "s1c" "s2c" + #permutation "s2u1" "s2u2" "s1u1" "s1u2" "s2c" "s1c" + permutation "s2u1" "s2u2" "s1u1" "s2c" "s1u2" "s1c" + #permutation "s2u1" "s2u2" "s2c" "s1u1" "s1u2" "s1c"