Compare commits

..

23 commits

Author SHA1 Message Date
ac1c06dd31 Untangle and move around setup structs 2023-03-07 22:07:35 +01:00
877af00806 Simplify setup a bit 2023-03-05 22:28:31 +01:00
06ff70c38d Add a helpful comment 2023-03-05 22:28:31 +01:00
b9d1303d06 Pull in fix for tokio-rs/tokio#5502 2023-03-05 22:28:31 +01:00
b32b0eaedc Emit Errors in CommandRunner 2023-03-05 22:28:31 +01:00
d71a676c2f Pull in fix for https://github.com/dtolnay/async-trait/issues/238 2023-03-05 22:28:31 +01:00
bca1d2036a Much nicer logs 2023-03-05 22:28:31 +01:00
0fd1e44121 Coding style 2023-03-03 19:17:40 +01:00
e143bf8578 Simplify loggers 2023-03-03 18:46:02 +01:00
e745ef3ad0 Add tests for templates and improve signatures 2023-02-25 14:51:40 +01:00
dd7c6c10fd Fix unisolated git checkout test
This depends on the target dir to exist; `target` does exist unless
a different build dir is used.
2023-02-24 18:05:48 +01:00
e569bdb416 Minor style improvements 2023-02-24 00:41:08 +01:00
4eeb280f0d Clippy 2023-02-24 00:41:08 +01:00
0d41e8a833 Better tests 2023-02-24 00:41:08 +01:00
395fb1c9fa Switch to slog 2023-02-24 00:41:08 +01:00
f27e981259 Add workaround for tokio-rs/tokio#5502 2023-02-24 00:41:08 +01:00
7d973372cc Add uwsgi 2023-02-24 00:41:08 +01:00
e23b41c37c Remove unused export 2023-02-24 00:41:08 +01:00
ae5d6837af Rewrite build.rs 2023-02-24 00:41:08 +01:00
e4af85726f Check TLS Key bits 2021-12-26 01:45:16 +01:00
6954359209 Remove SuCommandRunner 2021-12-26 01:45:00 +01:00
70786ebf40 Simplify SetuidCommandRunner 2021-12-26 01:44:46 +01:00
229eb3a9e9 Remove unnecessary clippy allow 2021-12-26 01:18:50 +01:00
34 changed files with 1097 additions and 897 deletions

View file

@ -9,9 +9,12 @@ build = "src/build.rs"
users = "0.11.0" users = "0.11.0"
regex = "1.0.1" regex = "1.0.1"
futures-util = "0.3" futures-util = "0.3"
async-trait = "0.1" async-trait = "0.1.65"
tokio = { version = "1.6.1", features = ["rt", "process", "io-util", "macros", "sync"] } tokio = { version = "1.26", features = ["rt", "process", "io-util", "macros", "sync"] }
once_cell = "1.4" once_cell = "1.4"
slog = { version = "2", features = ["max_level_trace", "release_max_level_trace"] }
slog-term = "2.5"
slog-async = "2.7"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"

View file

@ -36,26 +36,20 @@ impl Future for TimerFuture {
type Output = (); type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
if let State::Completed = *state { match *state {
return Poll::Ready(()); State::Completed => return Poll::Ready(()),
} State::NotStarted(duration) => {
if let State::NotStarted(duration) = *state {
let thread_state = self.state.clone(); let thread_state = self.state.clone();
thread::spawn(move || { thread::spawn(move || {
thread::sleep(duration); thread::sleep(duration);
let mut state = thread_state.lock().unwrap(); let mut state = thread_state.lock().unwrap();
let waker = if let State::Running(waker) = &*state { if let State::Running(waker) = std::mem::replace(&mut *state, State::Completed) {
Some(waker.clone()) waker.wake();
} else {
None
}; };
*state = State::Completed;
if let Some(w) = waker {
w.wake();
}
}); });
} }
State::Running(_) => {}
};
*state = State::Running(cx.waker().clone()); *state = State::Running(cx.waker().clone());
Poll::Pending Poll::Pending
@ -84,10 +78,10 @@ mod test {
loop { loop {
futures_util::select! { futures_util::select! {
_ = sleep => {}, _ = sleep => {},
_ = ok => assert!((Instant::now() - start).as_millis() < 100), _ = ok => assert!(start.elapsed().as_millis() < 100),
complete => break, complete => break,
} }
} }
}) });
} }
} }

View file

@ -1,57 +1,53 @@
use std::env; use std::env;
use std::fs::{read_dir, File}; use std::error::Error;
use std::fs::{read as read_file, File};
use std::io::ErrorKind::NotFound; use std::io::ErrorKind::NotFound;
use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::Path;
fn get_const_name<P: Clone + Into<PathBuf>>(p: &P) -> String { pub fn create_static_output(
let mut file_name_without_extension = p.clone().into(); source_path: &Path,
file_name_without_extension.set_extension(""); mut target: impl Write,
String::from( ) -> Result<(), Box<dyn Error>> {
file_name_without_extension let const_name = source_path
.file_name() .file_stem()
.unwrap() .ok_or("Not a filename")?
.to_string_lossy(), .to_str()
) .ok_or("Filename is not valid unicode")?
.to_uppercase() .to_uppercase();
let content = String::from_utf8(read_file(source_path)?)?;
let fence = content.chars().filter(|&c| c == '#').collect::<String>() + "#";
writeln!(
target,
"pub const {const_name}: &str = r{fence}\"{content}\"{fence};"
)?;
Ok(())
} }
pub fn create_static_output_files(source_dir: &str) { pub fn create_static_output_files(
let out_dir = env::var("OUT_DIR").unwrap(); source_dir: &Path,
let dest_path = Path::new(&out_dir).join("static_files.rs"); dest_path: &Path,
let mut f = File::create(&dest_path).unwrap(); ) -> Result<(), Box<dyn Error>> {
match read_dir(source_dir) { let mut f = File::create(dest_path)?;
match source_dir.read_dir() {
Ok(dir_content) => { Ok(dir_content) => {
for maybe_dir_entry in dir_content { for maybe_dir_entry in dir_content {
let file_path = maybe_dir_entry.unwrap().path(); create_static_output(&maybe_dir_entry?.path(), &mut f)?;
let mut buffer = String::new();
File::open(file_path.clone())
.unwrap()
.read_to_string(&mut buffer)
.unwrap();
let fence = buffer.chars().filter(|c| *c == '#').collect::<String>() + "#";
f.write_all(
format!(
"pub const {}: &str = r{1}\"{2}\"{1};\n",
get_const_name(&file_path),
fence,
buffer
)
.as_bytes(),
)
.unwrap();
} }
} }
Err(err) => { Err(err) => {
if err.kind() != NotFound { if err.kind() != NotFound {
panic!("Unexpected error: {}", err) return Err(format!("Unexpected error: {err}").into());
} }
} }
} }
Ok(())
} }
#[allow(unused)] pub fn main() -> Result<(), Box<dyn Error>> {
fn main() { create_static_output_files(
create_static_output_files("static_files"); Path::new("static_files"),
&(Path::new(&env::var("OUT_DIR")?).join("static_files.rs")),
)
} }

View file

@ -115,12 +115,8 @@ impl<D: Clone> ImplementationBuilder<Cert<D>> for DefaultBuilder {
) )
} }
type Implementation = CertSymbol< type Implementation =
SetuidCommandRunner<'static, String, StdCommandRunner>, CertSymbol<SetuidCommandRunner<String>, SetuidCommandRunner<String>, D, PathBuf>;
SetuidCommandRunner<'static, String, StdCommandRunner>,
D,
PathBuf,
>;
fn create( fn create(
resource: &Cert<D>, resource: &Cert<D>,
target: &<Cert<D> as Resource>::Artifact, target: &<Cert<D> as Resource>::Artifact,
@ -128,7 +124,7 @@ impl<D: Clone> ImplementationBuilder<Cert<D>> for DefaultBuilder {
) -> Self::Implementation { ) -> Self::Implementation {
CertSymbol::new( CertSymbol::new(
resource.0.clone(), resource.0.clone(),
SetuidCommandRunner::new(user_name.0, &StdCommandRunner), SetuidCommandRunner::new(user_name.0),
root_cert.into(), root_cert.into(),
account_key.into(), account_key.into(),
challenges_dir.into(), challenges_dir.into(),
@ -286,7 +282,7 @@ impl<D: Clone + Display, P: AsRef<Path>, C: Clone + Into<PhpFpmPoolConfig>>
&resource.0, &resource.0,
cert, cert,
key, key,
nginx::php_snippet(resource.2, &pool.0, &resource.1) + &resource.3, nginx::php_snippet(resource.2, pool.0, &resource.1) + &resource.3,
challenges_snippet_path, challenges_snippet_path,
), ),
), ),
@ -432,7 +428,7 @@ impl<D: Clone> ImplementationBuilder<PhpFpmPool<D>> for DefaultBuilder {
( (
FileSymbol::new( FileSymbol::new(
conf_path.clone().into(), conf_path.clone().into(),
php_fpm_pool_config(&user_name.0, &socket_path, &resource.1), php_fpm_pool_config(&user_name.0, socket_path, &resource.1),
), ),
ReloadServiceSymbol::new(StdCommandRunner, service_name.0.clone()), ReloadServiceSymbol::new(StdCommandRunner, service_name.0.clone()),
) )
@ -448,7 +444,7 @@ impl<D, P: AsRef<Path>> ImplementationBuilder<SystemdSocketService<D, P>> for De
FileSymbol<PathBuf, String>, FileSymbol<PathBuf, String>,
SystemdUserSessionSymbol<'static, String, StdCommandRunner>, SystemdUserSessionSymbol<'static, String, StdCommandRunner>,
OwnerSymbol<StdCommandRunner, StdCommandRunner, PathBuf, String>, OwnerSymbol<StdCommandRunner, StdCommandRunner, PathBuf, String>,
UserServiceSymbol<'static, PathBuf, String, StdCommandRunner>, UserServiceSymbol<'static, PathBuf, String>,
); );
fn create( fn create(
resource: &SystemdSocketService<D, P>, resource: &SystemdSocketService<D, P>,
@ -475,12 +471,7 @@ impl<D, P: AsRef<Path>> ImplementationBuilder<SystemdSocketService<D, P>> for De
user_name.0.clone(), user_name.0.clone(),
StdCommandRunner, StdCommandRunner,
), ),
UserServiceSymbol::new( UserServiceSymbol::new(socket_path.clone().into(), user_name.0.clone(), resource.1),
socket_path.clone().into(),
user_name.0.clone(),
resource.1,
&StdCommandRunner,
),
) )
} }
} }

View file

