-
Notifications
You must be signed in to change notification settings - Fork 0
Add buffer-size cap and Python/Rust boundary property tests (#82, #90) #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d34b3da
Add buffer-size cap and Python/Rust boundary property tests
5f203a3
Add CodeScene code-health overrides with justifications
b171861
Address review: self-contained wheel test, docs, platform guards
4b4f138
Address review round 2: narrow suppression, ExitStack, pump coverage
12e62d3
Use a valid writer fd in the reader-descriptor property
f8dc751
Use ExitStack for the reader-descriptor writer cleanup
9aac6b9
Document the 1 GiB buffer_size cap in the user and dev guides
5e250cb
Qualify buffer_size ValueError docs and simplify the bounds proptest
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "usage": "Repo-scoped Code Health overrides. Each rule_set documents why the override is safe; keep them narrowly scoped and justified.", | ||
| "rule_sets": [ | ||
| { | ||
| "matching_content_path": "rust/cuprum-rust/src/lib.rs", | ||
| "matching_content_path_doc": "PyO3 FFI boundary. rust_pump_stream / rust_consume_stream / convert_fd / validate_buffer_size / checked_buffer_size receive raw i64 file descriptors and buffer sizes handed over by Python. Those primitives are the untyped inputs this boundary exists to validate INTO domain newtypes (BufferSize, ReaderFd, WriterFd, PlatformFd). Wrapping the raw i64 inputs in further domain types before validation would add ceremony without value - the conversion IS the abstraction - so Primitive Obsession is disabled for this file only.", | ||
| "rules": [ | ||
| { "name": "Primitive Obsession", "weight": 0.0 } | ||
| ] | ||
| }, | ||
| { | ||
| "matching_content_path": "rust/cuprum-rust/src/io_utils/tests.rs", | ||
| "matching_content_path_doc": "The EINTR-retry tests for the read seam (read_raw_fd_with) and the write seam (write_all_unix_with) are deliberately parallel to document that both seams honour the identical retry contract. Extracting a shared representation would couple independent module boundaries and obscure which seam each test exercises. Scoped to this exact file so the exception does not silently cover unrelated future test modules.", | ||
| "rules": [ | ||
| { "name": "Code Duplication", "weight": 0.0 } | ||
| ] | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
220 changes: 220 additions & 0 deletions
220
cuprum/unittests/test_rust_streams_boundary_property.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| """Property-based boundary tests for the Rust stream entry points. | ||
|
|
||
| The example-based suite in ``test_rust_streams.py`` covers a curated set of | ||
| buffer sizes and I/O failures. These properties fuzz the argument-validation | ||
| boundary of ``rust_pump_stream`` / ``rust_consume_stream`` at the Python/Rust | ||
| seam: | ||
|
|
||
| - Non-positive and over-cap ``buffer_size`` values raise ``ValueError`` | ||
| (validation happens before any descriptor is touched). | ||
| - Negative and out-of-``i32``-range descriptors raise ``ValueError``. | ||
| - Omitting ``buffer_size`` is equivalent to passing the explicit default. | ||
|
|
||
| Buffer-size validation runs before descriptor conversion, so the | ||
| buffer-size properties can pass a throwaway descriptor without performing | ||
| I/O. The descriptor properties use the default (valid) buffer size so that | ||
| conversion is the failing step. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import os | ||
| import sys | ||
| import typing as typ | ||
|
|
||
| import pytest | ||
| from hypothesis import HealthCheck, given, settings | ||
| from hypothesis import strategies as st | ||
|
|
||
| if typ.TYPE_CHECKING: | ||
| from types import ModuleType | ||
|
|
||
| # Mirror of MAX_BUFFER_SIZE in rust/cuprum-rust/src/lib.rs (1 GiB). | ||
| _MAX_BUFFER_SIZE = 1 << 30 | ||
| _DEFAULT_BUFFER_SIZE = 65536 | ||
| _I32_MAX = (1 << 31) - 1 | ||
| _I64_MAX = (1 << 63) - 1 | ||
| # A descriptor value that is deterministically invalid, used only where | ||
| # validation fails before the descriptor is dereferenced. -1 never names an | ||
| # open descriptor, so a validation-order regression fails loudly instead of | ||
| # blocking on a real fd such as stdin (0). | ||
| _UNUSED_FD = -1 | ||
|
|
||
| # The descriptor properties assert the Unix i32 file-descriptor conversion | ||
| # contract. On Windows the wrapper routes fds through msvcrt.get_osfhandle and | ||
| # the native path accepts pointer-sized handles, so those assertions do not | ||
| # hold; skip them there rather than encode platform-specific error semantics. | ||
| _unix_only = pytest.mark.skipif( | ||
| sys.platform == "win32", | ||
| reason="asserts the Unix i32 file-descriptor conversion contract", | ||
| ) | ||
|
|
||
| _SUPPRESS_FIXTURE = settings( | ||
| suppress_health_check=[HealthCheck.function_scoped_fixture], | ||
| max_examples=50, | ||
| ) | ||
|
|
||
|
|
||
| def _safe_close(fd: int) -> None: | ||
| """Close ``fd``, ignoring an already-closed or invalid descriptor. | ||
|
|
||
| Defined locally so the packaged test module stays importable from an | ||
| installed wheel, which ships ``cuprum/unittests`` but not ``tests``. | ||
| """ | ||
| with contextlib.suppress(OSError): | ||
| os.close(fd) | ||
|
|
||
|
|
||
| @_SUPPRESS_FIXTURE | ||
| @given(bad_size=st.integers(min_value=-(1 << 62), max_value=0)) | ||
| def test_consume_rejects_nonpositive_buffer( | ||
| rust_streams: ModuleType, | ||
| bad_size: int, | ||
| ) -> None: | ||
| """A non-positive ``buffer_size`` raises ``ValueError`` before any read.""" | ||
| with pytest.raises(ValueError, match="buffer_size"): | ||
| rust_streams.rust_consume_stream(_UNUSED_FD, buffer_size=bad_size) | ||
|
|
||
|
|
||
| @_SUPPRESS_FIXTURE | ||
| @given(bad_size=st.integers(min_value=_MAX_BUFFER_SIZE + 1, max_value=_I64_MAX)) | ||
| def test_consume_rejects_oversized_buffer( | ||
| rust_streams: ModuleType, | ||
| bad_size: int, | ||
| ) -> None: | ||
| """A ``buffer_size`` above the 1 GiB cap raises ``ValueError``.""" | ||
| with pytest.raises(ValueError, match="buffer_size"): | ||
| rust_streams.rust_consume_stream(_UNUSED_FD, buffer_size=bad_size) | ||
|
|
||
|
|
||
| @_SUPPRESS_FIXTURE | ||
| @given(bad_size=st.integers(min_value=-(1 << 62), max_value=0)) | ||
| def test_pump_rejects_nonpositive_buffer( | ||
| rust_streams: ModuleType, | ||
| bad_size: int, | ||
| ) -> None: | ||
| """``rust_pump_stream`` rejects a non-positive ``buffer_size``.""" | ||
| with pytest.raises(ValueError, match="buffer_size"): | ||
| rust_streams.rust_pump_stream(_UNUSED_FD, _UNUSED_FD, buffer_size=bad_size) | ||
|
|
||
|
|
||
| @_unix_only | ||
| @_SUPPRESS_FIXTURE | ||
| @given( | ||
| bad_fd=st.one_of( | ||
| st.integers(min_value=-(1 << 62), max_value=-1), | ||
| st.integers(min_value=_I32_MAX + 1, max_value=_I64_MAX), | ||
| ), | ||
| ) | ||
| def test_consume_rejects_invalid_descriptor( | ||
| rust_streams: ModuleType, | ||
| bad_fd: int, | ||
| ) -> None: | ||
| """Negative or out-of-i32-range descriptors raise ``ValueError``.""" | ||
| with pytest.raises(ValueError, match="file descriptor"): | ||
| rust_streams.rust_consume_stream(bad_fd) | ||
|
|
||
|
|
||
| @_unix_only | ||
| @_SUPPRESS_FIXTURE | ||
| @given( | ||
| bad_fd=st.one_of( | ||
| st.integers(min_value=-(1 << 62), max_value=-1), | ||
| st.integers(min_value=_I32_MAX + 1, max_value=_I64_MAX), | ||
| ), | ||
| ) | ||
| def test_pump_rejects_invalid_reader_descriptor( | ||
| rust_streams: ModuleType, | ||
| bad_fd: int, | ||
| ) -> None: | ||
| """``rust_pump_stream`` rejects an invalid reader descriptor. | ||
|
|
||
| The writer is a genuinely valid descriptor, so the ``ValueError`` can only | ||
| originate from the invalid reader, not from a coincidentally invalid writer. | ||
| """ | ||
| with contextlib.ExitStack() as stack: | ||
| writer_fd = os.open(os.devnull, os.O_WRONLY) | ||
| stack.callback(_safe_close, writer_fd) | ||
| with pytest.raises(ValueError, match="file descriptor"): | ||
| rust_streams.rust_pump_stream(bad_fd, writer_fd) | ||
|
|
||
|
|
||
| def _feed_pipe(write_fd: int, payload: bytes) -> None: | ||
| """Write ``payload`` fully into ``write_fd``.""" | ||
| view = memoryview(payload) | ||
| while view: | ||
| written = os.write(write_fd, view) | ||
| view = view[written:] | ||
|
|
||
|
|
||
| def _consume_via_pipe( | ||
| rust_streams: ModuleType, | ||
| payload: bytes, | ||
| **kwargs: object, | ||
| ) -> str: | ||
| """Write ``payload`` through a pipe and consume it with the Rust decoder.""" | ||
| with contextlib.ExitStack() as stack: | ||
| read_fd, write_fd = os.pipe() | ||
| stack.callback(_safe_close, read_fd) | ||
| stack.callback(_safe_close, write_fd) | ||
| _feed_pipe(write_fd, payload) | ||
| # Close the writer so the consumer observes EOF; the ExitStack's | ||
| # second close of the same descriptor is a harmless no-op. | ||
| _safe_close(write_fd) | ||
| return typ.cast( | ||
| "str", | ||
| rust_streams.rust_consume_stream(read_fd, **kwargs), | ||
| ) | ||
|
|
||
|
|
||
| def _pump_via_pipes( | ||
| rust_streams: ModuleType, | ||
| payload: bytes, | ||
| **kwargs: object, | ||
| ) -> int: | ||
| """Pump ``payload`` from a source pipe to a sink pipe; return bytes written.""" | ||
| with contextlib.ExitStack() as stack: | ||
| src_read, src_write = os.pipe() | ||
| sink_read, sink_write = os.pipe() | ||
| for fd in (src_read, src_write, sink_read, sink_write): | ||
| stack.callback(_safe_close, fd) | ||
| _feed_pipe(src_write, payload) | ||
| # Close the source writer so the pump reaches EOF; the payload is small | ||
| # enough to fit the sink pipe buffer without draining `sink_read`. | ||
| _safe_close(src_write) | ||
| return int(rust_streams.rust_pump_stream(src_read, sink_write, **kwargs)) | ||
|
|
||
|
|
||
| @_SUPPRESS_FIXTURE | ||
| @given(payload=st.binary(max_size=96)) | ||
| def test_default_buffer_matches_explicit( | ||
| rust_streams: ModuleType, | ||
| payload: bytes, | ||
| ) -> None: | ||
| """Omitting ``buffer_size`` equals passing the explicit default.""" | ||
| explicit = _consume_via_pipe( | ||
| rust_streams, payload, buffer_size=_DEFAULT_BUFFER_SIZE | ||
| ) | ||
| default = _consume_via_pipe(rust_streams, payload) | ||
| assert explicit == default, ( | ||
| "omitting buffer_size must equal the explicit 65536 default" | ||
| ) | ||
| assert default == payload.decode("utf-8", errors="replace"), ( | ||
| "decoded output must match Python's UTF-8 replace decoding" | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| @_SUPPRESS_FIXTURE | ||
| @given(payload=st.binary(max_size=96)) | ||
| def test_pump_default_buffer_matches_explicit( | ||
| rust_streams: ModuleType, | ||
| payload: bytes, | ||
| ) -> None: | ||
| """``rust_pump_stream`` omitting ``buffer_size`` equals the explicit default.""" | ||
| explicit = _pump_via_pipes(rust_streams, payload, buffer_size=_DEFAULT_BUFFER_SIZE) | ||
| default = _pump_via_pipes(rust_streams, payload) | ||
| assert explicit == default, ( | ||
| "omitting buffer_size must equal the explicit 65536 default" | ||
| ) | ||
| assert default == len(payload), "the pump must transfer every payload byte" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.