Skip to content

Commit b8b382f

Browse files
authored
Merge pull request #182 from Wandalen/time
[time] basic implementation
2 parents 720d8d8 + e6ba13d commit b8b382f

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-0
lines changed

datetime/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ path = "./date.rs"
2525
name = "sleep"
2626
path = "./sleep.rs"
2727

28+
[[bin]]
29+
name = "time"
30+
path = "./time.rs"

datetime/tests/datetime-tests.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
mod time;

datetime/tests/time/mod.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
use std::{io::Write, process::{Command, Output, Stdio}};
11+
12+
use plib::TestPlan;
13+
14+
fn run_test_base(cmd: &str, args: &Vec<String>, stdin_data: &[u8]) -> Output {
15+
let relpath = if cfg!(debug_assertions) {
16+
format!("target/debug/{}", cmd)
17+
} else {
18+
format!("target/release/{}", cmd)
19+
};
20+
let test_bin_path = std::env::current_dir()
21+
.unwrap()
22+
.parent()
23+
.unwrap() // Move up to the workspace root from the current package directory
24+
.join(relpath); // Adjust the path to the binary
25+
26+
let mut command = Command::new(test_bin_path);
27+
let mut child = command
28+
.args(args)
29+
.stdin(Stdio::piped())
30+
.stdout(Stdio::piped())
31+
.stderr(Stdio::piped())
32+
.spawn()
33+
.expect("failed to spawn head");
34+
35+
let stdin = child.stdin.as_mut().expect("failed to get stdin");
36+
stdin
37+
.write_all(stdin_data)
38+
.expect("failed to write to stdin");
39+
40+
let output = child.wait_with_output().expect("failed to wait for child");
41+
output
42+
}
43+
44+
fn get_output(plan: TestPlan) -> Output {
45+
let output = run_test_base(&plan.cmd, &plan.args, plan.stdin_data.as_bytes());
46+
47+
output
48+
}
49+
50+
fn run_test_time(
51+
args: &[&str],
52+
expected_output: &str,
53+
expected_error: &str,
54+
expected_exit_code: i32,
55+
) {
56+
let str_args: Vec<String> = args.iter().map(|s| String::from(*s)).collect();
57+
58+
let output = get_output(TestPlan {
59+
cmd: String::from("time"),
60+
args: str_args,
61+
stdin_data: String::new(),
62+
expected_out: String::from(expected_output),
63+
expected_err: String::from(expected_error),
64+
expected_exit_code,
65+
});
66+
67+
let stderr = String::from_utf8_lossy(&output.stderr);
68+
69+
assert!(stderr.contains(expected_error));
70+
}
71+
72+
#[test]
73+
fn simple_test() {
74+
run_test_time(&["--", "ls", "-l"], "", "User time", 0);
75+
}
76+
77+
#[test]
78+
fn p_test() {
79+
run_test_time(&["-p", "--", "ls", "-l"], "", "user", 0);
80+
}
81+
82+
#[test]
83+
fn parse_error_test() {
84+
run_test_time(&[], "", "not provided", 0);
85+
}
86+
87+
#[test]
88+
fn command_error_test() {
89+
run_test_time(&["-s", "ls", "-l"], "", "unexpected argument found", 0);
90+
}

