Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
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
128 changes: 115 additions & 13 deletions eden/scm/lib/cpython-ext/src/io_error.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,26 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

use cpython::ObjectProtocol;
use cpython::Python;
use cpython::PythonObject;
use cpython::ToPyObject;
use cpython::exc;
use util::path_error_details;

pub fn translate_io_error(py: Python, e: &std::io::Error) -> cpython::PyErr {
if let Some(details) = path_error_details(e) {
let e = details.original_io_error;
let errno = io_error_errno(e);
return cpython::PyErr::new::<exc::OSError, _>(
py,
(
errno,
io_error_strerror(e, errno),
details.path.display().to_string(),
),
);
}
let (e, filename) = match path_error_details(e) {
Some(details) => (
details.original_io_error,
Some(details.path.display().to_string()),
),
None => (e, None),
};

let errno = io_error_errno(e);
cpython::PyErr::new::<exc::OSError, _>(py, (errno, io_error_strerror(e, errno)))
let strerror = io_error_strerror(e, errno);

// On Windows `raw_os_error` is a Win32 error code, not an errno. Report it
// as `OSError.winerror` so CPython replaces the errno above with the
// matching one, like it does for its own Windows errors in
// `PyErr_SetExcFromWindowsErrWithFilenameObjects`. Without this,
// ERROR_PATH_NOT_FOUND (3) would be taken as ESRCH, and ERROR_ACCESS_DENIED
// (5) as EIO, picking the wrong exception type below.
#[cfg(windows)]
let args = (errno, strerror, filename, e.raw_os_error()).to_py_object(py);
#[cfg(not(windows))]
let args = (errno, strerror, filename).to_py_object(py);

// Instantiate the exception instead of using
// `PyErr::new::<exc::OSError, _>(py, args)`, which would keep `OSError` as
// the exception type and only build the instance when the error gets
// normalized. Normalization does not adjust the type, so the type stays
// `OSError` even though `OSError.__new__` picks a subclass based on errno.
// Interpreters matching `except` clauses against the type instead of the
// instance (CPython 3.10 and older) then fail to run `except
// FileNotFoundError` for an ENOENT error raised from Rust.
let os_error_type = py.get_type::<exc::OSError>().into_object();
match os_error_type.call(py, args, None) {
Ok(instance) => cpython::PyErr::from_instance(py, instance),
// Building the exception failed. Report that failure instead.
Err(err) => err,
}
}

fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
Expand All@@ -51,6 +75,11 @@ fn io_error_strerror(e: &std::io::Error, errno: Option<i32>) -> String {
message
}

/// The raw OS error code, or an errno inferred from the error type.
///
/// On Windows `raw_os_error` is a Win32 error code rather than an errno. It is
/// still the code Rust puts in the error message, and it is passed to Python as
/// `OSError.winerror` so that CPython derives the real errno from it.
fn io_error_errno(e: &std::io::Error) -> Option<i32> {
if let Some(errno) = e.raw_os_error() {
return Some(errno);
Expand DownExpand Up@@ -84,3 +113,76 @@ fn io_error_errno(e: &std::io::Error) -> Option<i32> {
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::io;

use cpython::PythonObjectWithTypeObject;

use super::*;

#[test]
fn test_missing_file_is_a_file_not_found_error() {
let gil = Python::acquire_gil();
let py = gil.python();

let io_error = std::fs::metadata("this-path-does-not-exist").unwrap_err();
let err = translate_io_error(py, &io_error);

// `PyErr::matches` is what an `except FileNotFoundError` clause does on
// CPython 3.10 and older: it compares against the exception type rather
// than the exception instance. Newer CPython compares against the
// instance and hides the difference when the error is actually raised,
// so this checks the exception type directly to stay meaningful on
// every Python version.
assert!(err.matches(py, py.get_type::<exc::FileNotFoundError>()));
}

#[cfg(unix)]
#[test]
fn test_errno_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

assert_error::<exc::FileNotFoundError>(py, libc::ENOENT, libc::ENOENT);
assert_error::<exc::PermissionError>(py, libc::EACCES, libc::EACCES);
}

#[cfg(windows)]
#[test]
fn test_win32_error_code_selects_the_exception_type() {
let gil = Python::acquire_gil();
let py = gil.python();

// Win32 error codes are not errno values. Used as errno,
// ERROR_PATH_NOT_FOUND would be ESRCH and ERROR_ACCESS_DENIED would be
// EIO.
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
const ERROR_ALREADY_EXISTS: i32 = 183;

assert_error::<exc::FileNotFoundError>(py, ERROR_FILE_NOT_FOUND, libc::ENOENT);
assert_error::<exc::FileNotFoundError>(py, ERROR_PATH_NOT_FOUND, libc::ENOENT);
assert_error::<exc::PermissionError>(py, ERROR_ACCESS_DENIED, libc::EACCES);
assert_error::<exc::FileExistsError>(py, ERROR_ALREADY_EXISTS, libc::EEXIST);
}

/// Check that an `io::Error` with `raw_os_error` becomes an exception of
/// type `T` carrying `errno`.
fn assert_error<T: PythonObjectWithTypeObject>(py: Python, raw_os_error: i32, errno: i32) {
let mut err = translate_io_error(py, &io::Error::from_raw_os_error(raw_os_error));
assert!(
err.matches(py, py.get_type::<T>()),
"os error {raw_os_error} has an unexpected exception type"
);
let actual = err
.instance(py)
.getattr(py, "errno")
.unwrap()
.extract::<i32>(py)
.unwrap();
assert_eq!(actual, errno, "os error {raw_os_error} has a wrong errno");
}
}
64 changes: 64 additions & 0 deletions eden/scm/tests/test-rust-io-errors.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.

"""I/O errors raised by Rust are catchable as the matching OSError subclass.

Rust reports I/O errors as `OSError` plus its constructor arguments, and
CPython only builds the concrete exception when the error is normalized.
Normalization keeps `OSError` as the exception type even though
`OSError.__new__` picks a subclass from errno, so on CPython 3.10 and older -
which matches `except` clauses against the type rather than the instance -
`except FileNotFoundError` did not catch a missing file reported by Rust.
`localrepo.transaction()` relies on exactly that, which used to make every
locked transaction (clone, pull, rebase) abort on Windows builds, where the
embedded interpreter is CPython 3.10.

CPython 3.12 and newer normalize exceptions when they are raised, so these
tests pass with or without the fix there. The check that is meaningful on
every Python version lives in the Rust unit tests of
eden/scm/lib/cpython-ext/src/io_error.rs.
"""

import os
import tempfile
import unittest

import silenttestrunner
from sapling import vfs as vfsmod


class testrustioerrors(unittest.TestCase):
def setUp(self):
self.vfs = vfsmod.vfs(tempfile.mkdtemp(dir=os.getcwd()), audit=False)

def teststat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.stat("missing")

def testlstat(self):
with self.assertRaises(FileNotFoundError):
self.vfs.lstat("missing")

def testread(self):
with self.assertRaises(FileNotFoundError):
self.vfs.read("missing")

def testlistdir(self):
with self.assertRaises(FileNotFoundError):
self.vfs.listdir("missing")

def testunlink(self):
with self.assertRaises(FileNotFoundError):
self.vfs.unlink("missing")

def testlexistsofmissingvfs(self):
# lexists() swallows FileNotFoundError to report a vfs whose own base
# directory is gone.
missing = vfsmod.vfs(os.path.join(self.vfs.base, "missing"), audit=False)
self.assertFalse(missing.lexists("anything"))


if __name__ == "__main__":
silenttestrunner.main(__name__)
Loading