This commit introduces a comprehensive suite of unit tests for the py_debug_graph module, which is responsible for building debug graphs in Python. The tests cover various scenarios including linked lists, cycles, scalar values, and the handling of local and global variables in the debugging context. Additionally, it verifies the behavior of the debug_graph function and ensures that the builtins.root patch works as intended. These tests aim to enhance the reliability and robustness of the debugging functionality.
211 lines
8.6 KiB
JavaScript
211 lines
8.6 KiB
JavaScript
"use strict";
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { loadInternals } = require("./load-visualizer.js");
|
|
|
|
const h = loadInternals();
|
|
|
|
const NODE_WIDTH = 220;
|
|
const H_GAP = 70;
|
|
|
|
// ---- layeredPositions ------------------------------------------------
|
|
|
|
test("layeredPositions: empty graph yields no positions", () => {
|
|
// Objects returned from the vm sandbox aren't reference-equal to
|
|
// same-shaped objects built in this realm (different Object.prototype),
|
|
// so deepEqual against a plain {} would spuriously fail -- compare via
|
|
// Object.keys instead of object identity/structure.
|
|
assert.deepEqual(Object.keys(h.layeredPositions([], [], {})), []);
|
|
});
|
|
|
|
test("layeredPositions: single node with no edges sits at level 0", () => {
|
|
const pos = h.layeredPositions(["n1"], [], { n1: 40 });
|
|
assert.equal(pos.n1.x, NODE_WIDTH / 2);
|
|
assert.equal(pos.n1.y, 20);
|
|
});
|
|
|
|
test("layeredPositions: a->b->c chain places each node one level further right", () => {
|
|
const boxHeights = { a: 40, b: 40, c: 40 };
|
|
const pos = h.layeredPositions(
|
|
["a", "b", "c"],
|
|
[{ from: "a", to: "b" }, { from: "b", to: "c" }],
|
|
boxHeights
|
|
);
|
|
assert.equal(pos.a.x, NODE_WIDTH / 2);
|
|
assert.equal(pos.b.x, NODE_WIDTH + H_GAP + NODE_WIDTH / 2);
|
|
assert.equal(pos.c.x, 2 * (NODE_WIDTH + H_GAP) + NODE_WIDTH / 2);
|
|
// all alone in their column -> same y
|
|
assert.equal(pos.a.y, 20);
|
|
assert.equal(pos.b.y, 20);
|
|
assert.equal(pos.c.y, 20);
|
|
});
|
|
|
|
test("layeredPositions: two nodes at the same level stack into separate columns", () => {
|
|
// root -> x, root -> y (siblings at level 1)
|
|
const boxHeights = { root: 40, x: 40, y: 40 };
|
|
const pos = h.layeredPositions(
|
|
["root", "x", "y"],
|
|
[{ from: "root", to: "x" }, { from: "root", to: "y" }],
|
|
boxHeights
|
|
);
|
|
assert.equal(pos.x.x, pos.y.x); // same level -> same column x
|
|
assert.notEqual(pos.x.y, pos.y.y); // different rows within that level
|
|
});
|
|
|
|
test("layeredPositions: a pure cycle (no zero-incoming node) still terminates and levels from an arbitrary root", () => {
|
|
const boxHeights = { a: 40, b: 40 };
|
|
const pos = h.layeredPositions(["a", "b"], [{ from: "a", to: "b" }, { from: "b", to: "a" }], boxHeights);
|
|
assert.equal(pos.a.x, NODE_WIDTH / 2); // a picked as the arbitrary BFS root -> level 0
|
|
assert.equal(pos.b.x, NODE_WIDTH + H_GAP + NODE_WIDTH / 2); // reached via a->b -> level 1
|
|
});
|
|
|
|
test("layeredPositions: nodes never reached by the BFS still get a position (the lvl===undefined fallback)", () => {
|
|
// A node with zero incoming edges is always seeded as a BFS root, so an
|
|
// isolated *cycle* is the only way to get a node the main walk never
|
|
// reaches: "a" and "b" each have incoming=1 (from each other), so
|
|
// neither is seeded, and neither is reachable from "root" (which has no
|
|
// outgoing edges at all). Both should still land somewhere via the
|
|
// `if (lvl === undefined) { maxLevel++; lvl = maxLevel; }` fallback,
|
|
// one level further right each time, in nodeIds iteration order.
|
|
const boxHeights = { root: 40, a: 40, b: 40 };
|
|
const pos = h.layeredPositions(
|
|
["root", "a", "b"],
|
|
[{ from: "a", to: "b" }, { from: "b", to: "a" }],
|
|
boxHeights
|
|
);
|
|
assert.equal(pos.root.x, NODE_WIDTH / 2); // the only seeded root -> level 0
|
|
assert.ok(pos.a.x > pos.root.x);
|
|
assert.ok(pos.b.x > pos.a.x); // "a" then "b": each bumps maxLevel further right
|
|
});
|
|
|
|
// ---- boxEdgePoint ------------------------------------------------------
|
|
|
|
test("boxEdgePoint: coincident points return the center", () => {
|
|
// Objects returned from the vm sandbox aren't reference-equal to
|
|
// same-shaped objects built in this realm, so compare fields directly
|
|
// rather than via deepEqual against a plain-object literal.
|
|
const p = h.boxEdgePoint(5, 5, 100, 50, 5, 5);
|
|
assert.equal(p.x, 5);
|
|
assert.equal(p.y, 5);
|
|
});
|
|
|
|
test("boxEdgePoint: approaching straight from the right hits the right edge midpoint", () => {
|
|
const p = h.boxEdgePoint(0, 0, 100, 50, 1000, 0);
|
|
assert.equal(p.x, 100);
|
|
assert.equal(p.y, 0);
|
|
});
|
|
|
|
test("boxEdgePoint: approaching straight from below hits the bottom edge midpoint", () => {
|
|
const p = h.boxEdgePoint(0, 0, 100, 50, 0, 1000);
|
|
assert.equal(p.x, 0);
|
|
assert.equal(p.y, 50);
|
|
});
|
|
|
|
test("boxEdgePoint: diagonal approach picks whichever axis's boundary is closer (min t)", () => {
|
|
// halfW=100, halfH=50: for a 45-degree line, ty (50/1=50) < tx (100/1=100),
|
|
// so it should clip on the vertical (height) boundary first.
|
|
const p = h.boxEdgePoint(0, 0, 100, 50, 1000, 1000);
|
|
assert.equal(p.y, 50);
|
|
assert.equal(p.x, 50); // t=0.05 applied to both dx and dy equally here since dx===dy
|
|
});
|
|
|
|
// ---- subtreeSize --------------------------------------------------------
|
|
|
|
test("subtreeSize: counts all reachable descendants across multiple children", () => {
|
|
const childrenOf = { a: ["b", "c"], b: ["d"], c: [], d: [] };
|
|
assert.equal(h.subtreeSize(["b", "c"], childrenOf), 3); // b, c, d
|
|
});
|
|
|
|
test("subtreeSize: a childless start list counts as zero", () => {
|
|
assert.equal(h.subtreeSize([], { a: ["b"] }), 0);
|
|
});
|
|
|
|
test("subtreeSize: cycles terminate instead of infinite-looping", () => {
|
|
const childrenOf = { a: ["b"], b: ["a"] };
|
|
assert.equal(h.subtreeSize(["a"], childrenOf), 2); // a, b -- visited once each
|
|
});
|
|
|
|
// ---- rootKind ------------------------------------------------------------
|
|
|
|
test("rootKind: recognizes local/watched/global, defaults unknowns to watched", () => {
|
|
assert.equal(h.rootKind({ kind: "local" }), "local");
|
|
assert.equal(h.rootKind({ kind: "watched" }), "watched");
|
|
assert.equal(h.rootKind({ kind: "global" }), "global");
|
|
assert.equal(h.rootKind({ kind: "bogus" }), "watched");
|
|
assert.equal(h.rootKind({}), "watched");
|
|
});
|
|
|
|
// ---- createSimulation ----------------------------------------------------
|
|
|
|
function makeNode(id, x, y, height) {
|
|
return { id, x, y, vx: 0, vy: 0, fixed: false, height: height || 40 };
|
|
}
|
|
|
|
test("createSimulation: unlinked nodes repel -- their separation grows after one step", () => {
|
|
const a = makeNode("a", 0, 0);
|
|
const b = makeNode("b", 50, 0);
|
|
const sim = h.createSimulation([a, b], [], () => true);
|
|
const before = Math.abs(a.x - b.x);
|
|
sim.step();
|
|
const after = Math.abs(a.x - b.x);
|
|
assert.ok(after > before, "repulsion should push unlinked nodes further apart");
|
|
});
|
|
|
|
test("createSimulation: a linked pair far beyond spring length is pulled closer", () => {
|
|
const a = makeNode("a", 0, 0);
|
|
const b = makeNode("b", 1000, 0);
|
|
const sim = h.createSimulation([a, b], [{ from: "a", to: "b" }], () => true);
|
|
const before = Math.abs(a.x - b.x);
|
|
sim.step();
|
|
const after = Math.abs(a.x - b.x);
|
|
assert.ok(after < before, "the spring should dominate repulsion at this distance and pull them together");
|
|
});
|
|
|
|
test("createSimulation: an invisible node is frozen (zeroed velocity, doesn't move)", () => {
|
|
const a = makeNode("a", 0, 0);
|
|
const b = makeNode("b", 50, 0);
|
|
const sim = h.createSimulation([a, b], [], (id) => id !== "b"); // b is hidden
|
|
sim.step();
|
|
assert.equal(b.x, 50);
|
|
assert.equal(b.y, 0);
|
|
assert.equal(b.vx, 0);
|
|
assert.equal(b.vy, 0);
|
|
});
|
|
|
|
test("createSimulation: a fixed node doesn't move even if visible", () => {
|
|
const a = makeNode("a", 0, 0);
|
|
const b = makeNode("b", 50, 0);
|
|
b.fixed = true;
|
|
const sim = h.createSimulation([a, b], [], () => true);
|
|
sim.step();
|
|
assert.equal(b.x, 50);
|
|
assert.equal(b.y, 0);
|
|
});
|
|
|
|
test("createSimulation: overlapping tall boxes get pushed apart along the smaller-overlap axis", () => {
|
|
// Tall boxes (height 300) make minDY (300+16=316) larger than minDX
|
|
// (NODE_WIDTH+16=236), so with a small equal dx/dy offset, overlapX
|
|
// (~231) < overlapY (~311) and the X-axis collision branch should fire.
|
|
const a = makeNode("a", 5, 5, 300);
|
|
const b = makeNode("b", 0, 0, 300);
|
|
const sim = h.createSimulation([a, b], [], () => true);
|
|
const beforeGapX = Math.abs(a.x - b.x);
|
|
sim.step();
|
|
const afterGapX = Math.abs(a.x - b.x);
|
|
assert.ok(afterGapX > beforeGapX, "collision resolution should widen the x gap between overlapping tall boxes");
|
|
});
|
|
|
|
test("createSimulation: step() returns the total kinetic energy, ~0 once at rest", () => {
|
|
// Two nodes already sitting exactly at spring length apart, unlinked (no
|
|
// spring), but repulsion/gravity are still nonzero forces in general --
|
|
// use a single node with no peers so there's truly nothing to move it.
|
|
const a = makeNode("a", 0, 0);
|
|
const sim = h.createSimulation([a], [], () => true);
|
|
const kinetic = sim.step();
|
|
// Gravity alone (-x*GRAVITY at x=0) contributes nothing either, so a
|
|
// lone node at the origin has zero net force and should stay put.
|
|
assert.equal(kinetic, 0);
|
|
assert.equal(a.x, 0);
|
|
assert.equal(a.y, 0);
|
|
});
|