Skip to content
Draft
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
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@ Instead, try to encode those constraints in the type system.
- prefer let chains (if let combined with &&) over nested if let statements
to reduce indentation and improve readability.
- if you have to suppress a clippy lint, prefer to use #[expect()] over [allow()],
where possible.
where possible, with a comment for why its supressed
- in python, always prefer strong, precise typing. use concrete types, typed
protocols, dataclasses, NamedTuple, or TypedDict over `object` / `Any` / untyped
dict. fix the underlying typing problem rather than reaching for `# noqa`,
`# type: ignore`, `cast()`, `getattr()`/`hasattr()`, or other dynamic escape
hatches. if a type checker complains, the first move is to tighten the types,
not silence the checker.
- use comments purposefully. don't use comments to narrate code,
but do use them to explain invariants and why something unusual
was done a particular way.

## style guidelines

- use comments purposefully. don't use comments to narrate code,
but do use them to explain invariants and why something unusual
was done a particular way.
- comments and docs should be capitalized: <https://google.github.io/styleguide/cppguide.html#Punctuation_Spelling_and_Grammar> and <https://peps.python.org/pep-0008/#comments>
79 changes: 17 additions & 62 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion crates/tryke_runner/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,48 @@ use crate::protocol::{
const STDERR_RETAIN_BYTES: usize = 1 << 20; // 1 MiB

pub struct WorkerProcess {
/// The spawned python worker (`python -m tryke.worker`).
///
/// Held so the
/// process stays alive for this struct's lifetime and can be killed /
/// awaited on shutdown.
child: Child,

/// Buffered writer over the worker's stdin.
///
/// JSON-RPC requests are
/// written here as newline-delimited JSON and flushed once per `call`.
stdin: BufWriter<ChildStdin>,

/// Buffered reader over the worker's stdout. JSON-RPC responses are read
/// line by line; non-JSON lines a native library may leak to fd 1 during
/// import are skipped in `call`.
stdout: BufReader<ChildStdout>,
/// Continuously-drained worker stderr. A background task reads the

/// Continuously-drained worker stderr.
///
/// A background task reads the
/// child's stderr pipe into this buffer so the pipe never fills and
/// the worker can't block on a stderr write mid-RPC.
stderr_buf: Arc<Mutex<VecDeque<u8>>>,

/// Handle to the stderr-drainer task. `drain_stderr` joins this
/// (with a short timeout) so any bytes still in the kernel pipe at
/// the moment of a worker failure end up in `stderr_buf` before we
/// snapshot it — without this, a worker that dies during startup
/// can lose its python traceback to a race with the RPC error path.
stderr_drainer: Option<tokio::task::JoinHandle<()>>,

/// Monotonic JSON-RPC request id, stamped onto each request and bumped
/// in `call`. The transport is strictly synchronous (one request in
/// flight at a time), so this isn't used to match responses to requests
/// — it's only here to give each request the unique id the JSON-RPC 2.0
/// spec requires.
next_id: u64,

/// Is this worker the special `debugpy` worker that debug tests get routed to.
#[expect(dead_code)]
is_debug: bool,
}

impl WorkerProcess {
Expand Down Expand Up @@ -67,7 +95,9 @@ impl WorkerProcess {
if let Some(value) = worker_log_env_value(log_level) {
command.env("TRYKE_LOG", value);
}

let mut child = command.spawn()?;

let stdin = BufWriter::new(child.stdin.take().ok_or_else(|| anyhow!("no stdin"))?);
let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow!("no stdout"))?);
let stderr = child.stderr.take().ok_or_else(|| anyhow!("no stderr"))?;
Expand Down Expand Up @@ -102,6 +132,7 @@ impl WorkerProcess {
stderr_buf,
stderr_drainer: Some(stderr_drainer),
next_id: 1,
is_debug: false,
})
}

Expand Down Expand Up @@ -299,6 +330,7 @@ impl Drop for WorkerProcess {
// dropped (e.g. on the error-respawn path in pool.rs). start_kill() is
// the synchronous variant — safe to call on already-dead processes.
let _ = self.child.start_kill();

// Dropping a Tokio JoinHandle detaches the task, so abort it
// explicitly to avoid an orphan drainer outliving the worker on
// respawn paths (the drainer holds stderr_buf + the stderr FD).
Expand All @@ -321,6 +353,7 @@ fn spawn_stderr_drainer(
) -> Result<tokio::task::JoinHandle<()>> {
let handle = tokio::runtime::Handle::try_current()
.map_err(|e| anyhow!("WorkerProcess::spawn requires an active tokio runtime: {e}"))?;

Ok(handle.spawn(async move {
let mut reader = stderr;
let mut chunk = [0u8; 8192];
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ description = "Tryke - A Python testing tool"
readme = "README.md"
authors = [{ name = "Justin Chapman", email = "commonmodestudio@gmail.com" }]
requires-python = ">=3.12"
dependencies = []
dependencies = [
"debugpy>=1.8.21",
]
keywords = ["testing", "test", "framework", "runner", "assertions"]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down
27 changes: 26 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading