Skip to content

Commit

Permalink
Replace QuickCheck with an own propcheck utility
Browse files Browse the repository at this point in the history
...and switch the 32-bit integer parser to just exhaustive checking.
(More on that later.)

Why move away from QuickCheck?

1. The maintainer appears to have little interest in actually
   maintaining it. BurntSushi/quickcheck#315

2. Its API is incredibly inefficient, especially on failure, and it's
   far too rigid for my needs. For one, I need something looser than
   `Arbitrary: Clone` so things like `std::io::Error` can be generated
   more easily. Also, with larger structures, efficiency will directly
   correlate to faster test runs. Also, I've run into the limitations
   of not being able to access the underlying random number generator
   far too many times to count, as I frequently need to generate random
   values within ranges, among other things.
   - BurntSushi/quickcheck#279
   - BurntSushi/quickcheck#312
   - BurntSushi/quickcheck#320
   - BurntSushi/quickcheck#267

3. It correctly limits generated `Vec` and `String` length, but it
   doesn't similarly enforce limits on test length.

4. There's numerous open issues in it that I've addressed, in some
   cases by better core design. To name a few particularly bad ones:
   - Misuse of runtime bounds in `Duration` generation, `SystemTime`
     generation able to panic for unrelated reasons:
     BurntSushi/quickcheck#321
   - Incorrect generation of `SystemTime`:
     BurntSushi/quickcheck#321
   - Unbounded float shrinkers:
     BurntSushi/quickcheck#295
   - Avoiding pointless debug string building:
     BurntSushi/quickcheck#303
   - Signed shrinker shrinks to the most negative value, leading to
     occasional internal panics:
     BurntSushi/quickcheck#301

There's still some room for improvement, like switching away from a
recursive loop: BurntSushi/quickcheck#285.
But, this is good enough for my use cases right now. And this code
base is structured such that such a change is *much* easier to do.
(It's also considerably simpler.)

As for the integer parser change, I found a way to re-structure it so
I could perform true exhaustive testing on it. Every code path has
every combination of inputs tested, except for memory space as a whole.
This gives me enough confidence that I can ditch the randomized
property checking for it.
  • Loading branch information
dead-claudia committed Jul 6, 2024
1 parent 527805c commit b3c20e1
Show file tree
Hide file tree
Showing 25 changed files with 2,807 additions and 1,224 deletions.
105 changes: 29 additions & 76 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ base64 = "0.21.0"
once_cell = "1.17.1"

[dev-dependencies]
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
tempfile = "3.4.0"
rand = "0.8.5"

[profile.test]
# 1. Test with release optimization settings (but retain safety checks), for increased test
Expand Down
2 changes: 2 additions & 0 deletions src/child/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod server;
mod start;

pub use start::start_child;
pub use start::ChildOpts;
// pub use start::ChildTls;

// This shouldn't be seeing very many requests. If this many concurrent requests are occurring,
// it's clearly a sign that *way* too many requests are being sent.
Expand Down
81 changes: 53 additions & 28 deletions src/child/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,55 @@ use crate::ffi::ExitCode;
use crate::ffi::ExitResult;
use std::net::Ipv4Addr;
use std::net::TcpListener;
use std::num::NonZeroU16;
use std::os::unix::prelude::OsStrExt;
use std::os::unix::prelude::OsStringExt;

static SERVER_STATE: ServerState<TinyHttpResponseContext> = ServerState::new();
static REQUEST_CHANNEL: Channel<(Instant, tiny_http::Request), PENDING_REQUEST_CAPACITY> =
Channel::new();

fn get_port() -> Option<NonZeroU16> {
let result = std::env::var_os("PORT")?;
let port_num = parse_u32(result.as_bytes())?;
NonZeroU16::new(u16::try_from(port_num).ok()?)
pub struct ChildTls {
pub certificate: Vec<u8>,
pub private_key: Vec<u8>,
}

pub fn start_child() -> io::Result<ExitResult> {
pub struct ChildOpts {
pub port: u16,
pub tls: Option<ChildTls>,
}

impl ChildOpts {
pub fn from_env() -> io::Result<ChildOpts> {
let port = 'port: {
if let Some(result) = std::env::var_os("PORT") {
if let Some(num @ 0..=65535) = parse_u32(result.as_bytes()) {
// Not sure why it's warning here. It's obviously checked.
#[allow(clippy::as_conversions)]
break 'port num as u16;
}
}

return Err(error!("Port is invalid or missing."));
};

let tls = match (
std::env::var_os("TLS_CERTIFICATE"),
std::env::var_os("TLS_PRIVATE_KEY"),
) {
(Some(certificate), Some(private_key)) => Some(ChildTls {
certificate: certificate.into_vec(),
private_key: private_key.into_vec(),
}),
(None, None) => None,
(None, Some(_)) => return Err(error!("Received private key but not certificate.")),
(Some(_), None) => return Err(error!("Received certificate but not private key.")),
};

Ok(ChildOpts { port, tls })
}
}

pub fn start_child(opts: ChildOpts) -> io::Result<ExitResult> {
// Set the standard input and output to non-blocking mode so reads will correctly not block.
set_non_blocking(libc::STDIN_FILENO);
set_non_blocking(libc::STDOUT_FILENO);
Expand All @@ -39,38 +73,29 @@ pub fn start_child() -> io::Result<ExitResult> {
initialized: Instant::now(),
};

let port = match get_port() {
Some(port) => port,
None => return Err(error!("Port is invalid or missing.")),
};

let tls = match (
std::env::var_os("TLS_CERTIFICATE"),
std::env::var_os("TLS_PRIVATE_KEY"),
) {
(Some(certificate), Some(private_key)) => Some(tiny_http::SslConfig {
certificate: certificate.into_vec(),
private_key: private_key.into_vec(),
}),
(None, None) => None,
(None, Some(_)) => return Err(error!("Received private key but not certificate.")),
(Some(_), None) => return Err(error!("Received certificate but not private key.")),
};

let listener = match TcpListener::bind((Ipv4Addr::UNSPECIFIED, port.into())) {
let listener = match TcpListener::bind((Ipv4Addr::UNSPECIFIED, opts.port)) {
Ok(listener) => listener,
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(error!(
ErrorKind::AddrInUse,
"TCP port {port} is already in use."
"TCP port {} is already in use.", opts.port,
))
}
Err(e) => return Err(e),
};

log::info!("Server listener bound at port {port}.");
log::info!(
"Server listener bound at port {}.",
listener.local_addr()?.port()
);

let server = match tiny_http::Server::from_listener(listener, tls) {
let server = match tiny_http::Server::from_listener(
listener,
opts.tls.map(|tls| tiny_http::SslConfig {
certificate: tls.certificate,
private_key: tls.private_key,
}),
) {
Ok(server) => server,
Err(e) => return Err(Error::new(ErrorKind::Other, e)),
};
Expand Down
3 changes: 2 additions & 1 deletion src/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::prelude::*;

use super::args::Args;
use crate::child::start_child;
use crate::child::ChildOpts;
use crate::cli::args::parse_args;
use crate::ffi::normalize_errno;
use crate::ffi::request_signal_when_parent_terminates;
Expand Down Expand Up @@ -41,8 +42,8 @@ pub fn main() {
};

let result = match args {
Args::Child => start_child(),
Args::Parent(args) => start_parent(args),
Args::Child => ChildOpts::from_env().and_then(start_child),
};

match result {
Expand Down
Loading

0 comments on commit b3c20e1

Please sign in to comment.