258 lines
7.7 KiB
Rust
258 lines
7.7 KiB
Rust
use std::borrow::BorrowMut;
|
|
use std::fs::File;
|
|
use std::io::prelude::*;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::time;
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
use linkme::distributed_slice;
|
|
use rand::Rng;
|
|
|
|
pub use itest_macro::itest;
|
|
pub use with_procspawn_tempdir::with_procspawn_tempdir;
|
|
|
|
// HTTP Server deps
|
|
use futures_util::future;
|
|
use hyper::service::{make_service_fn, service_fn};
|
|
use hyper::{Body, Request, Response};
|
|
use hyper_staticfile::Static;
|
|
use tokio::runtime::Runtime;
|
|
|
|
pub(crate) type TestFn = fn() -> Result<()>;
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Test {
|
|
pub(crate) name: &'static str,
|
|
pub(crate) f: TestFn,
|
|
}
|
|
|
|
pub(crate) type TestImpl = libtest_mimic::Test<&'static Test>;
|
|
|
|
#[distributed_slice]
|
|
pub(crate) static NONDESTRUCTIVE_TESTS: [Test] = [..];
|
|
#[distributed_slice]
|
|
pub(crate) static DESTRUCTIVE_TESTS: [Test] = [..];
|
|
|
|
/// Run command and assert that its stderr contains pat
|
|
pub(crate) fn cmd_fails_with<C: BorrowMut<Command>>(mut c: C, pat: &str) -> Result<()> {
|
|
let c = c.borrow_mut();
|
|
let o = c.output()?;
|
|
if o.status.success() {
|
|
bail!("Command {:?} unexpectedly succeeded", c);
|
|
}
|
|
if twoway::find_bytes(&o.stderr, pat.as_bytes()).is_none() {
|
|
dbg!(String::from_utf8_lossy(&o.stdout));
|
|
dbg!(String::from_utf8_lossy(&o.stderr));
|
|
bail!("Command {:?} stderr did not match: {}", c, pat);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Run command and assert that its stdout contains pat
|
|
pub(crate) fn cmd_has_output<C: BorrowMut<Command>>(mut c: C, pat: &str) -> Result<()> {
|
|
let c = c.borrow_mut();
|
|
let o = c.output()?;
|
|
if !o.status.success() {
|
|
bail!("Command {:?} failed", c);
|
|
}
|
|
if twoway::find_bytes(&o.stdout, pat.as_bytes()).is_none() {
|
|
dbg!(String::from_utf8_lossy(&o.stdout));
|
|
bail!("Command {:?} stdout did not match: {}", c, pat);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn write_file<P: AsRef<Path>>(p: P, buf: &str) -> Result<()> {
|
|
let p = p.as_ref();
|
|
let mut f = File::create(p)?;
|
|
f.write_all(buf.as_bytes())?;
|
|
f.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Default, Debug, Copy, Clone)]
|
|
pub(crate) struct TestHttpServerOpts {
|
|
pub(crate) basicauth: bool,
|
|
pub(crate) random_delay: Option<time::Duration>,
|
|
}
|
|
|
|
pub(crate) const TEST_HTTP_BASIC_AUTH: &str = "foouser:barpw";
|
|
|
|
fn validate_authz(value: &[u8]) -> Result<bool> {
|
|
let buf = std::str::from_utf8(&value)?;
|
|
if let Some(o) = buf.find("Basic ") {
|
|
let (_, buf) = buf.split_at(o + "Basic ".len());
|
|
let buf = base64::decode(buf).context("decoding")?;
|
|
let buf = std::str::from_utf8(&buf)?;
|
|
Ok(buf == TEST_HTTP_BASIC_AUTH)
|
|
} else {
|
|
bail!("Missing Basic")
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn http_server<P: AsRef<Path>>(
|
|
p: P,
|
|
opts: TestHttpServerOpts,
|
|
) -> Result<std::net::SocketAddr> {
|
|
let addr = ([127, 0, 0, 1], 0).into();
|
|
let sv = Static::new(p.as_ref());
|
|
|
|
async fn handle_request<B: std::fmt::Debug>(
|
|
req: Request<B>,
|
|
sv: Static,
|
|
opts: TestHttpServerOpts,
|
|
) -> Result<Response<Body>> {
|
|
if let Some(random_delay) = opts.random_delay {
|
|
let slices = 100u32;
|
|
let n: u32 = rand::thread_rng().gen_range(0, slices);
|
|
std::thread::sleep((random_delay / slices) * n);
|
|
}
|
|
if opts.basicauth {
|
|
if let Some(ref authz) = req.headers().get(http::header::AUTHORIZATION) {
|
|
match validate_authz(authz.as_ref()) {
|
|
Ok(true) => {
|
|
return Ok(sv.clone().serve(req).await?);
|
|
}
|
|
Ok(false) => {
|
|
// Fall through
|
|
}
|
|
Err(e) => {
|
|
return Ok(Response::builder()
|
|
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
|
|
.body(Body::from(e.to_string()))
|
|
.unwrap());
|
|
}
|
|
}
|
|
};
|
|
return Ok(Response::builder()
|
|
.status(hyper::StatusCode::FORBIDDEN)
|
|
.header("x-test-auth", "true")
|
|
.body(Body::from("not authorized\n"))
|
|
.unwrap());
|
|
}
|
|
Ok(sv.clone().serve(req).await?)
|
|
}
|
|
|
|
let make_service = make_service_fn(move |_| {
|
|
let sv = sv.clone();
|
|
future::ok::<_, hyper::Error>(service_fn(move |req| handle_request(req, sv.clone(), opts)))
|
|
});
|
|
let server: hyper::Server<_, _, _> = hyper::Server::bind(&addr).serve(make_service);
|
|
let addr = server.local_addr();
|
|
tokio::spawn(async move {
|
|
let r = server.await;
|
|
dbg!("server finished!");
|
|
r
|
|
});
|
|
Ok(addr)
|
|
}
|
|
|
|
pub(crate) fn with_webserver_in<P: AsRef<Path>, F>(
|
|
path: P,
|
|
opts: &TestHttpServerOpts,
|
|
f: F,
|
|
) -> Result<()>
|
|
where
|
|
F: FnOnce(&std::net::SocketAddr) -> Result<()>,
|
|
F: Send + 'static,
|
|
{
|
|
let path = path.as_ref();
|
|
let rt = Runtime::new()?;
|
|
rt.block_on(async move {
|
|
let addr = http_server(path, *opts).await?;
|
|
tokio::task::spawn_blocking(move || f(&addr)).await?
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Parse an environment variable as UTF-8
|
|
pub(crate) fn getenv_utf8(n: &str) -> Result<Option<String>> {
|
|
if let Some(v) = std::env::var_os(n) {
|
|
Ok(Some(
|
|
v.to_str()
|
|
.ok_or_else(|| anyhow::anyhow!("{} is invalid UTF-8", n))?
|
|
.to_string(),
|
|
))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Defined by the autopkgtest specification
|
|
pub(crate) fn get_reboot_mark() -> Result<Option<String>> {
|
|
getenv_utf8("AUTOPKGTEST_REBOOT_MARK")
|
|
}
|
|
|
|
/// Initiate a clean reboot; on next boot get_reboot_mark() will return `mark`.
|
|
#[allow(dead_code)]
|
|
pub(crate) fn reboot<M: AsRef<str>>(mark: M) -> std::io::Error {
|
|
let mark = mark.as_ref();
|
|
use std::os::unix::process::CommandExt;
|
|
std::process::Command::new("/tmp/autopkgtest-reboot")
|
|
.arg(mark)
|
|
.exec()
|
|
}
|
|
|
|
/// Prepare a reboot - you should then initiate a reboot however you like.
|
|
/// On next boot get_reboot_mark() will return `mark`.
|
|
#[allow(dead_code)]
|
|
pub(crate) fn prepare_reboot<M: AsRef<str>>(mark: M) -> Result<()> {
|
|
let mark = mark.as_ref();
|
|
let s = std::process::Command::new("/tmp/autopkgtest-reboot-prepare")
|
|
.arg(mark)
|
|
.status()?;
|
|
if !s.success() {
|
|
anyhow::bail!("{:?}", s);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// I put tests in your tests so you can test while you test
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn oops() -> Command {
|
|
let mut c = Command::new("/bin/bash");
|
|
c.args(&["-c", "echo oops 1>&2; exit 1"]);
|
|
c
|
|
}
|
|
|
|
#[test]
|
|
fn test_fails_with_matches() -> Result<()> {
|
|
cmd_fails_with(Command::new("false"), "")?;
|
|
cmd_fails_with(oops(), "oops")?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_fails_with_fails() {
|
|
cmd_fails_with(Command::new("true"), "somepat").expect_err("true");
|
|
cmd_fails_with(oops(), "nomatch").expect_err("nomatch");
|
|
}
|
|
|
|
#[test]
|
|
fn test_output() -> Result<()> {
|
|
cmd_has_output(Command::new("true"), "")?;
|
|
assert!(cmd_has_output(Command::new("true"), "foo").is_err());
|
|
cmd_has_output(
|
|
sh_inline::bash_command!("echo foobarbaz; echo fooblahbaz").unwrap(),
|
|
"blah",
|
|
)?;
|
|
assert!(
|
|
cmd_has_output(sh_inline::bash_command!("echo foobarbaz").unwrap(), "blah").is_err()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_authz() -> Result<()> {
|
|
assert!(validate_authz("Basic Zm9vdXNlcjpiYXJwdw==".as_bytes())?);
|
|
assert!(!validate_authz("Basic dW5rbm93bjpiYWRwdw==".as_bytes())?);
|
|
assert!(validate_authz("Basic oops".as_bytes()).is_err());
|
|
assert!(validate_authz("oops".as_bytes()).is_err());
|
|
Ok(())
|
|
}
|
|
}
|