Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Verify coverage threshold fixtures
shell: pwsh
run: .\eng\coverage\coverage.threshold.tests.ps1

- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ Run the mixed-language coverage pipeline:

The script builds instrumented Python and Node native modules, runs the
Rust/Python/JavaScript tests and E2E suite, and writes HTML, LCOV, XML, and JSON
reports under `artifacts\coverage`.
reports under `artifacts\coverage`. Default line gates are Rust 45%, Python
70%, and JavaScript 18%; override them with the `-Min*LineCoverage` parameters,
or re-check an existing report tree without rerunning tests via `-ValidateOnly`.

### Code Style

Expand Down
87 changes: 69 additions & 18 deletions bindings/py/src/async_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ use std::cell::Cell;
use std::future::IntoFuture;
use std::sync::{Arc, Mutex, MutexGuard};

use crate::errors::{
map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error_with_context,
};
use crate::runtime::DynWinRTValue;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use windows::Win32::Foundation::CO_E_NOTINITIALIZED;
Expand All @@ -14,11 +18,6 @@ use windows::Win32::System::Com::{
};
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize, RoUninitialize};

use crate::errors::{
map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error_with_context,
};
use crate::runtime::DynWinRTValue;

thread_local! {
static TOKIO_RO_INITIALIZED: Cell<bool> = const { Cell::new(false) };
}
Expand Down Expand Up @@ -235,6 +234,22 @@ impl AsyncOperation {
}
}

pub(crate) fn finish_progress_registration(
set_result: dynwinrt::Result<()>,
is_started_after: impl FnOnce() -> PyResult<bool>,
) -> PyResult<()> {
match set_result {
Ok(()) => Ok(()),
Err(error) => {
if is_started_after()? {
Err(map_dynwinrt_error_with_context(error, "SetProgress failed"))
} else {
Ok(())
}
}
}
}

#[pyclass(name = "_DynWinRTAsync")]
pub struct DynWinRTAsync {
operation: Option<Arc<AsyncOperation>>,
Expand Down Expand Up @@ -407,22 +422,58 @@ impl DynWinRTAsyncWithProgress {
});
let handler =
dynwinrt::create_progress_handler(handler_iid, progress_type, progress_callback);
match info.set_progress_handler(&handler) {
Ok(()) => Ok(()),
Err(error) => {
let is_started = info.is_started().map_err(map_dynwinrt_error)?;
if is_started {
Err(map_dynwinrt_error_with_context(error, "SetProgress failed"))
} else {
// Completion raced with put_Progress; there is no handler
// left to install and no future progress to deliver.
Ok(())
}
}
}
finish_progress_registration(info.set_progress_handler(&handler), || {
info.is_started().map_err(map_dynwinrt_error)
})
}

fn __repr__(&self) -> &'static str {
"_DynWinRTAsyncWithProgress(...)"
}
}

#[cfg(test)]
mod tests {
use std::sync::{
Arc, Barrier,
atomic::{AtomicBool, Ordering},
};

use super::*;

fn set_progress_error() -> dynwinrt::Error {
dynwinrt::Error::WindowsError(windows::core::Error::from_hresult(windows::core::HRESULT(
0x80004005u32 as i32,
)))
}

#[test]
fn progress_registration_ignores_failure_after_concurrent_completion() {
let started = Arc::new(AtomicBool::new(true));
let begin_transition = Arc::new(Barrier::new(2));
let transition_done = Arc::new(Barrier::new(2));
let worker_started = started.clone();
let worker_begin = begin_transition.clone();
let worker_done = transition_done.clone();
let worker = std::thread::spawn(move || {
worker_begin.wait();
worker_started.store(false, Ordering::SeqCst);
worker_done.wait();
});

let result = finish_progress_registration(Err(set_progress_error()), || {
begin_transition.wait();
transition_done.wait();
Ok(started.load(Ordering::SeqCst))
});

worker.join().expect("completion worker failed");
assert!(result.is_ok());
}

#[test]
fn progress_registration_surfaces_failure_while_operation_is_started() {
let result = finish_progress_registration(Err(set_progress_error()), || Ok(true));
assert!(result.is_err());
}
}
5 changes: 4 additions & 1 deletion bindings/py/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

use pyo3::exceptions::asyncio::CancelledError as PyCancelledError;
use pyo3::exceptions::{PyOSError, PyRuntimeError};
use pyo3::exceptions::{PyIndexError, PyOSError, PyRuntimeError};
use pyo3::prelude::*;

