tests: consolidate the duplicated harness shapes (review-002 N-5)

- new tests/common/mod.rs — the single-sourced shapes, compiled into
  the lib test crate (#[cfg(test)] #[path] mod in src/lib.rs +
  extern crate self as alksocks) and the integration suite
  (tests/harness.rs declares it and re-exports under the consumed
  names; the per-test use-harness surface is unchanged)
- single-sourced: the echo dial (plain + witnessing), the refusing
  dial, the queue egress + echo factory (the harness ChanEgress /
  producer QueueEgress mutex-wrapper divergence collapsed), the
  refusing egress, the raw RFC 1928 client trio (generic over the
  stream; harness RawRfcClient delegates), the datagram frame
  codec helpers (three copies to one), the parked accept factory
- deliberately left test-local: HotNoneEgress / PureHotNoneEgress
  (R-1's two shapes), Witness / FailingSendWitness (drop flags),
  core-integration's HotNoneEgress (concurrency-load variant),
  the typed accept-refusal factories
- zero behavior change: same test population (145 lib / 38
  associate / 41 bind / 40 connect / 41 integration / 30 harness /
  37 differential / 3 doc-tests), no tests folded, net -307 lines

Verification: cargo test + --all-features green (same counts),
clippy -D warnings clean (native + wasm), fmt clean, doc clean,
both wasm checks clean, publish dry-run completes.
This commit is contained in:
glm-5.3-flash committed 2026-09-25 03:39:01 +00:00
1 parent 8dbe73d49b
commit 07b8a74241
6 files changed
+519 -396

No files matched your search

+17
View File
@@ -64,6 +64,23 @@ pub mod params;
pub mod producer;
pub mod wire;
/// The shared test helpers (review 002 N-5): the single-sourced
/// harness shapes, compiled into the lib test crate. The file lives
/// under `tests/common/` so the integration suite reaches the same
/// copy through its own `mod` declaration (`tests/harness.rs`
/// re-exports the consumed subset). `#[cfg(test)]` keeps it out of
/// the published crate; the `#[path]` resolves relative to `src/`.
#[cfg(test)]
#[path = "../tests/common/mod.rs"]
pub(crate) mod test_common;
// The shared module is written once against the extern-crate paths
// (`alksocks::`) the integration suite resolves; inside the lib test
// crate those same paths resolve via this alias (`extern crate`
// self-linkage, the standard `mod common` companion).
#[cfg(test)]
extern crate self as alksocks;
pub use consumer::{
AssociateSession, BindDataPlane, BindReply1, BindSession, Socks5ChannelSession,
SpentSessionError, TakenAssociateInner,
+21 -141
View File
@@ -751,57 +751,23 @@ impl Socks5ProducerBuilder {
#[cfg(test)]
mod tests {
use super::*;
use crate::egress::{BindAcceptError, BindAcceptFactoryError, SocksDialError, TargetHandle};
use crate::test_common as common;
use crate::egress::{BindAcceptError, BindAcceptFactoryError, SocksDialError};
use bytes::Bytes;
use common::{
datagram_frame, decode_datagram, echo_dial_witnessed, read_reply_v4, refusing_dial,
write_request,
};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
/// The test dial closure (the harness's echo shape, inline): one
/// duplex per dial; a spawned task echoes the far end. The
/// target's identity is returned to the test through the shared
/// witness so the interception-point shape is pinned.
/// The test dial closure (the common module's witnessed echo
/// shape — the target's identity is returned to the test through
/// the shared witness so the interception-point shape is pinned).
fn echo_dial(targets: Arc<tokio::sync::Mutex<Vec<TargetAddr>>>) -> DialFn {
Arc::new(move |target: TargetAddr| {
let targets = Arc::clone(&targets);
Box::pin(async move {
targets.lock().await.push(target.clone());
let (mut echo_end, target_end) = tokio::io::duplex(64 * 1024);
tokio::spawn(async move {
loop {
let mut buf = vec![0u8; 4096];
match echo_end.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if echo_end.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (read_half, write_half) = tokio::io::split(target_end);
Ok(TargetHandle {
read: Box::new(read_half) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(write_half) as Box<dyn AsyncWrite + Send + Sync + Unpin>,
})
})
})
}
fn refusing_dial(code: SocksDialError) -> DialFn {
Arc::new(move |_target: TargetAddr| {
let err = match &code {
SocksDialError::Refused(m) => SocksDialError::Refused(m.clone()),
SocksDialError::NetworkUnreachable(m) => {
SocksDialError::NetworkUnreachable(m.clone())
}
SocksDialError::HostUnreachable(m) => SocksDialError::HostUnreachable(m.clone()),
SocksDialError::NotAllowed(m) => SocksDialError::NotAllowed(m.clone()),
SocksDialError::General(m) => SocksDialError::General(m.clone()),
};
Box::pin(async move { Err(err) })
})
echo_dial_witnessed(targets)
}
/// Drive a raw RFC 1928 client against [`serve_socks5_over`] over
@@ -835,27 +801,11 @@ mod tests {
}
async fn greet(client: &mut DuplexStream) {
client.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut chosen = [0u8; 2];
client.read_exact(&mut chosen).await.unwrap();
assert_eq!(chosen, [0x05, 0x00]);
}
async fn write_request(client: &mut DuplexStream, cmd: &Socks5Command, target: &TargetAddr) {
let addr = target.to_be_bytes().unwrap();
let mut req = vec![0x05, cmd.as_u8(), 0x00];
req.extend_from_slice(&addr);
client.write_all(&req).await.unwrap();
common::greet(client).await
}
async fn read_reply(client: &mut DuplexStream) -> (u8, [u8; 6]) {
let mut head = [0u8; 4];
client.read_exact(&mut head).await.unwrap();
assert_eq!(head[0], 0x05);
assert_eq!(head[3], 0x01, "tests use v4 BND.ADDR");
let mut addr = [0u8; 6];
client.read_exact(&mut addr).await.unwrap();
(head[1], addr)
read_reply_v4(client).await
}
/// The full CONNECT conversation: dial is invoked at the
@@ -990,66 +940,11 @@ mod tests {
// -- UDP ASSOCIATE: the datagram stage (ADR-003, POC #2 shape,
// in-process over duplexes) ------------------------------------
/// The test egress (the harness's ChanEgress shape, inline): one
/// unbounded queue; `send` frames the reply inner bytes for echo
/// (`addr + data` — the egress speaks inner bytes, no length
/// prefix); `next_reply` parks on the queue (`Pending` when
/// nothing is ready — the readiness contract).
struct QueueEgress {
inbound: tokio::sync::mpsc::UnboundedSender<Bytes>,
outbound: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Bytes>>,
}
impl crate::egress::Egress for QueueEgress {
fn send(
&mut self,
addr: TargetAddr,
payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), crate::egress::EgressError>> {
Box::pin(async move {
let mut inner = addr.to_be_bytes().map_err(|e| {
crate::egress::EgressError::General(format!("addr encode: {e}"))
})?;
inner.extend_from_slice(&payload);
self.inbound
.send(Bytes::from(inner))
.map_err(|_| crate::egress::EgressError::General("egress closed".into()))
})
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, crate::egress::EgressError>>
{
Box::pin(async move {
match self.outbound.lock().await.recv().await {
Some(inner) => Ok(Some(inner)),
None => Err(crate::egress::EgressError::General("egress closed".into())),
}
})
}
}
// (the queue egress + its echo factory + the frame helpers ride
// the common module — the harness's single-sourced shapes.)
use crate::egress::EgressError;
/// The frame a raw client sends on the datagram stage:
/// `[len: u16 BE][addr + data]`.
fn datagram_frame(addr: &TargetAddr, payload: &[u8]) -> Vec<u8> {
crate::wire::frame(addr, payload).unwrap().to_vec()
}
/// Split the wire into (addr, payload) — the raw client's reply
/// decode (one frame per read batch, in order).
fn decode_datagram(wire: &[u8]) -> (TargetAddr, Bytes) {
let mut reader = crate::wire::DatagramReader::new();
let mut got = None;
for dg in reader.feed(wire) {
got = Some(dg);
}
got.expect("one datagram decoded").expect("frame decodes")
}
/// The full ASSOCIATE conversation over a duplex: greet →
/// associate request → sentinel reply. Returns (server handle,
/// client duplex). `egress_factory_err` simulates a refusing
@@ -1087,19 +982,11 @@ mod tests {
(handle, client)
}
/// The echo egress over the queue shape (the harness's
/// `echo_egress`, inline): datagrams sent echo back as inner
/// bytes.
/// The echo egress over the queue shape (the common module's
/// `echo_egress_factory` — the harness's `echo_egress`, same
/// shape): datagrams sent echo back as inner bytes.
fn echo_egress_factory() -> EgressFactory {
Arc::new(|| {
Box::pin(async {
let (inbound, outbound) = tokio::sync::mpsc::unbounded_channel();
Ok(Box::new(QueueEgress {
inbound,
outbound: tokio::sync::Mutex::new(outbound),
}) as Box<dyn Egress + Send>)
})
})
common::echo_egress_factory()
}
/// The ASSOCIATE happy path: sentinel reply → datagram
@@ -2057,16 +1944,9 @@ mod tests {
/// An accept factory whose future parks forever (the quiet wait's
/// other exits are then deterministic — garbage/EOF win, the
/// accept never does).
/// accept never does): the common module's shape.
fn parked_accept_factory() -> BindAcceptFactory {
Arc::new(move |_target: TargetAddr| {
Box::pin(async move {
Ok(BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
std::future::pending::<Result<BindTargetHandle, BindAcceptError>>(),
))
})
})
common::parked_accept_factory()
}
/// The BIND happy path through the phase model: reply#1 carries