Merge pull request #167 from fuad1502/dmesg-filter
Support `dmesg` filtering options (`--facility`, `--level`, `--since`, and `--until` options).
This commit is contained in:
commit
7c7809ec99
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -405,6 +405,12 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@ -417,6 +423,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
@ -482,6 +498,17 @@ dependencies = [
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_datetime"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
@ -1012,6 +1039,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"parse_datetime",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -17,6 +17,7 @@ regex = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
chrono = "0.4.38"
|
||||
parse_datetime = "0.6.0"
|
||||
|
||||
[features]
|
||||
fixed-boot-time = []
|
||||
|
@ -3,11 +3,18 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use clap::{crate_version, Arg, ArgAction, Command};
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
hash::Hash,
|
||||
io::{BufRead, BufReader},
|
||||
sync::OnceLock,
|
||||
};
|
||||
use uucore::{
|
||||
error::{FromIo, UResult, USimpleError},
|
||||
error::{FromIo, UError, UResult, USimpleError},
|
||||
format_usage, help_about, help_usage,
|
||||
};
|
||||
|
||||
@ -43,7 +50,69 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
}
|
||||
};
|
||||
}
|
||||
dmesg.parse()?.print();
|
||||
if let Some(list_args) = matches.get_many::<String>(options::FACILITY) {
|
||||
let mut facility_filters = HashSet::new();
|
||||
for list in list_args {
|
||||
for arg in list.split(',') {
|
||||
let facility = match arg {
|
||||
"kern" => Facility::Kern,
|
||||
"user" => Facility::User,
|
||||
"mail" => Facility::Mail,
|
||||
"daemon" => Facility::Daemon,
|
||||
"auth" => Facility::Auth,
|
||||
"syslog" => Facility::Syslog,
|
||||
"lpr" => Facility::Lpr,
|
||||
"news" => Facility::News,
|
||||
"uucp" => Facility::Uucp,
|
||||
"cron" => Facility::Cron,
|
||||
"authpriv" => Facility::Authpriv,
|
||||
"ftp" => Facility::Ftp,
|
||||
"res0" => Facility::Res0,
|
||||
"res1" => Facility::Res1,
|
||||
"res2" => Facility::Res2,
|
||||
"res3" => Facility::Res3,
|
||||
"local0" => Facility::Local0,
|
||||
"local1" => Facility::Local1,
|
||||
"local2" => Facility::Local2,
|
||||
"local3" => Facility::Local3,
|
||||
"local4" => Facility::Local4,
|
||||
"local5" => Facility::Local5,
|
||||
"local6" => Facility::Local6,
|
||||
"local7" => Facility::Local7,
|
||||
_ => return Err(USimpleError::new(1, format!("unknown facility '{arg}'"))),
|
||||
};
|
||||
facility_filters.insert(facility);
|
||||
}
|
||||
}
|
||||
dmesg.facility_filters = Some(facility_filters);
|
||||
}
|
||||
if let Some(list_args) = matches.get_many::<String>(options::LEVEL) {
|
||||
let mut level_filters = HashSet::new();
|
||||
for list in list_args {
|
||||
for arg in list.split(',') {
|
||||
let level = match arg {
|
||||
"emerg" => Level::Emerg,
|
||||
"alert" => Level::Alert,
|
||||
"crit" => Level::Crit,
|
||||
"err" => Level::Err,
|
||||
"warn" => Level::Warn,
|
||||
"notice" => Level::Notice,
|
||||
"info" => Level::Info,
|
||||
"debug" => Level::Debug,
|
||||
_ => return Err(USimpleError::new(1, format!("unknown level '{arg}'"))),
|
||||
};
|
||||
level_filters.insert(level);
|
||||
}
|
||||
}
|
||||
dmesg.level_filters = Some(level_filters);
|
||||
}
|
||||
if let Some(since) = matches.get_one::<String>(options::SINCE) {
|
||||
dmesg.since_filter = Some(time_formatter::parse_datetime(since)?);
|
||||
}
|
||||
if let Some(until) = matches.get_one::<String>(options::UNTIL) {
|
||||
dmesg.until_filter = Some(time_formatter::parse_datetime(until)?);
|
||||
}
|
||||
dmesg.print()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -75,19 +144,52 @@ pub fn uu_app() -> Command {
|
||||
)
|
||||
.action(ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::FACILITY)
|
||||
.short('f')
|
||||
.long("facility")
|
||||
.help("restrict output to defined facilities")
|
||||
.action(ArgAction::Append),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::LEVEL)
|
||||
.short('l')
|
||||
.long("level")
|
||||
.help("restrict output to defined levels")
|
||||
.action(ArgAction::Append),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::SINCE)
|
||||
.long("since")
|
||||
.help("display the lines since the specified time")
|
||||
.action(ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::UNTIL)
|
||||
.long("until")
|
||||
.help("display the lines until the specified time")
|
||||
.action(ArgAction::Set),
|
||||
)
|
||||
}
|
||||
|
||||
mod options {
|
||||
pub const KMSG_FILE: &str = "kmsg-file";
|
||||
pub const JSON: &str = "json";
|
||||
pub const TIME_FORMAT: &str = "time-format";
|
||||
pub const FACILITY: &str = "facility";
|
||||
pub const LEVEL: &str = "level";
|
||||
pub const SINCE: &str = "since";
|
||||
pub const UNTIL: &str = "until";
|
||||
}
|
||||
|
||||
struct Dmesg<'a> {
|
||||
kmsg_file: &'a str,
|
||||
output_format: OutputFormat,
|
||||
time_format: TimeFormat,
|
||||
records: Option<Vec<Record>>,
|
||||
facility_filters: Option<HashSet<Facility>>,
|
||||
level_filters: Option<HashSet<Level>>,
|
||||
since_filter: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
until_filter: Option<chrono::DateTime<chrono::FixedOffset>>,
|
||||
}
|
||||
|
||||
impl Dmesg<'_> {
|
||||
@ -96,86 +198,105 @@ impl Dmesg<'_> {
|
||||
kmsg_file: "/dev/kmsg",
|
||||
output_format: OutputFormat::Normal,
|
||||
time_format: TimeFormat::Raw,
|
||||
records: None,
|
||||
facility_filters: None,
|
||||
level_filters: None,
|
||||
since_filter: None,
|
||||
until_filter: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(mut self) -> UResult<Self> {
|
||||
let mut records = vec![];
|
||||
let re = Self::record_regex();
|
||||
let lines = self.read_lines_from_kmsg_file()?;
|
||||
for line in lines {
|
||||
for (_, [pri_fac, seq, time, msg]) in re.captures_iter(&line).map(|c| c.extract()) {
|
||||
records.push(Record::from_str_fields(
|
||||
pri_fac,
|
||||
seq,
|
||||
time,
|
||||
msg.to_string(),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
self.records = Some(records);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn record_regex() -> Regex {
|
||||
let valid_number_pattern = "0|[1-9][0-9]*";
|
||||
let additional_fields_pattern = ",^[,;]*";
|
||||
let record_pattern = format!(
|
||||
"(?m)^({0}),({0}),({0}),.(?:{1})*;(.*)$",
|
||||
valid_number_pattern, additional_fields_pattern
|
||||
);
|
||||
Regex::new(&record_pattern).expect("invalid regex.")
|
||||
}
|
||||
|
||||
fn read_lines_from_kmsg_file(&self) -> UResult<Vec<String>> {
|
||||
let kmsg_bytes = fs::read(self.kmsg_file)
|
||||
.map_err_context(|| format!("cannot open {}", self.kmsg_file))?;
|
||||
let lines = kmsg_bytes
|
||||
.split(|&byte| byte == 0)
|
||||
.map(|line| String::from_utf8_lossy(line).to_string())
|
||||
.collect();
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn print(&self) {
|
||||
fn print(&self) -> UResult<()> {
|
||||
match self.output_format {
|
||||
OutputFormat::Json => self.print_json(),
|
||||
OutputFormat::Normal => self.print_normal(),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(&self) {
|
||||
if let Some(records) = &self.records {
|
||||
println!("{}", json::serialize_records(records));
|
||||
fn print_json(&self) -> UResult<()> {
|
||||
let records: UResult<Vec<Record>> = self.try_filtered_iter()?.collect();
|
||||
println!("{}", json::serialize_records(&records?));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_normal(&self) -> UResult<()> {
|
||||
let mut reltime_formatter = time_formatter::ReltimeFormatter::new();
|
||||
let mut delta_formatter = time_formatter::DeltaFormatter::new();
|
||||
for record in self.try_filtered_iter()? {
|
||||
let record = record?;
|
||||
match self.time_format {
|
||||
TimeFormat::Delta => {
|
||||
print!("[{}] ", delta_formatter.format(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Reltime => {
|
||||
print!("[{}] ", reltime_formatter.format(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Ctime => {
|
||||
print!("[{}] ", time_formatter::ctime(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Iso => {
|
||||
print!("{} ", time_formatter::iso(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Raw => {
|
||||
print!("[{}] ", time_formatter::raw(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Notime => (),
|
||||
}
|
||||
println!("{}", record.message);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_filtered_iter(&self) -> UResult<impl Iterator<Item = UResult<Record>> + '_> {
|
||||
Ok(self
|
||||
.try_iter()?
|
||||
.filter(Self::is_record_in_set(&self.facility_filters))
|
||||
.filter(Self::is_record_in_set(&self.level_filters))
|
||||
.filter(Self::is_record_since(&self.since_filter))
|
||||
.filter(Self::is_record_until(&self.until_filter)))
|
||||
}
|
||||
|
||||
fn try_iter(&self) -> UResult<RecordIterator> {
|
||||
let file = File::open(self.kmsg_file)
|
||||
.map_err_context(|| format!("cannot open {}", self.kmsg_file))?;
|
||||
let file_reader = BufReader::new(file);
|
||||
Ok(RecordIterator { file_reader })
|
||||
}
|
||||
|
||||
fn is_record_in_set<T>(
|
||||
set: &Option<HashSet<T>>,
|
||||
) -> impl Fn(&Result<Record, Box<dyn UError>>) -> bool + '_
|
||||
where
|
||||
T: From<u32> + Eq + Hash,
|
||||
{
|
||||
move |record: &UResult<Record>| match (record, set) {
|
||||
(Ok(record), Some(set)) => set.contains(&T::from(record.priority_facility)),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_normal(&self) {
|
||||
if let Some(records) = &self.records {
|
||||
let mut reltime_formatter = time_formatter::ReltimeFormatter::new();
|
||||
let mut delta_formatter = time_formatter::DeltaFormatter::new();
|
||||
for record in records {
|
||||
match self.time_format {
|
||||
TimeFormat::Delta => {
|
||||
print!("[{}] ", delta_formatter.format(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Reltime => {
|
||||
print!("[{}] ", reltime_formatter.format(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Ctime => {
|
||||
print!("[{}] ", time_formatter::ctime(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Iso => {
|
||||
print!("{} ", time_formatter::iso(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Raw => {
|
||||
print!("[{}] ", time_formatter::raw(record.timestamp_us))
|
||||
}
|
||||
TimeFormat::Notime => (),
|
||||
}
|
||||
println!("{}", record.message);
|
||||
fn is_record_since(
|
||||
since: &Option<DateTime<FixedOffset>>,
|
||||
) -> impl Fn(&UResult<Record>) -> bool + '_ {
|
||||
move |record: &UResult<Record>| match (record, since) {
|
||||
(Ok(record), Some(since)) => {
|
||||
let time =
|
||||
time_formatter::datetime_from_microseconds_since_boot(record.timestamp_us);
|
||||
time >= *since
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_record_until(
|
||||
until: &Option<DateTime<FixedOffset>>,
|
||||
) -> impl Fn(&UResult<Record>) -> bool + '_ {
|
||||
move |record: &UResult<Record>| match (record, until) {
|
||||
(Ok(record), Some(until)) => {
|
||||
let time =
|
||||
time_formatter::datetime_from_microseconds_since_boot(record.timestamp_us);
|
||||
time <= *until
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,6 +315,102 @@ enum TimeFormat {
|
||||
Raw,
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq)]
|
||||
enum Facility {
|
||||
Kern,
|
||||
User,
|
||||
Mail,
|
||||
Daemon,
|
||||
Auth,
|
||||
Syslog,
|
||||
Lpr,
|
||||
News,
|
||||
Uucp,
|
||||
Cron,
|
||||
Authpriv,
|
||||
Ftp,
|
||||
Res0,
|
||||
Res1,
|
||||
Res2,
|
||||
Res3,
|
||||
Local0,
|
||||
Local1,
|
||||
Local2,
|
||||
Local3,
|
||||
Local4,
|
||||
Local5,
|
||||
Local6,
|
||||
Local7,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq)]
|
||||
enum Level {
|
||||
Emerg,
|
||||
Alert,
|
||||
Crit,
|
||||
Err,
|
||||
Warn,
|
||||
Notice,
|
||||
Info,
|
||||
Debug,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
struct RecordIterator {
|
||||
file_reader: BufReader<File>,
|
||||
}
|
||||
|
||||
impl Iterator for RecordIterator {
|
||||
type Item = UResult<Record>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.read_record_line() {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(None) => None,
|
||||
Ok(Some(line)) => match self.parse_record(&line) {
|
||||
None => self.next(),
|
||||
Some(record) => Some(Ok(record)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordIterator {
|
||||
fn read_record_line(&mut self) -> UResult<Option<String>> {
|
||||
let mut buf = vec![];
|
||||
let num_bytes = self.file_reader.read_until(0, &mut buf)?;
|
||||
match num_bytes {
|
||||
0 => Ok(None),
|
||||
_ => Ok(Some(String::from_utf8_lossy(&buf).to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record(&self, record_line: &str) -> Option<Record> {
|
||||
record_regex()
|
||||
.captures_iter(record_line)
|
||||
.map(|c| c.extract())
|
||||
.filter_map(|(_, [pri_fac, seq, time, msg])| {
|
||||
Record::from_str_fields(pri_fac, seq, time, msg.to_string()).ok()
|
||||
})
|
||||
.next()
|
||||
}
|
||||
}
|
||||
|
||||
fn record_regex() -> &'static Regex {
|
||||
RECORD_REGEX.get_or_init(|| {
|
||||
let valid_number_pattern = "0|[1-9][0-9]*";
|
||||
let additional_fields_pattern = ",^[,;]*";
|
||||
let record_pattern = format!(
|
||||
"(?m)^({0}),({0}),({0}),.(?:{1})*;(.*)$",
|
||||
valid_number_pattern, additional_fields_pattern
|
||||
);
|
||||
Regex::new(&record_pattern).expect("invalid regex.")
|
||||
})
|
||||
}
|
||||
|
||||
static RECORD_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
struct Record {
|
||||
priority_facility: u32,
|
||||
_sequence: u64,
|
||||
@ -217,3 +434,53 @@ impl Record {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Level {
|
||||
fn from(value: u32) -> Self {
|
||||
let priority = value & 0b111;
|
||||
match priority {
|
||||
0 => Level::Emerg,
|
||||
1 => Level::Alert,
|
||||
2 => Level::Crit,
|
||||
3 => Level::Err,
|
||||
4 => Level::Warn,
|
||||
5 => Level::Notice,
|
||||
6 => Level::Info,
|
||||
7 => Level::Debug,
|
||||
_ => Level::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Facility {
|
||||
fn from(value: u32) -> Self {
|
||||
let facility = (value >> 3) as u8;
|
||||
match facility {
|
||||
0 => Facility::Kern,
|
||||
1 => Facility::User,
|
||||
2 => Facility::Mail,
|
||||
3 => Facility::Daemon,
|
||||
4 => Facility::Auth,
|
||||
5 => Facility::Syslog,
|
||||
6 => Facility::Lpr,
|
||||
7 => Facility::News,
|
||||
8 => Facility::Uucp,
|
||||
9 => Facility::Cron,
|
||||
10 => Facility::Authpriv,
|
||||
11 => Facility::Ftp,
|
||||
12 => Facility::Res0,
|
||||
13 => Facility::Res1,
|
||||
14 => Facility::Res2,
|
||||
15 => Facility::Res3,
|
||||
16 => Facility::Local0,
|
||||
17 => Facility::Local1,
|
||||
18 => Facility::Local2,
|
||||
19 => Facility::Local3,
|
||||
20 => Facility::Local4,
|
||||
21 => Facility::Local5,
|
||||
22 => Facility::Local6,
|
||||
23 => Facility::Local7,
|
||||
_ => Facility::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use chrono::{DateTime, FixedOffset, TimeDelta};
|
||||
#[cfg(feature = "fixed-boot-time")]
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use std::sync::OnceLock;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
|
||||
pub fn raw(timestamp_us: i64) -> String {
|
||||
let seconds = timestamp_us / 1000000;
|
||||
@ -116,6 +117,17 @@ impl DeltaFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_datetime(s: &str) -> UResult<DateTime<FixedOffset>> {
|
||||
parse_datetime::parse_datetime(s)
|
||||
.map_err(|_| USimpleError::new(1, format!("invalid time value \"{s}\"")))
|
||||
}
|
||||
|
||||
pub fn datetime_from_microseconds_since_boot(microseconds: i64) -> DateTime<FixedOffset> {
|
||||
boot_time()
|
||||
.checked_add_signed(TimeDelta::microseconds(microseconds))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
static BOOT_TIME: OnceLock<DateTime<FixedOffset>> = OnceLock::new();
|
||||
|
||||
#[cfg(feature = "fixed-boot-time")]
|
||||
|
@ -81,3 +81,111 @@ fn test_invalid_time_format() {
|
||||
.code_is(1)
|
||||
.stderr_only("dmesg: unknown time format: definitely-invalid\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_facility() {
|
||||
let facilities = [
|
||||
"kern", "user", "mail", "daemon", "auth", "syslog", "lpr", "news", "uucp", "cron",
|
||||
"authpriv", "ftp", "local0", "local1", "local2", "local3", "local4", "local5", "local6",
|
||||
"local7",
|
||||
];
|
||||
for facility in facilities {
|
||||
let facility_filter_arg = format!("--facility={facility}");
|
||||
let mut cmd = new_ucmd!();
|
||||
let result = cmd
|
||||
.arg("--kmsg-file")
|
||||
.arg("kmsg.input")
|
||||
.arg(facility_filter_arg)
|
||||
.succeeds();
|
||||
let stdout = result.no_stderr().stdout_str();
|
||||
assert_eq!(stdout.lines().count(), 8);
|
||||
let expected = format!("LOG_{}", facility.to_uppercase());
|
||||
stdout
|
||||
.lines()
|
||||
.for_each(|line| assert!(line.contains(&expected)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_levels() {
|
||||
let levels = [
|
||||
"emerg", "alert", "crit", "err", "warn", "notice", "info", "debug",
|
||||
];
|
||||
for level in levels {
|
||||
let level_filter_arg = format!("--level={level}");
|
||||
let mut cmd = new_ucmd!();
|
||||
let result = cmd
|
||||
.arg("--kmsg-file")
|
||||
.arg("kmsg.input")
|
||||
.arg(level_filter_arg)
|
||||
.succeeds();
|
||||
let stdout = result.no_stderr().stdout_str();
|
||||
assert_eq!(stdout.lines().count(), 20);
|
||||
let expected = format!("LOG_{}", level.to_uppercase());
|
||||
stdout
|
||||
.lines()
|
||||
.for_each(|line| assert!(line.contains(&expected)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_facility_argument() {
|
||||
new_ucmd!()
|
||||
.arg("--facility=definitely-invalid")
|
||||
.fails()
|
||||
.code_is(1)
|
||||
.stderr_only("dmesg: unknown facility 'definitely-invalid'\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_level_argument() {
|
||||
new_ucmd!()
|
||||
.arg("--level=definitely-invalid")
|
||||
.fails()
|
||||
.code_is(1)
|
||||
.stderr_only("dmesg: unknown level 'definitely-invalid'\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_multiple() {
|
||||
let mut cmd = new_ucmd!();
|
||||
let result = cmd
|
||||
.arg("--kmsg-file")
|
||||
.arg("kmsg.input")
|
||||
.arg("--facility=kern,user")
|
||||
.arg("--level=emerg,alert")
|
||||
.succeeds();
|
||||
let stdout = result.no_stderr().stdout_str();
|
||||
assert_eq!(stdout.lines().count(), 4);
|
||||
stdout.lines().for_each(|line| {
|
||||
assert!(
|
||||
(line.contains("LOG_KERN") || line.contains("LOG_USER"))
|
||||
&& (line.contains("LOG_EMERG") || line.contains("LOG_ALERT"))
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_since_until() {
|
||||
new_ucmd!()
|
||||
.arg("--kmsg-file")
|
||||
.arg("kmsg.input")
|
||||
.arg("--since=2024-11-19 17:47:32 +0700")
|
||||
.arg("--until=2024-11-19 18:55:52 +0700")
|
||||
.succeeds()
|
||||
.no_stderr()
|
||||
.stdout_is_templated_fixture("test_since_until.expected", &[("\r\n", "\n")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_since_until_invalid_time() {
|
||||
let options = ["--since", "--until"];
|
||||
for option in options {
|
||||
new_ucmd!()
|
||||
.arg(format!("{option}=definitely-invalid"))
|
||||
.fails()
|
||||
.stderr_only(format!(
|
||||
"dmesg: invalid time value \"definitely-invalid\"\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
5
tests/fixtures/dmesg/test_since_until.expected
vendored
Normal file
5
tests/fixtures/dmesg/test_since_until.expected
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
[80000.000000] LOG_WARNING LOG_AUTH
|
||||
[81000.000000] LOG_WARNING LOG_AUTHPRIV
|
||||
[82000.000000] LOG_WARNING LOG_CRON
|
||||
[83000.000000] LOG_WARNING LOG_DAEMON
|
||||
[84000.000000] LOG_WARNING LOG_FTP
|
Loading…
x
Reference in New Issue
Block a user