refactor: clean up some query code (#1949)

* tmp

* intermediate work

* delete some code

* hook things up

* use QueryOptions everywhere instead

* comments

* more cleanup

* even more cleanup

* fmt

* update comment

* some more tests while I'm at it

* add todo for additional tests

* more empty quotes

* more empty quote testing

* even more tests

* driveby test for arg tests
This commit is contained in:
Clement Tsang
2026-01-05 03:51:43 -05:00
committed by GitHub
parent a462cc650f
commit b3445b3044
8 changed files with 728 additions and 505 deletions
+3 -3
View File
@@ -197,19 +197,19 @@ impl Painter {
})];
// Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.proc_search.is_ignoring_case {
let case_style = if !proc_widget_state.proc_search.query_options.ignore_case {
self.styles.selected_text_style
} else {
self.styles.text_style
};
let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word {
let whole_word_style = if proc_widget_state.proc_search.query_options.whole_word {
self.styles.selected_text_style
} else {
self.styles.text_style
};
let regex_style = if proc_widget_state.proc_search.is_searching_with_regex {
let regex_style = if proc_widget_state.proc_search.query_options.use_regex {
self.styles.selected_text_style
} else {
self.styles.text_style
+11 -23
View File
@@ -14,6 +14,7 @@ use query::{ProcessQuery, parse_query};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use sort_table::SortTableColumn;
use crate::widgets::query::QueryOptions;
use crate::{
app::{
AppConfigFields, AppSearchState,
@@ -29,35 +30,23 @@ use crate::{
/// ProcessSearchState only deals with process' search's current settings and
/// state.
#[derive(Default)]
pub struct ProcessSearchState {
pub search_state: AppSearchState,
pub is_ignoring_case: bool,
pub is_searching_whole_word: bool,
pub is_searching_with_regex: bool,
}
impl Default for ProcessSearchState {
fn default() -> Self {
ProcessSearchState {
search_state: AppSearchState::default(),
is_ignoring_case: true,
is_searching_whole_word: false,
is_searching_with_regex: false,
}
}
pub query_options: QueryOptions,
}
impl ProcessSearchState {
pub fn search_toggle_ignore_case(&mut self) {
self.is_ignoring_case = !self.is_ignoring_case;
self.query_options.ignore_case = !self.query_options.ignore_case;
}
pub fn search_toggle_whole_word(&mut self) {
self.is_searching_whole_word = !self.is_searching_whole_word;
self.query_options.whole_word = !self.query_options.whole_word;
}
pub fn search_toggle_regex(&mut self) {
self.is_searching_with_regex = !self.is_searching_with_regex;
self.query_options.use_regex = !self.query_options.use_regex;
}
}
@@ -1089,6 +1078,9 @@ impl ProcWidgetState {
&self.proc_search.search_state.current_search_query
}
/// Update the current search query.
///
/// TODO: Maybe debounce this.
pub fn update_query(&mut self) {
if self
.proc_search
@@ -1102,9 +1094,7 @@ impl ProcWidgetState {
} else {
match parse_query(
&self.proc_search.search_state.current_search_query,
self.proc_search.is_searching_whole_word,
self.proc_search.is_ignoring_case,
self.proc_search.is_searching_with_regex,
&self.proc_search.query_options,
) {
Ok(parsed_query) => {
self.proc_search.search_state.query = Some(parsed_query);
@@ -1153,9 +1143,7 @@ impl ProcWidgetState {
#[cfg(test)]
pub(crate) fn test_equality(&self, other: &Self) -> bool {
self.mode == other.mode
&& self.proc_search.is_ignoring_case == other.proc_search.is_ignoring_case
&& self.proc_search.is_searching_whole_word == other.proc_search.is_searching_whole_word
&& self.proc_search.is_searching_with_regex == other.proc_search.is_searching_with_regex
&& self.proc_search.query_options == other.proc_search.query_options
&& self
.table
.columns
+465 -104
View File
@@ -2,23 +2,21 @@
//!
//! Yes, this is a hand-rolled parser. I originally wrote this back in uni where writing
//! a parser was basically a thing I did every year, and parsing crate options were not
//! as good as they are now.
//! as good as they are now. This will be rewritten as time goes on, though.
mod and;
mod attribute;
mod error;
mod or;
mod prefix;
use std::{collections::VecDeque, time::Duration};
use and::And;
use attribute::ProcessAttribute;
use error::{QueryError, QueryResult};
use or::Or;
use prefix::Prefix;
use std::{
collections::VecDeque,
fmt::{Debug, Formatter},
time::Duration,
};
use regex::Regex;
use crate::{collection::processes::ProcessHarvest, multi_eq_ignore_ascii_case};
@@ -29,11 +27,62 @@ const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
/// A node type that can take a query and read it, advancing the current read state
/// and returning an instance of the node.
trait QueryProcessor {
fn process(query: &mut VecDeque<String>) -> QueryResult<Self>
fn process(query: &mut VecDeque<String>, regex_options: &QueryOptions) -> QueryResult<Self>
where
Self: Sized;
}
/// Process a new regex given a `base` string and some settings.
///
/// TODO: Push this into a struct so I don't have to throw the options around so much.
fn new_regex(base: &str, regex_options: &QueryOptions) -> QueryResult<Regex> {
let QueryOptions {
whole_word: is_searching_whole_word,
ignore_case: is_ignoring_case,
use_regex: is_searching_with_regex,
} = regex_options;
let escaped_regex: String; // Needed for ownership reasons.
let final_regex_string = &format!(
"{}{}{}{}",
if *is_searching_whole_word { "^" } else { "" },
if *is_ignoring_case { "(?i)" } else { "" },
if !(*is_searching_with_regex) {
escaped_regex = regex::escape(base);
&escaped_regex
} else {
base
},
if *is_searching_whole_word { "$" } else { "" },
);
Ok(Regex::new(final_regex_string)?)
}
/// Options when creating a new query.
#[derive(PartialEq, Eq)]
pub struct QueryOptions {
/// Whether we only allow matches on the entire word.
pub whole_word: bool,
/// Whether to ignore case-sensitivity when searching. On by default.
pub ignore_case: bool,
/// Whether we should use regex syntax when searching. If not set, then it
/// should treat everything as a literal string.
pub use_regex: bool,
}
impl Default for QueryOptions {
fn default() -> Self {
Self {
ignore_case: true,
whole_word: false,
use_regex: false,
}
}
}
/// In charge of parsing the given query, case-insensitive, possibly marked
/// by a prefix. For example:
///
@@ -54,16 +103,15 @@ trait QueryProcessor {
/// adjacent non-prefixed or quoted elements after splitting to treat as process
/// names. Furthermore, we want to support boolean joiners like AND and OR, and
/// brackets.
pub(crate) fn parse_query(
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> QueryResult<ProcessQuery> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> QueryResult<ProcessQuery> {
let lhs = Or::process(query)?;
pub(crate) fn parse_query(search_query: &str, options: &QueryOptions) -> QueryResult<ProcessQuery> {
fn process_string_to_filter(
query: &mut VecDeque<String>, options: &QueryOptions,
) -> QueryResult<ProcessQuery> {
let lhs = Or::process(query, options)?;
let mut list_of_ors = vec![lhs];
while query.front().is_some() {
list_of_ors.push(Or::process(query)?);
list_of_ors.push(Or::process(query, options)?);
}
Ok(ProcessQuery { query: list_of_ors })
@@ -72,7 +120,7 @@ pub(crate) fn parse_query(
let mut split_query = VecDeque::new();
search_query.split_whitespace().for_each(|s| {
// From https://stackoverflow.com/a/56923739 in order to get a split, but include the parentheses
// From https://stackoverflow.com/a/56923739 get a split but include the parentheses
let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
if last != index {
@@ -86,37 +134,16 @@ pub(crate) fn parse_query(
}
});
let mut process_filter = process_string_to_filter(&mut split_query)?;
process_filter.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
Ok(process_filter)
process_string_to_filter(&mut split_query, options)
}
#[derive(Debug)]
pub struct ProcessQuery {
/// Remember, AND > OR, but AND must come after OR when we parse.
query: Vec<Or>,
}
impl ProcessQuery {
fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> QueryResult<()> {
for or in &mut self.query {
or.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub(crate) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
self.query
.iter()
@@ -124,22 +151,16 @@ impl ProcessQuery {
}
}
impl Debug for ProcessQuery {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{:?}", self.query))
}
}
#[derive(Debug)]
enum PrefixType {
Pid,
PCpu,
CpuPercentage,
MemBytes,
PMem,
Rps,
Wps,
TRead,
TWrite,
MemPercentage,
ReadPerSecond,
WritePerSecond,
TotalRead,
TotalWrite,
Name,
State,
User,
@@ -148,12 +169,11 @@ enum PrefixType {
Nice,
Priority,
#[cfg(feature = "gpu")]
PGpu,
GpuPercentage,
#[cfg(feature = "gpu")]
GMem,
GpuMemoryBytes,
#[cfg(feature = "gpu")]
PGMem,
__Nonexhaustive,
GpuMemoryPercentage,
}
impl std::str::FromStr for PrefixType {
@@ -167,19 +187,19 @@ impl std::str::FromStr for PrefixType {
let mut result = Name;
if multi_eq_ignore_ascii_case!(s, "cpu" | "cpu%") {
result = PCpu;
result = CpuPercentage;
} else if multi_eq_ignore_ascii_case!(s, "mem" | "mem%") {
result = PMem;
result = MemPercentage;
} else if multi_eq_ignore_ascii_case!(s, "memb") {
result = MemBytes;
} else if multi_eq_ignore_ascii_case!(s, "read" | "r/s" | "rps") {
result = Rps;
result = ReadPerSecond;
} else if multi_eq_ignore_ascii_case!(s, "write" | "w/s" | "wps") {
result = Wps;
result = WritePerSecond;
} else if multi_eq_ignore_ascii_case!(s, "tread" | "t.read") {
result = TRead;
result = TotalRead;
} else if multi_eq_ignore_ascii_case!(s, "twrite" | "t.write") {
result = TWrite;
result = TotalWrite;
} else if multi_eq_ignore_ascii_case!(s, "pid") {
result = Pid;
} else if multi_eq_ignore_ascii_case!(s, "state") {
@@ -199,11 +219,11 @@ impl std::str::FromStr for PrefixType {
#[cfg(feature = "gpu")]
{
if multi_eq_ignore_ascii_case!(s, "gmem") {
result = GMem;
result = GpuMemoryBytes;
} else if multi_eq_ignore_ascii_case!(s, "gmem%") {
result = PGMem;
result = GpuMemoryPercentage;
} else if multi_eq_ignore_ascii_case!(s, "gpu%") {
result = PGpu;
result = GpuPercentage;
}
}
Ok(result)
@@ -219,30 +239,49 @@ enum QueryComparison {
GreaterOrEqual,
}
#[derive(Debug)]
enum StringQuery {
Value(String),
Regex(Regex),
}
#[derive(Debug)]
enum ComparableQuery {
Numerical(NumericalQuery),
Time(TimeQuery),
}
#[derive(Debug)]
struct NumericalQuery {
condition: QueryComparison,
value: f64,
}
impl NumericalQuery {
/// Compare `lhs` to the value in the query as `rhs`.
fn check<I: Into<f64>>(&self, lhs: I) -> bool {
let lhs: f64 = lhs.into();
let rhs: f64 = self.value;
match self.condition {
QueryComparison::Equal => (lhs - rhs).abs() < f64::EPSILON,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
}
#[derive(Debug)]
struct TimeQuery {
condition: QueryComparison,
duration: Duration,
}
impl TimeQuery {
/// Compare `lhs` to the value in the query as `rhs`.
fn check(&self, lhs: Duration) -> bool {
let rhs = self.duration;
match self.condition {
QueryComparison::Equal => lhs == rhs,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -254,9 +293,20 @@ mod tests {
}
}
fn parse_query_no_options(query: &str) -> QueryResult<ProcessQuery> {
parse_query(
query,
&QueryOptions {
whole_word: false,
ignore_case: false,
use_regex: false,
},
)
}
#[test]
fn basic_query() {
let query = parse_query("test", false, false, false).unwrap();
let query = parse_query_no_options("test").unwrap();
let exact_match = simple_process("test");
let contains = simple_process("test string");
@@ -269,7 +319,7 @@ mod tests {
#[test]
fn basic_or_query() {
let query = parse_query("a or b", false, false, false).unwrap();
let query = parse_query_no_options("a or b").unwrap();
let a = simple_process("a");
let b = simple_process("b");
@@ -282,7 +332,7 @@ mod tests {
#[test]
fn basic_and_query() {
let query = parse_query("a and b", false, false, false).unwrap();
let query = parse_query_no_options("a and b").unwrap();
let a = simple_process("a");
let b = simple_process("b");
@@ -295,11 +345,26 @@ mod tests {
assert!(query.check(&a_and_b, false));
}
#[test]
fn implied_and_query() {
let query = parse_query_no_options("a b c").unwrap();
let a = simple_process("a");
let b = simple_process("b");
let c = simple_process("c");
let all = simple_process("a b c");
assert!(!query.check(&a, false));
assert!(!query.check(&b, false));
assert!(!query.check(&c, false));
assert!(query.check(&all, false));
}
/// Ensure that quoted keywords are treated as strings. In this case, rather than `"a" OR "b"`, it should be treated
/// as the string `"a or b"`.
#[test]
fn quoted_query() {
let query = parse_query("a \"or\" b", false, false, false).unwrap();
let query = parse_query_no_options("a \"or\" b").unwrap();
let a = simple_process("a");
let b = simple_process("b");
@@ -320,7 +385,7 @@ mod tests {
/// as the string `"a or b"`.
#[test]
fn quoted_multi_word_query() {
let query = parse_query("\"a or b\"", false, false, false).unwrap();
let query = parse_query_no_options("\"a or b\"").unwrap();
let a = simple_process("a");
let b = simple_process("b");
@@ -339,7 +404,7 @@ mod tests {
#[test]
fn basic_cpu_query() {
let query = parse_query("cpu > 50", false, false, false).unwrap();
let query = parse_query_no_options("cpu > 50").unwrap();
let mut over = simple_process("a");
over.cpu_usage_percent = 60.0;
@@ -357,7 +422,7 @@ mod tests {
#[test]
fn basic_mem_query() {
let query = parse_query("memb > 1 GiB", false, false, false).unwrap();
let query = parse_query_no_options("memb > 1 GiB").unwrap();
let mut over = simple_process("a");
over.mem_usage = 2 * 1024 * 1024 * 1024;
@@ -376,7 +441,7 @@ mod tests {
/// This test sees if parentheses work.
#[test]
fn nested_query_1() {
let query = parse_query("(a or b) and (c or a)", false, false, false).unwrap();
let query = parse_query_no_options("(a or b) and (c or a)").unwrap();
let a = simple_process("a");
let b = simple_process("b");
@@ -392,7 +457,7 @@ mod tests {
/// This test sees if parentheses and mixed query types work.
#[test]
fn nested_query_2() {
let query = parse_query("(cpu > 10 or cpu < 5) and (c or a)", false, false, false).unwrap();
let query = parse_query_no_options("(cpu > 10 or cpu < 5) and (c or a)").unwrap();
let mut a_valid_1 = simple_process("a");
a_valid_1.cpu_usage_percent = 100.0;
@@ -424,13 +489,8 @@ mod tests {
/// This test adds a further layer of nesting to consider.
#[test]
fn nested_query_3() {
let query = parse_query(
"((cpu > 10 or cpu < 5) or d) and ((c or a) or d)",
false,
false,
false,
)
.unwrap();
let query =
parse_query_no_options("((cpu > 10 or cpu < 5) or d) and ((c or a) or d)").unwrap();
let mut a_valid_1 = simple_process("a");
a_valid_1.cpu_usage_percent = 100.0;
@@ -459,18 +519,38 @@ mod tests {
assert!(!query.check(&b, false));
}
/// Test an ambiguous or.
#[test]
fn ambiguous_or() {}
fn ambiguous_precedence_1() {
let query = parse_query_no_options("a and b or c").unwrap();
let a = simple_process("a");
let b = simple_process("b");
let c = simple_process("c");
assert!(!query.check(&a, false));
assert!(!query.check(&b, false));
assert!(query.check(&c, false));
}
#[test]
fn ambiguous_precedence_2() {
let query = parse_query_no_options("a or b and c").unwrap();
let a = simple_process("a");
let b = simple_process("b");
let c = simple_process("c");
assert!(query.check(&a, false));
assert!(!query.check(&b, false));
assert!(!query.check(&c, false));
}
/// Test if a complicated query even parses.
#[test]
fn parse_complicated_query() {
parse_query(
parse_query_no_options(
"cpu > 10.5 AND (memb = 1 MiB || state = sleeping) and (a or b) and (read >= 0 or write >= 0)",
false,
false,
false,
)
.unwrap();
}
@@ -478,27 +558,308 @@ mod tests {
/// Test empty quotes works.
#[test]
fn parse_empty_quotes() {
parse_query("\"\"", false, false, false).unwrap();
parse_query_no_options("\"\"").unwrap();
parse_query_no_options("\"\"\"\"").unwrap();
parse_query_no_options("\"\" OR \"\"").unwrap();
}
#[test]
fn search_empty_quotes() {
let a = parse_query_no_options("\"\"").unwrap();
let b = parse_query_no_options("\"\" OR test").unwrap();
let process = simple_process("test");
assert!(a.check(&process, false));
assert!(b.check(&process, false));
}
/// Test unfinished quotes error.
#[test]
fn parse_unfinished_quotes() {
parse_query("\"", false, false, false).unwrap_err();
parse_query_no_options("\"").unwrap_err();
parse_query_no_options("\"asdf").unwrap_err();
parse_query_no_options("asdf\"").unwrap_err();
}
/// Test a fix for a bug with closing quotations. The problem seems to arise from quotes being used as an argument
/// to a prefix... but this should probably be valid.
#[test]
fn parse_nested_closing_quotes() {
parse_query("state = \"test\"", false, false, false).unwrap();
parse_query("state = \"2 words\"", false, false, false).unwrap();
parse_query("(memb = 1 MiB || state = \"test\")", false, false, false).unwrap();
parse_query("(memb = 1 MiB || state = \"2 words\")", false, false, false).unwrap();
parse_query_no_options("state = \"test\"").unwrap();
parse_query_no_options("state = \"2 words\"").unwrap();
parse_query_no_options("(memb = 1 MiB || state = \"test\")").unwrap();
parse_query_no_options("(memb = 1 MiB || state = \"2 words\")").unwrap();
}
// TODO: Add this after fixed.
// /// Test if units can ignore spaces from their preceding value.
// #[test]
// fn units_with_and_without_spaces() {}
#[test]
fn invalid_uncompleted_queries_1() {
parse_query_no_options("state =").unwrap_err();
parse_query_no_options("a or").unwrap_err();
parse_query_no_options("a >").unwrap_err();
}
#[track_caller]
fn invalid_lhs_rhs(op: &str) {
parse_query_no_options(&format!("a {op} asdf = 100")).unwrap_err();
parse_query_no_options(&format!("asdf = 100 {op} b")).unwrap_err();
parse_query_no_options(&format!("a {op} asdf = 100 {op} b")).unwrap_err();
parse_query_no_options(&format!("asdf = 100 {op} bsdf = \"")).unwrap_err();
parse_query_no_options(&format!("a {op} bsdf = \"")).unwrap_err();
}
#[test]
fn invalid_or() {
invalid_lhs_rhs("OR");
invalid_lhs_rhs("||");
}
#[test]
fn invalid_and() {
invalid_lhs_rhs("AND");
invalid_lhs_rhs("&&");
invalid_lhs_rhs("");
}
// /// Test keywords.
// ///
// /// TODO: Should these be invalid...?
// #[test]
// fn invalid_query_x() {
// parse_query_no_options("or").unwrap_err();
// parse_query_no_options("and").unwrap_err();
// parse_query_no_options("a or >").unwrap_err();
// parse_query_no_options("a and >").unwrap_err();
// }
#[test]
fn test_command_check() {
let query = parse_query_no_options("command").unwrap();
let mut process_a = simple_process("test");
process_a.command = "command".into();
let mut process_b = simple_process("test");
process_b.command = "no".into();
assert!(query.check(&process_a, true));
assert!(!query.check(&process_b, true));
}
#[test]
fn test_non_ascii_only_1() {
let query = parse_query_no_options("").unwrap();
let process_a = simple_process("施氏食獅史");
let process_b = simple_process("沒有");
assert!(query.check(&process_a, false));
assert!(!query.check(&process_b, false));
}
#[test]
fn test_non_ascii_only_2() {
let query = parse_query_no_options("परीक्षा").unwrap();
let process_a = simple_process("परीक्षा");
let process_b = simple_process("उपलब्ध नहीं है");
assert!(query.check(&process_a, false));
assert!(!query.check(&process_b, false));
}
#[test]
fn test_non_ascii_only_3() {
let query = parse_query_no_options("🇨🇦").unwrap();
let process_a = simple_process("🇨🇦");
let process_b = simple_process("❤️🇨🇦❤️");
let process_c = simple_process("❤️");
assert!(query.check(&process_a, false));
assert!(query.check(&process_b, false));
assert!(!query.check(&process_c, false));
}
#[test]
fn test_non_ascii_only_4() {
let query = parse_query_no_options("獅 or 狮").unwrap();
let process_a = simple_process("施氏食獅史");
let process_b = simple_process("施氏食狮史");
let process_c = simple_process("沒有");
assert!(query.check(&process_a, false));
assert!(query.check(&process_b, false));
assert!(!query.check(&process_c, false));
}
#[test]
fn test_invalid_non_ascii() {
parse_query_no_options("cpu = 食").unwrap_err();
}
#[test]
fn test_mixed_unicode() {
let query = parse_query_no_options("食 or test").unwrap();
let process_a = simple_process("施氏食獅史");
let process_b = simple_process("test");
let process_c = simple_process("施氏食獅史test");
let process_d = simple_process("沒有");
let process_e = simple_process("nope");
assert!(query.check(&process_a, false));
assert!(query.check(&process_b, false));
assert!(query.check(&process_c, false));
assert!(!query.check(&process_d, false));
assert!(!query.check(&process_e, false));
}
#[test]
fn test_regex_1() {
let query = parse_query(
"(a|b)",
&QueryOptions {
whole_word: false,
ignore_case: true,
use_regex: true,
},
)
.unwrap();
let process_a = simple_process("abc");
let process_b = simple_process("test");
assert!(query.check(&process_a, false));
assert!(!query.check(&process_b, false));
}
#[test]
fn test_regex_2() {
let query = parse_query(
"^a.*z$",
&QueryOptions {
whole_word: false,
ignore_case: true,
use_regex: true,
},
)
.unwrap();
let process_a = simple_process("atoz");
let process_b = simple_process("atob");
let process_c = simple_process("ytoz");
let process_d = simple_process("atozoops");
assert!(query.check(&process_a, false));
assert!(!query.check(&process_b, false));
assert!(!query.check(&process_c, false));
assert!(!query.check(&process_d, false));
}
#[test]
fn test_whole_word_1() {
let query = parse_query(
"test",
&QueryOptions {
whole_word: true,
ignore_case: true,
use_regex: false,
},
)
.unwrap();
let process_a = simple_process("test");
let process_b = simple_process("testa");
let process_c = simple_process("atest");
assert!(query.check(&process_a, false));
assert!(!query.check(&process_b, false));
assert!(!query.check(&process_c, false));
}
#[test]
fn test_case_sensitive_1() {
let query = parse_query(
"tEsT",
&QueryOptions {
whole_word: false,
ignore_case: false,
use_regex: false,
},
)
.unwrap();
let process_a = simple_process("tEsT");
let process_b = simple_process("tEsT a");
let process_c = simple_process("a tEsT");
assert!(query.check(&process_a, false));
assert!(query.check(&process_b, false));
assert!(query.check(&process_c, false));
let process_d = simple_process("test");
let process_e = simple_process("test a");
let process_f = simple_process("a test");
assert!(!query.check(&process_d, false));
assert!(!query.check(&process_e, false));
assert!(!query.check(&process_f, false));
}
#[cfg(feature = "gpu")]
#[test]
fn test_gpu_queries() {
let mem = parse_query_no_options("gmem > 50 b").unwrap();
let mem_percent = parse_query_no_options("gmem% = 50").unwrap();
let use_percent = parse_query_no_options("gpu% = 50").unwrap();
let mut process_a = simple_process("test");
process_a.gpu_mem = 100;
process_a.gpu_mem_percent = 50.0;
process_a.gpu_util = 50;
assert!(mem.check(&process_a, false));
assert!(mem_percent.check(&process_a, false));
assert!(use_percent.check(&process_a, false));
let mut process_b = simple_process("test");
process_b.gpu_mem = 0;
process_b.gpu_mem_percent = 10.0;
process_b.gpu_util = 10;
assert!(!mem.check(&process_b, false));
assert!(!mem_percent.check(&process_b, false));
assert!(!use_percent.check(&process_b, false));
}
/// Test GPU queries that involve invalid string comparisons.
#[cfg(feature = "gpu")]
#[test]
fn test_invalid_gpu_queries() {
parse_query_no_options("gmem = \"what\"").unwrap_err();
parse_query_no_options("gmem% = \"the\"").unwrap_err();
parse_query_no_options("gpu% = \"heck\"").unwrap_err();
}
// TODO: Test all attribute keywords (e.g. cpu, mem, etc.)
// #[test]
// fn test_all_attribute_keywords() {}
// TODO: Support 'bytes' or similar
// #[test]
// fn test_bytes_keyword() {
// let mem = parse_query_no_options("mem > 50 bytes").unwrap();
// let mut process_a = simple_process("test");
// process_a.mem_usage = 100;
// assert!(mem.check(&process_a, false));
// }
}
+13 -43
View File
@@ -1,39 +1,22 @@
use std::fmt::{Debug, Formatter};
use std::collections::VecDeque;
use crate::{
collection::processes::ProcessHarvest,
widgets::query::{COMPARISON_LIST, Or, Prefix, QueryProcessor, QueryResult, error::QueryError},
widgets::query::{
COMPARISON_LIST, Or, Prefix, QueryOptions, QueryProcessor, QueryResult, error::QueryError,
},
};
/// A node where both the left hand side or the right hand side are considered.
/// Note that the right hand side is optional, as that's how I implemented it a long time ago.
#[derive(Default)]
#[derive(Debug)]
pub(super) struct And {
pub(super) lhs: Prefix,
// TODO: Maybe don't need to box rhs?
pub(super) rhs: Option<Box<Prefix>>,
}
impl And {
pub(super) fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> QueryResult<()> {
self.lhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
if let Some(rhs) = &mut self.rhs {
rhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
@@ -43,23 +26,14 @@ impl And {
}
}
impl Debug for And {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.rhs {
Some(rhs) => f.write_fmt(format_args!("({:?} AND {:?})", self.lhs, rhs)),
None => f.write_fmt(format_args!("{:?}", self.lhs)),
}
}
}
impl QueryProcessor for And {
fn process(query: &mut std::collections::VecDeque<String>) -> QueryResult<Self>
fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>
where
Self: Sized,
{
const AND_LIST: [&str; 2] = ["and", "&&"];
let mut lhs = Prefix::process(query)?;
let mut lhs = Prefix::process(query, options)?;
let mut rhs: Option<Box<Prefix>> = None;
while let Some(queue_top) = query.front() {
@@ -67,19 +41,15 @@ impl QueryProcessor for And {
if AND_LIST.contains(&current_lowercase.as_str()) {
query.pop_front();
rhs = Some(Box::new(Prefix::process(query)?));
rhs = Some(Box::new(Prefix::process(query, options)?));
if let Some(next_queue_top) = query.front() {
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = Prefix {
or: Some(Box::new(Or {
lhs: And { lhs, rhs },
rhs: None,
})),
regex_prefix: None,
compare_prefix: None,
};
lhs = Prefix::Or(Box::new(Or {
lhs: And { lhs, rhs },
rhs: None,
}));
rhs = None;
} else {
break;
@@ -0,0 +1,141 @@
//! Code related to attributes, which should be "searchable" leaf nodes.
use regex::Regex;
use crate::{
collection::processes::ProcessHarvest,
widgets::query::{
NumericalQuery, PrefixType, QueryOptions, TimeQuery,
error::{QueryError, QueryResult},
new_regex,
},
};
/// An attribute (leaf node) for a process.
#[derive(Debug)]
pub(super) enum ProcessAttribute {
/// This is a bit of a hack to allow for "empty" attributes. We can fix it properly,
/// but it would potentially require handling "empty" queries better. Currently, we just
/// treat it as a leaf node that always succeeds on matches.
Empty,
Pid(Regex),
CpuPercentage(NumericalQuery),
MemBytes(NumericalQuery),
MemPercentage(NumericalQuery),
ReadPerSecond(NumericalQuery),
WritePerSecond(NumericalQuery),
TotalRead(NumericalQuery),
TotalWrite(NumericalQuery),
/// Note this is an "untagged" attribute (e.g. "btm", "firefox").
Name(Regex),
State(Regex),
User(Regex),
Time(TimeQuery),
#[cfg(unix)]
Nice(NumericalQuery),
Priority(NumericalQuery),
#[cfg(feature = "gpu")]
GpuPercentage(NumericalQuery),
#[cfg(feature = "gpu")]
GpuMemoryPercentage(NumericalQuery),
#[cfg(feature = "gpu")]
GpuMemoryBytes(NumericalQuery),
}
impl ProcessAttribute {
pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
match self {
ProcessAttribute::Empty => true,
ProcessAttribute::Pid(re) => re.is_match(process.pid.to_string().as_str()),
ProcessAttribute::CpuPercentage(cmp) => cmp.check(process.cpu_usage_percent),
ProcessAttribute::MemBytes(cmp) => cmp.check(process.mem_usage as f64),
ProcessAttribute::MemPercentage(cmp) => cmp.check(process.mem_usage_percent),
ProcessAttribute::ReadPerSecond(cmp) => cmp.check(process.read_per_sec as f64),
ProcessAttribute::WritePerSecond(cmp) => cmp.check(process.write_per_sec as f64),
ProcessAttribute::TotalRead(cmp) => cmp.check(process.total_read as f64),
ProcessAttribute::TotalWrite(cmp) => cmp.check(process.total_write as f64),
ProcessAttribute::Name(re) => re.is_match(if is_using_command {
process.command.as_str()
} else {
process.name.as_str()
}),
ProcessAttribute::State(re) => re.is_match(process.process_state.0),
ProcessAttribute::User(re) => match process.user.as_ref() {
Some(user) => re.is_match(user),
None => re.is_match("N/A"),
},
ProcessAttribute::Time(time) => time.check(process.time),
// TODO: It's a bit silly for some of these, like nice/priority, where it's casted to an f64.
#[cfg(unix)]
ProcessAttribute::Nice(cmp) => cmp.check(process.nice as f64),
ProcessAttribute::Priority(cmp) => cmp.check(process.priority as f64),
#[cfg(feature = "gpu")]
ProcessAttribute::GpuPercentage(cmp) => cmp.check(process.gpu_util as f64),
#[cfg(feature = "gpu")]
ProcessAttribute::GpuMemoryPercentage(cmp) => cmp.check(process.gpu_mem_percent as f64),
#[cfg(feature = "gpu")]
ProcessAttribute::GpuMemoryBytes(cmp) => cmp.check(process.gpu_mem as f64),
}
}
}
/// Given a string prefix type, obtain the appropriate [`ProcessAttribute`].
pub(super) fn new_string_attribute(
prefix_type: PrefixType, base: &str, regex_options: &QueryOptions,
) -> QueryResult<ProcessAttribute> {
match prefix_type {
PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User => {
let re = new_regex(base, regex_options)?;
match prefix_type {
PrefixType::Pid => Ok(ProcessAttribute::Pid(re)),
PrefixType::Name => Ok(ProcessAttribute::Name(re)),
PrefixType::State => Ok(ProcessAttribute::State(re)),
PrefixType::User => Ok(ProcessAttribute::User(re)),
_ => unreachable!(),
}
}
_ => Err(QueryError::new(format!(
"process attribute type {prefix_type:?} is not a supported string attribute"
))),
}
}
/// Given a time prefix type, obtain the appropriate [`ProcessAttribute`].
pub(super) fn new_time_attribute(
prefix_type: PrefixType, query: TimeQuery,
) -> QueryResult<ProcessAttribute> {
match prefix_type {
PrefixType::Time => Ok(ProcessAttribute::Time(query)),
_ => Err(QueryError::new(format!(
"process attribute type {prefix_type:?} is not a supported time attribute"
))),
}
}
/// Given a numerical prefix type, obtain the appropriate [`ProcessAttribute`].
pub(super) fn new_numerical_attribute(
prefix_type: PrefixType, query: NumericalQuery,
) -> QueryResult<ProcessAttribute> {
match prefix_type {
PrefixType::CpuPercentage => Ok(ProcessAttribute::CpuPercentage(query)),
PrefixType::MemBytes => Ok(ProcessAttribute::MemBytes(query)),
PrefixType::MemPercentage => Ok(ProcessAttribute::MemPercentage(query)),
PrefixType::ReadPerSecond => Ok(ProcessAttribute::ReadPerSecond(query)),
PrefixType::WritePerSecond => Ok(ProcessAttribute::WritePerSecond(query)),
PrefixType::TotalRead => Ok(ProcessAttribute::TotalRead(query)),
PrefixType::TotalWrite => Ok(ProcessAttribute::TotalWrite(query)),
#[cfg(unix)]
PrefixType::Nice => Ok(ProcessAttribute::Nice(query)),
PrefixType::Priority => Ok(ProcessAttribute::Priority(query)),
#[cfg(feature = "gpu")]
PrefixType::GpuPercentage => Ok(ProcessAttribute::GpuPercentage(query)),
#[cfg(feature = "gpu")]
PrefixType::GpuMemoryBytes => Ok(ProcessAttribute::GpuMemoryBytes(query)),
#[cfg(feature = "gpu")]
PrefixType::GpuMemoryPercentage => Ok(ProcessAttribute::GpuMemoryPercentage(query)),
_ => Err(QueryError::new(format!(
"process attribute type {prefix_type:?} is not a supported numerical attribute"
))),
}
}
+10 -45
View File
@@ -1,44 +1,22 @@
use std::{
collections::VecDeque,
fmt::{Debug, Formatter},
};
use std::collections::VecDeque;
use crate::{
collection::processes::ProcessHarvest,
widgets::query::{
And, COMPARISON_LIST, Prefix, QueryProcessor, QueryResult, error::QueryError,
And, COMPARISON_LIST, Prefix, QueryOptions, QueryProcessor, QueryResult, error::QueryError,
},
};
/// A node where either the left hand side or the right hand side are considered.
/// Note that the right hand side is optional, as that's how I implemented it a long time ago.
#[derive(Default)]
/// A node where either the left-hand side or the right-hand side are considered.
/// Note that the right-hand side is optional, as that's how I implemented it a long time ago.
#[derive(Debug)]
pub(super) struct Or {
pub(super) lhs: And,
// TODO: Maybe don't need to box rhs?
pub(super) rhs: Option<Box<And>>,
}
impl Or {
pub(super) fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> QueryResult<()> {
self.lhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
if let Some(rhs) = &mut self.rhs {
rhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
@@ -48,40 +26,27 @@ impl Or {
}
}
impl Debug for Or {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.rhs {
Some(rhs) => f.write_fmt(format_args!("({:?} OR {:?})", self.lhs, rhs)),
None => f.write_fmt(format_args!("{:?}", self.lhs)),
}
}
}
impl QueryProcessor for Or {
fn process(query: &mut VecDeque<String>) -> QueryResult<Self>
fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>
where
Self: Sized,
{
const OR_LIST: [&str; 2] = ["or", "||"];
let mut lhs = And::process(query)?;
let mut lhs = And::process(query, options)?;
let mut rhs: Option<Box<And>> = None;
while let Some(queue_top) = query.front() {
let current_lowercase = queue_top.to_lowercase();
if OR_LIST.contains(&current_lowercase.as_str()) {
query.pop_front();
rhs = Some(Box::new(And::process(query)?));
rhs = Some(Box::new(And::process(query, options)?));
if let Some(queue_next) = query.front() {
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = And {
lhs: Prefix {
or: Some(Box::new(Or { lhs, rhs })),
regex_prefix: None,
compare_prefix: None,
},
lhs: Prefix::Or(Box::new(Or { lhs, rhs })),
rhs: None,
};
rhs = None;
+75 -287
View File
@@ -1,18 +1,15 @@
use std::{
collections::VecDeque,
fmt::{Debug, Formatter},
time::Duration,
};
use std::{collections::VecDeque, fmt::Debug};
use humantime::parse_duration;
use regex::Regex;
use crate::{
collection::processes::ProcessHarvest,
utils::data_units::*,
widgets::query::{
And, ComparableQuery, NumericalQuery, Or, PrefixType, QueryComparison, QueryProcessor,
QueryResult, StringQuery, TimeQuery, error::QueryError,
And, NumericalQuery, Or, PrefixType, ProcessAttribute, QueryComparison, QueryOptions,
QueryProcessor, QueryResult, TimeQuery,
attribute::{new_numerical_attribute, new_string_attribute, new_time_attribute},
error::QueryError,
},
};
@@ -54,188 +51,28 @@ fn process_prefix_units(query: &mut VecDeque<String>, value: &mut f64) {
}
}
/// Either contains a further `Or` recursively, or a "prefix" which is a leaf that can be searched.
/// Either contains a further `Or` recursively, or an attribute that can be queried, possibly as
/// part of a larger query.
///
// TODO: Represent this using an enum instead or something...
#[derive(Default)]
pub(super) struct Prefix {
pub(super) or: Option<Box<Or>>,
pub(super) regex_prefix: Option<(PrefixType, StringQuery)>,
pub(super) compare_prefix: Option<(PrefixType, ComparableQuery)>,
/// In theory, this can be made generic to work on all table types, though for now, it's
/// hardcoded for processes.
#[derive(Debug)]
pub(super) enum Prefix {
Or(Box<Or>),
Attribute(ProcessAttribute),
}
impl Prefix {
pub(super) fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> QueryResult<()> {
if let Some(or) = &mut self.or {
return or.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
);
} else if let Some((
PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User,
StringQuery::Value(regex_string),
)) = &mut self.regex_prefix
{
let escaped_regex: String;
let final_regex_string = &format!(
"{}{}{}{}",
if is_searching_whole_word { "^" } else { "" },
if is_ignoring_case { "(?i)" } else { "" },
if !is_searching_with_regex {
escaped_regex = regex::escape(regex_string);
&escaped_regex
} else {
regex_string
},
if is_searching_whole_word { "$" } else { "" },
);
let taken_pwc = self.regex_prefix.take();
if let Some((taken_pt, _)) = taken_pwc {
self.regex_prefix = Some((
taken_pt,
StringQuery::Regex(Regex::new(final_regex_string)?),
));
}
}
Ok(())
}
pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
fn matches_condition<I: Into<f64>, J: Into<f64>>(
condition: &QueryComparison, lhs: I, rhs: J,
) -> bool {
let lhs: f64 = lhs.into();
let rhs: f64 = rhs.into();
match condition {
QueryComparison::Equal => (lhs - rhs).abs() < f64::EPSILON,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
fn matches_duration(condition: &QueryComparison, lhs: Duration, rhs: Duration) -> bool {
match condition {
QueryComparison::Equal => lhs == rhs,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
if let Some(and) = &self.or {
and.check(process, is_using_command)
} else if let Some((prefix_type, query_content)) = &self.regex_prefix {
if let StringQuery::Regex(r) = query_content {
match prefix_type {
PrefixType::Name => r.is_match(if is_using_command {
process.command.as_str()
} else {
process.name.as_str()
}),
PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
PrefixType::State => r.is_match(process.process_state.0),
PrefixType::User => match process.user.as_ref() {
Some(user) => r.is_match(user),
None => r.is_match("N/A"),
},
_ => true, // TODO: Change prefix types to be tied to the query type so we don't have the wildcard.
}
} else {
true
}
} else if let Some((prefix_type, comparable_query)) = &self.compare_prefix {
match comparable_query {
ComparableQuery::Numerical(numerical_query) => match prefix_type {
PrefixType::PCpu => matches_condition(
&numerical_query.condition,
process.cpu_usage_percent,
numerical_query.value,
),
PrefixType::PMem => matches_condition(
&numerical_query.condition,
process.mem_usage_percent,
numerical_query.value,
),
PrefixType::MemBytes => matches_condition(
&numerical_query.condition,
process.mem_usage as f64,
numerical_query.value,
),
PrefixType::Rps => matches_condition(
&numerical_query.condition,
process.read_per_sec as f64,
numerical_query.value,
),
PrefixType::Wps => matches_condition(
&numerical_query.condition,
process.write_per_sec as f64,
numerical_query.value,
),
PrefixType::TRead => matches_condition(
&numerical_query.condition,
process.total_read as f64,
numerical_query.value,
),
PrefixType::TWrite => matches_condition(
&numerical_query.condition,
process.total_write as f64,
numerical_query.value,
),
#[cfg(feature = "gpu")]
PrefixType::PGpu => matches_condition(
&numerical_query.condition,
process.gpu_util,
numerical_query.value,
),
#[cfg(feature = "gpu")]
PrefixType::GMem => matches_condition(
&numerical_query.condition,
process.gpu_mem as f64,
numerical_query.value,
),
#[cfg(feature = "gpu")]
PrefixType::PGMem => matches_condition(
&numerical_query.condition,
process.gpu_mem_percent,
numerical_query.value,
),
#[cfg(unix)]
PrefixType::Nice => matches_condition(
&numerical_query.condition,
process.nice,
numerical_query.value,
),
PrefixType::Priority => matches_condition(
&numerical_query.condition,
process.priority,
numerical_query.value,
),
_ => true,
},
ComparableQuery::Time(time_query) => match prefix_type {
PrefixType::Time => {
matches_duration(&time_query.condition, process.time, time_query.duration)
}
_ => true,
},
}
} else {
// Somehow we have an empty condition... oh well. Return true.
true
match self {
Prefix::Or(or) => or.check(process, is_using_command),
Prefix::Attribute(attribute) => attribute.check(process, is_using_command),
}
}
fn process_in_quotes(query: &mut VecDeque<String>) -> QueryResult<Self> {
fn process_in_quotes(
query: &mut VecDeque<String>, options: &QueryOptions,
) -> QueryResult<Self> {
if let Some(queue_top) = query.pop_front() {
if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal
@@ -243,11 +80,7 @@ impl Prefix {
// stack. Ugly fix but whatever.
query.push_front("\"".to_string());
Ok(Prefix {
or: None,
regex_prefix: Some((PrefixType::Name, StringQuery::Value(String::default()))),
compare_prefix: None,
})
Ok(Prefix::Attribute(ProcessAttribute::Empty))
} else {
let mut intern_string = vec![queue_top];
@@ -263,11 +96,11 @@ impl Prefix {
let quoted_string = intern_string.join(" ");
Ok(Prefix {
or: None,
regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
compare_prefix: None,
})
Ok(Prefix::Attribute(new_string_attribute(
PrefixType::Name,
&quoted_string,
options,
)?))
}
} else {
// Uh oh, there's nothing left in the stack, but we're inside quotes!
@@ -276,22 +109,8 @@ impl Prefix {
}
}
impl Debug for Prefix {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(or) = &self.or {
f.write_fmt(format_args!("{or:?}"))
} else if let Some(regex_prefix) = &self.regex_prefix {
f.write_fmt(format_args!("{regex_prefix:?}"))
} else if let Some(compare_prefix) = &self.compare_prefix {
f.write_fmt(format_args!("{compare_prefix:?}"))
} else {
f.write_str("")
}
}
}
impl QueryProcessor for Prefix {
fn process(query: &mut VecDeque<String>) -> QueryResult<Self>
fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>
where
Self: Sized,
{
@@ -305,59 +124,42 @@ impl QueryProcessor for Prefix {
while let Some(in_paren_query_top) = query.front() {
if in_paren_query_top != ")" {
list_of_ors.push_back(Or::process(query)?);
list_of_ors.push_back(Or::process(query, options)?);
} else {
break;
}
}
// Ensure not empty
if list_of_ors.is_empty() {
let Some(front) = list_of_ors.pop_front() else {
return Err(QueryError::new("No values within parentheses group"));
}
};
// Now convert this back to a OR...
// TODO: This seems like a bad way to do it.
// TODO: is there a better way to do this than converting it?
let initial_or = Or {
lhs: And {
lhs: Prefix {
or: list_of_ors.pop_front().map(Box::new),
compare_prefix: None,
regex_prefix: None,
},
lhs: Prefix::Or(Box::new(front)),
rhs: None,
},
rhs: None,
};
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
lhs: And {
lhs: Prefix {
or: Some(Box::new(lhs)),
compare_prefix: None,
regex_prefix: None,
},
rhs: Some(Box::new(Prefix {
or: Some(Box::new(rhs)),
compare_prefix: None,
regex_prefix: None,
})),
lhs: Prefix::Or(Box::new(lhs)),
rhs: Some(Box::new(Prefix::Or(Box::new(rhs)))),
},
rhs: None,
});
if let Some(close_paren) = query.pop_front() {
return if let Some(close_paren) = query.pop_front() {
if close_paren == ")" {
return Ok(Prefix {
or: Some(Box::new(returned_or)),
regex_prefix: None,
compare_prefix: None,
});
Ok(Prefix::Or(Box::new(returned_or)))
} else {
return Err(QueryError::new("Missing closing parentheses"));
Err(QueryError::new("Missing closing parentheses"))
}
} else {
return Err(QueryError::new("Missing closing parentheses"));
}
Err(QueryError::new("Missing closing parentheses"))
};
} else if curr == ")" {
return Err(QueryError::new("Missing opening parentheses"));
} else if curr == "\"" {
@@ -365,16 +167,16 @@ impl QueryProcessor for Prefix {
// however, that we will DIRECTLY call another process_prefix
// call...
let prefix = Prefix::process_in_quotes(query)?;
if let Some(close_quote) = query.pop_front() {
let prefix = Prefix::process_in_quotes(query, options)?;
return if let Some(close_quote) = query.pop_front() {
if close_quote == "\"" {
return Ok(prefix);
Ok(prefix)
} else {
return Err(QueryError::new("Missing closing quotation"));
Err(QueryError::new("Missing closing quotation"))
}
} else {
return Err(QueryError::new("Missing closing quotation"));
}
Err(QueryError::new("Missing closing quotation"))
};
} else {
// Get prefix type.
let prefix_type = curr.parse::<PrefixType>()?;
@@ -389,11 +191,11 @@ impl QueryProcessor for Prefix {
if let Some(content) = content {
match &prefix_type {
PrefixType::Name => {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
});
return Ok(Prefix::Attribute(new_string_attribute(
prefix_type,
&content,
options,
)?));
}
PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an "="...
@@ -429,21 +231,18 @@ impl QueryProcessor for Prefix {
string_value
};
return Ok(Prefix {
or: None,
regex_prefix: Some((
prefix_type,
StringQuery::Value(final_value),
)),
compare_prefix: None,
});
return Ok(Prefix::Attribute(new_string_attribute(
prefix_type,
&final_value,
options,
)?));
}
} else {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
});
return Ok(Prefix::Attribute(new_string_attribute(
prefix_type,
&content,
options,
)?));
}
}
PrefixType::Time => {
@@ -481,17 +280,13 @@ impl QueryProcessor for Prefix {
)
.map_err(|err| QueryError::new(err.to_string()))?;
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
ComparableQuery::Time(TimeQuery {
condition,
duration,
}),
)),
});
return Ok(Prefix::Attribute(new_time_attribute(
prefix_type,
TimeQuery {
condition,
duration,
},
)?));
}
}
_ => {
@@ -549,30 +344,23 @@ impl QueryProcessor for Prefix {
match prefix_type {
PrefixType::MemBytes
| PrefixType::Rps
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
| PrefixType::ReadPerSecond
| PrefixType::WritePerSecond
| PrefixType::TotalRead
| PrefixType::TotalWrite => {
process_prefix_units(query, &mut value);
}
#[cfg(feature = "gpu")]
PrefixType::GMem => {
PrefixType::GpuMemoryBytes => {
process_prefix_units(query, &mut value);
}
_ => {}
}
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
ComparableQuery::Numerical(NumericalQuery {
condition,
value,
}),
)),
});
return Ok(Prefix::Attribute(new_numerical_attribute(
prefix_type,
NumericalQuery { condition, value },
)?));
}
}
}
+10
View File
@@ -141,6 +141,16 @@ fn test_missing_default_widget_type() {
));
}
#[test]
fn test_invalid_default_cpu_entry() {
no_cfg_btm_command()
.arg("--default_cpu_entry")
.arg("invalid")
.assert()
.failure()
.stderr(predicate::str::contains("possible values"));
}
#[test]
#[cfg_attr(feature = "battery", ignore)]
fn test_battery_flag() {