diff --git a/README.md b/README.md index b7dd2c76..7b8df778 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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: diff --git a/readme_ruby_version_test.go b/readme_ruby_version_test.go new file mode 100644 index 00000000..36109317 --- /dev/null +++ b/readme_ruby_version_test.go @@ -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) +}