use std::{
env,
error::Error,
ffi::OsString,
fmt, fs, io, panic,
path::{Path, PathBuf},
process::{Child, Command},
thread,
time::Duration,
};
use {
env_logger, log, reqwest,
serde::Serialize,
tempfile::{tempdir, TempDir},
url::Url,
};
const LIBRARY_FILENAME: &str = "libbasic-plugin.so";
#[test]
fn test_user_api() {
let context = set_up().expect("Setup failed");
log::debug!("{:?}", context);
let post_data = PostData {
name: "foo",
library_id: 0,
};
let patch_attr_0 = PatchDouble {
variant: "double",
value: 42.0,
};
let patch_attr_3 = PatchString {
variant: "string",
value: "foobarbaz",
};
let cases: Vec<Case> = vec![
("/api/v0/libraries", Http::Get),
("/api/v0/libraries/0", Http::Get),
("/api/v0/peripherals", Http::Post(post_data)),
("/api/v0/peripherals", Http::Get),
("/api/v0/peripherals/0", Http::Get),
("/api/v0/peripherals/0/attributes", Http::Get),
("/api/v0/peripherals/0/attributes/0", Http::Get),
(
"/api/v0/peripherals/0/attributes/0",
Http::PatchDouble(patch_attr_0),
),
(
"/api/v0/peripherals/0/attributes/3",
Http::PatchString(patch_attr_3),
),
];
let result = {
let result = panic::catch_unwind(|| {
for case in &cases {
subtest_user_api(case, &context.server_url);
}
});
tear_down(context);
result
};
assert!(result.is_ok());
}
fn subtest_user_api((route, http): &Case, base: &Url) {
log::info!("Testing route: {}", route);
let client = reqwest::Client::new();
let base = base
.join(route)
.expect("Could not produce full URL for the test");
log::debug!("Making HTTP request {:?} to {}", http, base);
let req = match http {
Http::Get => client.get(base.as_str()).send().expect("Request failed"),
Http::Post(data) => client
.post(base.as_str())
.json(&data)
.send()
.expect("Request failed"),
Http::PatchDouble(data) => client
.patch(base.as_str())
.json(&data)
.send()
.expect("Request failed"),
Http::PatchString(data) => client
.patch(base.as_str())
.json(&data)
.send()
.expect("Request failed"),
};
log::debug!("Made request {:?}", req);
assert!(req.status().is_success());
}
type Case = (&'static str, Http);
#[derive(Debug)]
enum Http {
Get,
Post(PostData),
PatchDouble(PatchDouble),
PatchString(PatchString),
}
#[derive(Debug, Serialize)]
struct PostData {
name: &'static str,
library_id: usize,
}
#[derive(Debug, Serialize)]
struct PatchDouble {
variant: &'static str,
value: f64,
}
#[derive(Debug, Serialize)]
struct PatchString {
variant: &'static str,
value: &'static str,
}
#[derive(Debug)]
struct Context {
bin_exe: PathBuf,
daemon: Child,
library_dir: TempDir,
server_addr: String,
server_url: Url,
}
fn set_up() -> Result<Context, io::Error> {
let _ = env_logger::builder().is_test(true).try_init();
let library_dir = tempdir()?;
let mut library_file_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
library_file_src.push(artifacts_dir());
library_file_src.push(format!("examples/{}", LIBRARY_FILENAME));
let mut library_file_dest = PathBuf::from(library_dir.path());
library_file_dest.push(LIBRARY_FILENAME);
fs::copy(library_file_src.as_path(), library_file_dest.as_path())?;
let mut bin_exe = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
bin_exe.push(artifacts_dir());
bin_exe.push("kpald");
let server_addr: String = env::var_os("SERVER_ADDRESS")
.unwrap_or_else(|| OsString::from("0.0.0.0:8000"))
.into_string()
.expect("Could not get SERVER_ADDRESS environment variable");
let server_url =
Url::parse(&format!("http://{}", &server_addr)).expect("Could not get base URL");
let daemon = start_daemon(
bin_exe.as_path(),
library_dir.path(),
&server_addr,
&server_url,
)
.unwrap();
Ok(Context {
bin_exe,
daemon,
library_dir,
server_addr,
server_url,
})
}
fn tear_down(mut context: Context) {
let _ = context.daemon.kill();
}
fn start_daemon(
bin_exe: &Path,
library_dir: &Path,
server_addr: &str,
server_url: &Url,
) -> Result<Child, StartDaemonError> {
let mut daemon = Command::new(bin_exe)
.arg("--library-dir")
.arg(library_dir)
.arg("--server-address")
.arg(server_addr)
.spawn()
.expect("daemon failed to start");
let mut attempt = 0;
let num_attempts = 3;
let mut sleep_time = 250;
while let Err(e) = reqwest::get(server_url.as_str()) {
log::debug!(
"Server is not ready: {}\nRetrying in {} ms...",
e,
sleep_time
);
attempt += 1;
if attempt == num_attempts {
log::error!("Maximum number of attempts reached. Killing the daemon...");
let _ = daemon.kill();
return Err(StartDaemonError {});
}
thread::sleep(Duration::from_millis(sleep_time));
sleep_time *= 2;
}
Ok(daemon)
}
fn artifacts_dir() -> PathBuf {
let mut dir = env::current_exe().expect("Could not determine current executable");
dir.pop(); dir.pop(); dir
}
#[derive(Debug)]
struct StartDaemonError {}
impl fmt::Display for StartDaemonError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "StartDaemonError")
}
}
impl Error for StartDaemonError {
fn description(&self) -> &str {
"An error occurred when starting the daemon"
}
}