@ -63,13 +63,9 @@ impl CommandRunner for StdCommandRunner {
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()?;
.expect("Failed to spawn child process");
let stdin = child.stdin.as_mut().expect("Failed to open stdin"); let stdin = child.stdin.as_mut().expect("Failed to open stdin");
stdin stdin.write_all(input).await?;
.write_all(input)
.await
.expect("Failed to write to stdin");
let res = child.wait_with_output().await; let res = child.wait_with_output().await;
//println!("{:?}", res); //println!("{:?}", res);
#[allow(clippy::let_and_return)] #[allow(clippy::let_and_return)]
@ -78,17 +74,13 @@ impl CommandRunner for StdCommandRunner {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct SetuidCommandRunner<'a, U: AsRef<str>, C: CommandRunner> { pub struct SetuidCommandRunner<U: AsRef<str>> {
command_runner: &'a C,
user_name: U, user_name: U,
} }
impl<'a, U: AsRef<str>, C: CommandRunner> SetuidCommandRunner<'a, U, C> { impl<U: AsRef<str>> SetuidCommandRunner<U> {
pub fn new(user_name: U, command_runner: &'a C) -> Self { pub const fn new(user_name: U) -> Self {
SetuidCommandRunner { Self { user_name }
command_runner,
user_name,
}
} }
} }
@ -121,13 +113,13 @@ impl Drop for TempSetEnv<'_> {
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl<U: AsRef<str>, C: CommandRunner> CommandRunner for SetuidCommandRunner<'_, U, C> { impl<U: AsRef<str>> CommandRunner for SetuidCommandRunner<U> {
async fn run(&self, program: &str, args: &[&OsStr], input: &[u8]) -> IoResult<Output> { async fn run(&self, program: &str, args: &[&OsStr], input: &[u8]) -> IoResult<Output> {
let uid = get_user_by_name(self.user_name.as_ref()) let uid = get_user_by_name(self.user_name.as_ref())
.expect("User does not exist") .expect("User does not exist")
.uid(); .uid();
let set_home = TempSetEnv::new("HOME", format!("/home/{}", self.user_name.as_ref())); let set_home = TempSetEnv::new("HOME", format!("/home/{}", self.user_name.as_ref()));
let set_dbus = TempSetEnv::new("XDG_RUNTIME_DIR", format!("/run/user/{}", uid)); let set_dbus = TempSetEnv::new("XDG_RUNTIME_DIR", format!("/run/user/{uid}"));
//println!("{} {:?}", program, args); //println!("{} {:?}", program, args);
let mut child = Command::new(program) let mut child = Command::new(program)
.args(args) .args(args)
@ -151,42 +143,6 @@ impl<U: AsRef<str>, C: CommandRunner> CommandRunner for SetuidCommandRunner<'_,
} }
} }
#[derive(Debug)]
pub struct SuCommandRunner<'a, C>
where
C: CommandRunner,
{
command_runner: &'a C,
user_name: &'a str,
}
impl<'a, C> SuCommandRunner<'a, C>
where
C: 'a + CommandRunner,
{
pub fn new(user_name: &'a str, command_runner: &'a C) -> SuCommandRunner<'a, C> {
SuCommandRunner {
command_runner,
user_name,
}
}
}
// Su doesn't set XDG_RUNTIME_DIR
// https://github.com/systemd/systemd/blob/master/src/login/pam_systemd.c#L439
#[async_trait(?Send)]
impl<'a, C> CommandRunner for SuCommandRunner<'a, C>
where
C: 'a + CommandRunner,
{
async fn run(&self, program: &str, args: &[&OsStr], input: &[u8]) -> IoResult<Output> {
let raw_new_args = [self.user_name, "-s", "/usr/bin/env", "--", program];
let mut new_args: Vec<&OsStr> = raw_new_args.iter().map(AsRef::as_ref).collect();
new_args.extend_from_slice(args);
self.command_runner.run("su", &new_args, input).await
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::args; use crate::args;
@ -207,10 +163,10 @@ mod test {
loop { loop {
futures_util::select! { futures_util::select! {
_ = res => {}, _ = res => {},
_ = ps => assert!((Instant::now() - start).as_millis() < 1000), _ = ps => assert!(start.elapsed().as_millis() < 1000),
complete => break, complete => break,
} }
} }
}) });
} }
} }

View file

@ -21,6 +21,7 @@
clippy::cargo_common_metadata, clippy::cargo_common_metadata,
clippy::future_not_send, clippy::future_not_send,
clippy::missing_errors_doc, clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions, clippy::module_name_repetitions,
rustdoc::all, rustdoc::all,
missing_docs, missing_docs,

View file

@ -37,7 +37,7 @@ pub trait Policy {
#[must_use] #[must_use]
fn path_for_data(name: impl Display) -> PathBuf { fn path_for_data(name: impl Display) -> PathBuf {
Path::new("/root/data").join(format!("_{}", name)) Path::new("/root/data").join(format!("_{name}"))
} }
} }
@ -121,7 +121,7 @@ impl<P, D: AsRef<str>> ResourceLocator<KeyAndCertBundle<D>> for DefaultLocator<P
} }
} }
impl<'a, POLICY, P: AsRef<Path>> ResourceLocator<File<P>> for DefaultLocator<POLICY> { impl<POLICY, P: AsRef<Path>> ResourceLocator<File<P>> for DefaultLocator<POLICY> {
type Prerequisites = Dir<PathBuf>; type Prerequisites = Dir<PathBuf>;
fn locate(resource: &File<P>) -> (<File<P> as Resource>::Artifact, Self::Prerequisites) { fn locate(resource: &File<P>) -> (<File<P> as Resource>::Artifact, Self::Prerequisites) {
((), Dir(resource.0.as_ref().parent().unwrap().into())) ((), Dir(resource.0.as_ref().parent().unwrap().into()))
@ -370,7 +370,7 @@ impl<D: Clone + AsRef<str>, P: Policy> ResourceLocator<PhpFpmPool<D>> for Defaul
php_version, user.0 php_version, user.0
)), )),
user, user,
ServiceNameArtifact(format!("php{}-fpm", php_version)), ServiceNameArtifact(format!("php{php_version}-fpm")),
), ),
(), (),
) )

View file