pub(crate) fn map_windows_error(error: windows::core::Error) -> PyErr {
Expand All @@ -28,6 +28,9 @@ pub(crate) fn map_dynwinrt_error(error: dynwinrt::Error) -> PyErr {
dynwinrt::Error::Canceled => {
PyCancelledError::new_err("WinRT async operation was canceled")
}
dynwinrt::Error::IndexOutOfBounds { index, len } => {
PyIndexError::new_err(format!("Index {index} out of bounds (len {len})"))
}
other => PyRuntimeError::new_err(other.message()),
}
}
Expand Down
81 changes: 80 additions & 1 deletion bindings/py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ from collections.abc import (
Sequence as _Sequence,
)
from datetime import datetime as _datetime, timedelta as _timedelta, timezone as _timezone
from itertools import count as _count
from contextvars import ContextVar as _ContextVar
from operator import index as _index
from threading import get_ident as _thread_get_ident
from typing import Protocol as _Protocol, TypeVar as _TypeVar
from typing import Awaitable as _Awaitable, Callable as _Callable
from uuid import UUID as _UUID
from weakref import WeakValueDictionary as _WeakValueDictionary

_T = _TypeVar('_T', covariant=True)
_P = _TypeVar('_P', covariant=True)
Expand All @@ -48,6 +51,8 @@ _active_projected_lifetime_scope = _ContextVar(
'dynwinrt_active_projected_lifetime_scope',
default=None,
)
_projected_wrapper_cache = _WeakValueDictionary()
_projected_scope_serial = _count(1)

def _dynwinrt_projected_native_values(value):
native_values = []
Expand All @@ -68,13 +73,86 @@ def _dynwinrt_projected_native_values(value):
native_values.append(value)
return native_values

def _dynwinrt_projection_scope_token():
scope = _active_projected_lifetime_scope.get()
if scope is None or not scope._active or scope._disposed:
return None
token = getattr(scope, '_projection_cache_token', None)
if token is None:
token = next(_projected_scope_serial)
scope._projection_cache_token = token
return token

def _dynwinrt_projected_cache_key(wrapper_type, native):
if not isinstance(native, DynWinRTValue):
return None
try:
identity = native.identity_raw()
except RuntimeError:
return None
return (
_thread_get_ident(),
_dynwinrt_projection_scope_token(),
wrapper_type,
identity,
)

def _dynwinrt_projected_wrapper_is_live(wrapper):
native_values = _dynwinrt_projected_native_values(wrapper)
if not native_values:
return False
for native in native_values:
is_null = getattr(native, 'is_null', None)
if callable(is_null) and is_null():
return False
return True

def _dynwinrt_release_redundant_native(native, wrapper):
if not callable(getattr(native, 'release', None)):
return
for existing in _dynwinrt_projected_native_values(wrapper):
if existing is native:
return
native.release()

def _dynwinrt_cache_projected(value):
wrapper_type = type(value)
for native in _dynwinrt_projected_native_values(value):
key = _dynwinrt_projected_cache_key(wrapper_type, native)
if key is None:
continue
try:
_projected_wrapper_cache[key] = value
except TypeError:
pass
return value

def _dynwinrt_projected_from_native(wrapper_type, native, initializer_name):
key = _dynwinrt_projected_cache_key(wrapper_type, native)
if key is not None:
cached = _projected_wrapper_cache.get(key)
if cached is not None:
if _dynwinrt_projected_wrapper_is_live(cached):
_dynwinrt_release_redundant_native(native, cached)
return cached
_projected_wrapper_cache.pop(key, None)
wrapper = object.__new__(wrapper_type)
getattr(wrapper_type, initializer_name)(wrapper, native)
if key is not None:
try:
_projected_wrapper_cache[key] = wrapper
except TypeError:
pass
return wrapper

class ProjectedLifetimeScope:
def __init__(self):
self._registry = {}
self._token = None
self._active = False
self._disposed = False
self._retry_pending = False
self._projection_cache_token = None

@property
def disposed(self):
Expand Down Expand Up @@ -175,6 +253,8 @@ def _dynwinrt_uuid(value):
def _dynwinrt_datetime_to_ticks(value):
if not isinstance(value, _datetime):
raise TypeError('requires datetime.datetime object')
if value.utcoffset() is None:
raise ValueError('requires a timezone-aware datetime.datetime object')
value = value.astimezone(_timezone.utc)
delta = value - _WINRT_EPOCH
return ((delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds) * 10
Expand Down Expand Up @@ -391,7 +471,6 @@ def _dynwinrt_dispatch_progress(callback, converter, value):
m
)?)?;
m.add_function(wrap_pyfunction!(super::runtime::get_computer_name, m)?)?;

m.py().run(
c"
__all__ = [name for name in __all__ if not name.startswith('_')]
Expand Down
Loading
Loading