diff --git a/datetime/Cargo.toml b/datetime/Cargo.toml index 18c0fd91..4174253d 100644 --- a/datetime/Cargo.toml +++ b/datetime/Cargo.toml @@ -25,3 +25,6 @@ path = "./date.rs" name = "sleep" path = "./sleep.rs" +[[bin]] +name = "time" +path = "./time.rs" diff --git a/datetime/tests/datetime-tests.rs b/datetime/tests/datetime-tests.rs new file mode 100644 index 00000000..64690c99 --- /dev/null +++ b/datetime/tests/datetime-tests.rs @@ -0,0 +1,10 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +mod time; \ No newline at end of file diff --git a/datetime/tests/time/mod.rs b/datetime/tests/time/mod.rs new file mode 100644 index 00000000..e58a6b5e --- /dev/null +++ b/datetime/tests/time/mod.rs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::{io::Write, process::{Command, Output, Stdio}}; + +use plib::TestPlan; + +fn run_test_base(cmd: &str, args: &Vec, stdin_data: &[u8]) -> Output { + let relpath = if cfg!(debug_assertions) { + format!("target/debug/{}", cmd) + } else { + format!("target/release/{}", cmd) + }; + let test_bin_path = std::env::current_dir() + .unwrap() + .parent() + .unwrap() // Move up to the workspace root from the current package directory + .join(relpath); // Adjust the path to the binary + + let mut command = Command::new(test_bin_path); + let mut child = command + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn head"); + + let stdin = child.stdin.as_mut().expect("failed to get stdin"); + stdin + .write_all(stdin_data) + .expect("failed to write to stdin"); + + let output = child.wait_with_output().expect("failed to wait for child"); + output +} + +fn get_output(plan: TestPlan) -> Output { + let output = run_test_base(&plan.cmd, &plan.args, plan.stdin_data.as_bytes()); + + output +} + +fn run_test_time( + args: &[&str], + expected_output: &str, + expected_error: &str, + expected_exit_code: i32, +) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + let output = get_output(TestPlan { + cmd: String::from("time"), + args: str_args, + stdin_data: String::new(), + expected_out: String::from(expected_output), + expected_err: String::from(expected_error), + expected_exit_code, + }); + + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(stderr.contains(expected_error)); +} + +#[test] +fn simple_test() { + run_test_time(&["--", "ls", "-l"], "", "User time", 0); +} + +#[test] +fn p_test() { + run_test_time(&["-p", "--", "ls", "-l"], "", "user", 0); +} + +#[test] +fn parse_error_test() { + run_test_time(&[], "", "not provided", 0); +} + +#[test] +fn command_error_test() { + run_test_time(&["-s", "ls", "-l"], "", "unexpected argument found", 0); +} \ No newline at end of file diff --git a/datetime/time.rs b/datetime/time.rs new file mode 100644 index 00000000..55b676be --- /dev/null +++ b/datetime/time.rs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::process::{Command, Stdio}; +use std::time::Instant; +use std::io::{self, Write}; + +use clap::Parser; + +use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; +use plib::PROJECT_NAME; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Write timing output to standard error in POSIX format + #[arg(short, long)] + posix: bool, + + /// The utility to be invoked + utility: String, + + /// Arguments for the utility + #[arg(name = "ARGUMENT", trailing_var_arg = true)] + arguments: Vec, +} + +enum TimeError { + ExecCommand(String), + ExecTime, + CommandNotFound(String), +} + +fn time(args: Args) -> Result<(), TimeError> { + let start_time = Instant::now(); + // SAFETY: std::mem::zeroed() is used to create an instance of libc::tms with all fields set to zero. + // This is safe here because libc::tms is a Plain Old Data type, and zero is a valid value for all its fields. + let mut tms_start: libc::tms = unsafe { std::mem::zeroed() }; + // SAFETY: sysconf is a POSIX function that returns the number of clock ticks per second. + // It is safe to call because it does not modify any memory and has no side effects. + let clock_ticks_per_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) as f64 }; + + // SAFETY: times is a POSIX function that fills the provided tms structure with time-accounting information. + // It is safe to call because we have correctly allocated and initialized tms_start, and the function + // only writes to this structure. + unsafe { libc::times(&mut tms_start) }; + + let mut child = Command::new(&args.utility) + .args(args.arguments) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|e| { + match e.kind() { + io::ErrorKind::NotFound => TimeError::CommandNotFound(args.utility), + _ => TimeError::ExecCommand(args.utility), + } + })?; + + let _ = child.wait().map_err(|_| TimeError::ExecTime)?; + + let elapsed = start_time.elapsed(); + let tms_end: libc::tms = unsafe { std::mem::zeroed() }; + + let user_time = (tms_start.tms_utime - tms_end.tms_utime) as f64 / clock_ticks_per_second; + let system_time = (tms_start.tms_stime - tms_end.tms_stime) as f64 / clock_ticks_per_second; + + if args.posix { + writeln!( + io::stderr(), + "real {:.6}\nuser {:.6}\nsys {:.6}", + elapsed.as_secs_f64(), + user_time, + system_time + ).map_err(|_| TimeError::ExecTime)?; + } else { + writeln!( + io::stderr(), + "Elapsed time: {:.6} seconds\nUser time: {:.6} seconds\nSystem time: {:.6} seconds", + elapsed.as_secs_f64(), + user_time, + system_time + ).map_err(|_| TimeError::ExecTime)?; + } + + Ok(()) +} + +enum Status { + Ok, + TimeError, + UtilError, + UtilNotFound, +} + +impl Status { + fn exit(self) -> ! { + let res = match self { + Status::Ok => 0, + Status::TimeError => 1, + Status::UtilError => 126, + Status::UtilNotFound => 127, + }; + + std::process::exit(res) + } +} + + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + setlocale(LocaleCategory::LcAll, ""); + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + if let Err(err) = time(args) { + match err { + TimeError::CommandNotFound(err) => { + eprintln!("Command not found: {}", err); + Status::UtilNotFound.exit() + }, + TimeError::ExecCommand(err) => { + eprintln!("Error while executing command: {}", err); + Status::UtilError.exit() + } + TimeError::ExecTime => { + eprintln!("Error while executing time utility"); + Status::TimeError.exit() + }, + } + } + + Status::Ok.exit() +}