Upstream Nukesor/pueue state of the touched files before a1d47a62e4

This commit is contained in:
upstream committed 2026-09-02 09:42:40 +02:00
commit eb9a320061
2 files changed
+1016

No files matched your search

@@ -0,0 +1,381 @@
#![allow(bindings_with_variant_name)]
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta};
use pest::iterators::Pair;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use super::{QueryResult, Rule};
use crate::internal_prelude::*;
enum DateOrDateTime {
DateTime(DateTime<Local>),
Date(NaiveDate),
}
/// Parse a datetime/date/time filter.
/// Such a filter can be applied to either the `start`, `end` or `enqueue_at` field.
///
/// This filter syntax looks like this is expected to be:
/// `[enqueue_at|start|end] [>|<|=|!=] [YYYY-MM-DD HH:mm:SS|HH:mm:SS|YYYY-MM-DD]`
///
/// The data structure looks something like this:
/// Pair {
/// rule: datetime_filter,
/// span: Span {
/// str: "start=2022-09-01",
/// start: 0,
/// end: 16,
/// },
/// inner: [
/// Pair {
/// rule: column_start,
/// span: Span {
/// str: "start",
/// start: 0,
/// end: 5,
/// },
/// inner: [],
/// },
/// Pair {
/// rule: operator,
/// span: Span {
/// str: "=",
/// start: 5,
/// end: 6,
/// },
/// inner: [
/// Pair {
/// rule: eq,
/// span: Span {
/// str: "=",
/// start: 5,
/// end: 6,
/// },
/// inner: [],
/// },
/// ],
/// },
/// Pair {
/// rule: date,
/// span: Span {
/// str: "2022-09-01",
/// start: 6,
/// end: 16,
/// },
/// inner: [],
/// },
/// ],
/// }
pub fn datetime(section: Pair<'_, Rule>, query_result: &mut QueryResult) -> Result<()> {
let mut filter = section.into_inner();
// Get the column this filter should be applied to.
// Either of [Rule::column_enqueue_at | Rule::column_start | Rule::column_end]
let column = filter.next().unwrap();
let column = column.as_rule();
// Get the operator that should be applied in this filter.
// Either of [Rule::eq | Rule::neq | Rule::lt | Rule::gt]
let operator = filter.next().unwrap().as_rule();
// Get the point in time which we should filter for.
// This can be either a Date or a DateTime.
let operand = filter.next().unwrap();
let operand_rule = operand.as_rule();
let operand = match operand_rule {
Rule::time => {
let time = NaiveTime::parse_from_str(operand.as_str(), "%X")
.context("Expected hh:mm:ss time format")?;
let today = Local::now().date_naive();
let datetime = today.and_time(time).and_local_timezone(Local).unwrap();
DateOrDateTime::DateTime(datetime)
}
Rule::datetime => {
let datetime = NaiveDateTime::parse_from_str(operand.as_str(), "%F %X")
.context("Expected YYYY-MM-SS hh:mm:ss date time format")?;
DateOrDateTime::DateTime(datetime.and_local_timezone(Local).unwrap())
}
Rule::date => {
let date = NaiveDate::parse_from_str(operand.as_str(), "%F")
.context("Expected YYYY-MM-SS date format")?;
DateOrDateTime::Date(date)
}
_ => bail!("Expected either a date, datetime or time expression."),
};
let filter_function = Box::new(move |task: &Task| -> bool {
// Get the field we should apply the filter to.
let field = match column {
Rule::column_enqueue_at => {
let TaskStatus::Stashed {
enqueue_at: Some(enqueue_at),
} = task.status
else {
return false;
};
enqueue_at
}
Rule::column_start => {
let (start, _) = task.start_and_end();
let Some(start) = start else {
return false;
};
start
}
Rule::column_end => {
let (_, end) = task.start_and_end();
let Some(end) = end else {
return false;
};
end
}
_ => return true,
};
// Apply the operator to the operands.
// The operator might have a different meaning depending on the type of datetime/date
// we're dealing with.
// E.g. when working with dates, `>` should mean bigger than the end of that day.
// `<` however should mean before that day.
match operand {
DateOrDateTime::DateTime(datetime) => match operator {
Rule::eq => field == datetime,
Rule::neq => field != datetime,
Rule::lt => field < datetime,
Rule::gt => field > datetime,
_ => true,
},
DateOrDateTime::Date(date) => {
// Get the start of the given day.
// Use the most inclusive datetime in case of ambiguity
let start_of_day = date
.and_hms_opt(0, 0, 0)
.expect("Couldn't determine start of day for given date.")
.and_local_timezone(Local);
let start_of_day = match start_of_day.latest() {
None => return false,
Some(datetime) => datetime,
};
// Get the end of the given day.
// Use the most inclusive datetime in case of ambiguity
let end_of_day = (date + TimeDelta::try_days(1).unwrap())
.and_hms_opt(0, 0, 0)
.expect("Couldn't determine start of day for given date.")
.and_local_timezone(Local);
let end_of_day = match end_of_day.latest() {
None => return false,
Some(datetime) => datetime,
};
match operator {
Rule::eq => field > start_of_day && field < end_of_day,
Rule::neq => field < start_of_day && field > end_of_day,
Rule::lt => field < start_of_day,
Rule::gt => field > end_of_day,
_ => true,
}
}
}
});
query_result.filters.push(filter_function);
Ok(())
}
/// Parse a filter for the label field.
///
/// This filter syntax looks like this:
/// `label [=|!=] string`
///
/// The data structure looks something like this:
/// Pair {
/// rule: label_filter,
/// span: Span {
/// str: "label=test",
/// start: 0,
/// end: 10,
/// },
/// inner: [
/// Pair {
/// rule: column_label,
/// span: Span {
/// str: "label",
/// start: 0,
/// end: 5,
/// },
/// inner: [],
/// },
/// Pair {
/// rule: eq,
/// span: Span {
/// str: "=",
/// start: 5,
/// end: 6,
/// },
/// inner: [],
/// },
/// Pair {
/// rule: label,
/// span: Span {
/// str: "test",
/// start: 6,
/// end: 10,
/// },
/// inner: [],
/// },
/// ],
/// }
pub fn label(section: Pair<'_, Rule>, query_result: &mut QueryResult) -> Result<()> {
let mut filter = section.into_inner();
// The first word should be the `label` keyword.
let _label = 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 label we should filter for.
let operand = filter.next().unwrap().as_str().to_string();
// Build the label filter function.
let filter_function = Box::new(move |task: &Task| -> bool {
let Some(label) = &task.label else {
return operator == Rule::neq;
};
match operator {
Rule::eq => label == &operand,
Rule::neq => label != &operand,
Rule::contains => label.contains(&operand),
_ => false,
}
});
query_result.filters.push(filter_function);
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,
}
});
query_result.filters.push(filter_function);
Ok(())
}
/// Parse a filter for the status field.
///
/// This filter syntax looks like this:
/// `status [=|!=] [status]`
///
/// The data structure looks something like this:
/// Pair {
/// rule: status_filter,
/// span: Span {
/// str: "status=success",
/// start: 0,
/// end: 14,
/// },
/// inner: [
/// Pair {
/// rule: column_status,
/// span: Span {
/// str: "status",
/// start: 0,
/// end: 6,
/// },
/// inner: [],
/// },
/// Pair {
/// rule: eq,
/// span: Span {
/// str: "=",
/// start: 6,
/// end: 7,
/// },
/// inner: [],
/// },
/// Pair {
/// rule: status_success,
/// span: Span {
/// str: "success",
/// start: 7,
/// end: 14,
/// },
/// inner: [],
/// },
/// ],
/// }
pub fn status(section: Pair<'_, Rule>, query_result: &mut QueryResult) -> Result<()> {
let mut filter = section.into_inner();
// The first word should be the `status` keyword.
let _status = 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 status we should filter for.
let operand = filter.next().unwrap().as_rule();
// Build the filter function for the task's status.
let filter_function = Box::new(move |task: &Task| -> bool {
let matches = match operand {
Rule::status_queued => matches!(task.status, TaskStatus::Queued { .. }),
Rule::status_stashed => matches!(task.status, TaskStatus::Stashed { .. }),
Rule::status_running => matches!(task.status, TaskStatus::Running { .. }),
Rule::status_paused => matches!(task.status, TaskStatus::Paused { .. }),
Rule::status_success => {
matches!(
&task.status,
TaskStatus::Done {
result: TaskResult::Success,
..
}
)
}
Rule::status_failed => {
let mut matches = false;
if let TaskStatus::Done { result, .. } = &task.status
&& !matches!(result, TaskResult::Success)
{
matches = true;
}
matches
}
_ => return false,
};
match operator {
Rule::eq => matches,
Rule::neq => !matches,
_ => false,
}
});
query_result.filters.push(filter_function);
Ok(())
}
@@ -0,0 +1,635 @@
// Clippy generates a false-positive for an empty generated docstring in the query parser code.
#![allow(clippy::empty_docs)]
use chrono::prelude::*;
use pest::Parser;
use pest_derive::Parser;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use crate::internal_prelude::*;
mod column_selection;
mod filters;
mod limit;
mod order_by;
use limit::Limit;
use order_by::Direction;
/// See the pest docs on how this derive macro works and how to use pest:
/// <https://docs.rs/pest/latest/pest/>
#[derive(Parser)]
#[grammar = "./src/client/commands/state/query/syntax.pest"]
struct QueryParser;
type FilterFunction = dyn Fn(&Task) -> bool;
/// All applicable information that has been extracted from the query.
#[derive(Default)]
pub struct QueryResult {
/// Filter results for a single group.
group: Option<String>,
/// The list of selected columns based.
pub selected_columns: Vec<Rule>,
/// A list of filter functions that should be applied to the list of tasks.
filters: Vec<Box<FilterFunction>>,
/// A list of filter functions that should be applied to the list of tasks.
order_by: Option<(Rule, Direction)>,
/// Limit
limit: Option<(Limit, usize)>,
}
impl QueryResult {
/// Take a list of tasks and apply all filters to it.
pub fn apply_filters(&self, tasks: Vec<Task>) -> Vec<Task> {
let mut iter = tasks.into_iter();
// If requested, only look at tasks of a specific group.
if let Some(group) = &self.group {
iter = iter
.filter(|task| task.group == *group)
.collect::<Vec<Task>>()
.into_iter();
}
for filter in self.filters.iter() {
iter = iter.filter(filter).collect::<Vec<Task>>().into_iter();
}
iter.collect()
}
/// Take a list of tasks and apply all filters to it.
pub fn order_tasks(&self, mut tasks: Vec<Task>) -> Vec<Task> {
// Only apply ordering if it was requested.
let Some((column, direction)) = &self.order_by else {
return tasks;
};
// Sort the tasks by the specified column.
tasks.sort_by(|task1, task2| match column {
Rule::column_id => task1.id.cmp(&task2.id),
Rule::column_status => {
/// Rank a task status to allow ordering by status.
/// Returns a u8 based on the expected
fn rank_status(task: &Task) -> u8 {
match &task.status {
TaskStatus::Stashed { .. } => 0,
TaskStatus::Locked { .. } => 1,
TaskStatus::Queued { .. } => 2,
TaskStatus::Paused { .. } => 3,
TaskStatus::Running { .. } => 4,
TaskStatus::Done { result, .. } => match result {
TaskResult::Success => 6,
_ => 5,
},
}
}
rank_status(task1).cmp(&rank_status(task2))
}
Rule::column_label => task1.label.cmp(&task2.label),
Rule::column_command => task1.command.cmp(&task2.command),
Rule::column_path => task1.path.cmp(&task2.path),
Rule::column_enqueue_at => {
fn enqueue_date(task: &Task) -> DateTime<Local> {
match &task.status {
TaskStatus::Queued { enqueued_at, .. }
| TaskStatus::Running { enqueued_at, .. }
| TaskStatus::Paused { enqueued_at, .. }
| TaskStatus::Done { enqueued_at, .. }
| TaskStatus::Stashed {
enqueue_at: Some(enqueued_at),
..
} => *enqueued_at,
// considered far in the future when no explicit date:
_ => DateTime::<Utc>::MAX_UTC.into(),
}
}
enqueue_date(task1).cmp(&enqueue_date(task2))
}
Rule::column_start => {
let (start1, _) = task1.start_and_end();
let (start2, _) = task2.start_and_end();
start1.cmp(&start2)
}
Rule::column_end => {
let (_, end1) = task1.start_and_end();
let (_, end2) = task2.start_and_end();
end1.cmp(&end2)
}
_ => std::cmp::Ordering::Less,
});
// Reverse the order, if we're in ordering by descending order.
if let Direction::Descending = direction {
tasks.reverse();
}
tasks
}
/// Take a list of tasks and apply all filters to it.
pub fn limit_tasks(&self, tasks: Vec<Task>) -> Vec<Task> {
// Only apply limits if it was requested.
let Some((direction, count)) = &self.limit else {
return tasks;
};
// Don't do anything if:
// - we don't have to limit
// - the limit is invalid
if tasks.len() <= *count || *count == 0 {
return tasks;
}
match direction {
Limit::First => tasks[0..*count].to_vec(),
Limit::Last => tasks[(tasks.len() - count)..].to_vec(),
}
}
}
/// Take a given `pueue status QUERY` and apply it to all components that're involved in the
/// `pueue status` process:
///
/// - TableBuilder: The component responsible for building the table and determining which columns
/// should or need to be displayed. A `columns [columns]` statement will define the set of visible
/// columns.
pub fn apply_query(query: &str, group: &Option<String>) -> Result<QueryResult> {
let mut parsed = QueryParser::parse(Rule::query, query).context("Failed to parse query")?;
let mut query_result = QueryResult {
group: group.clone(),
..Default::default()
};
// Expect there to be exactly one pair for the full query.
// Return early if we got an empty query.
let Some(parsed) = parsed.next() else {
return Ok(query_result);
};
// Make sure we really got a query.
if parsed.as_rule() != Rule::query {
bail!("Expected a valid query");
}
// Get the sections of the query
let sections = parsed.into_inner();
// Go through each section and handle it accordingly
for section in sections {
// The `columns=[columns]` section
// E.g. `columns=id,status,start,end`
match section.as_rule() {
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)?,
_ => (),
}
}
Ok(query_result)
}
#[cfg(test)]
mod test {
use std::{collections::HashMap, path::PathBuf};
use assert_matches::assert_matches;
use chrono::{Local, TimeZone};
use pretty_assertions::assert_eq;
use pueue_lib::{
state::PUEUE_DEFAULT_GROUP,
task::{Task, TaskResult, TaskStatus},
};
use rstest::rstest;
use super::{Rule, apply_query};
use crate::internal_prelude::*;
const TEST_COMMAND_SLEEP: &str = "sleep 60";
const TEST_COMMAND_HELLO: &str = "echo Hello Pueue";
/// A small helper function to reduce a bit of boilerplate.
pub fn build_task() -> Task {
Task::new(
TEST_COMMAND_SLEEP.to_owned(),
PathBuf::from("/tmp"),
HashMap::new(),
PUEUE_DEFAULT_GROUP.to_owned(),
TaskStatus::Queued {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
},
Vec::new(),
0,
None,
)
}
/// Build a list of some pre-build tasks that are used to test.
pub fn test_tasks() -> Vec<Task> {
let mut tasks = Vec::new();
// Failed task
let mut failed = build_task();
failed.id = 0;
failed.status = TaskStatus::Done {
result: TaskResult::Failed(255),
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 10, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 10, 10, 10, 0).unwrap(),
};
failed.label = Some("label-10-0".to_string());
tasks.insert(failed.id, failed);
// Successful task
let mut successful = build_task();
successful.id = 1;
successful.status = TaskStatus::Done {
result: TaskResult::Success,
enqueued_at: Local.with_ymd_and_hms(2022, 1, 8, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 8, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 8, 10, 10, 0).unwrap(),
};
successful.label = Some("label-10-1".to_string());
tasks.insert(successful.id, successful);
// Stashed task
let mut stashed = build_task();
stashed.status = TaskStatus::Stashed { enqueue_at: None };
stashed.id = 2;
stashed.label = Some("label-10-2".to_string());
tasks.insert(stashed.id, stashed);
// Scheduled task
let mut scheduled = build_task();
scheduled.status = TaskStatus::Stashed {
enqueue_at: Some(Local.with_ymd_and_hms(2022, 1, 10, 11, 0, 0).unwrap()),
};
scheduled.id = 3;
scheduled.group = "testgroup".to_string();
tasks.insert(scheduled.id, scheduled);
// Running task
let mut running = build_task();
running.status = TaskStatus::Running {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 2, 12, 0, 0).unwrap(),
};
running.id = 4;
tasks.insert(running.id, running);
// Add two queued tasks with different command
let mut queued = build_task();
queued.id = 5;
queued.command = TEST_COMMAND_HELLO.to_string();
tasks.insert(queued.id, queued.clone());
// Task 6 depends on task 5
queued.id = 6;
queued.dependencies.push(5);
tasks.insert(queued.id, queued);
tasks
}
fn test_tasks_with_query(query: &str, group: &Option<String>) -> Result<Vec<Task>> {
let mut tasks = test_tasks();
let query_result = apply_query(query, group)?;
tasks = query_result.apply_filters(tasks);
tasks = query_result.order_tasks(tasks);
tasks = query_result.limit_tasks(tasks);
Ok(tasks)
}
/// Select only specific columns for printing
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn column_selection() -> Result<()> {
let result = apply_query("columns=id,status,command", &None)?;
assert_eq!(
result.selected_columns,
[Rule::column_id, Rule::column_status, Rule::column_command]
);
Ok(())
}
/// Select the first few entries of the list
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn limit_first() -> Result<()> {
let tasks = test_tasks_with_query("first 4", &None)?;
assert!(tasks.len() == 4);
assert_eq!(tasks[0].id, 0);
assert_eq!(tasks[3].id, 3);
Ok(())
}
/// Select the last few entries of the list
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn limit_last() -> Result<()> {
let tasks = test_tasks_with_query("last 4", &None)?;
assert!(tasks.len() == 4);
assert_eq!(tasks[0].id, 3);
assert_eq!(tasks[3].id, 6);
Ok(())
}
/// Filter by start date
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_start() -> Result<()> {
let tasks = test_tasks_with_query("start>2022-01-10 09:00:00", &None)?;
assert!(tasks.len() == 1);
assert_eq!(tasks[0].id, 0);
Ok(())
}
/// Filtering in combination with groups works as expected
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_with_group() -> Result<()> {
let tasks = test_tasks_with_query("status=stashed", &Some("testgroup".to_string()))?;
assert!(tasks.len() == 1);
assert_eq!(tasks[0].id, 3);
Ok(())
}
/// Filter by end date with the current time as a time and a date.
#[rstest]
#[case("2022-01-10")]
#[case("2022-01-10 09:00:00")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_end_with_time(#[case] format: &'static str) -> Result<()> {
let tasks = test_tasks_with_query(&format!("end<{format}"), &None)?;
assert!(tasks.len() == 1);
assert_eq!(tasks[0].id, 1);
Ok(())
}
/// Filter tasks by status
#[rstest]
#[case("queued", 2)]
#[case("running", 1)]
#[case("paused", 0)]
#[case("success", 1)]
#[case("failed", 1)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_status(#[case] status_filter: &str, #[case] match_count: usize) -> Result<()> {
// Get the correct query keyword for the given status.
let tasks = test_tasks_with_query(&format!("status={status_filter}"), &None)?;
for task in tasks.iter() {
match status_filter {
"queued" => {
assert_matches!(
task.status,
TaskStatus::Queued { .. },
"Only Queued tasks are allowed"
);
}
"stashed" => assert_matches!(
task.status,
TaskStatus::Stashed { .. },
"Only Stashed tasks are allowed"
),
"running" => assert_matches!(
task.status,
TaskStatus::Running { .. },
"Only Running tasks are allowed"
),
"paused" => assert_matches!(
task.status,
TaskStatus::Paused { .. },
"Only Paused tasks are allowed"
),
"success" => assert_matches!(
task.status,
TaskStatus::Done {
result: TaskResult::Success,
..
},
"Only Succesful tasks are allowed"
),
"failed" => assert_matches!(
task.status,
TaskStatus::Done {
result: TaskResult::Failed(_),
..
},
"Only Failed tasks are allowed"
),
_ => bail!("Got unexpected TaskStatus in filter_status"),
};
}
assert_eq!(
tasks.len(),
match_count,
"Got a different amount of tasks than expected for the status filter {status_filter:?}."
);
Ok(())
}
/// Order the test state by task status.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn order_by_status() -> Result<()> {
let tasks = test_tasks_with_query("order_by status", &None)?;
let expected = vec![
TaskStatus::Stashed { enqueue_at: None },
TaskStatus::Stashed {
enqueue_at: Some(Local.with_ymd_and_hms(2022, 1, 10, 11, 0, 0).unwrap()),
},
TaskStatus::Queued {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
},
TaskStatus::Queued {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
},
TaskStatus::Running {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 2, 12, 0, 0).unwrap(),
},
TaskStatus::Done {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 10, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 10, 10, 10, 0).unwrap(),
result: TaskResult::Failed(255),
},
TaskStatus::Done {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 8, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 8, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 8, 10, 10, 0).unwrap(),
result: TaskResult::Success,
},
];
let actual: Vec<TaskStatus> = tasks.iter().map(|task| task.status.clone()).collect();
assert_eq!(actual, expected);
Ok(())
}
/// Order the tasks by enqueue(d) date.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn order_by_enqueue_at() -> Result<()> {
let tasks = test_tasks_with_query("order_by enqueue_at asc", &None)?;
let expected = vec![
TaskStatus::Done {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 8, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 8, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 8, 10, 10, 0).unwrap(),
result: TaskResult::Success,
},
TaskStatus::Done {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 10, 10, 5, 0).unwrap(),
end: Local.with_ymd_and_hms(2022, 1, 10, 10, 10, 0).unwrap(),
result: TaskResult::Failed(255),
},
TaskStatus::Running {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
start: Local.with_ymd_and_hms(2022, 1, 2, 12, 0, 0).unwrap(),
},
TaskStatus::Queued {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
},
TaskStatus::Queued {
enqueued_at: Local.with_ymd_and_hms(2022, 1, 10, 10, 0, 0).unwrap(),
},
TaskStatus::Stashed {
enqueue_at: Some(Local.with_ymd_and_hms(2022, 1, 10, 11, 0, 0).unwrap()),
},
TaskStatus::Stashed { enqueue_at: None },
];
let actual: Vec<TaskStatus> = tasks.iter().map(|task| task.status.clone()).collect();
assert_eq!(actual, expected);
Ok(())
}
/// Filter tasks by label with the "eq" `=` "ne" `!=` and "contains" `%=`filter.
#[rstest]
#[case("%=", "label", 3)]
#[case("%=", "label-10", 3)]
#[case("%=", "label-10-1", 1)]
#[case("=", "label-10-1", 1)]
#[case("!=", "label-10-1", 6)]
#[case("!=", "label-10", 7)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_label(
#[case] operator: &'static str,
#[case] label_filter: &'static str,
#[case] match_count: usize,
) -> Result<()> {
let tasks = test_tasks_with_query(&format!("label{operator}{label_filter}"), &None)?;
for task in tasks.iter() {
// Make sure the task either has no label or the label doesn't match the filter.
if operator == "!=" {
if let Some(label) = &task.label {
assert_ne!(
label, label_filter,
"Label '{label}' matched exact filter '{label_filter}'"
);
}
continue;
}
let label = task.label.as_ref().expect("Expected task to have a label");
if operator == "%=" {
// Make sure the label contained our filter.
assert!(
label.contains(label_filter),
"Label '{label}' didn't contain filter '{label_filter}'"
);
} else if operator == "=" {
// Make sure the label exactly matches the filter.
assert_eq!(
label, &label_filter,
"Label '{label}' didn't match exact filter '{label_filter}'"
);
}
}
assert_eq!(
tasks.len(),
match_count,
"Got a different amount of tasks than expected for the label filter: {label_filter}."
);
Ok(())
}
/// Filter tasks by command with the "eq" `=` "ne" `!=` and "contains" `%=`filter.
#[rstest]
#[case("=", TEST_COMMAND_SLEEP, 5)]
#[case("!=", TEST_COMMAND_SLEEP, 2)]
#[case("%=", &TEST_COMMAND_SLEEP[..4], 5)]
#[case("=", TEST_COMMAND_HELLO, 2)]
#[case("!=", TEST_COMMAND_HELLO, 5)]
#[case("%=", &TEST_COMMAND_HELLO[..4], 2)]
#[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() {
let command = task.command.as_str();
if operator == "!=" {
// Make sure the task's command doesn't match the filter.
assert_ne!(
command, command_filter,
"Command '{command}' matched exact filter '{command_filter}'"
);
} else if operator == "%=" {
// 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(())
}
}