Campfire, One Short
For anyone arriving cold: Roundhouse transpiles Rails applications to other languages; Campfire is Basecamp's MIT-licensed chat app and the one I use to find out what that takes; and Spinel is Matz's ahead-of-time Ruby compiler, so the thing that comes out the far end is a native binary. The measure I trust is Campfire's own test suite, run file by file against that binary and published as a conformance page that clusters the failures by cause.
When Campfire was dockerized that page read 167 of 288 tests, 19 of 54 files green. Today it reads 299 of 300, 54 of 55 files green, none failing to link, and the interpreted lane — the same emitted source run under CRuby — reads the same.
Two moves inside that number
The denominator went up because the app moved under the suite. The pin advanced to Campfire's September 11 head, which brought fifteen upstream commits — all of them security hardening — and twelve new tests with them: authentication on Active Storage's direct-upload endpoints, closing a user's live cable connections on sign-out, bot keys redacted from request logs, and link previews rendered only from web URLs. Those tests pass as written. That is the strongest form of "unmodified source" this series has had to offer: not that the tests I had pass, but that the tests Basecamp wrote afterwards, about things they chose to lock down, pass on a binary they have never seen.
The numerator went up because the September 11 page was dominated by
one tail, and it is gone. Attachments work — images in messages,
uploaded avatars, the account logo — persisted on disk and served as
the thumbnails Rails serves, through libvips and a
spinel-ruby-vips package
(its entry in Spinel's package index is still a pending pull request;
the archive fetches it by git ref meanwhile). Direct uploads work: the
two Active Storage endpoints Campfire's own composer never calls but
Rails mounts on every app, guarded exactly as #267 guards them, which
is what makes that commit's tests pass rather than merely not fail.
Search works; the 500 from the last post is gone. Web push still does
not — the delivery job logs instead of sending — and the archive's
README still says so. The list is shorter, not empty.
The one that is left
test/models/action_text_attachment_test.rb, the invalid-sgid case. To
prove that a tampered signature resolves to a missing attachable, it
needs a well-formed signed global id for a record — any record — that it
can then break. It gets one like this:
# Make room instance attachable for testing purposes
room = rooms(:pets).tap { |r| r.extend ActionText::Attachable }
message, signature = rooms(:pets).attachable_sgid.split("--")
extend on an instance mixes a module into one live object. That needs a
per-object method table, and a per-object method table is the one thing
an ahead-of-time compiler does not have: the class graph, the ancestor
chains and every object's layout are fixed when the binary is built.
Spinel refuses the line at compile time, and every other strict target
Roundhouse emits would too. For a while that single line kept the whole
file from linking, so all three of its tests counted against it; now the
transpiler stubs just that test with a raise naming the construct, the
other two run and pass, and the ledger says what the honest answer is:
outside the subset. Not "not yet" — never, on any compiled target, and
the page says so rather than hiding the line.
But look at what the extend is for. It is there so that a Room can
answer attachable_sgid, and attachable_sgid is one line of
ActionText:
def attachable_sgid
to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
end
Every Active Record model already responds to to_sgid. Write that call
out and the test mints the identical bytes — same salt, same purpose,
same ?expires_in inside the signed payload — and asserts the identical
thing, without teaching one Room a trick for the length of one test. The
comment in the test calls the extend a testing-purposes hack. Removing
it is a small improvement to the test under stock Rails, whether or not
a compiler ever looks at it.
That is basecamp/once-campfire#279, and it is the first and only change I have proposed to Campfire's source in four weeks and 299 tests. In August I wrote down the rule I would work under: the port never asks for app-side divergence, and if I ever opened anything against Campfire it would be a plain-Rails change that stands on its own merits or it would not get opened. The rule has held. This is the shape of exception it was written to allow — a test reaching for dynamic Ruby it did not need — and it is still an exception, so it gets the room I have given it here rather than a footnote.
On my side of the line, to_sgid(for: ActionText::Attachable::LOCATOR_NAME).to_s
now lowers to the runtime's attachable mint with the model name baked in
at compile time, the same rule every signed id in the emit follows. With
the rewritten test in place the file is three of three on both lanes.
Until the pin moves past a merge, the page stays at one short, and the
line stays on it.
Update, three hours later: Rosa merged #279 the same afternoon. The
pin has moved to that commit — the only change upstream since the last
one — and Campfire's suite is 300 of 300 on both lanes, 55 of 55
files, measured on the new pin. The object-extend line stays on the
ledger at zero, as the tripwire for the next test that reaches for a
per-object extend; the construct is still outside the subset, this
test just stopped needing it.
Two days, and a sentence I have not said here before
Two things stood between this week's tests and green that were not mine to fix, and both were fixed upstream the day they were filed.
Campfire's log-redaction commit introduced a formatter whose call takes
four parameters. Spinel's fast path for calling a proc steps aside for
any user-defined call, but the dispatch it steps aside to admitted a
user call only when its parameter count fit the arguments at hand — so
a call of three or more parameters found no arm at all, and every test
in three files stopped there.
Filed with the four-line
discriminator, patched the same day; the compiled lane went from 286 to
290 on the merge.
Active Storage's direct-upload protocol checksums with MD5, and Spinel's
digest package bound SHA-256 and SHA-1 and nothing else. I wrote a
pure-Ruby MD5 into the runtime as a stand-in and
filed the gap with the
three-line repro and the contract that would let me delete the port.
Matz closed it within hours — a native MD5 beside the SHA pair, and
base64digest on all three classes. The port lived for one commit.
Readers of this series have watched that pattern before — the sanitizer and the trailing space in Campfire Chats went the same way — without my ever naming it. The name is in Roundhouse's contributor guide, where it has been an invariant since July: Spinel is part of this codebase. Not a dependency I build against and route around, but a second repository the work lands in, with a different owner. In practice the sentence means three things. A compiler defect goes upstream with a reproduction rather than getting a workaround. A genuine gap in what the compiler's subset can express gets recorded as a gap — on the ledger, under its own name — rather than papered over with something that pretends coverage exists. And a stand-in, when one is needed to keep moving, is written to be deleted, with the contract for deleting it filed alongside. The MD5 port is what that looks like when it goes well: one commit long.
What the page does not count
Three hundred is test/models plus test/controllers, which is what the
harness walks. Campfire also has test/lib, test/channels and
test/helpers — twelve files, ninety-seven tests — that it has never
seen, plus nine system tests that need a browser and will not be
measured this way at all. So "299 of 300" is 299 of the three-quarters
of the suite the page knows about, and that is the next thing to widen:
one directory at a time, so that each new red line arrives with its
cause and not as a lump.
Roundhouse is open source: dual-licensed MIT / Apache-2.0. Issues and discussion welcome.