Skip to content

Commit 0133608

Browse files
committed
tests: expected object support in tests/roundtrip
Add optional support to test the deserialized bytes against an expected static object. Uses a macro to concisely list modules that contain an `expected()` function that returns the expected `Request` or `Response` object. Existing tests could have expected objects added to improve the error reporting in case of test failure. Signed-off-by: Ross Williams <ross@ross-williams.net>
1 parent 1b556c8 commit 0133608

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

tests/roundtrip/expected.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#[macro_use]
2+
mod macros;
3+
4+
use ssh_agent_lib::proto::{Request, Response};
5+
6+
// This macro generates a function with the following signature:
7+
//
8+
// `fn request(path: impl AsRef<Path>) -> Option<Request>`
9+
//
10+
// When called, it will take the filename without extension from the provided path (replacing any
11+
// dashes with underscores) and compare that string to the list of modules in the bracketed list.
12+
// If one of the listed modules matches the filename (e.g. `req-example-test.bin`), the function
13+
// `req_example_test::expected()` will be called, which must have the signature `pub fn expected()
14+
// -> Request`. (If none of the modules match the filename, `None` will be returned.)
15+
//
16+
// The roundtrip test code calls this to enhance `Encode`/`Decode` roundtrip tests with a known
17+
// static object that must match the deserialized bytes.
18+
//
19+
// The macro also declares the listed modules.
20+
make_expected_fn!(request -> Request, {
21+
});
22+
23+
make_expected_fn!(response -> Response, {
24+
});

tests/roundtrip/expected/macros.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//!
2+
//! The macro in this file takes 3 arguments:
3+
//! 1. The name of the function to generate
4+
//! 2. The return type of the function
5+
//! 3. A brace-enclosed, comma-separated list of module
6+
//! names
7+
//!
8+
//! Each module in (3) should match the basename of a file in `tests/messages` but with dashes
9+
//! replaced by underscores to make it a valid Rust identifier. When the generated function is
10+
//! called with a filename that matches one of these modules, it will call the `expected()`
11+
//! function of that module, which should return an object of the return type specified in the
12+
//! macro, which will be returned as Some(expected()). If the generated function is called with a
13+
//! filename that does not match any module, it will return `None`.
14+
//!
15+
//! # Example #
16+
//!
17+
//! ## Macro call ##
18+
//!
19+
//! ```ignore
20+
//! use ssh_agent_lib::proto::Request;
21+
//!
22+
//! make_expected_fn!(get_expected_request, Request, {
23+
//! req_hello
24+
//! });
25+
//! ```
26+
//!
27+
//! ## Usage of generated function in test code ##
28+
//!
29+
//! ```ignore
30+
//! let test_data_path = PathBuf::from("test/messages/req-hello.bin");
31+
//! let expected = path::to::get_expected_request(&test_data_path);
32+
//! assert_eq!(expected, ...);
33+
//! ```
34+
//!
35+
//! ## `path/to/req_hello.rs` ##
36+
//!
37+
//! ```ignore
38+
//! pub fn expected() -> Request {
39+
//! ...
40+
//! }
41+
//! ```
42+
43+
macro_rules! make_expected_fn {
44+
($fn_name:ident -> $ty:ty, { $($case:ident),+ } ) => {
45+
$( mod $case; )+
46+
47+
pub fn $fn_name(path: impl ::core::convert::AsRef<::std::path::Path>) -> ::core::option::Option<$ty> {
48+
let cases: &[(&str, &dyn ::core::ops::Fn() -> $ty)] = &[
49+
$( (::core::stringify!($case), &self::$case::expected,) ),+
50+
];
51+
52+
let path_case_name = path
53+
.as_ref()
54+
.file_stem()
55+
.expect("test path has no filename")
56+
.to_str()
57+
.expect("test filename not UTF-8")
58+
.replace("-", "_");
59+
60+
cases
61+
.into_iter()
62+
.find(|(c, _)| c == &path_case_name)
63+
.map(|(_, f)| f())
64+
}
65+
};
66+
}

tests/roundtrip.rs renamed to tests/roundtrip/main.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1-
use std::path::PathBuf;
1+
mod expected;
2+
3+
use std::path::{Path, PathBuf};
24

35
use rstest::rstest;
46
use ssh_agent_lib::proto::{Request, Response};
57
use ssh_encoding::{Decode, Encode};
68
use testresult::TestResult;
79

8-
fn roundtrip<T: Decode + Encode + std::fmt::Debug>(path: PathBuf) -> TestResult
10+
fn roundtrip<T>(path: impl AsRef<Path>, expected: Option<T>) -> TestResult
911
where
12+
T: Decode + Encode + PartialEq + std::fmt::Debug,
1013
T::Error: std::fmt::Display,
1114
{
1215
let serialized = std::fs::read(path)?;
1316
let mut bytes: &[u8] = &serialized;
1417
let message = T::decode(&mut bytes)?;
1518
eprintln!("Message: {message:#?}");
19+
if let Some(expected) = expected {
20+
eprintln!("Expected: {expected:#?}");
21+
assert_eq!(
22+
expected, message,
23+
"parsed message does not match expected object"
24+
);
25+
}
1626
let mut out = vec![];
1727
message.encode(&mut out)?;
1828
assert_eq!(
@@ -29,10 +39,10 @@ where
2939

3040
#[rstest]
3141
fn roundtrip_requests(#[files("tests/messages/req-*.bin")] path: PathBuf) -> TestResult {
32-
roundtrip::<Request>(path)
42+
roundtrip::<Request>(&path, expected::request(&path))
3343
}
3444

3545
#[rstest]
3646
fn roundtrip_responses(#[files("tests/messages/resp-*.bin")] path: PathBuf) -> TestResult {
37-
roundtrip::<Response>(path)
47+
roundtrip::<Response>(&path, expected::response(&path))
3848
}

0 commit comments

Comments
 (0)