intertwingly

It’s just data

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.