Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support command filter on pueue status #560

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ TLDR: The new task state representation is more verbose but significantly cleane
- Add `pueue reset --groups [group_names]` to allow resetting individual groups. [#482](https://github.com/Nukesor/pueue/issues/482) \
This also refactors the way resets are done internally, resulting in a cleaner code architecture.
- Ability to set the Unix socket permissions through the new `unix_socket_permissions` configuration option. [#544](https://github.com/Nukesor/pueue/pull/544)
- Add `command` filter to `pueue status`. [#524](https://github.com/Nukesor/pueue/issues/524) [#560](https://github.com/Nukesor/pueue/pull/560)
- Allow `pueue status` to order tasks by `enqueue_at`. [#554](https://github.com/Nukesor/pueue/issues/554)

## \[3.4.1\] - 2024-06-04
Expand Down
10 changes: 9 additions & 1 deletion pueue/src/client/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ where:
- column := `id | status | command | label | path | enqueue_at | dependencies | start | end`
- filter := `[filter_column] [filter_op] [filter_value]`
(note: not all columns support all operators, see \"Filter columns\" below.)
- filter_column := `start | end | enqueue_at | status | label`
- filter_column := `status | command | label | start | end | enqueue_at`
- filter_op := `= | != | < | > | %=`
(`%=` means 'contains', as in the test value is a substring of the column value)
- order_by := `order_by [column] [order_direction]`
Expand All @@ -325,6 +325,12 @@ where:
- limit_count := a positive integer

Filter columns:
- `status` supports the operators `=`, `!=`
against test values that are:
- strings like `queued`, `stashed`, `paused`, `running`, `success`, `failed`
- `command`, `label` support the operators `=`, `!=`, `%=`
against test values that are:
- strings like `some text`
- `start`, `end`, `enqueue_at` contain a datetime
which support the operators `=`, `!=`, `<`, `>`
against test values that are:
Expand All @@ -335,6 +341,8 @@ Filter columns:

Examples:
- `status=running`
- `command%=echo`
- `label=mytask`
- `columns=id,status,command status=running start > 2023-05-2112:03:17 order_by command first 5`

The formal syntax is defined here:
Expand Down
31 changes: 31 additions & 0 deletions pueue/src/client/query/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,37 @@
Ok(())
}

/// Parse a filter for the command field.
///
/// This filter syntax is exactly the same as the [label] filter.
/// Only the keyword changed from `label` to `command`.
pub fn command(section: Pair<'_, Rule>, query_result: &mut QueryResult) -> Result<()> {
let mut filter = section.into_inner();
// The first word should be the `command` keyword.
let _command = filter.next().unwrap();

// Get the operator that should be applied in this filter.
// Can be either of [Rule::eq | Rule::neq].
let operator = filter.next().unwrap().as_rule();

// Get the name of the command we should filter for.
let operand = filter.next().unwrap().as_str().to_string();

// Build the command filter function.
let filter_function = Box::new(move |task: &Task| -> bool {
let command = task.command.as_str();
match operator {
Rule::eq => command == operand,
Rule::neq => command != operand,
Rule::contains => command.contains(&operand),
_ => false,

Check warning on line 281 in pueue/src/client/query/filters.rs

View check run for this annotation

Codecov / codecov/patch

pueue/src/client/query/filters.rs#L281

Added line #L281 was not covered by tests
}
});
query_result.filters.push(filter_function);

Ok(())
}

/// Parse a filter for the status field.
///
/// This filter syntax looks like this:
Expand Down
1 change: 1 addition & 0 deletions pueue/src/client/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ pub fn apply_query(query: &str, group: &Option<String>) -> Result<QueryResult> {
Rule::column_selection => column_selection::apply(section, &mut query_result)?,
Rule::datetime_filter => filters::datetime(section, &mut query_result)?,
Rule::label_filter => filters::label(section, &mut query_result)?,
Rule::command_filter => filters::command(section, &mut query_result)?,
Rule::status_filter => filters::status(section, &mut query_result)?,
Rule::order_by_condition => order_by::order_by(section, &mut query_result)?,
Rule::limit_condition => limit::limit(section, &mut query_result)?,
Expand Down
6 changes: 5 additions & 1 deletion pueue/src/client/query/syntax.pest
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ status_filter = { column_status ~ (eq | neq) ~ (status_queued | status_stashed |
label = { ANY* }
label_filter = { column_label ~ ( eq | neq | contains ) ~ label }

// Command filter
command = { ANY* }
command_filter = { column_command ~ ( eq | neq | contains ) ~ command }

// Time related filters
datetime = { ASCII_DIGIT{4} ~ "-" ~ ASCII_DIGIT{2} ~ "-" ~ ASCII_DIGIT{2} ~ ASCII_DIGIT{2} ~ ":" ~ ASCII_DIGIT{2} ~ (":" ~ ASCII_DIGIT{2})? }
date = { ASCII_DIGIT{4} ~ "-" ~ ASCII_DIGIT{2} ~ "-" ~ ASCII_DIGIT{2} }
Expand All @@ -67,4 +71,4 @@ limit_count = { ASCII_DIGIT* }
limit_condition = { (first | last) ~ limit_count }

// ----- The final query syntax -----
query = { SOI ~ column_selection? ~ ( datetime_filter | status_filter | label_filter )*? ~ order_by_condition? ~ limit_condition? ~ EOI }
query = { SOI ~ column_selection? ~ ( datetime_filter | status_filter | label_filter | command_filter )*? ~ order_by_condition? ~ limit_condition? ~ EOI }
63 changes: 60 additions & 3 deletions pueue/tests/client/unit/status_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ use pueue::client::query::{apply_query, Rule};
use pueue_lib::state::PUEUE_DEFAULT_GROUP;
use pueue_lib::task::{Task, TaskResult, TaskStatus};

const TEST_COMMAND_0: &str = "sleep 60";
const TEST_COMMAND_1: &str = "echo Hello Pueue";

/// A small helper function to reduce a bit of boilerplate.
pub fn build_task() -> Task {
Task::new(
"sleep 60".to_owned(),
TEST_COMMAND_0.to_owned(),
PathBuf::from("/tmp"),
HashMap::new(),
PUEUE_DEFAULT_GROUP.to_owned(),
Expand Down Expand Up @@ -79,9 +82,10 @@ pub fn test_tasks() -> Vec<Task> {
running.id = 4;
tasks.insert(running.id, running);

// Add two queued tasks
// Add two queued tasks with different command
let mut queued = build_task();
queued.id = 5;
queued.command = TEST_COMMAND_1.to_string();
tasks.insert(queued.id, queued.clone());

// Task 6 depends on task 5
Expand Down Expand Up @@ -321,7 +325,7 @@ async fn order_by_enqueue_at() -> Result<()> {
Ok(())
}

/// Filter tasks by label with the "contains" `%=` filter.
/// Filter tasks by label with the "eq" `=` "ne" `!=` and "contains" `%=`filter.
#[rstest]
#[case("%=", "label", 3)]
#[case("%=", "label-10", 3)]
Expand Down Expand Up @@ -373,3 +377,56 @@ async fn filter_label(

Ok(())
}

/// Filter tasks by command with the "eq" `=` "ne" `!=` and "contains" `%=`filter.
#[rstest]
#[case("=", TEST_COMMAND_0, 5)]
#[case("%=", &TEST_COMMAND_0[..4], 5)]
#[case("!=", TEST_COMMAND_0, 2)]
#[case("=", TEST_COMMAND_1, 2)]
#[case("!=", TEST_COMMAND_1, 5)]
#[case("%=", &TEST_COMMAND_1[..4], 2)]
magicwenli marked this conversation as resolved.
Show resolved Hide resolved
#[case("!=", "nonexist", 7)]
#[case("%=", "nonexist", 0)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_command(
#[case] operator: &'static str,
#[case] command_filter: &'static str,
#[case] match_count: usize,
) -> Result<()> {
let tasks = test_tasks_with_query(&format!("command{operator}{command_filter}"), &None)?;

for task in tasks.iter() {
// Make sure the task either has no command or the command doesn't match the filter.
magicwenli marked this conversation as resolved.
Show resolved Hide resolved
if operator == "!=" {
let command = &task.command;
assert_ne!(
command, command_filter,
"Command '{command}' matched exact filter '{command_filter}'"
);
}

let command = task.command.as_str();
magicwenli marked this conversation as resolved.
Show resolved Hide resolved
if operator == "%=" {
magicwenli marked this conversation as resolved.
Show resolved Hide resolved
// Make sure the command contained our filter.
assert!(
command.contains(command_filter),
"Command '{command}' didn't contain filter '{command_filter}'"
);
} else if operator == "=" {
// Make sure the command exactly matches the filter.
assert_eq!(
command, command_filter,
"Command '{command}' didn't match exact filter '{command_filter}'"
);
}
}

assert_eq!(
tasks.len(),
match_count,
"Got a different amount of tasks than expected for the command filter: {command_filter}."
);

Ok(())
}
Loading