test: organize integration tests into single crate

Put all integration tests in a single crate, and organized in modules.

This is similar to what was done in `cargo` repository:
https://github.com/rust-lang/cargo/pull/5022#issuecomment-364691154

```
$ rustup show
stable-x86_64-unknown-linux-gnu (default)
rustc 1.69.0 (84c898d65 2023-04-16)
```

Run on a recent high-end laptop:
12th Gen Intel(R) Core(TM) i9-12900HK

Original (multiple integration binaries):

- `cargo clean && cargo test --no-run`: 62.893s
- `cargo clean && cargo build --lib`: 54.959s
- `cargo clean && cargo build --bins`: 55.933s
- `cargo test --no-run` (after `cargo build`): 14.472s
- `cargo test` (after `cargo test --no-run`): 1.786s
- `du -hs target/`: 4.2G

After (ironrdp-testsuite):

- `cargo clean && cargo test --no-run`: 41.157s (crates with no tests are ignored)
- `cargo clean && cargo build --lib`: 53.983s
- `cargo clean && cargo build --bins`: 54.482s
- `cargo test --no-run` (after `cargo build`): 12.915s
- `cargo test` (after `cargo test --no-run`): 0.240s
- `du -hs target/`: 3.4G

Absolute diff:
- `cargo clean && cargo test --no-run`: -21.736s
- `cargo clean && cargo build --lib`: -0.976s
- `cargo clean && cargo build --bins`: -1.451s
- `cargo test --no-run` (after `cargo build`): -1.557s
- `cargo test` (after `cargo test --no-run`): -1.546s
- `du -hs target/`: -0.8G

