Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions datetime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ path = "./date.rs"
name = "sleep"
path = "./sleep.rs"

[[bin]]
name = "time"
path = "./time.rs"
10 changes: 10 additions & 0 deletions datetime/tests/datetime-tests.rs
Original file line number Diff line number Diff line change
@@ -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;
90 changes: 90 additions & 0 deletions datetime/tests/time/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>, 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<String> = 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);
}
141 changes: 141 additions & 0 deletions datetime/time.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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<dyn std::error::Error>> {
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()
}