Repository navigation
Dir.pwd raises when the working directory is gone (ruby_getcwd), and the corpus total drops 122 because of it - #685
Merged
Merged
Conversation
`Dir.pwd` discarded the error from `os.Getwd()` and returned the path that no
longer existed. MRI reaches `util.c ruby_getcwd()`, which never hands a failure
back to its caller: it calls `rb_syserr_fail(errno, "getcwd")`. So a removed
working directory is `Errno::ENOENT: No such file or directory - getcwd`.
Checking the error was NOT enough. On darwin Go's `syscall.Getwd` is
`getattrlist(".", ATTR_CMN_FULLPATH)`, which still reports the recorded path of
an unlinked directory and returns no error at all (measured: macOS 25, Go
1.26.4). The answer is therefore confirmed the way `getcwd(3)` confirms it, by
resolving the name and requiring it to still be the same file as ".". That one
check reproduces all three cases witnessed against MRI 4.0.5: directory removed
(ENOENT), removed and recreated so the name resolves to another inode (ENOENT),
and an unsearchable parent (EACCES). A cwd reached through a symlink still
passes, because os.Getwd names the physical path.
The same `ruby_getcwd()` call is reached by `file.c
rb_file_expand_path_internal` for a relative path with no base, so
`File.expand_path`, `File.absolute_path` and `File.realdirpath` raise there too.
Two siblings of the shape were measured NOT to raise, and are left alone with a
comment saying why:
- `Dir.chdir(d)` without a block never consults getcwd; `dir.c chdir_path`
takes the directory to restore only under `chdir_alone_block_p()`. The
block form now raises, the plain form still returns 0.
- `Dir#chdir` is `dir_chdir -> dir_s_fchdir`, which saves the previous
directory by opening "." rather than by calling getcwd, so it succeeds out
of a removed directory in MRI. Raising there would be a divergence.
While in `chdir_path`: a failed `chdir(2)` kept the real errno and the C
function's name, instead of asserting `Errno::ENOENT @ dir_chdir`. Changing into
a regular file is now `Errno::ENOTDIR: Not a directory @ chdir_path - /etc/hosts`,
as MRI reports it.
Read for this: `util.c ruby_getcwd`, `dir.c rb_dir_getwd_ospath` /
`rb_dir_getwd` / `dir_s_getwd` / `chdir_path` / `dir_s_fchdir`,
`file.c rb_file_expand_path_internal`, `error.c rb_syserr_fail`.
Refs #681
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The witnesses (each one fails against origin/main, and the mutation compiles):
Dir.pwd / Dir.getwd, File.expand_path, File.absolute_path,
File.expand_path(x, "."), File.realdirpath and Dir.chdir(d){} out of a removed
working directory; Dir.pwd out of one that was removed AND RECREATED, which a
bare error check would have missed entirely because os.Getwd names it happily;
and chdir into a regular file, now Errno::ENOTDIR @ chdir_path.
Two of the tests are GUARDS and say so in their comments: the plain Dir.chdir(d)
and Dir#chdir out of a removed directory, which MRI 4.0.5 was measured NOT to
fail, and Dir.pwd in an intact directory including one reached by symlink.
os.Getwd's error branch and stat(".")'s cannot be produced on darwin, so they go
through seams: osGetwd joins the existing osStat from filestat.go. The two
raiseChdirErr fallbacks (an errno with no registered class, an error carrying no
errno) are exercised by calling it directly and recovering the raise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Windows refuses to unlink a directory that is any process's current directory, so the state these tests need cannot be constructed there; and MRI takes a different implementation on that platform (rb_w32_ugetcwd, dir.c line 140), so there is no witness here for what it would report. The chdir errno mapping is skipped for the same reason. The 100% coverage gate runs on the POSIX lanes, so no coverage is lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h one The windows lane was red on three tests and two causes, both in the assertions rather than in the product. Neither fix adapts an expectation to observed behaviour: one drops a claim about the C LIBRARY that was never a claim about rbgo, the other skips a case that has no MRI witness at all. 1. The strerror sentence is the platform's. Windows maps syscall.ENOENT onto ERROR_FILE_NOT_FOUND, so errnoStrerror renders it "The system cannot find the file specified." What rb_syserr_fail guarantees everywhere is the Errno class and the OPERATION name, so that is what is asserted cross-platform — the class, and a message ending in "- getcwd" or containing "@ chdir_path - ". The POSIX wording is still pinned exactly, on the POSIX lanes. The narrowed dir_test.go row was re-A/B'd and still FAILS against origin/main (which says "@ dir_chdir"), so it lost the libc sentence and none of its discriminating power. 2. TestDirPwdIntact cannot run on Windows. GetCurrentDirectory answers the 8.3 short name (C:/Users/RUNNER~1/... where t.TempDir says runneradmin) and does not resolve a symlinked working directory. Both are about the platform's idea of a canonical path, MRI uses rb_w32_ugetcwd there and there is no Windows MRI on this host to witness against — the short-name half is open issue #636, which these three assertions are the first thing in the repo to REPRODUCE on a runner rather than reason about. Canonicalising the expected path to match would bake both behaviours into a test and make #636 harder to find, so it skips naming the gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 26, 2026
The provisional 22650 was the reading on #685 before #692 merged. The combined tree reads 23113 — the same total #692 measured on its own, so #685's -122 did NOT subtract from it. That is informative rather than a coincidence: the 122 examples #685 removed were the Dir specs continuing past a deleted working directory, and #692 restores the `after :all` hook whose absence deleted it. With the hook back, the directory survives and Dir.pwd never has to raise. The two changes address one root cause from opposite sides and overlap instead of composing, which is why predicting the sum would have been wrong in either direction. Taken from the lane that applies the floor, on the merge of this branch with main, not from a development host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 26, 2026
* Raise the ruby/spec floor to 22650 after waves 33-36 Provisional, and deliberately conservative: 22650 is the number CI's own ratchet read on #685 (Dir.pwd) before #692 (the mspec shim repair) merged, so it is known-good rather than predicted. The two land together for the first time on this branch, and they interact: #685 makes Dir.pwd raise when the working directory has been removed, while #692 restores the `after :all` hook whose absence was what removed it. Neither branch's CI ran with the other. So this PR is also the measurement. Its own ratchet lane reports the real total for the combined tree, and the floor is then set to that figure in a follow-up commit here — taken from the lane that applies it, never from a development host, which reads a different number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Set the floor to 23113, the figure this PR's own ratchet lane measured The provisional 22650 was the reading on #685 before #692 merged. The combined tree reads 23113 — the same total #692 measured on its own, so #685's -122 did NOT subtract from it. That is informative rather than a coincidence: the 122 examples #685 removed were the Dir specs continuing past a deleted working directory, and #692 restores the `after :all` hook whose absence deleted it. With the hook back, the directory survives and Dir.pwd never has to raise. The two changes address one root cause from opposite sides and overlap instead of composing, which is why predicting the sum would have been wrong in either direction. Taken from the lane that applies the floor, on the merge of this branch with main, not from a development host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #681.
The defect
Dir.pwddiscarded the error fromos.Getwd()and returned the path of aworking directory that no longer existed. MRI reaches
util.c ruby_getcwd(),which never hands a failure back to its caller — it calls
rb_syserr_fail(errno, "getcwd")— so a removed working directory isErrno::ENOENT: No such file or directory - getcwd.The C read behind each decision
"<strerror> - getcwd"with no@ funcpartutil.c ruby_getcwd→error.c rb_syserr_failDir.pwd/Dir.getwdreach itdir.c dir_s_getwd→rb_dir_getwd→rb_dir_getwd_ospathFile.expand_path/File.absolute_pathreach it for a relative path with no basefile.c rb_file_expand_path_internal,append_fspath(result, fname, ruby_getcwd(), &enc, fsenc)File.realdirpathreaches itfile.c rb_check_realpath_emulate(curdir = rb_dir_getwd_ospath())Dir.chdir(d)reaches it only with a blockdir.c chdir_path:if (chdir_alone_block_p()) { args.old_path = rb_str_encode_ospath(rb_dir_getwd()); … }Dir#chdirnever reaches itdir.c dir_chdir→dir_s_fchdir, which saves the previous directory withdir_initialize(…, ".")— an opendir, not a getcwdchdir(2)keeps its real errno under the namechdir_pathdir.c chdir_path,rb_sys_fail_path(path)File.realpathdoes not reach it on macOSfile.c rb_check_realpath_internaluses nativerealpath(3)on the unresolved path whenHAVE_REALPATHThe premise the measurement overturned
Adding the error check fixes nothing on this platform. Go's
syscall.Getwdondarwin is
getattrlist(".", ATTR_CMN_FULLPATH): it keeps reporting the recordedpath of an unlinked directory and returns
err == nil. Measured on macOS 25/ Go 1.26.4 — before and after the removal,
os.Getwd()returns the same pathwith no error.
So the answer is confirmed the way
getcwd(3)confirms it, and the wayos.Getwditself validates$PWD: the name it produced must still resolve tothe same file as
".". One check covers the three cases witnessed against MRI4.0.5:
Errno::ENOENT;Errno::ENOENTagain (os.Getwdis perfectly happy here);Errno::EACCES, which is whatgetcwd(3)reports.A working directory reached through a symlink still passes:
os.Getwdnames thephysical path, which is the same file.
Siblings of the shape — two of the four must NOT raise
grepfinds fouros.Getwd()calls. Measurement against MRI 4.0.5 says onlytwo of them should fail, and both of the others are now commented so the next
sweep has to argue with MRI rather than with a rule:
Dir.chdir(d)without a block — MRI never consults getcwd and returns 0.The block form now raises.
Dir#chdir— MRI saves the previous directory by opening".", so it succeedsout of a removed directory. Raising there would be a new divergence.
While in
chdir_path: a failedchdir(2)assertedErrno::ENOENT @ dir_chdirfor every failure. It now keeps the real errno under MRI's own label, so changing
into a regular file is
Errno::ENOTDIR: Not a directory @ chdir_path - /etc/hosts.Conformance: −122, and that is the fix working
The corpus was measured serially (
-jobs 2for the sweep,-jobs 1to re-readevery file that moved, three times each) at
SPEC_SHA 87b1631.22759 → 22637 (−122) locally, and CI's own ratchet reads the same move on a
different host: 22772 on
mainat2aa1c944, 22650 on this branch at018b156— also −122, with the floor still 175 clear.core/dir/glob_spec.rbcore/dir/element_reference_spec.rbcore/random/bytes_spec.rbNothing else moved across four full sweeps (one before, three after: 22637,
22637, 22636, 22636 — the ±1 is
core/random/bytes_spec.rbagain).Two readings that are NOT results, both caught because the harness separates a
timeout from a zero:
core/kernel/require_spec.rbcame backTIMEOUT/ 0 in one sweep, which wouldhave read as −69. Run on its own it takes 0.045s and reports 69, three times
for each binary. It hangs intermittently; nothing here changed it.
core/process/exit_spec.rbflipsNORESULT↔TIMEOUTbetween runs at 0passing either way — a timeout boundary, not a move.
The two
Dirfiles did not merely reach MRI's pass total. Their wholebreakdown is identical —
pass=16 fail=44 error=4andpass=16 fail=43 error=38— and so is the outcome of every single example, compared name byname: 48 and 81 detail lines,
diff-clean against MRI. Every example lost isone MRI 4.0.5 also fails or errors. Those examples were being counted with no
judge behind them, because the missing error was itself what let the file
continue past the teardown that removes the working directory.
FLOORis deliberately untouched.Left undone, with reasons
core/io/read_spec.rbis not this defect. The issue attributed its 104 vs82 gap to
Dir.pwd; measured, the file does not move at all. 24 of MRI's 28errors there are
TypeError: no implicit conversion of Hash into Stringon theio_read_internal_encodingshared specs. Minimal witness:IO.read(f, {mode: "r"})isTypeError: no implicit conversion of Hash into Integerin MRI 4.0.5 and returns the file's contents in rbgo;IO.read(f, nil, nil, {mode: "r"})isArgumentError: wrong number of arguments (given 3, expected 0..2)in MRI and also succeeds here. Owning fileinternal/vm/io_class.go(ioReadFile), outside this PR's cluster.Dir.entries(".")in a removed directory returns[".", ".."]where MRIreturns
[]: the synthesised dot entries are unconditional. Same file as thischange, different defect; it does not affect the corpus (the two
Dirfilesconverge exactly without it).
File.realpath's message in a removed directory is nowErrno::ENOENT … - getcwdwhere MRI reports… @ rb_check_realpath_internal - x, because MRI takes the nativerealpath(3)path on the raw relativeargument and never expands it. The class matches, and rbgo already reported the
wrong path there before this change (
… - <expanded>/x); making it exact meansnot expanding before resolving, which is a larger change than this one.
Dir#chdiron a handle whose directory was removed succeeds in MRI (itfchdirs an open fd) and is
Errno::ENOENThere. rbgo holds no fd for an openDir, so this needs the handle to carry one.GOOS=js GOARCH=wasmis still red onerrno.go— that is GOOS=js GOARCH=wasm has not compiled for 3 days, and the test that catches it is behind an env var no workflow sets #682, untouchedhere.
and two causes, both in the assertions rather than in the product. Fixed in
04e6fd3, and neither fix adapts an expectation to observed behaviour:the strerror sentence is the C library's. Windows maps
syscall.ENOENTonto
ERROR_FILE_NOT_FOUND, soerrnoStrerrorrenders it "The system cannotfind the file specified." The class was right on every failing line. What
rb_syserr_failguarantees on every platform is theErrno::Exxxclass andthe OPERATION name, so that is what is now asserted cross-platform — the
class, plus a message ending in
- getcwdor containing@ chdir_path -.The POSIX wording is still pinned exactly, on the POSIX lanes. The narrowed
dir_test.gorow was re-A/B'd and still fails againstorigin/main(which says
@ dir_chdir), so it lost the libc sentence and none of itsdiscriminating power.
TestDirPwdIntactnow skips on Windows naming File.realpath on Windows: 8.3 short names are not expanded, and MRI's behaviour there is unwitnessed #636.GetCurrentDirectoryanswers the 8.3 short name (
C:/Users/RUNNER~1/…wheret.TempDirsaysrunneradmin) and does not resolve a symlinked working directory; both areabout the platform's idea of a canonical path, MRI uses
rb_w32_ugetcwdthere,and there is no Windows MRI here to witness against. These three assertions are
the first thing in the repo to reproduce File.realpath on Windows: 8.3 short names are not expanded, and MRI's behaviour there is unwitnessed #636 on a runner rather than reason
about it, and they showed it is not confined to the realpath family — the short
name comes out of
os.Getwd, soDir.pwdandFile.expand_pathinherit ittoo. Recorded on File.realpath on Windows: 8.3 short names are not expanded, and MRI's behaviour there is unwitnessed #636 with the runner output; a second Windows fact found beside
it (a symlinked cwd is reported as the link) is recorded there as well.
Checked and not a third cause: mapping a Win32 code through the POSIX errno
table is safe here, because Go aliases exactly
ENOENT = ERROR_FILE_NOT_FOUNDand
ENOTDIR = ERROR_PATH_NOT_FOUNDand puts every otherE*aboveAPPLICATION_ERROR(1<<29), which Windows reserves and never returns. SoraiseChdirErrcannot misclassify a real Win32 error as an unrelatedErrno::Exxx.Gates
CGO_ENABLED=0 GOWORK=off go test ./...(full suite),go build ./...,GOOS=wasip1 GOARCH=wasm go build ./...,gofmt -l internal/ cmd/empty, andthe below-100%-coverage function list diffed against
origin/main's on the same-coverpkg=./internal/...profile, measured in a detached worktree atorigin/main: identical, 17 functions, same percentages, no new entry andnothing orphaned (the rounded total reads 99.9% on both, which is why the lists
and not the totals were compared).
Every new assertion was A/B'd: reverted to
origin/main'sdir.go/file.go,confirmed the mutation compiles (
go vetclean) and that the witnesses fail,then restored. The three tests that pass both ways say in their comments that
they are guards, not witnesses.
🤖 Generated with Claude Code