ostree/tests/inst/src/test.rs

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(())
}
}