feat(test): own process startup and cleanup
This commit is contained in:
@@ -52,7 +52,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
|||||||
- Prefer links or pointers to canonical sources over copied lists.
|
- Prefer links or pointers to canonical sources over copied lists.
|
||||||
- Avoid project trees, architecture maps, generated inventories, current file sizes, issue lists, TODO inventories, and other snapshots that will rot.
|
- Avoid project trees, architecture maps, generated inventories, current file sizes, issue lists, TODO inventories, and other snapshots that will rot.
|
||||||
- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate health --strict`. Use the xtask runner for full verification so jobs are capped from local CPU and memory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; the full path should stay within a documented 10 minute local timeout or be split into deterministic shards under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016
|
- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate health --strict`. Use the xtask runner for full verification so jobs are capped from local CPU and memory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; the full path should stay within a documented 10 minute local timeout or be split into deterministic shards under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016
|
||||||
- Example behavior tests should prefer `hemx_test` generated-resource assertion methods over raw slot constants, raw effect/payload matching, or boolean predicates wrapped in opaque `assert!`; failures should include the expectation and actual effects, while rendered target/handle assertions should name the generated resource. Keep browser selector helpers as test adapters only, not authoring APIs. req: test/008 req: test/009 req: test/010 req: test/017 req: test/018
|
- Example behavior tests should prefer `hemx_test` generated-resource assertion methods over raw slot constants, raw effect/payload matching, or boolean predicates wrapped in opaque `assert!`; failures should include the expectation and actual effects, while rendered target/handle assertions should name the generated resource. Keep browser selector helpers as test adapters only, not authoring APIs. Process-backed tests use the RAII `TestProcess` harness rather than duplicating readiness loops and child cleanup. req: test/008 req: test/009 req: test/010 req: test/017 req: test/018 req: test/019
|
||||||
- Run the workout product exemplar with `cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`; set `HEMX_WORKOUT_ADDR=127.0.0.1:3030` if the default port is busy. Its durable visual direction and recovery expectations live in `examples/workout/DESIGN.md`. req: examples/008
|
- Run the workout product exemplar with `cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`; set `HEMX_WORKOUT_ADDR=127.0.0.1:3030` if the default port is busy. Its durable visual direction and recovery expectations live in `examples/workout/DESIGN.md`. req: examples/008
|
||||||
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
|
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
|
||||||
- hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata and postcard/form-encoded envelopes, not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010
|
- hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata and postcard/form-encoded envelopes, not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010
|
||||||
|
|||||||
@@ -883,6 +883,9 @@ what a valid business email is. [north_star]
|
|||||||
### req: test/018
|
### req: test/018
|
||||||
0 018 `EffectInspector` assertion methods fail at the caller with the expected generated target and payload condition plus the actual effect operations, avoiding opaque boolean assertion failures. [north_star]
|
0 018 `EffectInspector` assertion methods fail at the caller with the expected generated target and payload condition plus the actual effect operations, avoiding opaque boolean assertion failures. [north_star]
|
||||||
|
|
||||||
|
### req: test/019
|
||||||
|
0 019 Repo-owned process-backed tests use one RAII harness that waits for TCP readiness, reports early exit or timeout with the process label and address, and always reaps the child. [north_star]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## check
|
## check
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use hemx_axum::runtime_js_path;
|
use hemx_axum::runtime_js_path;
|
||||||
use hemx_test::{inspect_wire, EffectInspector};
|
use hemx_test::{inspect_wire, EffectInspector, TestProcess};
|
||||||
use hemx_workout_example::ui::BUILD_FINGERPRINT;
|
use hemx_workout_example::ui::BUILD_FINGERPRINT;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::Command;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
child: Child,
|
_process: TestProcess,
|
||||||
addr: String,
|
addr: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,31 +17,20 @@ impl Server {
|
|||||||
let addr = listener.local_addr().unwrap().to_string();
|
let addr = listener.local_addr().unwrap().to_string();
|
||||||
drop(listener);
|
drop(listener);
|
||||||
|
|
||||||
let bin = env!("CARGO_BIN_EXE_hemx-workout-example");
|
let mut command = Command::new(env!("CARGO_BIN_EXE_hemx-workout-example"));
|
||||||
let mut child = Command::new(bin)
|
command.env("HEMX_WORKOUT_ADDR", &addr);
|
||||||
.env("HEMX_WORKOUT_ADDR", &addr)
|
let process = TestProcess::start(
|
||||||
.stdout(Stdio::null())
|
command,
|
||||||
.stderr(Stdio::null())
|
"hemx-workout-example",
|
||||||
.spawn()
|
&addr,
|
||||||
.expect("start hemx-workout-example");
|
Duration::from_secs(5),
|
||||||
|
)
|
||||||
|
.expect("start ready hemx-workout-example");
|
||||||
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
Self {
|
||||||
while Instant::now() < deadline {
|
_process: process,
|
||||||
if TcpStream::connect(&addr).is_ok() {
|
addr,
|
||||||
return Self { child, addr };
|
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(25));
|
|
||||||
}
|
|
||||||
let _ = child.kill();
|
|
||||||
let _ = child.wait();
|
|
||||||
panic!("hemx-workout-example did not listen on {addr}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Server {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.child.kill();
|
|
||||||
let _ = self.child.wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,63 @@ use hemx_core::{
|
|||||||
Atom, BuildFingerprint, Effect, EffectBatch, Form, GeneratedTarget, IntoEffect, KeyedSlot,
|
Atom, BuildFingerprint, Effect, EffectBatch, Form, GeneratedTarget, IntoEffect, KeyedSlot,
|
||||||
NavigateMode, Payload, ResourceId, ResourceKind, ResourceRef, ScopeKey, Slot,
|
NavigateMode, Payload, ResourceId, ResourceKind, ResourceRef, ScopeKey, Slot,
|
||||||
};
|
};
|
||||||
|
use std::io;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// A child process owned by an integration test and proven ready over TCP.
|
||||||
|
///
|
||||||
|
/// The process is killed and reaped on every return path, including panics. Startup failures name
|
||||||
|
/// the process and address and distinguish early exit from a readiness timeout.
|
||||||
|
/// req: test/019
|
||||||
|
pub struct TestProcess {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestProcess {
|
||||||
|
pub fn start(
|
||||||
|
mut command: Command,
|
||||||
|
label: impl Into<String>,
|
||||||
|
addr: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
let label = label.into();
|
||||||
|
let child = command
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|error| {
|
||||||
|
io::Error::new(error.kind(), format!("failed to spawn {label}: {error}"))
|
||||||
|
})?;
|
||||||
|
let mut process = Self { child };
|
||||||
|
let deadline = Instant::now() + timeout;
|
||||||
|
loop {
|
||||||
|
if TcpStream::connect(addr).is_ok() {
|
||||||
|
return Ok(process);
|
||||||
|
}
|
||||||
|
if let Some(status) = process.child.try_wait()? {
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"{label} exited with {status} before listening on {addr}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
format!("timed out after {timeout:?} waiting for {label} to listen on {addr}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(25));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestProcess {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.child.kill();
|
||||||
|
let _ = self.child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run<I, F, R>(handler: F, input: I) -> EffectInspector
|
pub fn run<I, F, R>(handler: F, input: I) -> EffectInspector
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
use hemx_test::TestProcess;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_harness_reports_early_exit_with_context() {
|
||||||
|
// req: test/019
|
||||||
|
let mut command = Command::new(std::env::current_exe().expect("current test executable"));
|
||||||
|
command
|
||||||
|
.arg("--exact")
|
||||||
|
.arg("helper_process_exits_successfully")
|
||||||
|
.arg("--nocapture");
|
||||||
|
|
||||||
|
let error = match TestProcess::start(
|
||||||
|
command,
|
||||||
|
"short-lived helper",
|
||||||
|
"127.0.0.1:9",
|
||||||
|
Duration::from_secs(2),
|
||||||
|
) {
|
||||||
|
Ok(_) => panic!("a process that exits before readiness must fail startup"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
let message = error.to_string();
|
||||||
|
|
||||||
|
assert!(message.contains("short-lived helper"), "{message}");
|
||||||
|
assert!(message.contains("127.0.0.1:9"), "{message}");
|
||||||
|
assert!(message.contains("exited with"), "{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helper_process_exits_successfully() {}
|
||||||
Reference in New Issue
Block a user