#![deny(missing_docs)]
#![deny(clippy::panic)]
#![warn(clippy::all, clippy::pedantic)]
#![cfg_attr(test, allow(clippy::disallowed_methods))]
mod config;
mod fstab;
mod unit;
use clap::Parser;
use std::process::ExitCode;
#[derive(Debug, Parser)]
#[command(
name = "trueno-zram-generator",
version,
about = "systemd generator for zram device configuration",
long_about = "systemd generator for zram device configuration.\n\n\
systemd invokes generators as `generator <NORMAL_DIR> <EARLY_DIR> <LATE_DIR>`; \
see systemd.generator(7). Units are written into NORMAL_DIR."
)]
#[allow(clippy::struct_field_names)]
struct Cli {
#[arg(value_name = "NORMAL_DIR")]
normal_dir: String,
#[allow(dead_code)]
#[arg(value_name = "EARLY_DIR")]
early_dir: Option<String>,
#[allow(dead_code)]
#[arg(value_name = "LATE_DIR")]
late_dir: Option<String>,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(&cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("trueno-zram-generator: {e}");
ExitCode::FAILURE
}
}
}
fn run(cli: &Cli) -> Result<(), Box<dyn std::error::Error>> {
let config = config::load_config()?;
unit::generate_units(&cli.normal_dir, &config)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::Cli;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
#[test]
fn test_cli_definition_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn test_systemd_three_positional_directories_in_order() {
let cli = Cli::try_parse_from([
"trueno-zram-generator",
"/run/systemd/generator",
"/run/systemd/generator.early",
"/run/systemd/generator.late",
])
.expect("the systemd generator invocation must parse");
assert_eq!(cli.normal_dir, "/run/systemd/generator");
assert_eq!(
cli.early_dir.as_deref(),
Some("/run/systemd/generator.early")
);
assert_eq!(cli.late_dir.as_deref(), Some("/run/systemd/generator.late"));
}
#[test]
fn test_normal_dir_alone_is_accepted() {
let cli = Cli::try_parse_from(["trueno-zram-generator", "/run/systemd/generator"])
.expect("normal_dir alone must still parse");
assert_eq!(cli.normal_dir, "/run/systemd/generator");
assert!(cli.early_dir.is_none());
assert!(cli.late_dir.is_none());
}
#[test]
fn test_missing_normal_dir_is_error() {
let err =
Cli::try_parse_from(["trueno-zram-generator"]).expect_err("normal_dir is required");
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
assert_ne!(err.exit_code(), 0, "must exit non-zero");
}
#[test]
fn test_unknown_flag_is_error_not_a_directory() {
let err = Cli::try_parse_from([
"trueno-zram-generator",
"--definitely-not-a-real-flag-xyz",
"/run/systemd/generator",
])
.expect_err("an unknown flag must be rejected, never used as a directory");
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
assert_ne!(err.exit_code(), 0, "must exit non-zero");
}
#[test]
fn test_lone_unknown_flag_is_error() {
let err = Cli::try_parse_from(["trueno-zram-generator", "--help-me-please"])
.expect_err("a lone unknown flag must be rejected");
assert_ne!(err.exit_code(), 0, "must exit non-zero");
assert_ne!(
err.kind(),
ErrorKind::DisplayHelp,
"an unknown flag is not a help request"
);
}
#[test]
fn test_too_many_positionals_is_error() {
let err = Cli::try_parse_from([
"trueno-zram-generator",
"/run/systemd/generator",
"/run/systemd/generator.early",
"/run/systemd/generator.late",
"/run/systemd/generator.extra",
])
.expect_err("a fourth directory must be rejected");
assert_ne!(err.exit_code(), 0, "must exit non-zero");
}
#[test]
fn test_help_exits_zero_with_substantial_output() {
let err = Cli::try_parse_from(["trueno-zram-generator", "--help"])
.expect_err("--help short-circuits parsing via an Err carrying the help text");
assert_eq!(err.kind(), ErrorKind::DisplayHelp);
assert_eq!(err.exit_code(), 0, "--help must exit 0");
let rendered = err.render().to_string();
assert!(
rendered.len() > 100,
"help must be substantial, got {} bytes",
rendered.len()
);
for expected in ["NORMAL_DIR", "EARLY_DIR", "LATE_DIR"] {
assert!(
rendered.contains(expected),
"help must document {expected}, got:\n{rendered}"
);
}
}
}