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:
1 parent
8dbe73d49b
commit
07b8a74241
6 files changed
+519
-396
No files matched your search
+17
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user