Skip to content

Commit 78fa6b5

Browse files
committed
chore(test): add test suite in tests/ folder and development tools
1 parent 5732e02 commit 78fa6b5

13 files changed

Lines changed: 1215 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.log
2+
node_modules/
3+
.DS_Store
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python 3.14.0t
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const http = require("http");
2+
const fs = require("fs");
3+
const path = require("path");
4+
5+
const PORT = 8088;
6+
const LOG_FILE = path.join(__dirname, "app.log");
7+
const MIME_TYPES = {
8+
".html": "text/html",
9+
".js": "text/javascript",
10+
".css": "text/css",
11+
".json": "application/json",
12+
".png": "image/png",
13+
".jpg": "image/jpeg",
14+
".gif": "image/gif",
15+
".svg": "image/svg+xml",
16+
".ico": "image/x-icon",
17+
};
18+
19+
const server = http.createServer((req, res) => {
20+
console.log(`${req.method} ${req.url}`);
21+
22+
// CORS headers
23+
res.setHeader("Access-Control-Allow-Origin", "*");
24+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
25+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
26+
27+
if (req.method === "OPTIONS") {
28+
res.writeHead(204);
29+
res.end();
30+
return;
31+
}
32+
33+
// File Logging Endpoint
34+
if (req.method === "POST" && req.url === "/log") {
35+
let body = "";
36+
req.on("data", (chunk) => {
37+
body += chunk.toString();
38+
});
39+
req.on("end", () => {
40+
fs.appendFile(LOG_FILE, body + "\n", (err) => {
41+
if (err) {
42+
console.error("Error writing to log file:", err);
43+
res.writeHead(500);
44+
res.end("Internal Server Error");
45+
} else {
46+
res.writeHead(200);
47+
res.end("Log captured");
48+
}
49+
});
50+
});
51+
return;
52+
}
53+
54+
// Static File Serving
55+
let filePath = "." + req.url;
56+
if (filePath === "./") {
57+
filePath = "./index.html";
58+
}
59+
60+
const extname = String(path.extname(filePath)).toLowerCase();
61+
const contentType = MIME_TYPES[extname] || "application/octet-stream";
62+
63+
fs.readFile(filePath, (error, content) => {
64+
if (error) {
65+
if (error.code === "ENOENT") {
66+
res.writeHead(404);
67+
res.end("404 Not Found");
68+
} else {
69+
res.writeHead(500);
70+
res.end(
71+
"Sorry, check with the site admin for error: " + error.code + " ..\n"
72+
);
73+
}
74+
} else {
75+
res.writeHead(200, { "Content-Type": contentType });
76+
res.end(content, "utf-8");
77+
}
78+
});
79+
});
80+
81+
server.listen(PORT, () => {
82+
console.log(`Server running at http://localhost:${PORT}/`);
83+
console.log(`Logs will be written to ${LOG_FILE}`);
84+
});
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { Complex } from "./src/core/complex.js";
2+
import { Geodesic } from "./src/geometry/geodesic.js";
3+
import { Solver } from "./src/physics/solver.js";
4+
import { Mobius } from "./src/core/mobius.js";
5+
6+
function assert(condition, message) {
7+
if (!condition) {
8+
console.error(`❌ FAIL: ${message}`);
9+
process.exit(1);
10+
} else {
11+
console.log(`✅ PASS: ${message}`);
12+
}
13+
}
14+
15+
console.log("=== Running Analytic Logic Tests ===");
16+
17+
// --- Mock Scene ---
18+
const scene = {
19+
mirrors: [],
20+
sources: [],
21+
};
22+
23+
// --- Test 1: Interval Splitting Logic ---
24+
// Scenario: Beam [-15, 45] (Width 60), Mirror [0, 90] (Front of it)
25+
// Expect:
26+
// 1. Fragment [-15, 0] (Miss)
27+
// 2. Fragment [0, 45] (Hit)
28+
// (Since beam ends at 45, it doesn't see the 45-90 mirror part)
29+
30+
function testIntervalLogic() {
31+
console.log("\n[Logic] Interval Visualization");
32+
33+
const solver = new Solver(scene);
34+
const source = new Complex(0, 0);
35+
36+
// Beam: -15 deg to 45 deg on Unit Circle
37+
const p1 = new Complex(
38+
Math.cos((-15 * Math.PI) / 180),
39+
Math.sin((-15 * Math.PI) / 180)
40+
);
41+
const p2 = new Complex(
42+
Math.cos((45 * Math.PI) / 180),
43+
Math.sin((45 * Math.PI) / 180)
44+
);
45+
46+
// Mirror: 0 deg to 90 deg
47+
const m1 = new Complex(Math.cos(0), Math.sin(0));
48+
const m2 = new Complex(Math.cos(Math.PI / 2), Math.sin(Math.PI / 2));
49+
const mirror = new Geodesic(m1, m2);
50+
51+
scene.mirrors = [mirror];
52+
solver.buildIndex(); // Build Spatial Index
53+
54+
const fragments = solver.computeVisibilityIntervals(source, p1, p2);
55+
56+
console.log(`Fragments: ${fragments.length}`);
57+
let hits = 0;
58+
let misses = 0;
59+
60+
for (const f of fragments) {
61+
const isHit = f.mirror !== null;
62+
if (isHit) hits++;
63+
else misses++;
64+
// console.log(`Frag: ${f.pStart.toString()} -> ${f.pEnd.toString()} | Hit: ${isHit}`);
65+
}
66+
67+
// Expect 2 (Miss then Hit).
68+
// Wait, Solver output order depends on sort.
69+
// [0, 60] range.
70+
// Mirror is relative [15, 75]? (Start is -15, Mirror starts at 0. diff is 15).
71+
// Overlap is significant.
72+
73+
if (hits === 1 && misses >= 1) {
74+
console.log("✅ PASS: Correctly split into Hit and Miss");
75+
} else {
76+
console.log(
77+
`❌ FAIL: Expected 1 Hit, >=1 Miss. Got ${hits} Hits, ${misses} Misses`
78+
);
79+
console.log("Fragments:", fragments);
80+
}
81+
}
82+
83+
function testSpatialGrid() {
84+
console.log("\n[Logic] Spatial Grid Query");
85+
const solver = new Solver(scene);
86+
const source = new Complex(0, 0);
87+
88+
// Create 4 mirrors in 4 quadrants
89+
const m1 = new Geodesic(new Complex(0.9, 0.9), new Complex(0.8, 0.8)); // Q1
90+
const m2 = new Geodesic(new Complex(-0.9, 0.9), new Complex(-0.8, 0.8)); // Q2
91+
const m3 = new Geodesic(new Complex(-0.9, -0.9), new Complex(-0.8, -0.8)); // Q3
92+
const m4 = new Geodesic(new Complex(0.9, -0.9), new Complex(0.8, -0.8)); // Q4
93+
94+
scene.mirrors = [m1, m2, m3, m4];
95+
solver.buildIndex();
96+
97+
// Query Q1 wedge
98+
const p1 = new Complex(1, 0);
99+
const p2 = new Complex(0, 1);
100+
const queryResults = solver.grid.query(source, p1, p2);
101+
102+
// Should find m1
103+
const foundM1 = queryResults.includes(m1);
104+
const foundM3 = queryResults.includes(m3); // Opposite side
105+
106+
if (foundM1 && !foundM3) {
107+
console.log("✅ PASS: Spatial Grid correctly filters quadrant");
108+
} else {
109+
console.log(
110+
`❌ FAIL: Grid logic flawed. Found M1=${foundM1}, Found M3=${foundM3}`
111+
);
112+
}
113+
}
114+
115+
testIntervalLogic();
116+
testSpatialGrid();
117+
118+
// The original "TEST 1: Interval Splitting (Proposed Logic)" block is replaced by the new testIntervalLogic() function.
119+
// The following comments and structure are from the original file, but the actual test logic is now in testIntervalLogic().
120+
// This section is kept for context if it was intended to be a separate test, but the new instruction implies replacement.
121+
// If the original block was meant to be a *different* test, it would need to be re-added as a separate function.
122+
// Given the instruction to "Add 'Spatial Grid' and 'Splitting' test cases" and the provided code for `testIntervalLogic`,
123+
// it's assumed `testIntervalLogic` is the new, improved version of the "Interval Splitting" test.
124+
125+
/*
126+
// TEST 1: Interval Splitting (Proposed Logic)
127+
{
128+
console.log("\n[Logic] Interval Visualization");
129+
const scene = { mirrors: [] };
130+
// Mirror covering [0, PI/2] roughly (First Quadrant)
131+
// Geodesic from (1,0) to (0,1) approximates this? No.
132+
// We need a mirror that subtends an angle AT THE SOURCE.
133+
// Source = Origin.
134+
// Mirror = Geodesic from (1,0) to (0,1)?
135+
// Tangent at (0,0) for Geodesic((1,0), (0,1))?
136+
// Center of that geodesic is (1,1). Radius 1.
137+
// Passes through (1,0) and (0,1).
138+
// It effectively blocks the first quadrant?
139+
// Let's use a simpler "Ideal" interval test if we can access the interval logic directly.
140+
// But we test the public API: `solver.solve()`.
141+
142+
const m1 = new Complex(1, 0); // 0 deg
143+
const m2 = new Complex(0, 1); // 90 deg
144+
const mirror = new Geodesic(m1, m2);
145+
scene.mirrors.push(mirror);
146+
147+
const solver = new Solver(scene);
148+
149+
// Beam source at origin.
150+
// Beam targets: p1 at -15 deg, p2 at +45 deg.
151+
// Cover [-15, 45] degrees.
152+
// Mirror effectively covers [0, 90] degrees (First Quadrant).
153+
// Overlap should be [0, 45].
154+
// Split should happen at 0 degrees.
155+
// Resulting Segments from `solve`:
156+
// 1. [-15, 0] -> Miss
157+
// 2. [0, 45] -> Hit Mirror
158+
159+
const source = {
160+
origin: new Complex(0, 0),
161+
p1: new Complex(0.96, -0.25), // ~ -15 deg
162+
p2: new Complex(0.707, 0.707), // ~ 45 deg
163+
};
164+
165+
try {
166+
const segments = solver.solve(source);
167+
168+
// Expected: At least 2 segments at depth 0 (or processed).
169+
// If the solver is recursive, 'segments' contains the LEAF nodes.
170+
// So we expect:
171+
// 1 segment that ends at the boundary (Miss).
172+
// 1 segment that ends at the mirror (Hit).
173+
// (And reflected segments).
174+
175+
const misses = segments.filter((s) => s.terminator === null);
176+
const hits = segments.filter((s) => s.terminator !== null);
177+
178+
console.log(
179+
`Segs: ${segments.length}, Hits: ${hits.length}, Misses: ${misses.length}`
180+
);
181+
182+
// With OLD logic: it might split adaptively into many small segments near 0.
183+
// With NEW logic: Exact split. 1 Hit, 1 Miss.
184+
185+
assert(hits.length >= 1, "Should detect hit portion");
186+
assert(misses.length >= 1, "Should detect miss portion");
187+
188+
// Strict check for Analytic Solver (Future):
189+
// assert(hits.length === 1 && misses.length === 1, "Exact Analytic Split");
190+
} catch (e) {
191+
console.log("Solver execution failed: " + e.message);
192+
}
193+
}
194+
*/
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Complex } from "./src/core/complex.js";
2+
import { Mobius } from "./src/core/mobius.js";
3+
4+
function assert(condition, message) {
5+
if (!condition) {
6+
console.error(`❌ FAIL: ${message}`);
7+
process.exit(1);
8+
} else {
9+
console.log(`✅ PASS: ${message}`);
10+
}
11+
}
12+
13+
function assertClose(a, b, epsilon = 1e-6, message = "Value check") {
14+
if (Math.abs(a - b) > epsilon) {
15+
console.error(`❌ FAIL: ${message} (Expected ${a}, Got ${b})`);
16+
process.exit(1);
17+
} else {
18+
console.log(`✅ PASS: ${message}`);
19+
}
20+
}
21+
22+
console.log("=== Running Core Tests (Complex, Mobius) ===");
23+
24+
// --- COMPLEX TESTS ---
25+
{
26+
console.log("\n[Complex] Arithmetic");
27+
const c1 = new Complex(1, 2);
28+
const c2 = new Complex(3, 4);
29+
30+
// Add
31+
const sum = new Complex();
32+
Complex.add(sum, c1, c2);
33+
assert(sum.re === 4 && sum.im === 6, "Complex Addition");
34+
35+
// Mul
36+
// (1+2i)*(3+4i) = 3 + 4i + 6i - 8 = -5 + 10i
37+
const prod = new Complex();
38+
Complex.mul(prod, c1, c2);
39+
assert(prod.re === -5 && prod.im === 10, "Complex Multiplication");
40+
41+
// Div
42+
// (1+2i)/(3+4i) = (1+2i)(3-4i)/25 = (3 -4i +6i +8)/25 = (11 + 2i)/25 = 0.44 + 0.08i
43+
const div = new Complex();
44+
Complex.div(div, c1, c2);
45+
assertClose(div.re, 0.44, 1e-9, "Complex Division Re");
46+
assertClose(div.im, 0.08, 1e-9, "Complex Division Im");
47+
}
48+
49+
// --- MOBIUS TESTS ---
50+
{
51+
console.log("\n[Mobius] Transform Logic");
52+
// Identity
53+
const id = new Mobius();
54+
const z = new Complex(10, -5);
55+
const out = new Complex();
56+
id.apply(out, z);
57+
assert(out.re === 10 && out.im === -5, "Identity Transform");
58+
59+
// Inverse
60+
// M(z) = z + 1 (Translation) -> [[1, 1], [0, 1]]
61+
const trans = new Mobius(
62+
new Complex(1, 0),
63+
new Complex(1, 0),
64+
new Complex(0, 0),
65+
new Complex(1, 0)
66+
);
67+
const inv = trans.inverse();
68+
// Inv(z) should be z - 1
69+
// Inv Matrix: [[1, -1], [0, 1]]
70+
// (1*z - 1) / 1 = z - 1
71+
inv.apply(out, z); // 10 - 5i -> 9 - 5i
72+
assert(out.re === 9 && out.im === -5, "Inverse Translation");
73+
74+
// Compose
75+
// T1(z) = z + 1. T2(z) = 2z.
76+
// T2(T1(z)) = 2(z+1) = 2z + 2.
77+
// Matrix Mul: [[2,0],[0,1]] * [[1,1],[0,1]] = [[2,2],[0,1]]
78+
const scale = new Mobius(
79+
new Complex(2, 0),
80+
new Complex(0, 0),
81+
new Complex(0, 0),
82+
new Complex(1, 0)
83+
);
84+
const comp = scale.compose(trans);
85+
console.log(
86+
`Comp Matrix: A=${comp.a.toString()}, B=${comp.b.toString()}, C=${comp.c.toString()}, D=${comp.d.toString()}`
87+
);
88+
comp.apply(out, new Complex(1, 0)); // (2(1)+2)/1 = 4
89+
assertClose(out.re, 4, 1e-9, "Composition T2(T1(z))");
90+
91+
// Normalize
92+
// M = [[2,0],[0,2]]. Det = 4. Sqrt(Det)=2.
93+
// Norm = [[1,0],[0,1]].
94+
const unNorm = new Mobius(
95+
new Complex(2, 0),
96+
new Complex(0, 0),
97+
new Complex(0, 0),
98+
new Complex(2, 0)
99+
);
100+
unNorm.normalize();
101+
assertClose(unNorm.a.re, 1.0, 1e-9, "Normalize A");
102+
assertClose(unNorm.d.re, 1.0, 1e-9, "Normalize D");
103+
}
104+
105+
console.log("=== Core Tests Passed ===");

0 commit comments

Comments
 (0)