Skip to content

Add test suite - #19

Open
SoundGoof wants to merge 6 commits into
masterfrom
add-test-suite
Open

Add test suite#19
SoundGoof wants to merge 6 commits into
masterfrom
add-test-suite

Conversation

@SoundGoof

@SoundGoof SoundGoof commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

dhmap's first tests: 118 across four tiers, plus a fake monitoring backend so the app runs locally without a real dhmon.

Nothing currently validates a change before it reaches /var/www/html/dhmap. With a jQuery 3→4 upgrade on the table, that needs fixing first.

Tier Tests Needs
make test-unit — JS logic in a vm sandbox 55 nothing
make test-py — generator, dev server, fake backend 49 (3 skip) nothing
make test-dom — menu under jsdom + vendored jQuery 21 podman
make test-e2e — full app in headless chromium 13 podman

The core tiers need only python3 and node. The browser tiers run in a container — deps live in a podman volume, browsers ship in the image, node_modules never touches the host or the puppet-deployed tree. CI uses the same image.

No source was restructured to make it testable: the scripts are loaded into a prepared vm context with jQuery and Raphael stubbed, so dhmap.init really runs.

Bug found

Hall ids are built by concatenation, so Hall 1 gives id="menu_hall_Hall 1". jQuery reads that as a descendant selector and matches nothing — menu hall colour roll-up has never worked for any production hall. Only visible once fixtures used realistic names. Pinned, not fixed.

Characterization, not correction

Tests pin current behaviour so they pass on a clean checkout. Known defects are labelled where pinned:

  • dhmap.jsrenderSwitch mutates its argument and init runs it twice, so centering offsets apply twice (width 5 → 8.4).
  • dhmap.js:358 — a Grid hall with no objects → TypeError.
  • dhmon.js:137alert_hosts dereferenced outside the :116 null-guard; a slow alerts.hosts fetch throws.
  • ipplan2dhmap.py:44.group(1) on a possible None; a name like VIP raises AttributeError.
  • dhmenu.js:107 — the hall id defect above.

Repaired (all were broken)

  • localserver.py imported SimpleHTTPServer — Python 2 only, so it could not run at all. Ported to http.server, redirect semantics unchanged.
  • src/examples/ crashed against current dhmap.init (pre-2017 data shape, booleans for status strings, missing DOM elements). Now covered by tests.
  • No runnable backend existed; make serve now brings the app up with fake SNMP data.

Fake backend

One scenario, two faces that cannot disagree: Prometheus (/api/v1/query, /metrics, using the real metric names from scripts/prometheus) and the seven analytics endpoints dhmon.js fetches. A test pins that the saturated fixture crosses both the dhmap threshold (>0.95) and the Prometheus rule's (>90%) — same formula, so map and alerts can't drift apart unnoticed.

Data handling

Fixtures are committed, outputs never are — data.json, temp DBs, reports and node_modules are gitignored, and git status is clean after a full run. Drop a real ipplan into local/ (gitignored) and every command uses it; make serve then renders the real event map. Real-data assertions are structural only and never name devices. CI has no database and never needs one.

Verification

  • Teeth-checked: inverting the CRITICAL ping threshold → 8 failures; a missing vendor path → E2E console-error test fails; a colour off rgb() form → 2 failures.
  • CI path: fresh clone, no install, no database → 55 + 49 pass, real-data tests skip.
  • Local mode: make serve renders the real event map (39 objects, 24 devices) with fake SNMP.