datetime/time.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
use std::process::{Command, Stdio};
11+
use std::time::Instant;
12+
use std::io::{self, Write};
13+
14+
use clap::Parser;
15+
16+
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
17+
use plib::PROJECT_NAME;
18+
19+
#[derive(Parser, Debug)]
20+
#[command(author, version, about, long_about = None)]
21+
struct Args {
22+
/// Write timing output to standard error in POSIX format
23+
#[arg(short, long)]
24+
posix: bool,
25+
26+
/// The utility to be invoked
27+
utility: String,
28+
29+
/// Arguments for the utility
30+
#[arg(name = "ARGUMENT", trailing_var_arg = true)]
31+
arguments: Vec<String>,
32+
}
33+
34+
enum TimeError {
35+
ExecCommand(String),
36+
ExecTime,
37+
CommandNotFound(String),
38+
}
39+
40+
fn time(args: Args) -> Result<(), TimeError> {
41+
let start_time = Instant::now();
42+
// SAFETY: std::mem::zeroed() is used to create an instance of libc::tms with all fields set to zero.
43+
// This is safe here because libc::tms is a Plain Old Data type, and zero is a valid value for all its fields.
44+
let mut tms_start: libc::tms = unsafe { std::mem::zeroed() };
45+
// SAFETY: sysconf is a POSIX function that returns the number of clock ticks per second.
46+
// It is safe to call because it does not modify any memory and has no side effects.
47+
let clock_ticks_per_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) as f64 };
48+
49+
// SAFETY: times is a POSIX function that fills the provided tms structure with time-accounting information.
50+
// It is safe to call because we have correctly allocated and initialized tms_start, and the function
51+
// only writes to this structure.
52+
unsafe { libc::times(&mut tms_start) };
53+
54+
let mut child = Command::new(&args.utility)
55+
.args(args.arguments)
56+
.stdout(Stdio::inherit())
57+
.stderr(Stdio::inherit())
58+
.spawn()
59+
.map_err(|e| {
60+
match e.kind() {
61+
io::ErrorKind::NotFound => TimeError::CommandNotFound(args.utility),
62+
_ => TimeError::ExecCommand(args.utility),
63+
}
64+
})?;
65+
66+
let _ = child.wait().map_err(|_| TimeError::ExecTime)?;
67+
68+
let elapsed = start_time.elapsed();
69+
let tms_end: libc::tms = unsafe { std::mem::zeroed() };
70+
71+
let user_time = (tms_start.tms_utime - tms_end.tms_utime) as f64 / clock_ticks_per_second;
72+
let system_time = (tms_start.tms_stime - tms_end.tms_stime) as f64 / clock_ticks_per_second;
73+
74+
if args.posix {
75+
writeln!(
76+
io::stderr(),
77+
"real {:.6}\nuser {:.6}\nsys {:.6}",
78+
elapsed.as_secs_f64(),
79+
user_time,
80+
system_time
81+
).map_err(|_| TimeError::ExecTime)?;
82+
} else {
83+
writeln!(
84+
io::stderr(),
85+
"Elapsed time: {:.6} seconds\nUser time: {:.6} seconds\nSystem time: {:.6} seconds",
86+
elapsed.as_secs_f64(),
87+
user_time,
88+
system_time
89+
).map_err(|_| TimeError::ExecTime)?;
90+
}
91+
92+
Ok(())
93+
}
94+
95+
enum Status {
96+
Ok,
97+
TimeError,
98+
UtilError,
99+
UtilNotFound,
100+
}
101+
102+
impl Status {
103+
fn exit(self) -> ! {
104+
let res = match self {
105+
Status::Ok => 0,
106+
Status::TimeError => 1,
107+
Status::UtilError => 126,
108+
Status::UtilNotFound => 127,
109+
};
110+
111+
std::process::exit(res)
112+
}
113+
}
114+
115+
116+
fn main() -> Result<(), Box<dyn std::error::Error>> {
117+
let args = Args::parse();
118+
119+
setlocale(LocaleCategory::LcAll, "");
120+
textdomain(PROJECT_NAME)?;
121+
bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?;
122+
123+
if let Err(err) = time(args) {
124+
match err {
125+
TimeError::CommandNotFound(err) => {
126+
eprintln!("Command not found: {}", err);
127+
Status::UtilNotFound.exit()
128+
},
129+
TimeError::ExecCommand(err) => {
130+
eprintln!("Error while executing command: {}", err);
131+
Status::UtilError.exit()
132+
}
133+
TimeError::ExecTime => {
134+
eprintln!("Error while executing time utility");
135+
Status::TimeError.exit()
136+
},
137+
}
138+
}
139+
140+
Status::Ok.exit()
141+
}

0 commit comments

Comments
 (0)