diff --git a/tests/browser/compiler-explorer.test.js b/tests/browser/compiler-explorer.test.js index fc409b2c..e441d2d1 100644 --- a/tests/browser/compiler-explorer.test.js +++ b/tests/browser/compiler-explorer.test.js @@ -1187,24 +1187,18 @@ public: ); } - // Compiles a flatten candidate into the harness and returns its results. - // The timeout keeps a serializer that walks forever from hanging the lane. - async function runFlatten(language, candidate) { - const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-flatten-`)); + // Compiles a candidate into the named judge's harness and returns its + // results. The timeout keeps a serializer that walks forever from hanging + // the lane. + async function runJudge(language, id, candidate) { + const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-${id}-`)); try { const source = join( dir, - language === "java" ? "Main.java" : `flatten.${language}`, - ); - const binary = join(dir, "flatten"); - writeFileSync( - source, - generateHarness( - language, - judges["flatten-binary-tree-to-linked-list"], - candidate, - ), + language === "java" ? "Main.java" : `candidate.${language}`, ); + const binary = join(dir, "candidate"); + writeFileSync(source, generateHarness(language, judges[id], candidate)); if (language === "java") await run("javac", [source]); else await run(language === "c" ? "cc" : "g++", [ @@ -1289,7 +1283,11 @@ public: { skip }, async () => { const judge = judges["flatten-binary-tree-to-linked-list"]; - const results = await runFlatten(language, cyclicFlatten[language]); + const results = await runJudge( + language, + "flatten-binary-tree-to-linked-list", + cyclicFlatten[language], + ); assert.equal(results.length, judge.cases.length); judge.cases.forEach((testCase, index) => { const result = results[index]; @@ -1344,8 +1342,9 @@ public: { skip }, async () => { const judge = judges["flatten-binary-tree-to-linked-list"]; - const results = await runFlatten( + const results = await runJudge( language, + "flatten-binary-tree-to-linked-list", twinChildren[language](shared), ); assert.equal(results.length, judge.cases.length); @@ -1371,6 +1370,154 @@ public: ); } } + + // A deep copy whose last node links back to the first copies every value + // and every random pointer, so only a serializer that refuses a revisited + // node can tell it from a correct answer. "empty random list" has no node. + const cyclicCopies = { + c: `struct Node* snapshotOutline(struct Node* head) { + int n = 0; + for (struct Node* p = head; p; p = p->next) n++; + if (!n) return NULL; + struct Node** copies = calloc(n, sizeof(*copies)); + struct Node* p = head; + for (int i = 0; i < n; i++, p = p->next) { + copies[i] = calloc(1, sizeof(*copies[i])); + copies[i]->val = p->val; + } + p = head; + for (int i = 0; i < n; i++, p = p->next) { + copies[i]->next = copies[(i + 1) % n]; + struct Node* q = head; + for (int j = 0; j < n; j++, q = q->next) + if (q == p->random) copies[i]->random = copies[j]; + } + struct Node* copy = copies[0]; + free(copies); + return copy; +}`, + cpp: `class Solution { +public: + Node* snapshotOutline(Node* head) { + if (!head) return nullptr; + unordered_map copies; + for (Node* p = head; p; p = p->next) copies[p] = new Node(p->val); + for (Node* p = head; p; p = p->next) { + copies[p]->next = p->next ? copies[p->next] : copies[head]; + copies[p]->random = p->random ? copies[p->random] : nullptr; + } + return copies[head]; + } +};`, + java: `class Solution { + public Node snapshotOutline(Node head) { + if (head == null) return null; + java.util.HashMap copies = new java.util.HashMap<>(); + for (Node p = head; p != null; p = p.next) copies.put(p, new Node(p.val)); + for (Node p = head; p != null; p = p.next) { + copies.get(p).next = p.next != null ? copies.get(p.next) : copies.get(head); + copies.get(p).random = p.random != null ? copies.get(p.random) : null; + } + return copies.get(head); + } +}`, + }; + const cyclicCopyRuns = [ + ["c", noCc], + ["cpp", noGpp], + ["java", noJava], + ]; + for (const [language, skip] of cyclicCopyRuns) { + it( + `${language} harness refuses a copied random list that loops back into itself`, + { skip }, + async () => { + const spec = judges["copy-list-with-random-pointer"]; + const results = await runJudge( + language, + "copy-list-with-random-pointer", + cyclicCopies[language], + ); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + if (testCase.expected.length === 0) { + assert.deepEqual(result.actual, [], testCase.label); + return; + } + assert.match( + result.error ?? "", + /Returned list must not reach a node twice\./, + testCase.label, + ); + }); + }, + ); + } + + // C used to print these two refusals where the answer goes, so the case + // failed as a wrong answer with no reason. C++ and Java throw them, and C + // reports them through the same error field. A shallow copy that also + // loops is refused as a shallow copy, as the other four harnesses refuse + // it, because the original head comes before any revisit. + const deepCopy = "Returned list must be a deep copy."; + const strayRandom = + "Returned random pointer must stay inside the copied list."; + const strayCopies = [ + { + name: "a shallow copy", + message: deepCopy, + candidate: `struct Node* snapshotOutline(struct Node* head) { return head; }`, + }, + { + name: "a shallow copy that loops", + message: deepCopy, + candidate: `struct Node* snapshotOutline(struct Node* head) { + struct Node* tail = head; + while (tail && tail->next) tail = tail->next; + if (tail) tail->next = head; + return head; +}`, + }, + { + name: "a random pointer into the original", + message: strayRandom, + candidate: `struct Node* snapshotOutline(struct Node* head) { + struct Node dummy = { 0 }; + struct Node* tail = &dummy; + for (struct Node* p = head; p; p = p->next) { + tail->next = calloc(1, sizeof(*tail->next)); + tail = tail->next; + tail->val = p->val; + tail->random = p->random; + } + return dummy.next; +}`, + }, + ]; + for (const { name, message, candidate } of strayCopies) { + it(`c harness reports ${name} as an error`, { skip: noCc }, async () => { + const spec = judges["copy-list-with-random-pointer"]; + const results = await runJudge( + "c", + "copy-list-with-random-pointer", + candidate, + ); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + const nodes = testCase.input[0]; + const strays = + message === deepCopy + ? nodes.length > 0 + : nodes.some(([, random]) => random !== null); + if (!strays) { + assert.deepEqual(result.actual, testCase.expected, testCase.label); + return; + } + assert.equal(result.actual, null, testCase.label); + assert.equal(result.error, message, testCase.label); + }); + }); + } }); test("harness generator handles output parameter and output prefix specs", () => { diff --git a/tests/browser/runners.test.js b/tests/browser/runners.test.js index ae1dd9ae..c8eaf9eb 100644 --- a/tests/browser/runners.test.js +++ b/tests/browser/runners.test.js @@ -682,6 +682,30 @@ test("both runners refuse a returned list that loops back into itself", () => { assert.doesNotMatch(python, /id\(node\) not in seen/); }); +test("both runners refuse a copied random list that loops back into itself", () => { + // A copy whose last node links back to the first holds every value and + // random pointer, so stopping at the revisit would print a correct answer. + const javascript = runners.slice( + runners.indexOf("const randomListToArray"), + runners.indexOf("};", runners.indexOf("const randomListToArray")), + ); + assert.match( + javascript, + /if \(seen\.has\(node\)\)\s*throw new TypeError\("Returned list must not reach a node twice\."\)/, + ); + assert.doesNotMatch(javascript, /while \(node && !seen\.has\(node\)\)/); + + const python = runners.slice( + runners.indexOf("def _random_list_to_array"), + runners.indexOf("def _graph_to_adjacency"), + ); + assert.match( + python, + /if id\(node\) in seen:\s*raise TypeError\("Returned list must not reach a node twice\."\)/, + ); + assert.doesNotMatch(python, /id\(node\) not in seen/); +}); + test("the runner evaluates no string, so the page needs no unsafe-eval", () => { // The candidate's code is the first statements of the worker, not an // argument to eval inside it. This is what lets the page's diff --git a/web/compiler-explorer.js b/web/compiler-explorer.js index bd462ee2..ee179411 100644 --- a/web/compiler-explorer.js +++ b/web/compiler-explorer.js @@ -399,7 +399,7 @@ static void json_char_matrix(FILE* out, char** values, int size, int* colSize) { static void json_string_matrix(FILE* out, char*** values, int size, int* colSize) { fputc('[', out); for (int i = 0; i < size; i++) { if (i) fputc(',', out); json_string_array(out, values[i], colSize[i]); } fputc(']', out); } static void json_list(FILE* out, struct ListNode* node) { for (struct ListNode *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } fputc('[', out); for (int i = 0; node; node = node->next, i++) { if (i) fputc(',', out); fprintf(out, "%d", node->val); } fputc(']', out); } static void json_tree(FILE* out, struct TreeNode* root) { if (!root) { fputs("[]", out); return; } struct TreeNode* queue[4096]; const char* values[4096]; char numbers[4096][32]; int head = 0, tail = 0, count = 0; queue[tail++] = root; while (head < tail && tail < 4096) { struct TreeNode* node = queue[head++]; if (!node) { values[count++] = "null"; continue; } for (int i = 0; i < head - 1; i++) if (queue[i] == node) { fputs("null,\\"error\\":", out); json_string(out, "Returned tree must not reach a node twice."); return; } snprintf(numbers[count], sizeof(numbers[count]), "%d", node->val); values[count] = numbers[count]; count++; queue[tail++] = node->left; queue[tail++] = node->right; } while (count > 0 && strcmp(values[count - 1], "null") == 0) count--; fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fputs(values[i], out); } fputc(']', out); } -static void json_random_list(FILE* out, struct Node* node) { struct Node* nodes[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); int random = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) random = j; fprintf(out, "[%d,", nodes[i]->val); if (nodes[i]->random && random < 0) { json_string(out, "Returned random pointer must stay inside the copied list."); fputc(']', out); continue; } if (random < 0) fputs("null", out); else fprintf(out, "%d", random); fputc(']', out); } fputc(']', out); } +static void json_random_list(FILE* out, struct Node* node) { struct Node* nodes[10000]; int randoms[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } for (struct Node *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } for (int i = 0; i < count; i++) { randoms[i] = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) { randoms[i] = j; break; } if (nodes[i]->random && randoms[i] < 0) { fputs("null,\\"error\\":", out); json_string(out, "Returned random pointer must stay inside the copied list."); return; } } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fprintf(out, "[%d,", nodes[i]->val); if (randoms[i] < 0) fputs("null", out); else fprintf(out, "%d", randoms[i]); fputc(']', out); } fputc(']', out); } static int compare_graph_neighbor_ids(const void* a, const void* b) { int left = *(const int*)a; int right = *(const int*)b; return (left > right) - (left < right); } static void json_graph(FILE* out, struct Node* node) { if (!node) { fputs("[]", out); return; } struct Node* nodes[1024]; struct Node* seen[1024] = {0}; int count = 0; nodes[count++] = node; for (int i = 0; i < count && count < 1024; i++) { struct Node* cur = nodes[i]; if (isOriginalGraphNode(cur)) { json_string(out, "Returned graph must be a deep copy."); return; } if (cur->val <= 0 || cur->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } seen[cur->val] = cur; for (int j = 0; j < cur->numNeighbors; j++) { struct Node* next = cur->neighbors[j]; if (!next) { json_string(out, "Returned graph has a NULL neighbor."); return; } if (next->val <= 0 || next->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } if (isOriginalGraphNode(next)) { json_string(out, "Returned graph must be a deep copy."); return; } if (seen[next->val] && seen[next->val] != next) { json_string(out, "Returned graph must not contain distinct nodes with the same ID."); return; } if (!seen[next->val]) { seen[next->val] = next; nodes[count++] = next; } } } fputc('[', out); for (int value = 1, printed = 0; value < 1024; value++) { struct Node* cur = NULL; for (int i = 0; i < count; i++) if (nodes[i]->val == value) cur = nodes[i]; if (!cur) continue; if (printed++) fputc(',', out); int degree = cur->numNeighbors; int* neighborIds = NULL; if (degree > 0) { neighborIds = malloc((size_t)degree * sizeof(*neighborIds)); if (!neighborIds) exit(EXIT_FAILURE); for (int j = 0; j < degree; j++) neighborIds[j] = cur->neighbors[j]->val; qsort(neighborIds, (size_t)degree, sizeof(*neighborIds), compare_graph_neighbor_ids); } json_int_array(out, neighborIds, degree); free(neighborIds); } fputc(']', out); } static void json_next_tree(FILE* out, struct Node* root) { if (!root) { fputs("[]", out); return; } struct Node* queue[4096]; int head = 0, tail = 0, firstLevel = 1; queue[tail++] = root; fputc('[', out); while (head < tail) { int end = tail; if (!firstLevel) fputc(',', out); firstLevel = 0; fputc('[', out); for (int i = head; i < end; i++) { if (i > head) fputc(',', out); fprintf(out, "%d", queue[i]->val); if (queue[i]->next != (i + 1 < end ? queue[i + 1] : NULL)) { fputs("]", out); fputs("]", out); return; } if (queue[i]->left) queue[tail++] = queue[i]->left; if (queue[i]->right) queue[tail++] = queue[i]->right; } fputc(']', out); head = end; } fputc(']', out); } @@ -560,7 +560,7 @@ ListNode* listNode(vector values, int pos) { ListNode* head = listNode(valu TreeNode* treeNode(vector> values) { if (values.empty() || !values[0]) return nullptr; auto* root = new TreeNode(*values[0]); queue q; q.push(root); size_t i = 1; while (!q.empty() && i < values.size()) { auto* node = q.front(); q.pop(); if (i < values.size() && values[i]) { node->left = new TreeNode(*values[i]); q.push(node->left); } ++i; if (i < values.size() && values[i]) { node->right = new TreeNode(*values[i]); q.push(node->right); } ++i; } return root; } TreeNode* findTreeNode(TreeNode* root, int value) { queue q; if (root) q.push(root); while (!q.empty()) { auto* node = q.front(); q.pop(); if (node->val == value) return node; if (node->left) q.push(node->left); if (node->right) q.push(node->right); } throw runtime_error("Could not find tree node value."); } Node* randomList(vector>> items) { vector nodes; for (auto item : items) nodes.push_back(new Node(item.first)); originalRandomNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < nodes.size(); ++i) { nodes[i]->next = i + 1 < nodes.size() ? nodes[i + 1] : nullptr; if (items[i].second) nodes[i]->random = nodes[*items[i].second]; } return nodes.empty() ? nullptr : nodes[0]; } -string randomListToJson(Node* node) { vector nodes; map index; set seen; for (auto* cur = node; cur && !seen.count(cur); cur = cur->next) { if (originalRandomNodes.count(cur)) throw runtime_error("Returned list must be a deep copy."); seen.insert(cur); index[cur] = nodes.size(); nodes.push_back(cur); } string out = "["; for (size_t i = 0; i < nodes.size(); ++i) { if (i) out += ","; if (nodes[i]->random && !index.count(nodes[i]->random)) throw runtime_error("Returned random pointer must stay inside the copied list."); out += "[" + to_string(nodes[i]->val) + "," + (nodes[i]->random ? to_string(index[nodes[i]->random]) : string("null")) + "]"; } return out + "]"; } +string randomListToJson(Node* node) { vector nodes; map index; set seen; for (auto* cur = node; cur; cur = cur->next) { if (!seen.insert(cur).second) throw runtime_error("Returned list must not reach a node twice."); if (originalRandomNodes.count(cur)) throw runtime_error("Returned list must be a deep copy."); index[cur] = nodes.size(); nodes.push_back(cur); } string out = "["; for (size_t i = 0; i < nodes.size(); ++i) { if (i) out += ","; if (nodes[i]->random && !index.count(nodes[i]->random)) throw runtime_error("Returned random pointer must stay inside the copied list."); out += "[" + to_string(nodes[i]->val) + "," + (nodes[i]->random ? to_string(index[nodes[i]->random]) : string("null")) + "]"; } return out + "]"; } Node* graphNode(vector> adjacency) { if (adjacency.empty()) { originalGraphNodes.clear(); return nullptr; } vector nodes; for (size_t i = 0; i < adjacency.size(); ++i) nodes.push_back(new Node(i + 1)); originalGraphNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < adjacency.size(); ++i) for (int value : adjacency[i]) nodes[i]->neighbors.push_back(nodes[value - 1]); return nodes[0]; } string graphToJson(Node* node) { if (!node) return "[]"; map byValue; queue q; q.push(node); while (!q.empty()) { auto* cur = q.front(); q.pop(); if (!cur) continue; if (originalGraphNodes.count(cur)) throw runtime_error("Returned graph must be a deep copy."); if (byValue.count(cur->val)) { if (byValue.at(cur->val) != cur) throw runtime_error("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue[cur->val] = cur; for (auto* next : cur->neighbors) { if (!next) throw runtime_error("Returned graph has a NULL neighbor."); q.push(next); } } string out = "["; bool first = true; for (auto& [value, cur] : byValue) { if (!first) out += ","; first = false; vector neighbors; for (auto* next : cur->neighbors) neighbors.push_back(next->val); sort(neighbors.begin(), neighbors.end()); out += toJson(neighbors); } return out + "]"; } Node* nextTree(vector> values) { if (values.empty() || !values[0]) return nullptr; auto* root = new Node(*values[0]); queue q; q.push(root); size_t i = 1; while (!q.empty() && i < values.size()) { auto* node = q.front(); q.pop(); if (i < values.size() && values[i]) { node->left = new Node(*values[i]); q.push(node->left); } ++i; if (i < values.size() && values[i]) { node->right = new Node(*values[i]); q.push(node->right); } ++i; } return root; } @@ -729,7 +729,7 @@ class Main { static TreeNode treeNode(Integer[] values) { if (values.length == 0 || values[0] == null) return null; TreeNode root = new TreeNode(values[0]); ArrayDeque q = new ArrayDeque<>(); q.add(root); int i = 1; while (!q.isEmpty() && i < values.length) { TreeNode node = q.remove(); if (i < values.length && values[i] != null) { node.left = new TreeNode(values[i]); q.add(node.left); } i++; if (i < values.length && values[i] != null) { node.right = new TreeNode(values[i]); q.add(node.right); } i++; } return root; } static TreeNode findTreeNode(TreeNode root, int value) { ArrayDeque q = new ArrayDeque<>(); if (root != null) q.add(root); while (!q.isEmpty()) { TreeNode node = q.remove(); if (node.val == value) return node; if (node.left != null) q.add(node.left); if (node.right != null) q.add(node.right); } throw new IllegalArgumentException("Could not find tree node value."); } static Node randomList(int[][] items) { ArrayList nodes = new ArrayList<>(); for (int[] item : items) nodes.add(new Node(item[0])); originalRandomNodes = new HashSet<>(nodes); for (int i = 0; i < nodes.size(); i++) { nodes.get(i).next = i + 1 < nodes.size() ? nodes.get(i + 1) : null; if (items[i][1] >= 0) nodes.get(i).random = nodes.get(items[i][1]); } return nodes.isEmpty() ? null : nodes.get(0); } - static String randomListJson(Node node) { ArrayList nodes = new ArrayList<>(); HashMap index = new HashMap<>(); HashSet seen = new HashSet<>(); for (Node cur = node; cur != null && !seen.contains(cur); cur = cur.next) { if (originalRandomNodes.contains(cur)) throw new IllegalArgumentException("Returned list must be a deep copy."); seen.add(cur); index.put(cur, nodes.size()); nodes.add(cur); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : nodes) { if (cur.random != null && !index.containsKey(cur.random)) throw new IllegalArgumentException("Returned random pointer must stay inside the copied list."); out.add("[" + cur.val + "," + (cur.random == null ? "null" : index.get(cur.random)) + "]"); } return out.toString(); } + static String randomListJson(Node node) { ArrayList nodes = new ArrayList<>(); HashMap index = new HashMap<>(); HashSet seen = new HashSet<>(); for (Node cur = node; cur != null; cur = cur.next) { if (!seen.add(cur)) throw new IllegalArgumentException("Returned list must not reach a node twice."); if (originalRandomNodes.contains(cur)) throw new IllegalArgumentException("Returned list must be a deep copy."); index.put(cur, nodes.size()); nodes.add(cur); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : nodes) { if (cur.random != null && !index.containsKey(cur.random)) throw new IllegalArgumentException("Returned random pointer must stay inside the copied list."); out.add("[" + cur.val + "," + (cur.random == null ? "null" : index.get(cur.random)) + "]"); } return out.toString(); } static Node graphNode(int[][] adjacency) { if (adjacency.length == 0) { originalGraphNodes = new HashSet<>(); return null; } ArrayList nodes = new ArrayList<>(); for (int i = 0; i < adjacency.length; i++) nodes.add(new Node(i + 1)); originalGraphNodes = new HashSet<>(nodes); for (int i = 0; i < adjacency.length; i++) for (int value : adjacency[i]) nodes.get(i).neighbors.add(nodes.get(value - 1)); return nodes.get(0); } static String graphJson(Node node) { if (node == null) return "[]"; TreeMap byValue = new TreeMap<>(); ArrayDeque q = new ArrayDeque<>(); q.add(node); while (!q.isEmpty()) { Node cur = q.remove(); if (cur == null) continue; if (originalGraphNodes.contains(cur)) throw new IllegalArgumentException("Returned graph must be a deep copy."); if (byValue.containsKey(cur.val)) { if (byValue.get(cur.val) != cur) throw new IllegalArgumentException("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue.put(cur.val, cur); for (Node next : cur.neighbors) q.add(next); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : byValue.values()) { ArrayList row = new ArrayList<>(); for (Node next : cur.neighbors) row.add(next.val); Collections.sort(row); out.add(json(row)); } return out.toString(); } static Node nextTree(Integer[] values) { if (values.length == 0 || values[0] == null) return null; Node root = new Node(values[0]); ArrayDeque q = new ArrayDeque<>(); q.add(root); int i = 1; while (!q.isEmpty() && i < values.length) { Node node = q.remove(); if (i < values.length && values[i] != null) { node.left = new Node(values[i]); q.add(node.left); } i++; if (i < values.length && values[i] != null) { node.right = new Node(values[i]); q.add(node.right); } i++; } return root; } diff --git a/web/runners.js b/web/runners.js index db93dc47..be607101 100644 --- a/web/runners.js +++ b/web/runners.js @@ -697,7 +697,9 @@ const JS_RUNNER_SOURCE = ` }; const randomListToArray = (node) => { const nodes = [], seen = new Set(); - while (node && !seen.has(node)) { + while (node) { + if (seen.has(node)) + throw new TypeError("Returned list must not reach a node twice."); if (originalRandomNodes.has(node)) throw new TypeError("Returned list must be a deep copy."); seen.add(node); nodes.push(node); @@ -1026,7 +1028,9 @@ def _quad_tree_to_preorder(root): def _random_list_to_array(node): nodes = [] seen = set() - while node is not None and id(node) not in seen: + while node is not None: + if id(node) in seen: + raise TypeError("Returned list must not reach a node twice.") if id(node) in _original_random_nodes: raise TypeError("Returned list must be a deep copy.") seen.add(id(node))