Review notes

  • whereami.html is a test.fixme(): no #menu_container, so dhmap.init throws. Recorded, not repaired.
  • localserver.py now reverse-proxies /analytics the way production apache does (ProxyPass /analytics http://localhost:5000) rather than issuing a 302. The redirect made these fetches cross-origin and preflighted, which production never does; proxying removes that divergence. Verified by stripping the fake backend's CORS headers and confirming the page still loads clean.

Follow-ups

  1. Extend the puppet dhmap manifest's ignore list (node_modules, test, .github, Makefile, package*.json).
  2. Fix the defects above, now that tests would catch the change.

Introduces the first tests this repo has had. The JS suites load the
browser scripts into a vm sandbox with jQuery and Raphael stubbed, so
dhmap.init genuinely runs and its private registries become observable
without a browser. The Python suite drives ipplan2dhmap.py as a
subprocess against a database built from a topology fixture, giving a
topology -> database -> generator -> topology round trip.

Tests characterize current behaviour rather than desired behaviour, so
they pass on a clean checkout. Four known defects are pinned as such:
the doubled renderSwitch offsets, the unguarded alert_hosts
dereference, the crash on a Grid hall with no objects, and the
AttributeError on a hall-less name without digits.

localserver.py imported SimpleHTTPServer/SocketServer and so could not
run on Python 3 at all despite its shebang; ported to http.server with
the /analytics/ redirect semantics unchanged, plus --port and
--analytics-port so tests can bind free ports.

src/examples/ had not been touched since the initial commit and
crashed against the current dhmap.init: the data was a flat array
predating the hall/Grid rewrite, example.js passed booleans where
status strings are expected, and index.html was missing the elements
init looks up. Repaired and now covered by tests so it cannot rot
again silently.

A real ipplan database can be dropped into local/ to run the same
assertions against real data; those tests skip when none is present,
which is how CI runs.
Completes the suite with the two tiers that need real browser
machinery, plus the backend they talk to.

The fake backend presents two faces over a single scenario: the
Prometheus HTTP API, using the metric names the real rules in
scripts/prometheus already use, and the seven analytics endpoints
dhmon.js fetches. Because both are rendered from the same data they
cannot disagree, and a test holds them to that. It also means `make
serve` gives a populated map with no monitoring system present, which
is the first time this repo has been runnable end to end locally.

The browser tiers run in a container rather than installing anything
on the host: jsdom and Playwright live in a podman volume and the
browsers come preinstalled in the image. CI uses the same image, so
the two environments match.

The E2E tier drives the real dev server and follows the /analytics
redirect rather than stubbing at the network layer. That redirect is a
development-only artifact - production reverse-proxies /analytics
same-origin - so it makes the fetches cross-origin and preflighted,
and the fake backend answers OPTIONS accordingly.

Testing with realistic hall names surfaced a defect worth noting: hall
element ids are built by string concatenation, so a hall named
"Hall 1" yields id="menu_hall_Hall 1", which jQuery parses as a
descendant selector and never matches. Hall colour roll-up therefore
does nothing for every hall in production. Pinned as a known defect
rather than fixed here.

The suite was checked for teeth: inverting the CRITICAL ping
threshold, pointing the page at a missing vendor file, and changing a
colour out of rgb() form each fail the relevant tier.
Production apache is configured with
`ProxyPass /analytics http://localhost:5000` (dhmon::analytics), so
the browser sees a single origin. localserver.py issued a 302 to
localhost:5000 instead, which made the same fetches cross-origin, and
jQuery's X-Requested-With header turned them into preflighted ones.

That divergence was only visible once the browser tier existed: it
forced CORS handling into the fake backend to work around behaviour
production never exhibits. Proxying instead removes the workaround -
verified by stripping the CORS headers entirely and confirming the
page still loads clean.

Also surfaces a clear 502 when the backend is unreachable, rather
than a redirect to a port with nothing behind it.

The fake backend keeps its CORS headers so it still works when a page
is pointed straight at port 5000, but nothing depends on them now.
The workflow pinned actions/checkout@v4, setup-node@v4,
setup-python@v5 and upload-artifact@v4; all four are on v7. Moved to
the moving v7 tags.

Node moves to 24, the current LTS, which is also what the playwright
image ships - so the two CI jobs and a local podman run now agree on
the runtime rather than testing node 22 in one place and 24 in
another.

ipplan2dhmap.py runs on whatever python the server happens to ship,
so the core job now runs a small matrix over 3.12 and 3.14 rather
than a single version. Both legs were verified before committing:
3.14 with node 22 on the host and 3.12 with node 24 in the container,
55 + 49 passing on each.

The playwright image is already on 1.62.0, which is current.
Picks up the merged vendor cleanup (PR #18) so this branch's diff is
computed against current master.
The proxy relayed the response to the client from inside the try that
catches OSError. BrokenPipeError is an OSError, so a browser that went
away mid-response - a reload, a closed tab, or the 10s poll racing a
navigation - was caught by the handler meant for an unreachable
backend. It then answered 502 "analytics backend unreachable" down the
socket that had just closed, which raised again and printed two
stacked tracebacks per occurrence.

Nothing failed as a result, but CI logs and local dev sessions filled
with tracebacks that point at the backend when the backend is fine.

Fetch upstream first, then relay, so writing to the client is outside
that handler; and treat BrokenPipeError/ConnectionResetError as the
ordinary event it is.

The test hangs up mid-request and asserts stderr stays clean. It fails
against the previous implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant