Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions tests/browser/compiler-explorer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,215 @@ public:
});
}

it(
"C next-pointer tree harness checks every next pointer, up to 6000 nodes",
{ skip: noCc },
async () => {
const dir = mkdtempSync(join(tmpdir(), "codetrial-c-next-"));
const judge = judges["populating-next-right-pointers-in-each-node-ii"];
const completeTree = (label, size, value = (n) => n) => {
const levels = [];
for (let first = 1; first <= size; first *= 2) {
const count = Math.min(first * 2, size + 1) - first;
levels.push(
Array.from({ length: count }, (_, i) => value(first + i)),
);
}
const input = [Array.from({ length: size }, (_, i) => value(i + 1))];
return { label, input, expected: levels };
};
// 64 nodes fill the harness's first queue exactly, so they never need
// it to grow, and all of them hold the same value, so only a node's
// address tells a repeat from a sibling. A candidate case can be as
// large as the problem's 6000-node limit, past what a fixed 4096-entry
// queue holds.
const exactFit = completeTree("exact fit", 64, () => 1);
const spec = {
...judge,
cases: [...judge.cases, exactFit, completeTree("full-size tree", 6000)],
};
// The bottom row's last node can point back to that row's first node
// instead of NULL, which printing the values alone hides, or take
// itself as a left child, which passes every next check while the
// tree never ends.
const linkRows = (lastNext, lastLeft) => `static struct Node* rows[6000];
struct Node* linkRowNeighbors(struct Node* root) {
int head = 0, tail = 0;
if (root) rows[tail++] = root;
while (head < tail) {
int end = tail;
for (int i = head; i < end; i++) {
rows[i]->next = i + 1 < end ? rows[i + 1] : NULL;
if (rows[i]->left) rows[tail++] = rows[i]->left;
if (rows[i]->right) rows[tail++] = rows[i]->right;
}
if (tail == end) {
rows[end - 1]->next = ${lastNext};
rows[end - 1]->left = ${lastLeft};
}
head = end;
}
return root;
}`;
const actuals = async (name, code, flags = []) => {
const source = join(dir, `${name}.c`);
const binary = join(dir, name);
writeFileSync(source, generateHarness("c", spec, code));
await run("cc", ["-std=c17", ...flags, source, "-o", binary]);
return parseCompilerResults(
(await run(binary, [], { encoding: "utf8", timeout: 10000 })).stdout,
).results.map((result) =>
result.error === undefined ? result.actual : { error: result.error },
);
};
try {
assert.deepEqual(
await actuals("correct", linkRows("NULL", "NULL")),
spec.cases.map((testCase) => testCase.expected),
);
const mismatch = {
error: "next pointers do not match the tree level order.",
};
assert.deepEqual(
await actuals("ring", linkRows("rows[head]", "NULL")),
Comment thread
winter77777 marked this conversation as resolved.
[mismatch, [], mismatch, mismatch, mismatch, mismatch, mismatch],
);
// Only the root's left child is wrong: its next points back at the
// root instead of across to its sibling. That is an upper level, a
// pointer that is not NULL, and in the exact fit a node with the
// sibling's value, so checking only the last pointer of each level,
// only whether a pointer is NULL, or only values would all miss it.
const linkAllRows = linkRows("NULL", "NULL").replace(
"linkRowNeighbors",
"linkAllRows",
);
const interior = `${linkAllRows}
struct Node* linkRowNeighbors(struct Node* root) {
linkAllRows(root);
if (root && root->left && root->right) root->left->next = root;
return root;
}`;
assert.deepEqual(
await actuals("interior", interior),
spec.cases.map((testCase) =>
testCase.expected[1]?.length === 2 ? mismatch : testCase.expected,
),
);
const twice = { error: "Returned tree must not reach a node twice." };
// The root's right child is its left child again, a repeat inside the
// level being queued rather than one already walked.
const sharedChild = `${linkAllRows}
struct Node* linkRowNeighbors(struct Node* root) {
linkAllRows(root);
if (root && root->left) root->right = root->left;
return root;
}`;
assert.deepEqual(
await actuals("shared-child", sharedChild),
spec.cases.map((testCase) =>
testCase.expected.length > 1 ? twice : testCase.expected,
),
);
// Every realloc in the harness is the queue's, so renaming it to one
// that always fails takes the full-size case down the grow failure,
// while the exact fit never asks to grow.
const failingRealloc = `void* failing_realloc(void* ptr, size_t size) {
(void)ptr;
(void)size;
return NULL;
}
`;
const noMemory = {
error: "Out of memory while checking next pointers.",
};
assert.deepEqual(
await actuals(
"no-memory",
failingRealloc + linkRows("NULL", "NULL"),
["-Drealloc=failing_realloc"],
),
[
...judge.cases.map((testCase) => testCase.expected),
exactFit.expected,
noMemory,
],
);
// A repeated child is refused before the queue would grow for it, so
// the exact fit reports the repeat rather than the failed grow.
assert.deepEqual(
await actuals(
"no-memory-self-child",
failingRealloc + linkRows("NULL", "rows[end - 1]"),
["-Drealloc=failing_realloc"],
),
[twice, [], twice, twice, twice, twice, noMemory],
);
// The bottom row's last node takes the root as its left child, a
// repeat of a level already walked. These repeats run with growth
// failing first, so a scan that missed one would report memory at 64
// nodes instead of growing the queue until the run is killed.
assert.deepEqual(
await actuals(
"back-to-root",
failingRealloc + linkRows("NULL", "root"),
["-Drealloc=failing_realloc"],
),
[twice, [], twice, twice, twice, twice, noMemory],
);
// With growth working, the full-size tree finds its repeat as well.
assert.deepEqual(
await actuals("self-child", linkRows("NULL", "rows[end - 1]")),
[twice, [], twice, twice, twice, twice, twice],
);
// The queue grows and then the level ends cannot, so the failure frees
// the queue at its new address and the old level ends.
const levelEndsFail = `#undef realloc
void* realloc(void* ptr, size_t size);
void* failing_realloc(void* ptr, size_t size) {
static int calls;
return ++calls % 2 ? realloc(ptr, size) : NULL;
}
`;
assert.deepEqual(
await actuals(
"level-ends-fail",
levelEndsFail + linkRows("NULL", "NULL"),
["-Drealloc=failing_realloc"],
),
[
...judge.cases.map((testCase) => testCase.expected),
exactFit.expected,
noMemory,
],
);
// In this problem only json_next_tree calls malloc (nextTree uses
// calloc), twice per non-empty case: the queue, then the level ends.
// Failing every odd call loses only the queue and every even call
// only the level ends, and each loss must be reported by itself; an
// empty tree never allocates.
for (const lost of [1, 0]) {
const failingMalloc = `#undef malloc
void* malloc(size_t size);
void* failing_malloc(size_t size) {
static int calls;
return ++calls % 2 == ${lost} ? NULL : malloc(size);
}
`;
assert.deepEqual(
await actuals(
`no-malloc-${lost}`,
failingMalloc + linkRows("NULL", "NULL"),
["-Dmalloc=failing_malloc"],
),
[noMemory, [], noMemory, noMemory, noMemory, noMemory, noMemory],
);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);

it(
"C Merge k Sorted Lists harness can produce passing Compiler Explorer JSON",
{ skip: noCc },
Expand Down
2 changes: 1 addition & 1 deletion web/compiler-explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ static void json_tree(FILE* out, struct TreeNode* root) { if (!root) { fputs("[]
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 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); }
static void json_next_tree(FILE* out, struct Node* root) { if (!root) { fputs("[]", out); return; } int capacity = 64, head = 0, tail = 0, levels = 0; struct Node** queue = malloc(capacity * sizeof(*queue)); int* levelEnd = malloc(capacity * sizeof(*levelEnd)); if (!queue || !levelEnd) { free(queue); free(levelEnd); fputs("null,\\"error\\":", out); json_string(out, "Out of memory while checking next pointers."); return; } queue[tail++] = root; while (head < tail) { int end = tail; for (int i = head; i < end; i++) { if (queue[i]->next != (i + 1 < end ? queue[i + 1] : NULL)) { fputs("null,\\"error\\":", out); json_string(out, "next pointers do not match the tree level order."); free(queue); free(levelEnd); return; } for (int side = 0; side < 2; side++) { struct Node* child = side ? queue[i]->right : queue[i]->left; if (!child) continue; for (int seen = 0; seen < tail; seen++) if (queue[seen] == child) { fputs("null,\\"error\\":", out); json_string(out, "Returned tree must not reach a node twice."); free(queue); free(levelEnd); return; } if (tail == capacity) { struct Node** grownQueue = realloc(queue, 2 * capacity * sizeof(*queue)); if (grownQueue) queue = grownQueue; int* grownEnds = grownQueue ? realloc(levelEnd, 2 * capacity * sizeof(*levelEnd)) : NULL; if (grownEnds) levelEnd = grownEnds; if (!grownQueue || !grownEnds) { free(queue); free(levelEnd); fputs("null,\\"error\\":", out); json_string(out, "Out of memory while checking next pointers."); return; } capacity *= 2; } queue[tail++] = child; } } levelEnd[levels++] = end; head = end; } fputc('[', out); for (int level = 0, start = 0; level < levels; start = levelEnd[level++]) { if (level) fputc(',', out); fputc('[', out); for (int i = start; i < levelEnd[level]; i++) { if (i > start) fputc(',', out); fprintf(out, "%d", queue[i]->val); } fputc(']', out); } fputc(']', out); free(queue); free(levelEnd); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repeated-node guard exists only here. The same self-child solution still hangs the other four serializers: the C++ nextTreeToJson, the Java nextTreeJson, and the next-tree walks in web/runners.js add one more level per loop with next == NULL, so the next check never fires and the candidate gets a timeout or OOM instead of "Returned tree must not reach a node twice.". This can be a follow-up: give each a seen set over queued nodes with the same message, plus the self-child case from this test for each language.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#428 covers these four, the C++ nextTreeToJson, the Java nextTreeJson and both walks in web/runners.js, with the same message and a self-child case for each, and quill0224 is assigned to it. This PR stays with json_next_tree, as scoped there.

static void json_quad_tree_visit(FILE* out, struct Node* node, int* printed) { if (!node) return; if ((*printed)++) fputc(',', out); fprintf(out, "[%d,%d]", node->isLeaf ? 1 : 0, node->isLeaf ? (node->val ? 1 : 0) : 1); if (!node->isLeaf) { json_quad_tree_visit(out, node->topLeft, printed); json_quad_tree_visit(out, node->topRight, printed); json_quad_tree_visit(out, node->bottomLeft, printed); json_quad_tree_visit(out, node->bottomRight, printed); } }
static void json_quad_tree(FILE* out, struct Node* root) { int printed = 0; fputc('[', out); json_quad_tree_visit(out, root, &printed); fputc(']', out); }
${candidateCode}
Expand Down