@ -1,44 +1,28 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::min; use std::cmp::min;
use std::io::stderr; use std::io::{stderr, Write};
use std::io::Write; use std::rc::Rc;
// The log crate defines // The log crate defines
// 1 - Error, 2 - Warn, 3 - Info, 4 - Debug, 5 - Trace // 1 - Error, 2 - Warn, 3 - Info, 4 - Debug, 5 - Trace
pub type Level = usize; pub type Level = usize;
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry<S>(pub Level, pub S); pub struct Entry<S>(pub Level, pub S);
pub trait Logger { pub trait Logger {
fn write<S: AsRef<str> + Into<String>>(&self, level: Level, msg: S) fn write(&self, level: Level, msg: &str);
where fn writeln(&self, level: Level, msg: &str);
Self: Sized; fn info(&self, msg: &str) {
fn writeln<S: AsRef<str> + Into<String>>(&self, level: Level, msg: S)
where
Self: Sized;
fn info<S: AsRef<str> + Into<String>>(&self, msg: S)
where
Self: Sized,
{
self.writeln(3, msg); self.writeln(3, msg);
} }
fn debug<S: AsRef<str> + Into<String>>(&self, msg: S) fn debug(&self, msg: &str) {
where
Self: Sized,
{
self.writeln(4, msg); self.writeln(4, msg);
} }
fn trace<S: AsRef<str> + Into<String>>(&self, msg: S) fn trace(&self, msg: &str) {
where
Self: Sized,
{
self.writeln(5, msg); self.writeln(5, msg);
} }
fn put<S: AsRef<str> + Into<String>>(&self, entries: impl IntoIterator<Item = Entry<S>>) -> usize fn put<'a>(&self, entries: impl IntoIterator<Item = Entry<&'a str>>) -> usize {
where
Self: Sized,
{
let mut c = 0; let mut c = 0;
for item in entries { for item in entries {
self.writeln(item.0, item.1); self.writeln(item.0, item.1);
@ -54,21 +38,21 @@ pub struct StdErrLogger {
} }
impl Logger for StdErrLogger { impl Logger for StdErrLogger {
fn write<S: AsRef<str> + Into<String>>(&self, _level: Level, msg: S) { fn write(&self, _level: Level, msg: &str) {
*self.line_started.borrow_mut() = true; *self.line_started.borrow_mut() = true;
write!(&mut stderr(), "{}", msg.as_ref()).unwrap(); write!(&mut stderr(), "{msg}").expect("Could not write to stderr");
} }
fn writeln<S: AsRef<str> + Into<String>>(&self, _level: Level, msg: S) { fn writeln(&self, _level: Level, msg: &str) {
if self.line_started.replace(false) { if self.line_started.replace(false) {
writeln!(&mut stderr()).unwrap(); writeln!(&mut stderr()).expect("Could not write to stderr");
} }
writeln!(&mut stderr(), "{}", msg.as_ref()).unwrap(); writeln!(&mut stderr(), "{msg}").expect("Could not write to stderr");
} }
} }
impl Drop for StdErrLogger { impl Drop for StdErrLogger {
fn drop(&mut self) { fn drop(&mut self) {
if *self.line_started.borrow() { if *self.line_started.borrow() {
writeln!(&mut stderr()).unwrap(); writeln!(&mut stderr()).expect("Could not write to stderr");
} }
} }
} }
@ -86,21 +70,21 @@ impl<'a, L> FilteringLogger<'a, L> {
} }
impl<'a, L: Logger> Logger for FilteringLogger<'a, L> { impl<'a, L: Logger> Logger for FilteringLogger<'a, L> {
fn write<S: AsRef<str> + Into<String>>(&self, level: Level, str: S) { fn write(&self, level: Level, msg: &str) {
if level <= self.max_level { if level <= self.max_level {
self.logger.write(level, str); self.logger.write(level, msg);
} }
} }
fn writeln<S: AsRef<str> + Into<String>>(&self, level: Level, str: S) { fn writeln(&self, level: Level, msg: &str) {
if level <= self.max_level { if level <= self.max_level {
self.logger.writeln(level, str); self.logger.writeln(level, msg);
} }
} }
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct StoringLogger { pub struct StoringLogger {
log: RefCell<(bool, Vec<Entry<String>>)>, log: Rc<RefCell<(bool, Vec<Entry<String>>)>>,
} }
impl StoringLogger { impl StoringLogger {
@ -109,30 +93,28 @@ impl StoringLogger {
Self::default() Self::default()
} }
#[must_use]
pub fn release(self) -> Vec<Entry<String>> { pub fn release(self) -> Vec<Entry<String>> {
self.log.into_inner().1 Rc::try_unwrap(self.log).unwrap().into_inner().1
} }
} }
impl Logger for StoringLogger { impl Logger for StoringLogger {
fn write<S: AsRef<str> + Into<String>>(&self, level: Level, line: S) { fn write(&self, level: Level, msg: &str) {
let mut log = self.log.borrow_mut(); let mut log = self.log.borrow_mut();
let entry = if log.0 { let entry = if log.0 {
log log.1.pop().map(|e| Entry(min(e.0, level), e.1 + msg))
.1
.pop()
.map(|e| Entry(min(e.0, level), e.1 + line.as_ref()))
} else { } else {
None None
} }
.unwrap_or_else(|| Entry(level, line.into())); .unwrap_or_else(|| Entry(level, msg.to_string()));
log.0 = true; log.0 = true;
log.1.push(entry); log.1.push(entry);
} }
fn writeln<S: AsRef<str> + Into<String>>(&self, level: Level, line: S) { fn writeln(&self, level: Level, msg: &str) {
let mut log = self.log.borrow_mut(); let mut log = self.log.borrow_mut();
log.0 = false; log.0 = false;
log.1.push(Entry(level, line.into())); log.1.push(Entry(level, msg.to_string()));
} }
} }

View file

@ -42,7 +42,7 @@ impl<D> Resource for KeyAndCertBundle<D> {
#[derive(Debug, Hash, PartialEq, Eq)] #[derive(Debug, Hash, PartialEq, Eq)]
pub struct File<P>(pub P, pub String); pub struct File<P>(pub P, pub String);
impl<'a, P> Resource for File<P> { impl<P> Resource for File<P> {
type Artifact = (); type Artifact = ();
} }
@ -228,7 +228,7 @@ impl<D> Resource for Cron<D> {
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
pub trait FromResource<R> { pub trait FromResource<R> {
fn from_resource(from: R) -> (Self, Weak<R>) fn from_resource(from: &Rc<R>) -> (Self, Weak<R>)
where where
Self: Sized; Self: Sized;
} }
@ -250,8 +250,7 @@ macro_rules! default_resources {
} }
$(impl<'a, D> FromResource<$type> for DefaultResources<'a, D> { $(impl<'a, D> FromResource<$type> for DefaultResources<'a, D> {
fn from_resource(from: $type) -> (Self, Weak<$type>) { fn from_resource(inner: &Rc<$type>) -> (Self, Weak<$type>) {
let inner = Rc::new(from);
(Self::$name(Rc::clone(&inner)), Rc::downgrade(&inner)) (Self::$name(Rc::clone(&inner)), Rc::downgrade(&inner))
} }
})* })*

88
src/setup/cache.rs Normal file
View file

@ -0,0 +1,88 @@
use super::{Add, AddResult, AddableResource};
use crate::async_utils::sleep;
use crate::resources::{FromArtifact, FromResource};
use async_trait::async_trait;
use futures_util::future::{FutureExt, Shared};
use slog::{trace, Logger};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Debug;
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Duration;
type ResourceCache<Rs, As> = HashMap<Rs, Shared<Pin<Box<dyn Future<Output = (As, bool)>>>>>;
#[derive(Debug)]
pub struct Cache<I, Rs, As> {
resources: RefCell<ResourceCache<Rs, As>>,
inner: Rc<I>,
}
impl<I, Rs, As> Cache<I, Rs, As> {
pub fn new(inner: I) -> Self {
Self {
resources: RefCell::default(),
inner: Rc::new(inner),
}
}
}
#[async_trait(?Send)]
impl<R: AddableResource + Debug, I, Rs, As> Add<R> for Cache<I, Rs, As>
where
Rs: Hash + Eq + 'static + FromResource<R>,
As: 'static + FromArtifact<R> + Clone,
I: 'static + Add<R>,
{
// FIXME: https://github.com/rust-lang/rust-clippy/issues/6353
#[allow(clippy::await_holding_refcell_ref)]
async fn add(&self, logger: &Rc<Logger>, resource: Rc<R>, force_run: bool) -> AddResult<R> {
let (storable_resource, weak_resource) = Rs::from_resource(&resource);
let mut resources = self.resources.borrow_mut();
let result = if let Some(future) = resources.remove(&storable_resource) {
assert!(
!force_run,
"Forcing to run an already-added resource is a logical error"
);
resources.insert(storable_resource, future.clone());
drop(resources);
trace!(logger, "Resource already added");
Ok(future.await)
} else {
let inner_weak = Rc::downgrade(&self.inner);
let logger_weak = Rc::downgrade(logger);
let future = Box::pin(async move {
let inner = inner_weak.upgrade().expect("Dangling!");
let logger = logger_weak.upgrade().expect("Dangling!");
let resource = weak_resource.upgrade().expect("Dangling!");
let result = inner.add(&logger, Rc::clone(&resource), force_run).await;
// Need to convert Box<Error> to String for Clone for Shared
result
.map(|(t, did_run)| (As::from_artifact(t), did_run))
.map_err(|e| e.to_string())
})
.shared();
let future_clone = future.clone();
resources.insert(
storable_resource,
(Box::pin(async move {
let result = future_clone.await;
if result.is_err() {
// Step back to give the initial caller time to handle the error before unwrapping
sleep(Duration::from_millis(0)).await;
}
result.unwrap()
}) as Pin<Box<dyn Future<Output = (As, bool)>>>)
.shared(),
);
drop(resources);
let result = future.await;
result.map_err(std::convert::Into::into)
};
result.map(|(t, did_run)| (t.into_artifact(), did_run))
}
}

View file

@ -1,204 +0,0 @@
use super::runnable::Runnable;
use super::setup::Setup;
use super::util::{AddableResource, InternalAddResult};
use super::SymbolRunner;
use crate::async_utils::join;
use crate::loggers::{Logger, StoringLogger};
use crate::resources::{FromArtifact, FromResource};
use crate::symbols::Symbol;
use crate::to_artifact::ToArtifact;
use crate::{ImplementationBuilder, ResourceLocator};
use async_trait::async_trait;
use std::error::Error;
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
#[async_trait(?Send)]
pub trait AddGeneric<X: ToArtifact> {
async fn add_generic(&self, x: X) -> InternalAddResult<X>;
}
macro_rules! add_generic {
( $($name:ident)* ) => (
#[async_trait(?Send)]
#[allow(non_snake_case)]
impl<SR: 'static, _L: 'static, _B: 'static, Rs: 'static + Hash + Eq, As: 'static + Clone, $($name: AddableResource,)*>
AddGeneric<($($name,)*)> for Setup<SR, _L, _B, Rs, As>
where
$(
RegularSetupCore<SR, _L, _B>: SetupCore<$name, Self>,
As: FromArtifact<$name>,
Rs: FromResource<$name>,
$name::Artifact: Clone
),*
{
#[allow(unused, clippy::shadow_unrelated)]
async fn add_generic(&self, ($($name,)*): ($($name,)*)) -> Result<(StoringLogger, ($($name::Artifact,)*), bool), (StoringLogger, Box<dyn Error>)>
{
let ($($name,)*) = join!($(self.add($name, false),)*);
let logger = StoringLogger::default();
let mut did_run_any = false;
$(
let (log, artifact, did_run) = $name?;
logger.put(log.release());
did_run_any = did_run_any || did_run;
let $name = artifact;
)*
Ok((logger, ($($name,)*), did_run_any))
}
}
);
}
for_each_tuple!(add_generic);
// This is for self-referential T
// FIXME: Wait for specialization
#[async_trait(?Send)]
impl<
SR: 'static + SymbolRunner,
T: AddableResource,
Rs: 'static + Hash + Eq + FromResource<T>,
As: 'static + FromArtifact<T> + Clone,
L: 'static + ResourceLocator<T, Prerequisites = Option<T>>,
B: 'static + ImplementationBuilder<T>,
> AddGeneric<Option<T>> for Setup<SR, L, B, Rs, As>
where
<B as ImplementationBuilder<T>>::Implementation: Runnable + Debug,
Self: AddGeneric<B::Prerequisites>,
T::Artifact: Clone,
{
async fn add_generic(&self, r: Option<T>) -> InternalAddResult<Option<T>> {
Ok(match r {
Some(r) => {
let (logger, result, did_run) = self.add(r, false).await?;
(logger, Some(result), did_run)
}
None => (StoringLogger::default(), None, false),
})
}
}
#[async_trait(?Send)]
impl<
T: AddableResource,
Rs: 'static + Hash + Eq + FromResource<T>,
As: 'static + FromArtifact<T> + Clone,
SR: 'static,
L: 'static,
B: 'static,
> AddGeneric<T> for Setup<SR, L, B, Rs, As>
where
T::Artifact: Clone,
RegularSetupCore<SR, L, B>: 'static + SetupCore<T, Self>,
{
async fn add_generic(&self, r: T) -> InternalAddResult<T> {
self.add(r, false).await
}
}
#[async_trait(?Send)]
pub trait SetupCore<R: AddableResource, S> {
async fn add<RR: AsRef<R>>(
&self,
setup: &S,
resource: RR,
force_run: bool,
) -> InternalAddResult<R>;
}
#[derive(Debug)]
pub struct RegularSetupCore<SR, L, B> {
symbol_runner: SR,
phantom: PhantomData<(L, B)>,
}
impl<SR, L, B> RegularSetupCore<SR, L, B> {
pub fn new(symbol_runner: SR) -> Self {
Self {
symbol_runner,
phantom: PhantomData::default(),
}
}
}
#[async_trait(?Send)]
impl<SR: SymbolRunner, L, B, R: AddableResource, S> SetupCore<R, S> for RegularSetupCore<SR, L, B>
where
B: ImplementationBuilder<R>,
<B as ImplementationBuilder<R>>::Implementation: Runnable + Debug,
L: ResourceLocator<R>,
S: AddGeneric<B::Prerequisites> + AddGeneric<<L as ResourceLocator<R>>::Prerequisites>,
{
async fn add<RR: AsRef<R>>(
&self,
setup: &S,
resource: RR,
force_run: bool,
) -> InternalAddResult<R> {
let resource = resource.as_ref();
let logger = StoringLogger::new();
logger.write(4, format!("Adding {:?} ... ", resource));
logger.write(4, format!("(force_run is {})", force_run));
let (location, location_prereqs) = L::locate(resource);
logger.trace(format!("Adding location prereqs for {:?}", resource));
let result = setup.add_generic(location_prereqs).await;
if let Err((log, e)) = result {
logger.put(log.release());
return Err((logger, e));
}
let (location_prereq_logger, _, location_prereqs_did_run) = result.unwrap();
logger.put(location_prereq_logger.release());
logger.trace(format!(
"Location prereqs for {:?} did_run: {}",
resource, location_prereqs_did_run
));
logger.trace(format!("Adding implementation prereqs for {:?}", resource));
let result = setup.add_generic(B::prerequisites(resource)).await;
if let Err((log, e)) = result {
logger.put(log.release());
return Err((logger, e));
}
let (impl_prereq_logger, prereqs, prereqs_did_run) = result.unwrap();
logger.put(impl_prereq_logger.release());
logger.trace(format!(
"Implementation prereqs for {:?} did_run: {}",
resource, prereqs_did_run
));
logger.trace(format!("Running implementation for {:?}", resource));
let implementation = B::create(resource, &location, prereqs);
let did_run_result = implementation
.run(
&self.symbol_runner,
&logger,
force_run || location_prereqs_did_run || prereqs_did_run,
)
.await;
match did_run_result {
Ok(did_run) => {
logger.write(4, "done.");
Ok((logger, location, did_run))
}
Err(e) => Err((logger, e)),
}
}
}
#[async_trait(?Send)]
impl<SR: SymbolRunner, L, B> SymbolRunner for RegularSetupCore<SR, L, B> {
async fn run_symbol<S: Symbol + Debug, LOG: Logger>(
&self,
symbol: &S,
parent_logger: &LOG,
force: bool,
) -> Result<bool, Box<dyn Error>> {
let logger = StoringLogger::new();
logger.write(4, format!("Directly running {:?} ...", symbol));
let result = self.symbol_runner.run_symbol(symbol, &logger, force).await;
logger.write(4, "done.");
let max_level = if result.is_err() { 5 } else { 3 };
parent_logger.put(logger.release().into_iter().filter(|e| e.0 <= max_level));
result
}
}

View file

@ -1,12 +1,388 @@
mod core; mod realizer;
mod util;
pub use util::{AddResult, AddableResource};
mod symbol_runner; mod symbol_runner;
mod util;
pub use symbol_runner::{ pub use symbol_runner::{
DelayingSymbolRunner, DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner, DelayingSymbolRunner, DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner,
SymbolRunner, SymbolRunner,
}; };
mod cache;
mod runnable; mod runnable;
#[allow(clippy::module_inception)] use crate::loggers::Logger;
mod setup; use crate::resources::{DefaultArtifacts, DefaultResources, FromArtifact, FromResource};
pub use setup::SetupFacade as Setup; use crate::symbols::Symbol;
use crate::{DefaultBuilder, DefaultLocator};
use crate::{ImplementationBuilder, ResourceLocator};
use async_trait::async_trait;
use cache::Cache;
use realizer::Realizer;
use runnable::Runnable;
use slog::o;
use std::error::Error;
use std::fmt::Debug;
use std::hash::Hash;
use std::rc::Rc;
use util::{Add, AddGeneric, AddResult, AddableResource, Recorder};
// Necessary for the recursive type
#[derive(Debug)]
pub struct ActualSetup<SR, L, B, Rs, As>(Cache<Realizer<SR, L, B, Self>, Rs, As>);
#[async_trait(?Send)]
impl<T: AddableResource + Debug, SR, L, B, Rs, As> Add<T> for ActualSetup<SR, L, B, Rs, As>
where
Cache<Realizer<SR, L, B, Self>, Rs, As>: Add<T>,
{
async fn add(&self, logger: &Rc<slog::Logger>, r: Rc<T>, force_run: bool) -> AddResult<T> {
self.0.add(logger, r, force_run).await
}
}
// This is for self-referential T
// FIXME: Wait for specialization
#[async_trait(?Send)]
impl<
SR: 'static + SymbolRunner,
T: AddableResource,
Rs: 'static + Hash + Eq + FromResource<T>,
As: 'static + FromArtifact<T> + Clone,
L: 'static + ResourceLocator<T, Prerequisites = Option<T>>,
B: 'static + ImplementationBuilder<T>,
> AddGeneric<Option<T>> for ActualSetup<SR, L, B, Rs, As>
where
<B as ImplementationBuilder<T>>::Implementation: Runnable + Debug,
Self: AddGeneric<B::Prerequisites>,
T::Artifact: Clone,
// These bounds cannot be replaced by
// `Realizer<SR, L, B>: Add<T, Self>`
// because the prerequisites are Option<T>, too, and thus this would
// require AddGeneric<Option<T>> to already be implemented
{
async fn add_generic(
&self,
logger: &Rc<slog::Logger>,
r: Option<T>,
force_run: bool,
) -> AddResult<Option<T>> {
Ok(match r {
Some(r) => {
let (result, did_run) = self.add(logger, Rc::new(r), force_run).await?;
(Some(result), did_run)
}
None => (None, false),
})
}
}
#[derive(Debug)]
pub struct Setup<
SR,
LOG,
L = DefaultLocator,
B = DefaultBuilder,
Rs = DefaultResources<'static, &'static str>,
As = DefaultArtifacts<'static, &'static str>,
>(LOG, Rc<SR>, Rc<ActualSetup<SR, L, B, Rs, As>>);
impl<SR, LOG> Setup<SR, LOG> {
pub fn new(symbol_runner: SR, logger: LOG) -> Self {
Self::new_with(symbol_runner, logger)
}
}
impl<L, B, As, SR, LOG, Rs: Hash + Eq> Setup<SR, LOG, L, B, Rs, As> {
pub fn new_with(symbol_runner: SR, logger: LOG) -> Self {
let runner = Rc::new(symbol_runner);
let inner = Rc::new_cyclic(|inner| {
ActualSetup(Cache::new(Realizer::new(Rc::clone(&runner), inner.clone())))
});
Self(logger, runner, inner)
}
}
impl<
SR: 'static,
LOG: 'static + Logger,
L: 'static,
B: 'static,
Rs: 'static + Eq + Hash,
As: 'static,
> Setup<SR, LOG, L, B, Rs, As>
{
pub async fn add_force<R: AddableResource>(&self, resource: R, force_run: bool) -> AddResult<R>
where
Rs: FromResource<R>,
As: FromArtifact<R> + Clone,
ActualSetup<SR, L, B, Rs, As>: Add<R>,
{
let recorder = Recorder::default();
let result = {
let log = Rc::new(slog::Logger::root(recorder.clone(), o!()));
self.2.add(&log, Rc::new(resource), force_run).await
};
self.log_result(recorder, result.as_ref().map(|(_, did_run)| *did_run));
result
}
pub async fn add<R: AddableResource>(&self, resource: R) -> AddResult<R>
where
Rs: FromResource<R>,
As: FromArtifact<R> + Clone,
R::Artifact: Clone,
ActualSetup<SR, L, B, Rs, As>: Add<R>,
{
self.add_force(resource, false).await
}
pub async fn run_symbol<S: Symbol + Debug>(
&self,
symbol: S,
force: bool,
) -> Result<bool, Box<dyn Error>>
where
SR: SymbolRunner,
{
let recorder = Recorder::default();
let result = {
let log = Rc::new(slog::Logger::root(
recorder.clone(),
o!("symbol" => format!("{symbol:?}")),
));
self.1.run_symbol(&symbol, &log, force).await
};
self.log_result(recorder, result.as_ref().copied());
result
}
fn log_result(&self, recorder: Recorder, result: Result<bool, &Box<dyn Error>>) {
let log = match result {
Ok(false) => String::new(),
Ok(true) => recorder.into_string(slog::Level::Info),
Err(e) => recorder.into_string(slog::Level::Trace),
};
if log.is_empty() {
self.0.write(3, ".");
} else {
self.0.writeln(3, &log);
}
}
}
#[cfg(test)]
mod test {
use super::SymbolRunner;
use crate::async_utils::run;
use crate::loggers::{Entry, StoringLogger};
use crate::resources::{FromArtifact, FromResource, Resource};
use crate::symbols::Symbol;
use crate::to_artifact::ToArtifact;
use crate::{ImplementationBuilder, ResourceLocator, Setup};
use async_trait::async_trait;
use regex::Regex;
use slog::{info, Logger};
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Debug;
use std::rc::{Rc, Weak};
struct TestSymbolRunner {
count: Rc<RefCell<usize>>,
}
#[async_trait(?Send)]
impl SymbolRunner for TestSymbolRunner {
async fn run_symbol<S: Symbol + Debug>(
&self,
symbol: &S,
logger: &Logger,
force: bool,
) -> Result<bool, Box<dyn Error>> {
info!(logger, "run");
let run = force || !symbol.target_reached().await?;
if run {
*self.count.borrow_mut() += 1;
}
Ok(run)
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct TestResource<T>(&'static str, T);
impl<T> Resource for TestResource<T> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum Resources {
A(Rc<TestResource<&'static str>>),
B(Rc<TestResource<()>>),
}
impl FromResource<TestResource<&'static str>> for Resources {
fn from_resource(
inner: &Rc<TestResource<&'static str>>,
) -> (Self, Weak<TestResource<&'static str>>) {
(Self::A(Rc::clone(&inner)), Rc::downgrade(&inner))
}
}
impl FromResource<TestResource<()>> for Resources {
fn from_resource(inner: &Rc<TestResource<()>>) -> (Self, Weak<TestResource<()>>) {
(Self::B(Rc::clone(&inner)), Rc::downgrade(&inner))
}
}
#[derive(Clone)]
struct Artifacts;
impl<V> FromArtifact<TestResource<V>> for Artifacts {
fn from_artifact(_from: ()) -> Self {
Self
}
#[allow(clippy::unused_unit)]
fn into_artifact(self) -> () {
#[allow(clippy::unused_unit)]
()
}
}
struct TestResourceLocator;
impl<T> ResourceLocator<TestResource<T>> for TestResourceLocator {
type Prerequisites = ();
fn locate(_resource: &TestResource<T>) -> (<TestResource<T> as ToArtifact>::Artifact, ()) {
((), ())
}
}
struct TestImplementationBuilder;
impl ImplementationBuilder<TestResource<&'static str>> for TestImplementationBuilder {
type Implementation = TestSymbol;
type Prerequisites = TestResource<()>;
fn prerequisites(resource: &TestResource<&'static str>) -> Self::Prerequisites {
TestResource(resource.1, ())
}
fn create(
resource: &TestResource<&'static str>,
(): &(),
_inputs: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Implementation {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
impl ImplementationBuilder<TestResource<()>> for TestImplementationBuilder {
type Implementation = TestSymbol;
type Prerequisites = ();
fn prerequisites(_resource: &TestResource<()>) -> Self::Prerequisites {}
fn create(resource: &TestResource<()>, (): &(), (): ()) -> Self::Implementation {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
#[derive(Debug)]
struct TestSymbol {
reached: bool,
}
#[async_trait(?Send)]
impl Symbol for TestSymbol {
async fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(self.reached)
}
async fn execute(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
#[allow(clippy::type_complexity)]
fn get_setup() -> (
Rc<RefCell<usize>>,
Setup<
TestSymbolRunner,
StoringLogger,
TestResourceLocator,
TestImplementationBuilder,
Resources,
Artifacts,
>,
StoringLogger,
) {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let logger = StoringLogger::new();
(count, Setup::new_with(runner, logger.clone()), logger)
}
#[test]
fn correctly_uses_force() {
run(async {
let (count, setup, _) = get_setup();
setup.add(TestResource("A", "b")).await.unwrap();
assert_eq!(*count.borrow(), 2);
setup.add(TestResource("A", "b")).await.unwrap();
assert_eq!(*count.borrow(), 2);
let (count, setup, _) = get_setup();
setup.add(TestResource("A", "B")).await.unwrap();
assert_eq!(*count.borrow(), 0);
});
}
#[test]
fn run_reached_symbol() {
run(async {
let (count, setup, log) = get_setup();
let did_run = setup
.run_symbol(TestSymbol { reached: true }, false)
.await
.unwrap();
drop(setup);
assert!(!did_run);
assert_eq!(*count.borrow(), 0);
assert_eq!(log.release(), vec![Entry(3, ".".into())]);
});
}
#[test]
fn run_not_reached_symbol() {
run(async {
let (count, setup, log) = get_setup();
let did_run = setup
.run_symbol(TestSymbol { reached: false }, false)
.await
.unwrap();
drop(setup);
assert!(did_run);
assert_eq!(*count.borrow(), 1);
let log = log.release();
assert_eq!(log.len(), 1);
assert_eq!(log[0].0, 3);
let re = Regex::new(r"^symbol: TestSymbol \{ reached: false \}\n \w+ \d{1,2} \d{2}:\d{2}:\d{2}.\d{3} INFO run\n$").unwrap();
assert!(re.is_match(&log[0].1));
});
}
use super::{ActualSetup, AddGeneric, Cache, Realizer};
#[test]
fn empty_tuple_add_generic() {
let setup = Rc::new_cyclic(|inner| {
ActualSetup(Cache::<Realizer<(), (), (), _>, (), ()>::new(
Realizer::new(Rc::new(()), inner.clone()),
))
});
run(async {
assert!(setup
.add_generic(
&Rc::new(slog::Logger::root(slog::Discard, slog::o!())),
(),
false
)
.await
.is_ok());
})
}
}

74
src/setup/realizer.rs Normal file
View file

@ -0,0 +1,74 @@
use super::{Add, AddGeneric, AddResult, AddableResource, Runnable, SymbolRunner};
use crate::{ImplementationBuilder, ResourceLocator};
use async_trait::async_trait;
use slog::{debug, o, trace, Logger};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::rc::{Rc, Weak};
#[derive(Debug)]
pub struct Realizer<SR, L, B, S> {
symbol_runner: Rc<SR>,
outer: Weak<S>,
phantom: PhantomData<(L, B)>,
}
impl<SR, L, B, S> Realizer<SR, L, B, S> {
pub fn new(symbol_runner: Rc<SR>, outer: Weak<S>) -> Self {
Self {
symbol_runner,
outer,
phantom: PhantomData::default(),
}
}
}
#[async_trait(?Send)]
impl<R, SR, L, B, S> Add<R> for Realizer<SR, L, B, S>
where
R: AddableResource,
SR: SymbolRunner,
L: ResourceLocator<R>,
B: ImplementationBuilder<R>,
<B as ImplementationBuilder<R>>::Implementation: Runnable + Debug,
S: AddGeneric<B::Prerequisites> + AddGeneric<<L as ResourceLocator<R>>::Prerequisites>,
{
async fn add(&self, logger: &Rc<Logger>, resource: Rc<R>, force_run: bool) -> AddResult<R> {
let setup = self.outer.upgrade().unwrap();
let logger = Rc::new(logger.new(o!("resource" => format!("{resource:?}"))));
trace!(logger, "(force_run is {})", force_run);
let (location, location_prereqs) = L::locate(&resource);
trace!(logger, "Adding location prereqs ...");
let (_, location_prereqs_did_run) = (*setup)
.add_generic(&logger, location_prereqs, false)
.await?;
trace!(
logger,
"Location prereqs did_run: {}",
location_prereqs_did_run
);
trace!(logger, "Adding implementation prereqs ...");
let (prereqs, prereqs_did_run) = (*setup)
.add_generic(&logger, B::prerequisites(&resource), false)
.await?;
trace!(
logger,
"Implementation prereqs did_run: {}",
prereqs_did_run
);
trace!(logger, "Running implementation ...");
let implementation = B::create(&resource, &location, prereqs);
let did_run = implementation
.run(
&*self.symbol_runner,
&logger,
force_run || location_prereqs_did_run || prereqs_did_run,
)
.await?;
debug!(logger, "done.");
Ok((location, did_run))
}
}

View file

@ -1,16 +1,17 @@
use super::SymbolRunner; use super::SymbolRunner;
use crate::loggers::Logger;
use crate::symbols::Symbol; use crate::symbols::Symbol;
use async_trait::async_trait; use async_trait::async_trait;
use slog::Logger;
use std::error::Error; use std::error::Error;
use std::fmt::Debug; use std::fmt::Debug;
// A generalization over symbols and tuples of symbols
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait Runnable { pub trait Runnable {
async fn run<R: SymbolRunner, L: Logger>( async fn run<R: SymbolRunner>(
&self, &self,
runner: &R, runner: &R,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>>; ) -> Result<bool, Box<dyn Error>>;
} }
@ -21,10 +22,10 @@ impl<S> Runnable for S
where where
Self: Symbol + Debug, Self: Symbol + Debug,
{ {
async fn run<R: SymbolRunner, L: Logger>( async fn run<R: SymbolRunner>(
&self, &self,
runner: &R, runner: &R,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
runner.run_symbol(self, logger, force).await runner.run_symbol(self, logger, force).await
@ -38,7 +39,7 @@ macro_rules! runnable_for_tuple {
#[allow(non_snake_case)] #[allow(non_snake_case)]
impl<$($name: Symbol + Debug,)*> Runnable for ($($name,)*) { impl<$($name: Symbol + Debug,)*> Runnable for ($($name,)*) {
#[allow(unused)] #[allow(unused)]
async fn run<_R: SymbolRunner, _L: Logger>(&self, runner: &_R, logger: &_L, force: bool) -> Result<bool, Box<dyn Error>> { async fn run<_R: SymbolRunner>(&self, runner: &_R, logger: &Logger, force: bool) -> Result<bool, Box<dyn Error>> {
let ($($name,)*) = self; let ($($name,)*) = self;
let mut result = false; let mut result = false;
$(result = runner.run_symbol($name, logger, force || result).await? || result;)* $(result = runner.run_symbol($name, logger, force || result).await? || result;)*
@ -54,10 +55,10 @@ for_each_tuple!(runnable_for_tuple);
mod test { mod test {
use super::Runnable; use super::Runnable;
use crate::async_utils::run; use crate::async_utils::run;
use crate::loggers::{Logger, StoringLogger};
use crate::symbols::Symbol; use crate::symbols::Symbol;
use crate::SymbolRunner; use crate::SymbolRunner;
use async_trait::async_trait; use async_trait::async_trait;
use slog::{o, Discard, Logger};
use std::cell::RefCell; use std::cell::RefCell;
use std::error::Error; use std::error::Error;
use std::fmt::Debug; use std::fmt::Debug;
@ -116,10 +117,10 @@ mod test {
#[async_trait(?Send)] #[async_trait(?Send)]
impl SymbolRunner for TestSymbolRunner { impl SymbolRunner for TestSymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
_logger: &L, _logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
let run = force || !symbol.target_reached().await?; let run = force || !symbol.target_reached().await?;
@ -135,7 +136,7 @@ mod test {
force: bool, force: bool,
) -> (Rc<RefCell<usize>>, Result<bool, Box<dyn Error>>) { ) -> (Rc<RefCell<usize>>, Result<bool, Box<dyn Error>>) {
let (count, runner) = get_runner(); let (count, runner) = get_runner();
let res = run(runnable.run(&runner, &StoringLogger::new(), force)); let res = run(runnable.run(&runner, &Logger::root(Discard, o!()), force));
(count, res) (count, res)
} }

View file

@ -1,338 +0,0 @@
use super::core::{RegularSetupCore, SetupCore};
use super::runnable::Runnable;
use super::util::{AddResult, AddableResource, InternalAddResult};
use super::SymbolRunner;
use crate::async_utils::sleep;
use crate::loggers::{Logger, StoringLogger};
use crate::resources::{DefaultArtifacts, DefaultResources, FromArtifact, FromResource};
use crate::{DefaultBuilder, DefaultLocator};
use futures_util::future::{FutureExt, Shared};
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::error::Error;
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Duration;
type Cache<Rs, As> = HashMap<Rs, Shared<Pin<Box<dyn Future<Output = (As, bool)>>>>>;
#[derive(Debug)]
struct SetupInner<CORE, Rs, As> {
core: CORE,
resources: RefCell<Cache<Rs, As>>,
}
#[derive(Debug)]
pub struct Setup<SR, L, B, Rs, As>(Rc<SetupInner<RegularSetupCore<SR, L, B>, Rs, As>>);
impl<L: 'static, B: 'static, SR: 'static, Rs: Hash + Eq + 'static, As: 'static>
Setup<SR, L, B, Rs, As>
{
fn borrow_resources(&self) -> RefMut<'_, Cache<Rs, As>> {
self.0.resources.borrow_mut()
}
// FIXME: https://github.com/rust-lang/rust-clippy/issues/6353
#[allow(clippy::await_holding_refcell_ref)]
pub async fn add<R: AddableResource>(&self, resource: R, force_run: bool) -> InternalAddResult<R>
where
Rs: FromResource<R>,
As: FromArtifact<R> + Clone,
R::Artifact: Clone,
RegularSetupCore<SR, L, B>: SetupCore<R, Self>,
{
let (storable_resource, weak_resource) = Rs::from_resource(resource);
let mut resources = self.borrow_resources();
let result = if let Some(future) = resources.remove(&storable_resource) {
assert!(
!force_run,
"Forcing to run an already-added resource is a logical error"
);
resources.insert(storable_resource, future.clone());
drop(resources);
let logger = StoringLogger::default();
logger.trace(format!(
"{:?} already added",
weak_resource.upgrade().expect("Dangling!")
));
let (t, did_run) = future.await;
Ok((logger, t, did_run))
} else {
let inner_weak = Rc::downgrade(&self.0);
let future = Box::pin(async move {
let this = Self(inner_weak.upgrade().expect("Dangling!"));
let resource = weak_resource.upgrade().expect("Dangling!");
// Need to convert Box<Error> to String for Clone for Shared
let result = this.0.core.add(&this, resource, force_run).await;
result
.map(|(logger, t, did_run)| (logger, As::from_artifact(t), did_run))
.map_err(|(logger, e)| (logger, e.to_string()))
})
.shared();
let future_clone = future.clone();
resources.insert(
storable_resource,
(Box::pin(async move {
let result = future_clone.await;
if result.is_err() {
// Step back to give the initial caller time to handle the error before unwrapping
sleep(Duration::from_millis(0)).await;
}
result.map(|(_, t, did_run)| (t, did_run)).unwrap()
}) as Pin<Box<dyn Future<Output = (As, bool)>>>)
.shared(),
);
drop(resources);
let result = future.await;
result.map_err(|(logger, e)| (logger, e.into()))
};
result.map(|(logger, t, did_run)| (logger, t.into_artifact(), did_run))
}
}
#[derive(Debug)]
pub struct SetupFacade<
SR,
LOG,
L = DefaultLocator,
B = DefaultBuilder,
Rs = DefaultResources<'static, &'static str>,
As = DefaultArtifacts<'static, &'static str>,
>(LOG, Setup<SR, L, B, Rs, As>);
impl<SR, LOG> SetupFacade<SR, LOG> {
pub fn new(symbol_runner: SR, logger: LOG) -> Self {
Self::new_with(symbol_runner, logger)
}
}
impl<L, B, As, SR, LOG, Rs: Hash + Eq> SetupFacade<SR, LOG, L, B, Rs, As> {
pub fn new_with(symbol_runner: SR, logger: LOG) -> Self {
Self(
logger,
Setup(Rc::new(SetupInner {
core: RegularSetupCore::new(symbol_runner),
resources: RefCell::default(),
})),
)
}
}
impl<
SR: 'static,
LOG: 'static + Logger,
L: 'static,
B: 'static,
Rs: 'static + Eq + Hash,
As: 'static,
> SetupFacade<SR, LOG, L, B, Rs, As>
{
pub async fn add_force<R: AddableResource>(&self, resource: R, force_run: bool) -> AddResult<R>
where
Rs: FromResource<R>,
As: FromArtifact<R> + Clone,
R::Artifact: Clone,
RegularSetupCore<SR, L, B>: SetupCore<R, Setup<SR, L, B, Rs, As>>,
{
let result = self.1.add(resource, force_run).await;
match result {
Ok((logger, t, did_run)) => {
if self
.0
.put(logger.release().into_iter().filter(|e| e.0 <= 3))
== 0
{
self.0.write(3, ".");
}
Ok((t, did_run))
}
Err((logger, e)) => {
self.0.put(logger.release());
Err(e)
}
}
}
pub async fn add<R: AddableResource>(&self, resource: R) -> AddResult<R>
where
RegularSetupCore<SR, L, B>: SetupCore<R, Setup<SR, L, B, Rs, As>>,
Rs: FromResource<R>,
As: FromArtifact<R> + Clone,
R::Artifact: Clone,
SR: SymbolRunner,
{
self.add_force(resource, false).await
}
pub async fn run_symbol<S: Runnable>(
&self,
symbol: S,
force: bool,
) -> Result<bool, Box<dyn Error>>
where
RegularSetupCore<SR, L, B>: SymbolRunner,
{
symbol.run(&(self.1).0.core, &self.0, force).await
}
}
#[cfg(test)]
mod test {
use super::SymbolRunner;
use crate::async_utils::run;
use crate::loggers::{Logger, StoringLogger};
use crate::resources::{FromArtifact, FromResource, Resource};
use crate::symbols::Symbol;
use crate::to_artifact::ToArtifact;
use crate::{ImplementationBuilder, ResourceLocator, Setup};
use async_trait::async_trait;
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Debug;
use std::rc::{Rc, Weak};
struct TestSymbolRunner {
count: Rc<RefCell<usize>>,
}
#[async_trait(?Send)]
impl SymbolRunner for TestSymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>(
&self,
symbol: &S,
_logger: &L,
force: bool,
) -> Result<bool, Box<dyn Error>> {
let run = force || !symbol.target_reached().await?;
if run {
*self.count.borrow_mut() += 1;
}
Ok(run)
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct TestResource<T>(&'static str, T);
impl<T> Resource for TestResource<T> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum Resources {
A(Rc<TestResource<&'static str>>),
B(Rc<TestResource<()>>),
}
impl FromResource<TestResource<&'static str>> for Resources {
fn from_resource(from: TestResource<&'static str>) -> (Self, Weak<TestResource<&'static str>>) {
let inner = Rc::new(from);
(Self::A(Rc::clone(&inner)), Rc::downgrade(&inner))
}
}
impl FromResource<TestResource<()>> for Resources {
fn from_resource(from: TestResource<()>) -> (Self, Weak<TestResource<()>>) {
let inner = Rc::new(from);
(Self::B(Rc::clone(&inner)), Rc::downgrade(&inner))
}
}
#[derive(Clone)]
struct Artifacts;
impl<V> FromArtifact<TestResource<V>> for Artifacts {
fn from_artifact(_from: ()) -> Self {
Self
}
#[allow(clippy::unused_unit)]
fn into_artifact(self) -> () {
#[allow(clippy::unused_unit)]
()
}
}
struct TestResourceLocator;
impl<T> ResourceLocator<TestResource<T>> for TestResourceLocator {
type Prerequisites = ();
fn locate(_resource: &TestResource<T>) -> (<TestResource<T> as ToArtifact>::Artifact, ()) {
((), ())
}
}
struct TestImplementationBuilder;
impl ImplementationBuilder<TestResource<&'static str>> for TestImplementationBuilder {
type Implementation = TestSymbol;
type Prerequisites = TestResource<()>;
fn prerequisites(resource: &TestResource<&'static str>) -> Self::Prerequisites {
TestResource(resource.1, ())
}
fn create(
resource: &TestResource<&'static str>,
(): &(),
_inputs: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Implementation {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
impl ImplementationBuilder<TestResource<()>> for TestImplementationBuilder {
type Implementation = TestSymbol;
type Prerequisites = ();
fn prerequisites(_resource: &TestResource<()>) -> Self::Prerequisites {}
fn create(resource: &TestResource<()>, (): &(), (): ()) -> Self::Implementation {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
#[derive(Debug)]
struct TestSymbol {
reached: bool,
}
#[async_trait(?Send)]
impl Symbol for TestSymbol {
async fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(self.reached)
}
async fn execute(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
fn get_setup() -> (
Rc<RefCell<usize>>,
Setup<
TestSymbolRunner,
StoringLogger,
TestResourceLocator,
TestImplementationBuilder,
Resources,
Artifacts,
>,
) {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
(count, Setup::new_with(runner, StoringLogger::new()))
}
#[test]
fn correctly_uses_force() {
run(async {
let (count, setup) = get_setup();
setup.add(TestResource("A", "b")).await.unwrap();
assert_eq!(*count.borrow(), 2);
setup.add(TestResource("A", "b")).await.unwrap();
assert_eq!(*count.borrow(), 2);
let (count, setup) = get_setup();
setup.add(TestResource("A", "B")).await.unwrap();
assert_eq!(*count.borrow(), 0);
});
}
}

View file

@ -1,7 +1,7 @@
use crate::async_utils::sleep; use crate::async_utils::sleep;
use crate::loggers::Logger;
use crate::symbols::Symbol; use crate::symbols::Symbol;
use async_trait::async_trait; use async_trait::async_trait;
use slog::{debug, info, o, trace, Logger};
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::fmt::Debug; use std::fmt::Debug;
@ -9,10 +9,10 @@ use std::time::Duration;
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait SymbolRunner { pub trait SymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>>; ) -> Result<bool, Box<dyn Error>>;
} }
@ -37,18 +37,19 @@ impl InitializingSymbolRunner {
Self Self
} }
async fn exec_symbol<S: Symbol + Debug, L: Logger>( async fn exec_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
logger.info(format!("Executing {:?}", symbol)); info!(logger, "Executing {:?}", symbol);
symbol.execute().await?; symbol.execute().await?;
let target_reached = symbol.target_reached().await?; let target_reached = symbol.target_reached().await?;
logger.trace(format!( trace!(
logger,
"Symbol reports target_reached: {:?} (should be true)", "Symbol reports target_reached: {:?} (should be true)",
target_reached target_reached
)); );
if target_reached { if target_reached {
Ok(()) Ok(())
} else { } else {
@ -59,25 +60,26 @@ impl InitializingSymbolRunner {
#[async_trait(?Send)] #[async_trait(?Send)]
impl SymbolRunner for InitializingSymbolRunner { impl SymbolRunner for InitializingSymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
let executed = if force { let executed = if force {
logger.debug("Forcing symbol execution"); debug!(logger, "Forcing symbol execution");
self.exec_symbol(symbol, logger).await?; self.exec_symbol(symbol, logger).await?;
true true
} else { } else {
let target_reached = symbol.target_reached().await?; let target_reached = symbol.target_reached().await?;
if target_reached { if target_reached {
logger.debug(format!("{:?} already reached", symbol)); debug!(logger, "{:?} already reached", symbol);
} else { } else {
logger.trace(format!( trace!(
logger,
"Symbol reports target_reached: {:?}", "Symbol reports target_reached: {:?}",
target_reached target_reached
)); );
self.exec_symbol(symbol, logger).await?; self.exec_symbol(symbol, logger).await?;
} }
!target_reached !target_reached
@ -106,10 +108,10 @@ where
clippy::cast_possible_truncation, clippy::cast_possible_truncation,
clippy::cast_precision_loss clippy::cast_precision_loss
)] )]
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
sleep(Duration::from_millis( sleep(Duration::from_millis(
@ -139,23 +141,23 @@ impl DrySymbolRunner {
#[async_trait(?Send)] #[async_trait(?Send)]
impl SymbolRunner for DrySymbolRunner { impl SymbolRunner for DrySymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
let would_execute = if force { let would_execute = if force {
logger.info(format!("Would force-execute {:?}", symbol)); info!(logger, "Would force-execute");
true true
} else { } else {
let target_reached = symbol.target_reached().await?; let target_reached = symbol.target_reached().await?;
logger.debug(format!( debug!(
"Symbol reports target_reached: {:?}", logger,
target_reached "Symbol reports target_reached: {:?}", target_reached
)); );
if !target_reached { if !target_reached {
logger.info(format!("Would execute {:?}", symbol)); info!(logger, "Would execute");
} }
!target_reached !target_reached
}; };
@ -178,18 +180,19 @@ impl<R> SymbolRunner for ReportingSymbolRunner<R>
where where
R: SymbolRunner, R: SymbolRunner,
{ {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
symbol: &S, symbol: &S,
logger: &L, logger: &Logger,
force: bool, force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
logger.debug(format!("Running symbol {:?}", symbol)); let logger = logger.new(o!("symbol" => format!("{symbol:?}")));
let res = self.0.run_symbol(symbol, logger, force).await; debug!(logger, "Running ...");
let res = self.0.run_symbol(symbol, &logger, force).await;
if let Err(ref e) = res { if let Err(ref e) = res {
logger.info(format!("Failed on {:?} with {}, aborting.", symbol, e)); info!(logger, "failed with {}", e);
} else { } else {
logger.debug(format!("Successfully finished {:?}", symbol)); debug!(logger, "Successfully finished");
} }
res res
} }
@ -200,9 +203,9 @@ mod test {
use super::{DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner, SymbolRunner}; use super::{DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner, SymbolRunner};
use crate::async_utils::sleep; use crate::async_utils::sleep;
use crate::async_utils::{run, try_join}; use crate::async_utils::{run, try_join};
use crate::loggers::StoringLogger;
use crate::symbols::Symbol; use crate::symbols::Symbol;
use async_trait::async_trait; use async_trait::async_trait;
use slog::{o, Discard, Logger};
use std::cell::RefCell; use std::cell::RefCell;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
@ -263,7 +266,7 @@ mod test {
} }
fn run_symbol<S: Symbol + Debug>(s: S) -> Result<bool, Box<dyn Error>> { fn run_symbol<S: Symbol + Debug>(s: S) -> Result<bool, Box<dyn Error>> {
run(InitializingSymbolRunner::new().run_symbol(&s, &StoringLogger::new(), false)) run(InitializingSymbolRunner::new().run_symbol(&s, &Logger::root(Discard, o!()), false))
} }
#[test] #[test]
@ -316,8 +319,8 @@ mod test {
let s1 = SleeperSymbol; let s1 = SleeperSymbol;
let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]);
let l1 = StoringLogger::new(); let l1 = Logger::root(Discard, o!());
let l2 = StoringLogger::new(); let l2 = Logger::root(Discard, o!());
let runner1 = InitializingSymbolRunner::new(); let runner1 = InitializingSymbolRunner::new();
let result = try_join!( let result = try_join!(
runner1.run_symbol(&s1, &l1, false), runner1.run_symbol(&s1, &l1, false),
@ -327,8 +330,6 @@ mod test {
assert_eq!(result, (false, true)); assert_eq!(result, (false, true));
let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]);
let l1 = StoringLogger::new();
let l2 = StoringLogger::new();
let runner2 = DrySymbolRunner::new(); let runner2 = DrySymbolRunner::new();
let result = try_join!( let result = try_join!(
runner2.run_symbol(&s1, &l1, false), runner2.run_symbol(&s1, &l1, false),
@ -338,8 +339,6 @@ mod test {
assert_eq!(result, (false, true)); assert_eq!(result, (false, true));
let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]);
let l1 = StoringLogger::new();
let l2 = StoringLogger::new();
let runner3 = ReportingSymbolRunner::new(runner1); let runner3 = ReportingSymbolRunner::new(runner1);
let result = try_join!( let result = try_join!(
runner3.run_symbol(&s1, &l1, false), runner3.run_symbol(&s1, &l1, false),

View file

@ -1,12 +1,131 @@
use crate::loggers::StoringLogger; use crate::async_utils::join;
use crate::resources::Resource; use crate::resources::Resource;
use crate::to_artifact::ToArtifact; use crate::to_artifact::ToArtifact;
use async_trait::async_trait;
use slog::{Drain, Filter, Logger, OwnedKVList, Record};
use slog_async::AsyncRecord;
use std::cell::RefCell;
use std::error::Error; use std::error::Error;
use std::fmt::Debug; use std::fmt::Debug;
use std::io::{self, Write};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
pub trait AddableResource: 'static + Resource + Debug {} pub trait AddableResource: 'static + Resource + Debug {}
impl<R> AddableResource for R where R: 'static + Resource + Debug {} impl<R> AddableResource for R where R: 'static + Resource + Debug {}
pub type AddResult<R> = Result<(<R as ToArtifact>::Artifact, bool), Box<dyn Error>>; pub type AddResult<R> = Result<(<R as ToArtifact>::Artifact, bool), Box<dyn Error>>;
pub type InternalAddResult<R> =
Result<(StoringLogger, <R as ToArtifact>::Artifact, bool), (StoringLogger, Box<dyn Error>)>; #[async_trait(?Send)]
pub trait Add<R: AddableResource> {
async fn add(&self, logger: &Rc<Logger>, resource: Rc<R>, force_run: bool) -> AddResult<R>;
}
#[async_trait(?Send)]
pub trait AddGeneric<X: ToArtifact> {
async fn add_generic(&self, logger: &Rc<Logger>, x: X, force_run: bool) -> AddResult<X>;
}
macro_rules! add_generic {
( $($name:ident)* ) => (
#[async_trait(?Send)]
#[allow(non_snake_case)]
impl<_S, $($name: AddableResource,)*>
AddGeneric<($($name,)*)> for _S
where
$(
_S: AddGeneric<$name>
),*
{
#[allow(unused, clippy::shadow_unrelated)]
async fn add_generic(&self, logger: &Rc<Logger>, ($($name,)*): ($($name,)*), force_run: bool) -> AddResult<($($name,)*)>
{
let ($($name,)*) = join!($(self.add_generic(logger, $name, force_run),)*);
let mut did_run_any = false;
$(
let (artifact, did_run) = $name?;
did_run_any = did_run_any || did_run;
let $name = artifact;
)*
Ok((($($name,)*), did_run_any))
}
}
);
}
for_each_tuple!(add_generic);
#[async_trait(?Send)]
impl<R: AddableResource + Debug, S: Add<R>> AddGeneric<R> for S {
async fn add_generic(&self, logger: &Rc<Logger>, r: R, force_run: bool) -> AddResult<R> {
self.add(logger, Rc::new(r), force_run).await
}
}
// From https://users.rust-lang.org/t/how-to-send-a-writer-into-a-thread/4965/10
#[derive(Clone)]
struct Output<W>(Rc<RefCell<W>>);
impl<W: Write> Output<W> {
pub fn new(w: W) -> Self {
Self(Rc::new(RefCell::new(w)))
}
}
impl<W: Write> Write for Output<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.borrow_mut().flush()
}
}
#[derive(Clone, Default)]
pub struct Recorder(Arc<Mutex<Vec<AsyncRecord>>>);
impl Drain for Recorder {
type Ok = ();
type Err = slog::Never;
fn log(&self, record: &Record<'_>, logger_values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
self
.0
.lock()
.unwrap()
.push(AsyncRecord::from(record, logger_values));
Ok(())
}
}
impl Recorder {
pub fn into_string(self, filter_level: slog::Level) -> String {
let output = Output::new(vec![]);
{
let decorator = slog_term::PlainDecorator::new(output.clone());
let drain = Filter::new(
slog_term::CompactFormat::new(decorator).build(),
move |record| record.level().is_at_least(filter_level),
);
let Ok(mutex) = Arc::try_unwrap(self.0) else { panic!("cannot unwrap Arc") }; // AsyncRecord does not implement Debug, so we cannot unwrap
for record in mutex.into_inner().unwrap() {
record.log_to(&drain).unwrap();
}
}
String::from_utf8(Rc::try_unwrap(output.0).unwrap().into_inner())
.expect("Record output should be valid UTF-8")
}
}
#[cfg(test)]
mod test {
use super::Recorder;
use slog::Level;
#[test]
fn records_no_output() {
let recorder = Recorder::default();
assert_eq!(recorder.into_string(Level::Trace), "");
}
}

View file

@ -17,7 +17,7 @@ pub struct Checkout<_C, C, P, S, B> {
phantom: PhantomData<_C>, phantom: PhantomData<_C>,
} }
impl<C, _C, P, S, B> Checkout<_C, C, P, S, B> { impl<_C, C, P, S, B> Checkout<_C, C, P, S, B> {
pub fn new(target: P, source: S, branch: B, command_runner: C) -> Self { pub fn new(target: P, source: S, branch: B, command_runner: C) -> Self {
Self { Self {
target, target,
@ -29,7 +29,7 @@ impl<C, _C, P, S, B> Checkout<_C, C, P, S, B> {
} }
} }
impl<C: CommandRunner, _C: Borrow<C>, P: AsRef<Path>, S, B> Checkout<C, _C, P, S, B> { impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef<Path>, S, B> Checkout<_C, C, P, S, B> {
async fn run_git(&self, args: &[impl AsRef<OsStr>]) -> Result<Vec<u8>, Box<dyn Error>> { async fn run_git(&self, args: &[impl AsRef<OsStr>]) -> Result<Vec<u8>, Box<dyn Error>> {
let mut new_args = Vec::with_capacity(args.len() + 2); let mut new_args = Vec::with_capacity(args.len() + 2);
new_args.extend_from_slice(args!["-C", self.target.as_ref()]); new_args.extend_from_slice(args!["-C", self.target.as_ref()]);
@ -43,8 +43,8 @@ impl<C: CommandRunner, _C: Borrow<C>, P: AsRef<Path>, S, B> Checkout<C, _C, P, S
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl<C: CommandRunner, _C: Borrow<C>, P: AsRef<Path>, S: AsRef<str>, B: AsRef<str>> Symbol impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef<Path>, S: AsRef<str>, B: AsRef<str>> Symbol
for Checkout<C, _C, P, S, B> for Checkout<_C, C, P, S, B>
{ {
async fn target_reached(&self) -> Result<bool, Box<dyn Error>> { async fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.target.as_ref().exists() { if !self.target.as_ref().exists() {
@ -132,7 +132,7 @@ mod test {
args: RefCell::new(vec![]), args: RefCell::new(vec![]),
}; };
let checkout: Checkout<DummyCommandRunner, _, _, _, _> = let checkout: Checkout<DummyCommandRunner, _, _, _, _> =
Checkout::new("target", "source", "branch", &c); Checkout::new(".", "source", "branch", &c);
let start = Instant::now(); let start = Instant::now();
assert!(run(checkout.target_reached()).unwrap()); assert!(run(checkout.target_reached()).unwrap());
let end = Instant::now(); let end = Instant::now();
@ -142,12 +142,11 @@ mod test {
assert_eq!( assert_eq!(
first_two_args, first_two_args,
[ [
["-C", "target", "fetch", "source", "branch"], ["-C", ".", "fetch", "source", "branch"],
["-C", "target", "rev-list", "-1", "HEAD"], ["-C", ".", "rev-list", "-1", "HEAD"],
] ]
); );
drop(first_two_args); assert_eq!(args[2], ["-C", ".", "rev-list", "-1", "FETCH_HEAD"]);
assert_eq!(args[2], ["-C", "target", "rev-list", "-1", "FETCH_HEAD"]);
assert!((end - start).as_millis() >= 100); assert!((end - start).as_millis() >= 100);
assert!((end - start).as_millis() < 150); assert!((end - start).as_millis() < 150);

View file

@ -12,7 +12,7 @@ pub struct Database<'a, D, S, C> {
} }
impl<'a, D, S, C: CommandRunner> Database<'a, D, S, C> { impl<'a, D, S, C: CommandRunner> Database<'a, D, S, C> {
pub fn new(db_name: D, seed_file: S, command_runner: &'a C) -> Self { pub const fn new(db_name: D, seed_file: S, command_runner: &'a C) -> Self {
Self { Self {
db_name, db_name,
seed_file, seed_file,

View file

@ -13,7 +13,7 @@ pub struct Dump<'a, N, C, S> {
} }
impl<'a, N, C: CommandRunner, S> Dump<'a, N, C, S> { impl<'a, N, C: CommandRunner, S> Dump<'a, N, C, S> {
pub fn new(db_name: N, storage: S, command_runner: &'a C) -> Self { pub const fn new(db_name: N, storage: S, command_runner: &'a C) -> Self {
Self { Self {
db_name, db_name,
storage, storage,

View file

@ -10,7 +10,7 @@ pub struct User<'a, U, C> {
} }
impl<'a, U: AsRef<str>, C: CommandRunner> User<'a, U, C> { impl<'a, U: AsRef<str>, C: CommandRunner> User<'a, U, C> {
pub fn new(user_name: U, command_runner: &'a C) -> Self { pub const fn new(user_name: U, command_runner: &'a C) -> Self {
Self { Self {
user_name, user_name,
command_runner, command_runner,

View file

@ -12,7 +12,7 @@ pub struct Install<'a, T: AsRef<Path>, C: CommandRunner> {
} }
impl<'a, T: AsRef<Path>, C: CommandRunner> Install<'a, T, C> { impl<'a, T: AsRef<Path>, C: CommandRunner> Install<'a, T, C> {
pub fn new(target: T, command_runner: &'a C) -> Self { pub const fn new(target: T, command_runner: &'a C) -> Self {
Self { Self {
target, target,
command_runner, command_runner,

View file

@ -12,7 +12,7 @@ pub struct PostgreSQLDatabase<'a, N: AsRef<str>, S: AsRef<str>, C: CommandRunner
} }
impl<'a, N: AsRef<str>, S: AsRef<str>, C: CommandRunner> PostgreSQLDatabase<'a, N, S, C> { impl<'a, N: AsRef<str>, S: AsRef<str>, C: CommandRunner> PostgreSQLDatabase<'a, N, S, C> {
pub fn new(name: N, seed_file: S, command_runner: &'a C) -> Self { pub const fn new(name: N, seed_file: S, command_runner: &'a C) -> Self {
PostgreSQLDatabase { PostgreSQLDatabase {
name, name,
seed_file, seed_file,
@ -25,7 +25,7 @@ impl<'a, N: AsRef<str>, S: AsRef<str>, C: CommandRunner> PostgreSQLDatabase<'a,
.command_runner .command_runner
.get_output( .get_output(
"su", "su",
args!["-", "postgres", "-c", format!("psql -t -c \"{}\"", sql)], args!["-", "postgres", "-c", format!("psql -t -c \"{sql}\"")],
) )
.await?; .await?;
Ok(String::from_utf8(b)?) Ok(String::from_utf8(b)?)

View file

@ -10,7 +10,7 @@ use std::marker::PhantomData;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum StorageDirection { pub enum StorageDirection {
Load, Load,
Store, Store,

View file

@ -8,28 +8,23 @@ use std::path::Path;
use std::time::Duration; use std::time::Duration;
#[derive(Debug)] #[derive(Debug)]
pub struct UserService<'a, S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> { pub struct UserService<'a, S: AsRef<Path>, U: AsRef<str>> {
socket_path: S, socket_path: S,
service_name: &'a str, service_name: &'a str,
command_runner: SetuidCommandRunner<'a, U, R>, command_runner: SetuidCommandRunner<U>,
} }
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> UserService<'static, S, U, R> { impl<S: AsRef<Path>, U: AsRef<str>> UserService<'static, S, U> {
pub fn new( pub const fn new(socket_path: S, user_name: U, service_name: &'static str) -> Self {
socket_path: S,
user_name: U,
service_name: &'static str,
command_runner: &'static R,
) -> Self {
Self { Self {
socket_path, socket_path,
service_name, service_name,
command_runner: SetuidCommandRunner::new(user_name, command_runner), command_runner: SetuidCommandRunner::new(user_name),
} }
} }
} }
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> UserService<'_, S, U, R> { impl<S: AsRef<Path>, U: AsRef<str>> UserService<'_, S, U> {
async fn systemctl_wait_for_dbus(&self, args: &[&OsStr]) -> Result<String, Box<dyn Error>> { async fn systemctl_wait_for_dbus(&self, args: &[&OsStr]) -> Result<String, Box<dyn Error>> {
let mut tries = 5; let mut tries = 5;
loop { loop {
@ -85,7 +80,7 @@ impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> UserService<'_, S, U, R> {
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> Symbol for UserService<'_, S, U, R> { impl<S: AsRef<Path>, U: AsRef<str>> Symbol for UserService<'_, S, U> {
async fn target_reached(&self) -> Result<bool, Box<dyn Error>> { async fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
self.check_if_service().await self.check_if_service().await
} }

View file

@ -8,7 +8,7 @@ use std::path::Path;
pub struct Key<C, P> { pub struct Key<C, P> {
file_path: P, file_path: P,
command_runner: C, command_runner: C,
bytes: u32, bits: u32,
} }
impl<C, P> Key<C, P> { impl<C, P> Key<C, P> {
@ -16,7 +16,7 @@ impl<C, P> Key<C, P> {
Self { Self {
file_path, file_path,
command_runner, command_runner,
bytes: 4096, bits: 4096,
} }
} }
} }
@ -42,8 +42,10 @@ impl<C: CommandRunner, P: AsRef<Path>> Symbol for Key<C, P> {
], ],
) )
.await?; .await?;
// FIXME check bytes Ok(
Ok(stdout.ends_with(b"RSA key ok\n")) stdout.ends_with(b"RSA key ok\n")
&& stdout.starts_with(format!("RSA Private-Key: ({} bit, 2 primes)\n", self.bits).as_ref()),
)
} }
async fn execute(&self) -> Result<(), Box<dyn Error>> { async fn execute(&self) -> Result<(), Box<dyn Error>> {
@ -55,7 +57,7 @@ impl<C: CommandRunner, P: AsRef<Path>> Symbol for Key<C, P> {
"genrsa", "genrsa",
"-out", "-out",
self.file_path.as_ref(), self.file_path.as_ref(),
self.bytes.to_string(), self.bits.to_string(),
], ],
) )
.await .await

View file

@ -16,7 +16,7 @@ pub struct Plugin<'a, P, N, R> {
} }
impl<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Plugin<'a, P, N, R> { impl<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Plugin<'a, P, N, R> {
pub fn new(base: P, name: N, command_runner: &'a R) -> Self { pub const fn new(base: P, name: N, command_runner: &'a R) -> Self {
Self { Self {
base, base,
name, name,

View file

@ -19,7 +19,7 @@ pub struct Translation<'a, C, D, R> {
} }
impl<'a, D, C: AsRef<str>, R: CommandRunner> Translation<'a, C, D, R> { impl<'a, D, C: AsRef<str>, R: CommandRunner> Translation<'a, C, D, R> {
pub fn new(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self { pub const fn new(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self {
Self { Self {
path, path,
version, version,
@ -33,7 +33,7 @@ impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Translation<'_, C, D, R> {
fn get_pairs(&self) -> Vec<(String, PathBuf)> { fn get_pairs(&self) -> Vec<(String, PathBuf)> {
let version_x = self let version_x = self
.version .version
.trim_end_matches(|c: char| c.is_digit(10)) .trim_end_matches(|c: char| c.is_ascii_digit())
.to_owned() .to_owned()
+ "x"; + "x";
let locale = self.locale.as_ref(); let locale = self.locale.as_ref();
@ -51,7 +51,7 @@ impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Translation<'_, C, D, R> {
] { ] {
for format in &["po", "mo"] { for format in &["po", "mo"] {
res.push(( res.push((
format!("https://translate.wordpress.org/projects/wp/{}/{}{}/default/export-translations?format={}", version_x, in_slug, path_locale, format), format!("https://translate.wordpress.org/projects/wp/{version_x}/{in_slug}{path_locale}/default/export-translations?format={format}"),
[self.path.as_ref(), format!("{}{}.{}", out_slug, self.locale.as_ref(), format).as_ref()].iter().collect() [self.path.as_ref(), format!("{}{}.{}", out_slug, self.locale.as_ref(), format).as_ref()].iter().collect()
)); ));
} }

View file

@ -11,3 +11,55 @@ pub fn acme_challenges_snippet<P: AsRef<Path>>(path: P) -> String {
path.as_ref().to_str().unwrap() path.as_ref().to_str().unwrap()
) )
} }
#[cfg(test)]
mod test {
use super::{server_config, uwsgi_snippet};
#[test]
fn test_uwsgi() {
assert_eq!(
server_config(
"testdomain",
"/certpath",
"/keypath",
uwsgi_snippet("/uwsgi.sock", "/static"),
"/challenges_snippet.conf"
),
"server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name testdomain;
include \"/challenges_snippet.conf\";
ssl_certificate /certpath;
ssl_certificate_key /keypath;
add_header Strict-Transport-Security \"max-age=31536000\";
root /static;
location / {
try_files $uri @proxy;
}
location @proxy {
include uwsgi_params;
uwsgi_pass unix:/uwsgi.sock;
}
}
# Redirect all HTTP links to the matching HTTPS page
server {
listen 80;
listen [::]:80;
server_name testdomain;
include \"/challenges_snippet.conf\";
location / {
return 301 https://$host$request_uri;
}
}
"
);
}
}

View file

@ -78,21 +78,26 @@ pub fn php_snippet<SOCKET: AsRef<Path>, STATIC: AsRef<Path>>(
pub fn redir_snippet(target: &str) -> String { pub fn redir_snippet(target: &str) -> String {
format!( format!(
"location / {{ "location / {{
return 301 $scheme://{}$request_uri; return 301 $scheme://{target}$request_uri;
}}", }}"
target
) )
} }
pub trait SocketSpec { pub trait SocketSpec {
fn to_nginx(&self) -> String; fn to_proxy_pass(&self) -> String;
fn to_uwsgi_pass(&self) -> String;
} }
impl<T: AsRef<Path>> SocketSpec for T { impl<T: AsRef<Path>> SocketSpec for T {
#[must_use] #[must_use]
fn to_nginx(&self) -> String { fn to_proxy_pass(&self) -> String {
format!("unix:{}:", self.as_ref().to_str().unwrap()) format!("unix:{}:", self.as_ref().to_str().unwrap())
} }
#[must_use]
fn to_uwsgi_pass(&self) -> String {
format!("unix:{}", self.as_ref().to_str().unwrap())
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -107,7 +112,12 @@ impl LocalTcpSocket {
impl SocketSpec for LocalTcpSocket { impl SocketSpec for LocalTcpSocket {
#[must_use] #[must_use]
fn to_nginx(&self) -> String { fn to_proxy_pass(&self) -> String {
format!("localhost:{}", self.0)
}
#[must_use]
fn to_uwsgi_pass(&self) -> String {
format!("localhost:{}", self.0) format!("localhost:{}", self.0)
} }
} }
@ -129,12 +139,33 @@ pub fn proxy_snippet<S: SocketSpec, STATIC: AsRef<Path>>(
proxy_redirect off; proxy_redirect off;
}}", }}",
static_path.as_ref().to_str().unwrap(), static_path.as_ref().to_str().unwrap(),
socket_path.to_nginx() socket_path.to_proxy_pass()
) )
} }
#[must_use] #[must_use]
pub fn static_snippet<S: AsRef<Path>>(static_path: S) -> String { pub fn uwsgi_snippet<S: SocketSpec, STATIC: AsRef<Path>>(
socket_path: S,
static_path: STATIC,
) -> String {
format!(
"root {};
location / {{
try_files $uri @proxy;
}}
location @proxy {{
include uwsgi_params;
uwsgi_pass {};
}}",
static_path.as_ref().to_str().unwrap(),
socket_path.to_uwsgi_pass()
)
}
#[must_use]
pub fn static_snippet(static_path: impl AsRef<Path>) -> String {
format!( format!(
"root {}; "root {};
try_files $uri $uri/ $uri.html =404; try_files $uri $uri/ $uri.html =404;
@ -144,7 +175,7 @@ pub fn static_snippet<S: AsRef<Path>>(static_path: S) -> String {
} }
#[must_use] #[must_use]
pub fn dokuwiki_snippet() -> String { pub const fn dokuwiki_snippet() -> &'static str {
" "
location ~ /(data/|conf/|bin/|inc/|install.php) { deny all; } location ~ /(data/|conf/|bin/|inc/|install.php) { deny all; }
@ -156,11 +187,11 @@ pub fn dokuwiki_snippet() -> String {
rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last; rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last; rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
rewrite ^/(.*) /doku.php?id=$1&$args last; rewrite ^/(.*) /doku.php?id=$1&$args last;
}".into() }"
} }
#[must_use] #[must_use]
pub fn nextcloud_snippet() -> String { pub const fn nextcloud_snippet() -> &'static str {
" "
client_max_body_size 500M; client_max_body_size 500M;
@ -210,5 +241,20 @@ pub fn nextcloud_snippet() -> String {
access_log off; access_log off;
} }
" "
.into() }
#[cfg(test)]
mod test {
use super::default_server;
#[test]
fn test_default_server() {
assert_eq!(
default_server("filename"),
r#"server {
listen 80 default_server;
listen [::]:80 default_server;
include "filename";
}"#
);
}
} }

View file

@ -56,3 +56,25 @@ env[PATH] = /usr/local/bin:/usr/bin:/bin
config config
) )
} }
#[cfg(test)]
mod test {
use super::fpm_pool_config;
#[test]
fn test_fpm_pool_config() {
assert_eq!(
fpm_pool_config("user", "socket", &5.into()),
r"[user]
user = user
group = www-data
listen = socket
listen.owner = www-data
pm = ondemand
catch_workers_output = yes
env[PATH] = /usr/local/bin:/usr/bin:/bin
pm.max_children = 5
"
);
}
}

View file

@ -17,8 +17,6 @@ macro_rules! to_artifact {
for_each_tuple!(to_artifact); for_each_tuple!(to_artifact);
impl<T: Resource> ToArtifact for Option<T> { impl<T: Resource> ToArtifact for Option<T> {
// FIXME: https://github.com/rust-lang/rust-clippy/issues/2843
#![allow(clippy::use_self)]
type Artifact = Option<T::Artifact>; type Artifact = Option<T::Artifact>;
} }

View file

@ -98,7 +98,7 @@ fn may_not_read_file() {
#[test] #[test]
fn may_not_create_file() { fn may_not_create_file() {
let symbol = get_symbol(&Path::new("/proc/somefile")); let symbol = get_symbol(Path::new("/proc/somefile"));
run(async { run(async {
// Could also return an error // Could also return an error
@ -109,7 +109,7 @@ fn may_not_create_file() {
#[test] #[test]
fn directory_missing() { fn directory_missing() {
let symbol = get_symbol(&Path::new("/nonexisting")); let symbol = get_symbol(Path::new("/nonexisting"));
run(async { run(async {
// Could also return an error // Could also return an error

View file

@ -1,53 +1,87 @@
use async_trait::async_trait; use async_trait::async_trait;
use regex::Regex;
use schematics::async_utils::{run, sleep}; use schematics::async_utils::{run, sleep};
use schematics::loggers::{Logger, StoringLogger}; use schematics::loggers::{Entry, StoringLogger};
use schematics::resources::{AcmeUser, Cert, Csr, GitCheckout}; use schematics::resources::{AcmeUser, Cert, Csr, GitCheckout};
use schematics::symbols::Symbol; use schematics::symbols::Symbol;
use schematics::Setup; use schematics::Setup;
use schematics::SymbolRunner; use schematics::SymbolRunner;
use std::cell::RefCell; use slog::{info, Logger as SlogLogger};
use std::error::Error; use std::error::Error;
use std::fmt::Debug; use std::fmt::Debug;
use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct TestSymbolRunner { struct TestSymbolRunner {
count: Rc<RefCell<usize>>, run: bool,
}
impl TestSymbolRunner {
fn new(run: bool) -> Self {
Self { run }
}
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl SymbolRunner for TestSymbolRunner { impl SymbolRunner for TestSymbolRunner {
async fn run_symbol<S: Symbol + Debug, L: Logger>( async fn run_symbol<S: Symbol + Debug>(
&self, &self,
_symbol: &S, _symbol: &S,
_logger: &L, logger: &SlogLogger,
_force: bool, _force: bool,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
*self.count.borrow_mut() += 1; info!(logger, "run_symbol");
sleep(Duration::from_millis(0)).await; sleep(Duration::from_millis(0)).await;
Ok(false) Ok(self.run)
} }
} }
fn test(
count: usize,
body: fn(setup: Setup<TestSymbolRunner, StoringLogger>) -> (),
) -> Vec<Entry<String>> {
let runner = TestSymbolRunner::new(false);
let logger = StoringLogger::new();
{
let setup = Setup::new(runner, logger.clone());
body(setup);
}
assert_eq!(logger.release(), vec![Entry(3, ".".repeat(count))]);
let runner = TestSymbolRunner::new(true);
let logger = StoringLogger::new();
{
let setup = Setup::new(runner, logger.clone());
body(setup);
}
logger.release()
}
#[test] #[test]
fn can_create_an_acme_user() { fn can_create_an_acme_user() {
let count = Rc::new(RefCell::new(0)); let mut result = test(1, |setup| {
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let setup = Setup::new(runner, StoringLogger::new());
assert_eq!((run(setup.add(AcmeUser)).unwrap().0).0, "acme"); assert_eq!((run(setup.add(AcmeUser)).unwrap().0).0, "acme");
});
let entry = result
.pop()
.expect("log is empty but should contain one entry");
assert_eq!(result.len(), 0, "log has more than one entry");
assert_eq!(entry.0, 3, "log entry has wrong level");
let re =
Regex::new(r"^resource: AcmeUser\n \w+ \d{1,2} \d{2}:\d{2}:\d{2}.\d{3} INFO run_symbol\n$")
.unwrap();
assert!(
re.is_match(&entry.1),
"log output {} does not match {}",
entry.1,
re
);
} }
#[test] #[test]
fn runs_only_once() { fn runs_only_once() {
let mut result = test(2, |setup| {
run(async { run(async {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let setup = Setup::new(runner, StoringLogger::new());
assert_eq!( assert_eq!(
(setup.add(Csr("somehost")).await.unwrap().0) (setup.add(Csr("somehost")).await.unwrap().0)
.as_ref() .as_ref()
@ -62,17 +96,24 @@ fn runs_only_once() {
.unwrap(), .unwrap(),
"/etc/ssl/local_certs/somehost.csr", "/etc/ssl/local_certs/somehost.csr",
); );
assert_eq!(*count.borrow(), 2 + 5); // Key and CSR + 5 dirs
}); });
});
let entry = result
.pop()
.expect("log is empty but should contain entries");
assert_eq!(entry.0, 3, "log entry has wrong level");
assert_eq!(entry.1, ".", "log entry has wrong content");
let entry = result
.pop()
.expect("log is empty but should contain entries");
assert_eq!(entry.0, 3, "log entry has wrong level");
assert_eq!(entry.1.matches("run_symbol").count(), 7); // Key and CSR + 5 dirs
assert_eq!(result.len(), 0, "log has more than one entry");
} }
#[test] #[test]
fn can_create_an_acme_cert() { fn can_create_an_acme_cert() {
let count = Rc::new(RefCell::new(0)); let mut result = test(1, |setup| {
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let setup = Setup::new(runner, StoringLogger::new());
assert_eq!( assert_eq!(
(run(setup.add(Cert("somehost"))).unwrap().0) (run(setup.add(Cert("somehost"))).unwrap().0)
.as_ref() .as_ref()
@ -80,21 +121,29 @@ fn can_create_an_acme_cert() {
.unwrap(), .unwrap(),
"/etc/ssl/local_certs/somehost.crt", "/etc/ssl/local_certs/somehost.crt",
); );
assert_eq!(*count.borrow(), 19); });
let entry = result
.pop()
.expect("log is empty but should contain one entry");
assert_eq!(entry.0, 3, "log entry has wrong level");
assert_eq!(entry.1.matches("run_symbol").count(), 19);
assert_eq!(result.len(), 0, "log has more than one entry");
} }
#[test] #[test]
fn can_create_a_git_checkout() { fn can_create_a_git_checkout() {
let count = Rc::new(RefCell::new(0)); let mut result = test(1, |setup| {
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let setup = Setup::new(runner, StoringLogger::new());
run(setup.add(GitCheckout( run(setup.add(GitCheckout(
"/tmp/somepath".into(), "/tmp/somepath".into(),
"/tmp/some_src_repo", "/tmp/some_src_repo",
"master", "master",
))) )))
.unwrap(); .unwrap();
assert_eq!(*count.borrow(), 3); });
let entry = result
.pop()
.expect("log is empty but should contain one entry");
assert_eq!(entry.0, 3, "log entry has wrong level");
assert_eq!(entry.1.matches("run_symbol").count(), 3);
assert_eq!(result.len(), 0, "log has more than one entry");
} }