ba0856e1ea
* 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
85 lines
3.3 KiB
Python
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
|