Add test harness for IO serial tests

This commit is contained in:
Juno Takano 2026-04-05 19:46:31 -03:00
commit 89d1634a63
14 changed files with 250 additions and 23 deletions

View file

@ -1,5 +1,5 @@
watch command="run" args="":
DEBUG=${DEBUG:-} watchexec -c -w src -- cargo {{ command }} {{ args }}
watch *args:
watchexec -c -w src -w Cargo.toml -- just {{ args }}
alias w := watch
@ -12,3 +12,31 @@ verify:
&& return
done
[env("PROPTEST_CASES", "16640")]
test pattern="":
cargo test {{ pattern}} --timings -- --test-threads=1 'serial_tests::'
cargo test {{ pattern}} --timings --bin tori
cargo test {{ pattern}} --timings --doc
cargo test {{ pattern}} --timings --lib -- --skip 'serial_tests::'
mutate:
cargo mutants --iterate \
-E '<impl Debug<' \
-E '<impl From<' \
-E '<impl std::fmt::Display for ' \
-E 'print_help -> bool' \
--output target/mutants
cover:
cargo llvm-cov --no-report
cargo llvm-cov report --html
cargo llvm-cov report \
| tail -1 | awk \
'{ print " [ Regions:", $4, "• Functions:", $7, "• Lines:", $10, "]" }'
cover-open:
cargo llvm-cov report --open

View file

@ -170,3 +170,9 @@ used_underscore_items = "warn"
useless_let_if_seq = "warn"
zero_sized_map_values = "warn"
zombie_processes = "deny"
[profile.test.package.proptest]
opt-level = 3
[profile.test.package.rand_chacha]
opt-level = 3

View file

@ -5,24 +5,28 @@ use std::{
};
use crate::{
log::{self, elog},
dev::{log::elog},
run::Command,
};
pub fn load() -> Result<Configuration, Error> {
log::elog("Loading configuration");
elog("Loading configuration");
let mut candidate = Configuration::default();
let root = get_root();
elog(&format!("Reading 'tori.conf' from: {root:?}"));
let contents = fs::read_to_string(root.join("tori.conf"))?;
elog(&format!("Read configuration: {contents:?}"));
let map: HashMap<String, String> = contents
.lines()
.filter_map(|line| line.split_once('='))
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
.collect();
elog(&format!("Assembled configuration map: {map:#?}"));
if let Some(su_command) = map.get("su_command") {
let wraps = map.get("su_command_wraps").is_some_and(|v| v == "true");
candidate.su_command = parse_su_command(su_command, wraps)?;
@ -36,6 +40,7 @@ pub fn load() -> Result<Configuration, Error> {
}
}
elog(&format!("Assembled configuration candidate: {candidate:?}"));
Ok(candidate)
}
@ -183,9 +188,10 @@ impl Default for SuCommand {
}
}
#[derive(Debug)]
pub struct Error {
message: String,
kind: ErrorKind,
pub message: String,
pub kind: ErrorKind,
}
impl Error {
@ -221,6 +227,7 @@ impl From<std::io::Error> for Error {
}
}
#[derive(Debug)]
pub enum ErrorKind {
CommandNotInPath,
VarError,
@ -242,3 +249,58 @@ impl std::fmt::Display for ErrorKind {
write!(f, "{s}")
}
}
#[cfg(test)]
#[expect(clippy::panic_in_result_fn, //clippy::unwrap_in_result
)]
mod serial_tests {
use std::{env, fs, os::unix::fs::PermissionsExt as _, io::{Write as _}};
use super::*;
use crate::{dev::test::{Directories, Error}};
#[test]
fn failed_config_read() -> Result<(), Error> {
let dirs = Directories::setup("failed_config_read")?;
fs::write(&dirs.conf, [1, 0, 1])?;
let mut permissions = fs::metadata(&dirs.conf)?.permissions();
permissions.set_mode(0o200);
fs::set_permissions(&dirs.conf, permissions)?;
let new_permissions = fs::metadata(&dirs.conf)?.permissions();
assert_eq!(new_permissions.mode() & 0o777, 0o200);
let error = load().unwrap_err();
assert!(matches!(&error.kind, ErrorKind::IO));
Ok(())
}
#[test]
fn prefer_system() -> Result<(), Error> {
let dirs = Directories::setup("prefer_system")?;
let mut conf = fs::File::create_new(&dirs.conf)?;
println!("conf: {conf:#?}");
println!("XDG_CONFIG_DIR: {:#?}", env::var("XDG_CONFIG_DIR"));
let conf_root_contents = dirs.conf_root.read_dir();
println!("conf_root_contents: {conf_root_contents:#?}");
let write_result = conf.write_all(b"merge_strategy = prefer system\n");
println!("write_result: {write_result:#?}");
conf.sync_all()?;
let mut perms = fs::metadata(&dirs.conf)?.permissions();
println!("perms: {perms:#?}");
perms.set_mode(0o664);
conf.set_permissions(perms)?;
let configuration = load()?;
println!("configuration: {configuration:#?}");
assert!(matches!(configuration.merge_strategy, MergeStrategy::PreferSystem));
Ok(())
}
}

2
src/dev.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod log;
pub mod test;

124
src/dev/test.rs Normal file
View file

