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>(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>(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: 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, } pub(crate) const TEST_HTTP_BASIC_AUTH: &str = "foouser:barpw"; fn validate_authz(value: &[u8]) -> Result { 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: P, opts: TestHttpServerOpts, ) -> Result { let addr = ([127, 0, 0, 1], 0).into(); let sv = Static::new(p.as_ref()); async fn handle_request( req: Request, sv: Static, opts: TestHttpServerOpts, ) -> Result> { 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, 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> { 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> { 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>(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>(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(()) } }