Legacy System Migration (Perl → Python)
A ~30,000-line, 30-module Perl application rewritten as ~25,000 lines of Python, with a replay oracle that proved identical behavior before anyone had to trust it — then extended with nine requested features.
Role
Sole engineer
Context
750+ person PCB manufacturer
Status
In production
Verified by
Dual-interpreter replay oracle · 704 automated tests, 0 skipped
Results
Automated tests · 0 skipped, from zero
Management-requested features · Delivered and live
Release archives · ~6 weeks
Figure
Differential comparison of two command streams. The same scripted CAM session runs through the original Perl application and the Python port, and every emitted command is captured and compared byte for byte by a replay oracle; matching pairs are marked with a check. During development a divergence, such as the one shown at row 0008, meant the two implementations emitted different bytes — each was investigated one by one, and one turned out to be a latent bug in the twenty-year-old original: a string-comparison typo in the soldermask path, root-caused and fixed. At cutover there were zero divergences — the output was byte-for-byte identical. Thirteen representative rows are shown and the row lengths are illustrative.
The problem
An IPC-2221 test-coupon generator — roughly 30,000 lines of Perl across 30 modules, with a twenty-year lineage — drove the Genesis 2000 CAM system on live production jobs. It had no maintainer, no tests, and no documentation of intent: only source code, which is a description of behavior but not of why.
Everyone agreed it should be modernized. Nobody could agree to the risk. A rewrite that is 99% faithful is not a modernization; it is an outage waiting for a specific input. The blocker was never the porting work — it was that no one could prove the new thing behaved like the old thing.
Constraints
- Zero tolerance for behavior change. Production runs on this daily, and downstream manufacturing tooling consumes the output — it had to match the Perl exactly, byte for byte.
- The only real specification was the original source, quirks and latent bugs included.
- A Python 3.6 runtime floor on the production Genesis servers, while developing on 3.11+ — no modern syntax anywhere.
- The port had to preserve the original’s structure — same module, function, and variable names — so the senior CAM engineer could diff Perl against Python line by line.
- The process’s stdout is the wire to the CAM engine: every command is a framed line, and a stray print corrupts the session.
How I built it
The port itself was module by module, line-matched and unglamorous. What made it safe was the verification system built in layers alongside it. The first layer is a protocol-replay oracle: a transcript format captures a full CAM conversation, a runner executes the original Perl and the Python port against the same scripted session, and a comparator diffs the two command streams. One byte of difference fails the test.
Under that sit fake-CAM and fake-GUI doubles — a stand-in CAM engine that answers like the real one, and a mock widget registry that lets the real GUI code run and be asserted on with no display — so all 704 tests run headless on the bare production servers. And every behavior change ships with deliberate sabotage runs: the new logic inverted, guards deleted, indexes hardcoded. A change is only accepted when a test catches every mutation — twenty-plus documented so far, all caught.
New features intentionally change output, which would break a byte-oracle — so a deviation classifier proves every difference against the Perl is exactly an intended, cataloged change and nothing else, and every feature parses to off when its config keys are absent. The byte-match bar was never loosened. The comparison also paid a dividend: it surfaced a latent bug in the twenty-year-old original — a string-comparison typo in the soldermask path — root-caused in the source and fixed in the port.
That safety net is what made the second phase possible: nine management-requested features, delivered nine for nine. The flagship was automated board markings — date codes, serialization, logos and cage-code text across roughly 92 config keys — built the same day the spec arrived and carried through seven same-or-next-day correction rounds. An XRF measurement section chained through the site’s InPlan REST API, and config-driven behavior gates shipped with parity-safe defaults. The whole run went out as 25+ dated release archives that assert their own contents, and the port replaced the Perl in production.
What I’d do differently
I would build the replay oracle first. It began as scaffolding for the port and turned out to be the actual product — the port was the easy half, and every feature after it rode on the oracle’s guarantee.
Stack
Python (3.6-compatible) · Perl · tkinter · Genesis 2000 / InCAM · protocol replay testing · mutation testing · REST