@ -0,0 +1,124 @@
use std::{env, fs, io, path::PathBuf};
use crate::{dev::log::elog, conf};
#[derive(Debug)]
pub struct Directories {
pub original: PathBuf,
pub tube: PathBuf,
pub conf: PathBuf,
pub conf_root: PathBuf,
}
impl Directories {
/// Sets up self-cleaning original, temporary and 'templates' directories.
///
/// # Errors
/// May return Error when:
/// - Current directory does not exist or lacking permissions
/// - Several I/O possibilities from directory creation failures
/// - Several I/O possibilities from working directory changing failures
pub fn setup(dir_name: &str) -> Result<Directories, Error> {
let original = env::current_dir()?;
let tube = original.join(format!("target/tubes/{dir_name}"));
let xdg_conf = tube.join(".config");
let conf_root = xdg_conf.join("tori");
let conf = conf_root.join("tori.conf");
drop(fs::remove_dir_all(&tube));
if let Err(error) = fs::create_dir_all(&conf_root) {
return Err(Error::with_io(
"Failed configuration root directory creation",
error,
))
}
if let Err(error) = env::set_current_dir(&tube) {
return Err(Error::with_io("Failed current directory change", error))
}
unsafe { env::set_var("XDG_CONFIG_DIR", &xdg_conf); }
Ok(Directories {
original,
tube,
conf,
conf_root,
})
}
}
impl Drop for Directories {
fn drop(&mut self) {
if let Err(error) = std::env::set_current_dir(&self.original) {
elog(&format!("Couldn't reset to original directory: {error}"));
}
if let Err(error) = std::fs::remove_dir_all(&self.tube) {
elog(&format!("Couldn't cleanup tube directory: {error}"));
}
}
}
#[derive(Debug)]
pub struct Error {
pub message: String,
pub inner: Option<InnerErrors>,
}
#[derive(Debug, Default)]
pub struct InnerErrors {
pub io: Option<io::Error>,
pub conf: Option<conf::Error>,
}
impl Error {
fn with_io(message: &str, inner: io::Error) -> Error {
Error {
message: String::from(message),
inner: Some(InnerErrors { io: Some(inner), conf: None }),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut message = self.message.clone();
if let Some(inner) = &self.inner {
message = format!("{message}\n{inner:#?}");
}
write!(f, "{message}")
}
}
impl From<String> for Error {
fn from(string: String) -> Error {
Error {
message: string,
inner: None,
}
}
}
impl From<&str> for Error {
fn from(str: &str) -> Error { Error::from(String::from(str)) }
}
impl From<io::Error> for Error {
fn from(inner: io::Error) -> Error {
let mut error = Error::from(inner.to_string());
error.inner = Some(InnerErrors { io: Some(inner), ..InnerErrors::default() });
error
}
}
impl From<conf::Error> for Error {
fn from(conf_error: conf::Error) -> Error {
Error {
message: conf_error.message.clone(),
inner: Some(InnerErrors { conf: Some(conf_error), io: None }),
}
}
}

View file

@ -1,7 +1,10 @@
#![feature(slice_partition_dedup)]
#![allow(unused_features)]
pub mod conf;
pub mod state;
pub mod os;
pub mod run;
pub mod log;
pub mod dev;

View file

@ -1,7 +1,7 @@
use tori::{conf, log, run, state};
use tori::{conf, dev::log::elog, run, state};
fn main() -> std::process::ExitCode {
log::elog(&format!("tori {}", env!("CARGO_PKG_VERSION")));
elog(&format!("tori {}", env!("CARGO_PKG_VERSION")));
let configuration = match conf::load() {
Ok(c) => c,
Err(error) => {
@ -9,13 +9,13 @@ fn main() -> std::process::ExitCode {
return 1.into();
}
};
log::elog(&format!("Configuration: {configuration:#?}"));
elog(&format!("Configuration: {configuration:#?}"));
let order = run::teller::parse(std::env::args());
log::elog(&format!("Order: {order:#?}"));
elog(&format!("Order: {order:#?}"));
let state = state::setup(configuration, &[order]);
log::elog(&format!("State: {state:#?}"));
let result = run::expeditor::fulfill(&state);
log::elog(&format!("Filled Order: {result:#?}"));
elog(&format!("State: {state:#?}"));
let result = run::expeditor::expedite(&state);
elog(&format!("Filled Order: {result:#?}"));
if result.is_ok() { 0.into() } else { 1.into() }
}

View file

@ -2,7 +2,7 @@ use std::{collections::HashSet, fs::read_to_string, iter};
use crate::{
conf::Configuration,
log::elog,
dev::log::elog,
os::{
Kind, OperatingSystem,
pkg::{self, Package, PackagerVariant, Packages},
@ -269,9 +269,11 @@ mod tests {
auto_set.insert("sunflower".to_string());
auto_set.insert("turnip".to_string());
let manual = Apt::determine_manual(raw_all, &auto_set);
let mut manual = Apt::determine_manual(raw_all, &auto_set);
let (uniques, dupes) = manual.as_mut_slice().partition_dedup();
assert!(dupes.is_empty());
assert_eq!(
manual,
uniques,
vec![
"avocado".into(),
"carrot".into(),

View file

@ -1,4 +1,4 @@
use crate::{conf::Configuration, log::elog};
use crate::{conf::Configuration, dev::log::elog};
pub mod executor;
pub mod expeditor;

View file

@ -1,7 +1,7 @@
use std::process;
use crate::{
log::elog,
dev::log::elog,
os::pkg::Package,
run::{Command, Transaction, TransactionCommandStatus},
};

View file

@ -4,7 +4,7 @@ use crate::{
state::State,
};
pub fn fulfill(state: &State) -> Result<(), Error> {
pub fn expedite(state: &State) -> Result<(), Error> {
let orders = state.orders();
for order in orders {

View file

@ -1,5 +1,5 @@
use crate::{
log::elog,
dev::log::elog,
run::{Order, Task, TaskKind},
};
use std::{env, path::PathBuf};

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use crate::{
conf::Configuration,
log::elog,
dev::log::elog,
os::OperatingSystem,
run::{Command, Order, executor::read},
};