Relative diff (%):
- `cargo clean && cargo test --no-run`: -34.5%
- `cargo clean && cargo build --lib`: -1.77%
- `cargo clean && cargo build --bins`: -2.59%
- `cargo test --no-run` (after `cargo build`): -10.7%
- `cargo test` (after `cargo test --no-run`): -86.5%
- `du -hs target/`: -19.0%
This commit is contained in:
Benoît CORTIER
2023-05-11 19:41:11 -04:00
committed by Benoît Cortier
parent 36a210aae9
commit 60bc72f873
76 changed files with 1499 additions and 1439 deletions
+57 -18
View File
@@ -12,50 +12,80 @@ This document describes the high-level architecture of IronRDP.
This section talks briefly about various important directories and data structures.
### Core Crates
### Core Tier
Set of foundational libraries for which strict quality standards must be observed.
Pay attention to the "**Architecture Invariant**" sections.
- `crates/ironrdp`: meta crate re-exporting important crates.
- `crates/ironrdp-pdu`: PDU encoding and decoding (no I/O, trivial to fuzz). <!-- TODO: important types and traits (PduDecode, PduEncode…) -->
- `crates/ironrdp-graphics`: image processing primitives (no I/O, trivial to fuzz).
- `crates/ironrdp-connector`: state machines to drive an RDP connection sequence (no I/O, not _too_ hard to fuzz).
- `crates/ironrdp-session`: state machines to drive an RDP session (no I/O, not _too_ hard to fuzz).
- `crates/ironrdp-input`: utilities to manage and build input packets (no I/O).
- `crates/ironrdp-pdu`: PDU encoding and decoding. (TODO: talk about important types and traits such as PduDecode, PduEncode…)
- `crates/ironrdp-graphics`: image processing primitives.
- `crates/ironrdp-connector`: state machines to drive an RDP connection sequence.
- `crates/ironrdp-session`: state machines to drive an RDP session.
- `crates/ironrdp-input`: utilities to manage and build input packets.
- `crates/ironrdp-rdcleanpath`: RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway.
### Utility Crates
**Architectural Invariant**: doing I/O is not allowed for these crates.
**Architectural Invariant**: all these crates must be fuzzed.
**Architectural Invariant**: no non-essential dependency is allowed.
**Architectural Invariant**: must be `#[no_std]`-compatible (eventually using the `alloc` crate). Usage of the standard
library must be opt-in through a feature flag called `std` that is enabled by default. When the `alloc` crate is optional,
a feature flag called `alloc` must exist to enable its use.
### Extra Tier
Higher level libraries and binaries built on top of the core tier.
Guidelines and constraints are relaxed to some extent.
- `crates/ironrdp-async`: provides `Future`s wrapping the state machines conveniently.
- `crates/ironrdp-tokio`: `Framed*` traits implementation above `tokio`s traits.
- `crates/ironrdp-futures`: `Framed*` traits implementation above `futures`s traits.
- `crates/ironrdp-tls`: TLS boilerplate common with most IronRDP clients.
### Client Crates
- `crates/ironrdp-client`: portable RDP client without GPU acceleration using softbuffer and winit for windowing.
- `crates/ironrdp-web`: WebAssembly high-level bindings targeting web browsers.
- `crates/ironrdp-glutin-renderer`: `glutin` primitives for OpenGL rendering.
- `crates/ironrdp-client-glutin`: GPU-accelerated RDP client using glutin.
- `crates/ironrdp-replay-client`: utility tool to replay RDP graphics pipeline for debugging purposes.
- `web-client/iron-remote-gui`: core frontend UI used by `iron-svelte-client` as a Web Component.
- `web-client/iron-svelte-client`: web-based frontend using `Svelte` and `Material` frameworks.
### Private Crates
### Internal Tier
Crates that are only used inside the IronRDP project, not meant to be published.
This is mostly test case generators, fuzzing oracles, build tools, and so on.
- `crates/ironrdp-pdu-generators`: `proptest` generators for `ironrdp-pdu` types.
- `crates/ironrdp-session-generators`: `proptest` generators for `ironrdp-session` types.
- `fuzz`: fuzz targets for core crates.
- `crates/ironrdp-testsuite-core`: contains all integration tests for code living in the core tier, in a single binary,
organized in modules. **Architectural Invariant**: no dependency from another tier is allowed.
It must be the case that compiling and running the core test suite does not require building any library from
the extra tier. This is to keep iteration time short.
- `crates/ironrdp-testsuite-extra`: contains all integration tests for code living in the extra tier, in a single binary,
organized in modules. (WIP: this crate does not exist yet.)
- `crates/ironrdp-fuzzing`: provides test case generators and oracles for use with fuzzing.
- `fuzz`: fuzz targets for code in core tier.
- `xtask`: IronRDPs free-form automation using Rust code.
**Architecture Invariant**: these crates are not, and will never be, an **API Boundary**.
### Community Tier
Crates provided and maintained by the community.
Core maintainers will not invest a lot of time into these.
One or several community maintainers are associated to each one
- `crates/ironrdp-glutin-renderer` (no maintainer): `glutin` primitives for OpenGL rendering.
- `crates/ironrdp-client-glutin` (no maintainer): GPU-accelerated RDP client using glutin.
- `crates/ironrdp-replay-client` (no maintainer): utility tool to replay RDP graphics pipeline for debugging purposes.
## Cross-Cutting Concerns
This section talks about the things which are everywhere and nowhere in particular.
### General
- Dependency injection when runtime information is necessary in core crates (no system call such as `gethostname`)
- Keep non-portable code out of core crates
- Dependency injection when runtime information is necessary in core tier crates (no system call such as `gethostname`)
- Keep non-portable code out of core tier crates
- Make crate `no_std`-compatible wherever possible
- Facilitate fuzzing
- In libraries, provide concrete error types either hand-crafted or using `thiserror` crate
@@ -64,7 +94,7 @@ This section talks about the things which are everywhere and nowhere in particul
### Avoid I/O wherever possible
**Architecture Invariant**: core crates must never interact with the outside world. Only client and utility crates
**Architecture Invariant**: core tier crates must never interact with the outside world. Only extra tier crates
such as `ironrdp-client`, `ironrdp-web` or `ironrdp-async` are allowed to do I/O.
### Continuous integration
@@ -77,6 +107,15 @@ be the case that a successful `cargo xtask ci` run implies a successful CI workf
### Testing
#### Test at the boundaries (test features, not code)
We should focus on testing the public API of libraries (keyword: **API boundary**).
Thats why most (if not all) tests should go into the `ironrdp-testsuite-core` and `ironrdp-testsuite-extra` crates.
#### Do not depend on external resources
**Architecture Invariant**: tests do not depend on any kind of external resources, they are perfectly reproducible.
#### Fuzzing
See [`fuzz/README.md`](../fuzz/README.md).
Generated
+23 -20
View File
@@ -1574,9 +1574,6 @@ dependencies = [
"lazy_static",
"num-derive 0.3.3",
"num-traits",
"proptest",
"rdp-rs",
"rstest",
"thiserror",
]
@@ -1584,11 +1581,8 @@ dependencies = [
name = "ironrdp-input"
version = "0.1.0"
dependencies = [
"anyhow",
"bitvec",
"ironrdp-pdu",
"proptest",
"rstest",
"smallvec",
]
@@ -1601,7 +1595,7 @@ dependencies = [
"byteorder",
"der-parser 8.2.0",
"expect-test",
"ironrdp-pdu-samples",
"ironrdp-testsuite-core",
"lazy_static",
"md-5 0.10.5",
"num-bigint 0.4.3",
@@ -1622,24 +1616,11 @@ dependencies = [
"proptest",
]
[[package]]
name = "ironrdp-pdu-samples"
version = "0.0.0"
dependencies = [
"array-concat",
"ironrdp-pdu",
"lazy_static",
"paste",
]
[[package]]
name = "ironrdp-rdcleanpath"
version = "0.1.0"
dependencies = [
"der 0.7.5",
"hex",
"pretty_assertions",
"rstest",
]
[[package]]
@@ -1663,6 +1644,28 @@ dependencies = [
"proptest",
]
[[package]]
name = "ironrdp-testsuite-core"
version = "0.0.0"
dependencies = [
"anyhow",
"arbitrary",
"array-concat",
"hex",
"ironrdp-connector",
"ironrdp-graphics",
"ironrdp-input",
"ironrdp-pdu",
"ironrdp-rdcleanpath",
"ironrdp-session",
"lazy_static",
"paste",
"pretty_assertions",
"proptest",
"rdp-rs",
"rstest",
]
[[package]]
name = "ironrdp-tls"
version = "0.1.0"
+1 -1
View File
@@ -28,11 +28,11 @@ ironrdp-futures = { version = "0.1", path = "crates/ironrdp-futures" }
ironrdp-graphics = { version = "0.1", path = "crates/ironrdp-graphics" }
ironrdp-input = { version = "0.1", path = "crates/ironrdp-input" }
ironrdp-pdu-generators = { path = "crates/ironrdp-pdu-generators" }
ironrdp-pdu-samples = { path = "crates/ironrdp-pdu-samples" }
ironrdp-pdu = { version = "0.1", path = "crates/ironrdp-pdu" }
ironrdp-rdcleanpath = { version = "0.1", path = "crates/ironrdp-rdcleanpath" }
ironrdp-session-generators = { path = "crates/ironrdp-session-generators" }
ironrdp-session = { version = "0.1", path = "crates/ironrdp-session" }
ironrdp-testsuite-core = { path = "crates/ironrdp-testsuite-core" }
ironrdp-tls = { version = "0.1", path = "crates/ironrdp-tls" }
ironrdp-tokio = { version = "0.1", path = "crates/ironrdp-tokio" }
ironrdp = { version = "0.5", path = "crates/ironrdp" }
+4
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[dependencies]
bytes = "1"
ironrdp-connector.workspace = true
+8
View File
@@ -12,6 +12,14 @@ keywords.workspace = true
categories.workspace = true
default-run = "ironrdp-client"
[lib]
doctest = false
test = false
[[bin]]
name = "ironrdp-client"
test = false
[features]
default = ["rustls"]
rustls = ["ironrdp-tls/rustls"]
+4
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[features]
arbitrary = ["dep:arbitrary"]
@@ -52,32 +52,3 @@ fn sanitize_server_name(name: impl Into<String>) -> String {
name
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("somehostname:2345", "somehostname")]
#[case("192.168.56.101:2345", "192.168.56.101")]
#[case("[2001:db8::8a2e:370:7334]:7171", "2001:db8::8a2e:370:7334")]
#[case("[2001:0db8:0000:0000:0000:8a2e:0370:7334]:433", "2001:db8::8a2e:370:7334")]
#[case("[::1]:2222", "::1")]
fn input_with_port(#[case] input: &str, #[case] expected: &str) {
let result = sanitize_server_name(input);
assert_eq!(result, expected);
}
#[rstest]
#[case("somehostname")]
#[case("192.168.56.101")]
#[case("2001:db8::8a2e:370:7334")]
#[case("2001:0db8:0000:0000:0000:8a2e:0370:7334")]
#[case("::1")]
fn input_without_port(#[case] input: &str) {
let result = sanitize_server_name(input);
assert_eq!(result, input);
}
}
+4
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[dependencies]
bytes = "1"
futures-util = { version = "0.3.26", features = ["io"] }
+1
View File
@@ -4,6 +4,7 @@ use std::pin::Pin;
use bytes::BytesMut;
use futures_util::io::{AsyncRead, AsyncWrite};
#[rustfmt::skip] // do not re-order this pub use
pub use ironrdp_async::*;
pub type FuturesFramed<S> = Framed<FuturesStream<S>>;
+4 -3
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
# test = false
[dependencies]
ironrdp-pdu.workspace = true
num-traits = "0.2.15"
@@ -25,6 +29,3 @@ lazy_static = "1.4.0"
[dev-dependencies]
bmp = "0.5"
expect-test.workspace = true
proptest = "1.1.0"
rdp-rs = { git = "https://github.com/citronneur/rdp-rs", rev = "7ac880d7efb7f05efef3c84476f7c24f4053e0ea" }
rstest = "0.17.0"
@@ -1,13 +1,10 @@
use crate::{
color_conversion::{Rgb, YCoCg},
rdp6::rle::{decompress_8bpp_plane, RleError},
};
use ironrdp_pdu::{
bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlanes},
decode, Error as PduError,
};
use ironrdp_pdu::bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlanes};
use ironrdp_pdu::{decode, Error as PduError};
use thiserror::Error;
use crate::color_conversion::{Rgb, YCoCg};
use crate::rdp6::rle::{decompress_8bpp_plane, RleError};
#[derive(Debug, Error)]
pub enum BitmapDecodeError {
#[error("Failed to decode RDP6 bitmap stream PDU: {0}")]
+4 -3
View File
@@ -1,5 +1,6 @@
use byteorder::ReadBytesExt;
use std::io::{Read, Write};
use byteorder::ReadBytesExt;
use thiserror::Error;
/// Maximum possible segment size is 47 (run_length = 2, raw_bytes_count = 15), which is treated as
@@ -160,10 +161,10 @@ pub fn decompress_8bpp_plane(
#[cfg(test)]
mod tests {
use super::*;
use expect_test::expect;
use super::*;
/// Performs decompression of 8bpp color plane into vector. Vector will be resized to fit decompressed data.
pub fn decompress(
src: &[u8],
+5 -6
View File
@@ -11,12 +11,11 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[dependencies]
ironrdp-pdu.workspace = true
bitvec = "1.0.1"
smallvec = "1.10.0"
[dev-dependencies]
proptest.workspace = true
rstest.workspace = true
anyhow = "1"
smallvec = "1.10.0"
+4
View File
@@ -5,6 +5,10 @@ edition = "2021"
description = "`proptest` generators for `ironrdp-pdu` types"
publish = false
[lib]
doctest = false
test = false
[dependencies]
ironrdp-pdu.workspace = true
proptest.workspace = true
-13
View File
@@ -1,14 +1 @@
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
-11
View File
@@ -1,11 +0,0 @@
[package]
name = "ironrdp-pdu-samples"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
ironrdp-pdu.workspace = true
array-concat = "0.5.2"
lazy_static = "1.4.0"
paste = "1"
+5 -1
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
# test = false
[features]
default = []
std = []
@@ -34,5 +38,5 @@ x509-cert = { version = "0.2.1", default-features = false, features = ["std"] }
[dev-dependencies]
expect-test.workspace = true
ironrdp-pdu-samples.workspace = true
ironrdp-testsuite-core.workspace = true # TODO: move more tests under ironrdp-testsuite-core itself
lazy_static = "1.4.0"
@@ -136,9 +136,10 @@ impl<'a> PduEncode for BitmapStream<'a> {
#[cfg(test)]
mod tests {
use expect_test::{expect, Expect};
use super::*;
use crate::{decode, encode_buf, PduEncode};
use expect_test::{expect, Expect};
fn assert_roundtrip(buffer: &[u8], expected: Expect) {
let pdu = decode::<BitmapStream>(buffer).unwrap();
+5 -6
View File
@@ -11,10 +11,9 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[dependencies]
der = { version = "0.7.1", features = ["alloc", "derive"] }
[lib]
doctest = false
test = false
[dev-dependencies]
rstest = "0.17.0"
hex = "0.4.3"
pretty_assertions = "1.3.0"
[dependencies]
der = { version = "0.7.1", features = ["alloc", "derive"] }
+4 -129
View File
@@ -2,14 +2,15 @@ use core::fmt;
use der::asn1::OctetString;
// Re-export der crate for convenience
#[rustfmt::skip] // do not re-order this pub use
pub use der;
pub const BASE_VERSION: u64 = 3389;
pub const VERSION_1: u64 = BASE_VERSION + 1;
pub const GENERAL_ERROR_CODE: u16 = 1;
// Re-export der crate for convenience
pub use der;
#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)]
#[asn1(tag_mode = "EXPLICIT")]
pub struct RDCleanPathErr {
@@ -358,129 +359,3 @@ impl From<RDCleanPath> for RDCleanPathPdu {
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn request() -> RDCleanPathPdu {
RDCleanPathPdu::new_request(
vec![0xDE, 0xAD, 0xBE, 0xFF],
"destination".to_owned(),
"proxy auth".to_owned(),
Some("PCB".to_owned()),
)
.unwrap()
}
const REQUEST_DER: &[u8] = &[
0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA2, 0xD, 0xC, 0xB, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6E, 0x61,
0x74, 0x69, 0x6F, 0x6E, 0xA3, 0xC, 0xC, 0xA, 0x70, 0x72, 0x6F, 0x78, 0x79, 0x20, 0x61, 0x75, 0x74, 0x68, 0xA5,
0x5, 0xC, 0x3, 0x50, 0x43, 0x42, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF,
];
fn response_success() -> RDCleanPathPdu {
RDCleanPathPdu::new_response(
"192.168.7.95".to_owned(),
vec![0xDE, 0xAD, 0xBE, 0xFF],
[
vec![0xDE, 0xAD, 0xBE, 0xFF],
vec![0xDE, 0xAD, 0xBE, 0xFF],
vec![0xDE, 0xAD, 0xBE, 0xFF],
],
)
.unwrap()
}
const RESPONSE_SUCCESS_DER: &[u8] = &[
0x30, 0x34, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0xA7, 0x14, 0x30,
0x12, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF,
0xA9, 0xE, 0xC, 0xC, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x37, 0x2E, 0x39, 0x35,
];
fn response_http_error() -> RDCleanPathPdu {
RDCleanPathPdu::new_http_error(500)
}
const RESPONSE_HTTP_ERROR_DER: &[u8] = &[
0x30, 0x15, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA1, 0xD, 0x30, 0xB, 0xA0, 0x3, 0x2, 0x1, 0x1, 0xA1, 0x4, 0x2,
0x2, 0x1, 0xF4,
];
fn response_tls_error() -> RDCleanPathPdu {
RDCleanPathPdu::new_tls_error(48)
}
const RESPONSE_TLS_ERROR_DER: &[u8] = &[
0x30, 0x14, 0xA0, 0x04, 0x02, 0x02, 0x0D, 0x3E, 0xA1, 0x0C, 0x30, 0x0A, 0xA0, 0x03, 0x02, 0x01, 0x01, 0xA3,
0x03, 0x02, 0x01, 0x30,
];
#[rstest]
#[case(request())]
#[case(response_success())]
#[case(response_http_error())]
#[case(response_tls_error())]
fn smoke(#[case] message: RDCleanPathPdu) {
let encoded = message.to_der().unwrap();
let decoded = RDCleanPathPdu::from_der(&encoded).unwrap();
assert_eq!(message, decoded);
}
macro_rules! assert_serialization {
($left:expr, $right:expr) => {{
if $left != $right {
let left = hex::encode(&$left);
let right = hex::encode(&$right);
let comparison = pretty_assertions::StrComparison::new(&left, &right);
panic!(
"assertion failed: `({} == {})`\n\n{comparison}",
stringify!($left),
stringify!($right),
);
}
}};
}
#[rstest]
#[case(request(), REQUEST_DER)]
#[case(response_success(), RESPONSE_SUCCESS_DER)]
#[case(response_http_error(), RESPONSE_HTTP_ERROR_DER)]
#[case(response_tls_error(), RESPONSE_TLS_ERROR_DER)]
fn serialization(#[case] message: RDCleanPathPdu, #[case] expected_der: &[u8]) {
let encoded = message.to_der().unwrap();
assert_serialization!(encoded, expected_der);
}
#[rstest]
#[case(REQUEST_DER)]
#[case(RESPONSE_SUCCESS_DER)]
#[case(RESPONSE_HTTP_ERROR_DER)]
#[case(RESPONSE_TLS_ERROR_DER)]
fn detect(#[case] der: &[u8]) {
let result = RDCleanPathPdu::detect(der);
let DetectionResult::Detected { version: detected_version, total_length: detected_length } = result else {
panic!("unexpected result: {result:?}");
};
assert_eq!(detected_version, VERSION_1);
assert_eq!(detected_length, der.len());
}
#[rstest]
#[case(&[])]
#[case(&[0x30])]
#[case(&[0x30, 0x15])]
#[case(&[0x30, 0x15, 0xA0])]
#[case(&[0x30, 0x32, 0xA0, 0x4])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD])]
fn detect_not_enough(#[case] payload: &[u8]) {
let result = RDCleanPathPdu::detect(payload);
assert_eq!(result, DetectionResult::NotEnoughBytes);
}
}

Some files were not shown because too many files have changed in this diff Show More