Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ in a Go program with `import "github.com/go-embedded-ruby/ruby"`, or ship it as
single static binary that cross-compiles wherever Go does, with no C toolchain and
no libruby.

**How complete is it?** It runs **23,672** of ruby/spec's `language/` + `core/`
**How complete is it?** It passes **23,672** of ruby/spec's `language/` + `core/`
examples ([what that counts](#runtime-conformance--rubyspec)) — a large and growing
subset of the language, and **not** a drop-in replacement for CRuby. Read
[What does not work yet](#what-does-not-work-yet) before you depend on it; the
Expand Down Expand Up @@ -749,6 +749,19 @@ been removed — two were, on that pass:
sends one byte to each; `warn` goes to stderr. Covered by
`internal/vm/diagnostic_stream_test.go`.

The two prose entries were re-checked again on `6eb7e4ad` (darwin/arm64,
2026-11-04), same method and same MRI; the table further down was *not* re-swept
on that pass, so its dates still stand. One of the two had gone:

- *"A backtick command holds the interpreter lock."* While `` `cmd` `` ran,
every other Ruby `Thread` was said to stop. The probe this file carried
printed `frozen` in October and prints `ran` now, five runs out of five, and
`rubydiff` reports the whole program AGREE with MRI 4.0.7 — the two engines
produce the same output, not merely different-but-no-longer-compared output.
It is removed rather than reworded, and what closed it is deliberately *not*
attributed here: an entry that stops reproducing is a measurement, and naming
a cause without a control would be a guess dressed as a fact.

**`Timeout.timeout` never times out.** The module and `Timeout::Error` exist and
`rescue Timeout::Error` resolves, but the block runs to completion and its value
is returned: enforcing a deadline needs pre-emptive interruption of a running
Expand All @@ -763,17 +776,8 @@ This one **fails open**, which is why it is stated here rather than left in the
table below. `Timeout.timeout` is the ordinary Ruby way to bound work that might
hang, so code relying on it for that is unprotected under rbgo — and unprotected
*silently*, because the call returns successfully. The block is also yielded
`nil` where MRI yields the limit.

**A backtick command holds the interpreter lock.** While `` `cmd` `` runs, every
other Ruby `Thread` is stopped; under MRI they keep running. `Kernel#sleep` does
yield, so this is specific to the subprocess path:

```console
$ rbgo -e 't = Thread.new { loop { $n = ($n||0)+1; sleep 0.01 } }; sleep 0.05
b = $n; `sleep 1`; puts($n > b ? "ran" : "frozen")'
frozen # MRI 4.0.7: ran
```
`nil` where MRI yields the limit — re-measured on 2026-11-04, where rbgo prints
`returned` then `nil` and MRI 4.0.7 prints `raised` then `0.3`.

Otherwise:

Expand Down
137 changes: 137 additions & 0 deletions readme_ruby_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package ruby_test

import (
"os"
"regexp"
"strings"
"testing"
)

// runtimeRubyVersion reads the single constant the interpreter answers
// RUBY_VERSION with. It is the same file the documentation site's figure gate
// reads, and it is the only authority here: a README that names a version the
// runtime does not report is wrong whichever of the two moved.
func runtimeRubyVersion(t *testing.T) string {
t.Helper()
const path = "internal/vm/kernel_introspect.go"
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
m := regexp.MustCompile(`(?m)^\trubyVersion\s*=\s*"(\d+\.\d+\.\d+)"`).FindSubmatch(src)
if m == nil {
t.Fatalf("%s has no `rubyVersion = \"x.y.z\"`; this test cannot judge the README without it", path)
}
return string(m[1])
}

// readmeRubyVersionClaims are the shapes in which the README states the version
// the interpreter reports NOW. Each is a sentence that exists in the file
// today; nothing speculative is listed, because a pattern written for a
// sentence nobody writes is a false positive waiting for the sentence that
// does.
//
// ⛔ The count is asserted below. A vocabulary list is a filter, and a filter
// that matches nothing looks exactly like a file with nothing wrong -- which is
// how go-embedded-ruby/docs#44 shipped two stale figures past a gate that
// reported itself clean. If a future rewording drops out of this list the
// denominator falls and the test fails, instead of quietly checking less.
var readmeRubyVersionClaims = []*regexp.Regexp{
// | `RUBY_VERSION` | rbgo reports `"4.0.9"`, the latest MRI release …
regexp.MustCompile("rbgo reports `\"(\\d+\\.\\d+\\.\\d+)\"`"),
// … and the summary above count the current `BASELINE` and so already
// reflect 4.0.9.
regexp.MustCompile(`already reflect (\d+\.\d+\.\d+)`),
}

// historicalMarkers are the phrases that let a line name a version OTHER than
// the one the runtime reports. Two lines qualify today, and they are different
// kinds of past tense:
//
// "a sweep taken at `RUBY_VERSION` 3.4.1" -- a dated measurement
// "The move from `3.4.1` was measured …" -- the version left behind
//
// Naming the exemptions rather than forbidding every deviation is deliberate. A
// check loosened to "any mismatch is fine" has stopped checking; this one still
// fails on an undeclared mismatch, and logs the lines it let through so the
// exemption stays visible instead of becoming invisible slack.
var historicalMarkers = []string{"sweep", "the move from"}

// historicalMarkerOf reports which marker, if any, lets this line name an older
// version.
func historicalMarkerOf(line string) (string, bool) {
low := strings.ToLower(line)
for _, m := range historicalMarkers {
if strings.Contains(low, m) {
return m, true
}
}
return "", false
}

// TestReadmeRubyVersionMatchesTheRuntime: the README's conformance TOTAL is
// gated by TestReadmeConformanceCountMatchesTheBaseline, which reads BASELINE.
// The version that total is counted AT was gated by nothing, and it selects the
// population counted: the move from 3.4.1 to 4.0.9 changed which
// `ruby_version_is` guards admit examples (+144 passing, 27 gone red), so the
// same number means a different thing at a different version.
func TestReadmeRubyVersionMatchesTheRuntime(t *testing.T) {
want := runtimeRubyVersion(t)

readme, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
lines := strings.Split(string(readme), "\n")

checked := 0
for i, line := range lines {
for _, re := range readmeRubyVersionClaims {
for _, m := range re.FindAllStringSubmatch(line, -1) {
checked++
if m[1] != want {
t.Errorf("README.md:%d claims the runtime reports %s, kernel_introspect.go says %s\n %s",
i+1, m[1], want, strings.TrimSpace(line))
}
}
}
}
if checked < len(readmeRubyVersionClaims) {
t.Errorf("only %d present-tense version claim(s) matched in README.md, one per shape was expected at least; "+
"a rewording has taken a claim out of this test's reach rather than out of the README", checked)
}

// The inverse: a line that talks about RUBY_VERSION and names a DIFFERENT
// version must say it is historical.
//
// ⛔ The two halves cover each other's blind spot, which is why both are
// here. Measured by setting the table row to 4.0.8: the forward check named
// it, while this one EXEMPTED the same 4.0.8 — the line also says "the move
// from", so a marker meant for the version left behind covered a wrong
// present-tense claim sitting beside it. A line-granular exemption cannot
// tell the two apart; the forward check, which reads the claim itself, can.
version := regexp.MustCompile(`\b(\d+\.\d+\.\d+)\b`)
exempt := 0
for i, line := range lines {
if !strings.Contains(line, "RUBY_VERSION") {
continue
}
for _, m := range version.FindAllStringSubmatch(line, -1) {
if m[1] == want {
continue
}
if marker, ok := historicalMarkerOf(line); ok {
exempt++
t.Logf("README.md:%d names %s and is exempt: the line says %q", i+1, m[1], marker)
continue
}
t.Errorf("README.md:%d names RUBY_VERSION %s, the runtime reports %s, and the line says none of %v\n %s",
i+1, m[1], want, historicalMarkers, strings.TrimSpace(line))
}
}
t.Logf("%d present-tense version claim(s) checked against %s, %d historical line(s) exempt", checked, want, exempt)
}
Loading