Files
Zecheng Zhang ba0856e1ea fix: one error vocabulary and one stat view per language (#764)
* fix(errors): one condition vocabulary, one classifier per language

A condition is named once (FsCondition) and every boundary keeps only
a table from that name to its own number: posix, wasi preview1, and
the monty guest view. CycleError gets its ELOOP seat (was EIO at every
kernel and guest boundary), the wasi wire's EXDEV is 75 per wasi-libc
(18 is EDOM there), and the cross-mount-rename-is-ENOENT decision
moves from an adapter arm into the wasi table row.

* fix(stat): one stat view per language, offset-less stamps read as UTC

mtime, dir-ness, content size and the mode bits live once per language
(utils/stat_view), delegating to the existing naive-stamp-is-UTC
parsers. Three of the four translators (node FUSE, the runtime bridge,
py wasm) read an offset-less backend stamp as local time, so python
FUSE and node FUSE disagreed by the host's UTC offset for the same
stamp; all four now answer the same epoch, pinned by tests.

* refactor(errors): move the wasi and cpython tables into their runtimes

* refactor(errors): classifier arm tables into constants modules

* fix(errors): address review on the ValueError arm and epoch-zero mtime
2026-08-12 01:41:13 -07:00

85 lines
3.3 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import errno
import pytest
from mirage.errors.classify import classify
from mirage.errors.posix import POSIX
from mirage.errors.types import FsCondition
from mirage.runtime.errors import CrossMountError
from mirage.utils.errors import enotsup, no_mount
from mirage.utils.path import CycleError
@pytest.mark.parametrize("exc,expected", [
(CycleError("/a"), FsCondition.ELOOP),
(CrossMountError("/a/x", "/b/x"), FsCondition.CROSS_MOUNT),
(FileNotFoundError("/x"), FsCondition.ENOENT),
(NotADirectoryError("/x"), FsCondition.ENOTDIR),
(IsADirectoryError("/x"), FsCondition.EISDIR),
(FileExistsError("/x"), FsCondition.EEXIST),
(PermissionError("/x"), FsCondition.EACCES),
(enotsup("ram", "unlink", "/x"), FsCondition.ENOTSUP),
(NotImplementedError("append"), FsCondition.ENOTSUP),
(no_mount("/x"), FsCondition.ENOENT),
])
def test_class_arms(exc, expected):
assert classify(exc) is expected
@pytest.mark.parametrize("code,expected", [
(errno.ENOTEMPTY, FsCondition.ENOTEMPTY),
(errno.EXDEV, FsCondition.EXDEV),
(errno.ELOOP, FsCondition.ELOOP),
(errno.EPERM, FsCondition.EPERM),
(errno.EBUSY, FsCondition.EBUSY),
(errno.EROFS, FsCondition.EROFS),
(errno.EINVAL, FsCondition.EINVAL),
(errno.EIO, FsCondition.EIO),
])
def test_errno_carrying_oserror_arms(code, expected):
assert classify(OSError(code, "x")) is expected
def test_xattr_miss_is_one_condition_whatever_the_platform_calls_it():
# ENOATTR on macOS, ENODATA on Linux: one condition, resolved through
# the posix row so the reverse arm matches the running host.
number = POSIX[FsCondition.NO_XATTR].errno
assert classify(OSError(number, "x")) is FsCondition.NO_XATTR
def test_a_subclass_wins_over_its_stamped_errno():
# FileNotFoundError IS an OSError with errno 2; the class arm answers
# before the errno lookup so the two can never disagree.
assert classify(FileNotFoundError(errno.ENOENT, "x")) is FsCondition.ENOENT
@pytest.mark.parametrize(
"exc",
[
OSError("bare message, no errno"),
RuntimeError("something else entirely"),
OSError(errno.ENAMETOOLONG, "outside the vocabulary"),
# A backend refusal that is not absence (an oversized read, a
# rename into the source's own subtree): only the registry's typed
# miss reads as ENOENT.
ValueError("row too large to render"),
])
def test_unnamed_conditions_answer_none(exc):
# None means "no named condition": the caller keeps its own fallback (FUSE
# passes a raw OSError errno through, wasi answers EIO/EINVAL).
assert classify(exc) is None