1use core::cmp::Ordering;
27use core::fmt;
28use core::str::FromStr;
29use std::borrow::Borrow;
30
31use crate::model::{MetricState, ProcessSnapshot, ProcessState, UserIdentity};
32use crate::units::{Percent, Rate};
33
34#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
43pub enum ProcessSortKey {
44 #[default]
46 Cpu,
47 Memory,
53 Read,
55 Write,
57 Pid,
59 Name,
61 Age,
63 User,
65 State,
67 Threads,
69 Virtual,
71}
72
73impl ProcessSortKey {
74 pub const ALL: [Self; 11] = [
79 Self::Cpu,
80 Self::Memory,
81 Self::Name,
82 Self::Pid,
83 Self::User,
84 Self::State,
85 Self::Read,
86 Self::Write,
87 Self::Age,
88 Self::Threads,
89 Self::Virtual,
90 ];
91
92 pub const NAMES: &'static str =
94 "cpu, memory, read, write, pid, name, age, user, state, threads, virtual";
95
96 #[must_use]
98 pub const fn as_str(self) -> &'static str {
99 match self {
100 Self::Cpu => "cpu",
101 Self::Memory => "memory",
102 Self::Read => "read",
103 Self::Write => "write",
104 Self::Pid => "pid",
105 Self::Name => "name",
106 Self::Age => "age",
107 Self::User => "user",
108 Self::State => "state",
109 Self::Threads => "threads",
110 Self::Virtual => "virtual",
111 }
112 }
113
114 #[must_use]
116 pub const fn label(self) -> &'static str {
117 match self {
118 Self::Cpu => "CPU%",
119 Self::Memory => "memory (RSS)",
120 Self::Read => "read rate",
121 Self::Write => "write rate",
122 Self::Pid => "PID",
123 Self::Name => "name",
124 Self::Age => "age",
125 Self::User => "user",
126 Self::State => "state",
127 Self::Threads => "threads",
128 Self::Virtual => "virtual memory",
129 }
130 }
131}
132
133impl fmt::Display for ProcessSortKey {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.write_str(self.as_str())
136 }
137}
138
139#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
144#[error(
145 "unknown process sort field `{name}`; expected one of {}",
146 ProcessSortKey::NAMES
147)]
148pub struct UnknownSortKey {
149 name: Box<str>,
150}
151
152impl UnknownSortKey {
153 #[must_use]
155 pub fn name(&self) -> &str {
156 &self.name
157 }
158}
159
160impl FromStr for ProcessSortKey {
161 type Err = UnknownSortKey;
162
163 fn from_str(text: &str) -> Result<Self, Self::Err> {
170 let normalized: String = text
171 .trim()
172 .chars()
173 .map(|character| match character {
174 '-' => '_',
175 other => other.to_ascii_lowercase(),
176 })
177 .collect();
178 match normalized.as_str() {
179 "cpu" | "cpu_percent" => Ok(Self::Cpu),
180 "memory" | "mem" | "rss" => Ok(Self::Memory),
181 "read" | "read_rate" => Ok(Self::Read),
182 "write" | "write_rate" => Ok(Self::Write),
183 "pid" => Ok(Self::Pid),
184 "name" | "command" | "comm" => Ok(Self::Name),
185 "age" | "started" => Ok(Self::Age),
186 "user" | "uid" => Ok(Self::User),
187 "state" => Ok(Self::State),
188 "threads" | "thread_count" => Ok(Self::Threads),
189 "virtual" | "virt" | "vsz" => Ok(Self::Virtual),
190 _ => Err(UnknownSortKey { name: text.into() }),
191 }
192 }
193}
194
195#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
199pub enum SortDirection {
200 Ascending,
202 #[default]
204 Descending,
205}
206
207impl SortDirection {
208 #[must_use]
210 pub const fn from_descending(descending: bool) -> Self {
211 if descending {
212 Self::Descending
213 } else {
214 Self::Ascending
215 }
216 }
217
218 #[must_use]
220 pub const fn is_descending(self) -> bool {
221 matches!(self, Self::Descending)
222 }
223
224 #[must_use]
226 pub const fn reversed(self) -> Self {
227 match self {
228 Self::Ascending => Self::Descending,
229 Self::Descending => Self::Ascending,
230 }
231 }
232
233 #[must_use]
235 pub const fn label(self) -> &'static str {
236 match self {
237 Self::Ascending => "ascending",
238 Self::Descending => "descending",
239 }
240 }
241
242 const fn apply(self, ordering: Ordering) -> Ordering {
244 match self {
245 Self::Ascending => ordering,
246 Self::Descending => ordering.reverse(),
247 }
248 }
249}
250
251#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
253pub struct ProcessSort {
254 pub key: ProcessSortKey,
256 pub direction: SortDirection,
258}
259
260impl ProcessSort {
261 #[must_use]
263 pub const fn new(key: ProcessSortKey, direction: SortDirection) -> Self {
264 Self { key, direction }
265 }
266
267 #[must_use]
270 pub const fn descending(key: ProcessSortKey) -> Self {
271 Self::new(key, SortDirection::Descending)
272 }
273
274 #[must_use]
276 pub const fn ascending(key: ProcessSortKey) -> Self {
277 Self::new(key, SortDirection::Ascending)
278 }
279
280 #[must_use]
282 pub const fn reversed(self) -> Self {
283 Self::new(self.key, self.direction.reversed())
284 }
285
286 #[must_use]
292 pub const fn with_key(self, key: ProcessSortKey) -> Self {
293 Self::new(key, self.direction)
294 }
295
296 #[must_use]
304 pub fn compare(&self, left: &ProcessSnapshot, right: &ProcessSnapshot) -> Ordering {
305 self.compare_column(left, right)
306 .then_with(|| left.identity.cmp(&right.identity))
307 }
308
309 pub fn sort<P: Borrow<ProcessSnapshot>>(&self, rows: &mut [P]) {
314 rows.sort_by(|left, right| self.compare(left.borrow(), right.borrow()));
315 }
316
317 #[must_use]
322 pub fn order<P: Borrow<ProcessSnapshot>>(&self, rows: &[P]) -> Vec<usize> {
323 let mut indexed: Vec<(usize, &ProcessSnapshot)> =
324 rows.iter().map(Borrow::borrow).enumerate().collect();
325 indexed.sort_by(|(_, left), (_, right)| self.compare(left, right));
326 indexed.into_iter().map(|(index, _)| index).collect()
327 }
328
329 fn compare_column(&self, left: &ProcessSnapshot, right: &ProcessSnapshot) -> Ordering {
331 let direction = self.direction;
332 match self.key {
333 ProcessSortKey::Cpu => {
334 compare_metric(&left.cpu, &right.cpu, direction, compare_percent)
335 }
336 ProcessSortKey::Memory => compare_metric(
337 &left.memory.rss_bytes,
338 &right.memory.rss_bytes,
339 direction,
340 Ord::cmp,
341 ),
342 ProcessSortKey::Virtual => compare_metric(
343 &left.memory.virtual_bytes,
344 &right.memory.virtual_bytes,
345 direction,
346 Ord::cmp,
347 ),
348 ProcessSortKey::Read => {
349 compare_metric(&left.io.read, &right.io.read, direction, compare_rate)
350 }
351 ProcessSortKey::Write => {
352 compare_metric(&left.io.write, &right.io.write, direction, compare_rate)
353 }
354 ProcessSortKey::Threads => {
355 compare_metric(&left.threads, &right.threads, direction, Ord::cmp)
356 }
357 ProcessSortKey::Age => compare_metric(&left.age, &right.age, direction, Ord::cmp),
358 ProcessSortKey::User => {
359 compare_metric(&left.user, &right.user, direction, compare_user)
360 }
361 ProcessSortKey::Pid => direction.apply(left.identity.pid.cmp(&right.identity.pid)),
362 ProcessSortKey::Name => direction.apply(
363 compare_ignoring_case(&left.name, &right.name).then_with(|| {
364 compare_ignoring_case(left.command_or_name(), right.command_or_name())
365 }),
366 ),
367 ProcessSortKey::State => direction.apply(compare_state(left.state, right.state)),
368 }
369 }
370}
371
372fn compare_metric<T, F>(
378 left: &MetricState<T>,
379 right: &MetricState<T>,
380 direction: SortDirection,
381 compare_value: F,
382) -> Ordering
383where
384 F: Fn(&T, &T) -> Ordering,
385{
386 match (left.displayable(), right.displayable()) {
387 (Some((left_value, left_age)), Some((right_value, right_age))) => direction
388 .apply(compare_value(left_value, right_value))
389 .then_with(|| left_age.cmp(&right_age)),
392 (Some(_), None) => Ordering::Less,
393 (None, Some(_)) => Ordering::Greater,
394 (None, None) => Ordering::Equal,
397 }
398}
399
400fn compare_percent(left: &Percent, right: &Percent) -> Ordering {
406 left.value().total_cmp(&right.value())
407}
408
409fn compare_rate(left: &Rate, right: &Rate) -> Ordering {
411 left.per_second().total_cmp(&right.per_second())
412}
413
414fn compare_user(left: &UserIdentity, right: &UserIdentity) -> Ordering {
421 match (&left.name, &right.name) {
422 (Some(left_name), Some(right_name)) => {
423 compare_ignoring_case(left_name, right_name).then_with(|| left.uid.cmp(&right.uid))
424 }
425 (Some(_), None) => Ordering::Less,
426 (None, Some(_)) => Ordering::Greater,
427 (None, None) => left.uid.cmp(&right.uid),
428 }
429}
430
431fn compare_state(left: ProcessState, right: ProcessState) -> Ordering {
438 let (left_code, right_code) = (left.code(), right.code());
439 left_code
440 .to_ascii_lowercase()
441 .cmp(&right_code.to_ascii_lowercase())
442 .then_with(|| left_code.cmp(&right_code))
443}
444
445fn compare_ignoring_case(left: &str, right: &str) -> Ordering {
452 let mut left_chars = left.chars().flat_map(char::to_lowercase);
453 let mut right_chars = right.chars().flat_map(char::to_lowercase);
454 loop {
455 match (left_chars.next(), right_chars.next()) {
456 (Some(left_char), Some(right_char)) => match left_char.cmp(&right_char) {
457 Ordering::Equal => {}
458 difference => return difference,
459 },
460 (None, None) => return left.cmp(right),
461 (None, Some(_)) => return Ordering::Less,
462 (Some(_), None) => return Ordering::Greater,
463 }
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use core::time::Duration;
470
471 use proptest::prelude::*;
472
473 use super::super::fixtures::process;
474 use super::*;
475 use crate::model::{ProcessIdentity, UnavailableReason};
476
477 fn identities(rows: &[ProcessSnapshot], order: &[usize]) -> Vec<ProcessIdentity> {
478 order
479 .iter()
480 .filter_map(|&index| rows.get(index).map(|row| row.identity))
481 .collect()
482 }
483
484 fn pids(rows: &[ProcessSnapshot], order: &[usize]) -> Vec<u32> {
485 identities(rows, order)
486 .into_iter()
487 .map(|identity| identity.pid)
488 .collect()
489 }
490
491 fn measured_for(key: ProcessSortKey, pid: u32, magnitude: u16) -> ProcessSnapshot {
493 let fixture = process(pid, u64::from(pid));
494 let wide = u64::from(magnitude);
495 let float = f32::from(magnitude);
496 match key {
497 ProcessSortKey::Cpu => fixture.cpu(float),
498 ProcessSortKey::Memory => fixture.rss(wide),
499 ProcessSortKey::Virtual => fixture.virtual_bytes(wide),
500 ProcessSortKey::Read => fixture.read(f64::from(magnitude)),
501 ProcessSortKey::Write => fixture.write(f64::from(magnitude)),
502 ProcessSortKey::Threads => fixture.threads(u32::from(magnitude)),
503 ProcessSortKey::Age => fixture.age(wide),
504 ProcessSortKey::User => {
505 fixture.user(u32::from(magnitude), Some(&format!("u{magnitude:03}")))
506 }
507 ProcessSortKey::Pid | ProcessSortKey::Name | ProcessSortKey::State => fixture,
510 }
511 .build()
512 }
513
514 #[test]
515 fn equal_keys_are_broken_by_identity_not_by_input_order() {
516 let hot_low_pid = process(700, 5).cpu(50.0).build();
517 let hot_high_pid = process(900, 5).cpu(50.0).build();
518 let sort = ProcessSort::default();
519
520 let mut forwards = vec![hot_low_pid.clone(), hot_high_pid.clone()];
521 let mut backwards = vec![hot_high_pid, hot_low_pid];
522 sort.sort(&mut forwards);
523 sort.sort(&mut backwards);
524
525 assert_eq!(
526 forwards.iter().map(|p| p.identity.pid).collect::<Vec<_>>(),
527 vec![700, 900]
528 );
529 assert_eq!(
530 backwards.iter().map(|p| p.identity.pid).collect::<Vec<_>>(),
531 vec![700, 900],
532 "input order must not influence the result"
533 );
534 }
535
536 #[test]
537 fn a_reused_pid_is_ordered_by_its_start_key() {
538 let old = process(4242, 100).cpu(10.0).build();
539 let new = process(4242, 900).cpu(10.0).build();
540 let mut rows = vec![new, old];
541 ProcessSort::default().sort(&mut rows);
542 assert_eq!(
543 rows.iter()
544 .map(|p| p.identity.start_key)
545 .collect::<Vec<_>>(),
546 vec![100, 900]
547 );
548 }
549
550 #[test]
551 fn two_refreshes_of_the_same_table_produce_the_same_order() {
552 let first: Vec<ProcessSnapshot> = (1..=6)
555 .map(|pid| process(pid, u64::from(pid)).cpu(0.0).build())
556 .collect();
557 let second: Vec<ProcessSnapshot> = first.iter().rev().cloned().collect();
558 let sort = ProcessSort::default();
559
560 assert_eq!(
561 identities(&first, &sort.order(&first)),
562 identities(&second, &sort.order(&second))
563 );
564 }
565
566 #[test]
567 fn reversing_the_direction_does_not_reverse_the_tie_break() {
568 let rows = vec![
569 process(1, 1).cpu(5.0).build(),
570 process(2, 2).cpu(5.0).build(),
571 process(3, 3).cpu(5.0).build(),
572 ];
573 let descending = ProcessSort::default();
574 assert_eq!(pids(&rows, &descending.order(&rows)), vec![1, 2, 3]);
575 assert_eq!(
576 pids(&rows, &descending.reversed().order(&rows)),
577 vec![1, 2, 3],
578 "tie-break is direction-independent so selection stays put"
579 );
580 }
581
582 #[test]
583 fn unavailable_values_sort_last_in_both_directions_for_every_key() {
584 for key in ProcessSortKey::ALL {
585 if matches!(
586 key,
587 ProcessSortKey::Pid | ProcessSortKey::Name | ProcessSortKey::State
588 ) {
589 continue;
591 }
592 let rows = vec![
593 measured_for(key, 1, 1),
594 process(2, 2).build(),
595 measured_for(key, 3, 9),
596 ];
597 for direction in [SortDirection::Descending, SortDirection::Ascending] {
598 let order = pids(&rows, &ProcessSort::new(key, direction).order(&rows));
599 assert_eq!(
600 order.last(),
601 Some(&2),
602 "{key:?} {direction:?}: the unmeasured row must sort last"
603 );
604 }
605 }
606 }
607
608 #[test]
609 fn an_unmeasured_value_is_not_treated_as_zero() {
610 let rows = vec![
611 process(1, 1).cpu(0.0).build(),
612 process(2, 2)
613 .cpu_state(MetricState::PermissionDenied)
614 .build(),
615 ];
616 let order = ProcessSort::ascending(ProcessSortKey::Cpu).order(&rows);
620 assert_eq!(pids(&rows, &order), vec![1, 2]);
621 }
622
623 #[test]
624 fn every_flavour_of_unavailable_ranks_equally_and_is_ordered_by_identity() {
625 let rows = vec![
626 process(30, 30)
627 .cpu_state(MetricState::TemporarilyUnavailable(
628 UnavailableReason::ProcessExited,
629 ))
630 .build(),
631 process(10, 10).cpu_state(MetricState::Unsupported).build(),
632 process(20, 20)
633 .cpu_state(MetricState::PermissionDenied)
634 .build(),
635 process(40, 40).cpu_state(MetricState::WarmingUp).build(),
636 ];
637 let order = ProcessSort::default().order(&rows);
638 assert_eq!(pids(&rows, &order), vec![10, 20, 30, 40]);
639 }
640
641 #[test]
642 fn a_stale_value_keeps_its_place_instead_of_dropping_to_the_bottom() {
643 let stale = MetricState::Available(Percent::new(90.0).expect("valid"))
644 .into_stale(Duration::from_secs(2));
645 let rows = vec![
646 process(1, 1).cpu(5.0).build(),
647 process(2, 2).cpu_state(stale).build(),
648 process(3, 3).cpu_state(MetricState::WarmingUp).build(),
649 ];
650 let order = ProcessSort::default().order(&rows);
651 assert_eq!(
652 pids(&rows, &order),
653 vec![2, 1, 3],
654 "stale 90% outranks fresh 5%, and only the valueless row sorts last"
655 );
656 }
657
658 #[test]
659 fn fresh_beats_stale_on_an_exact_tie() {
660 let stale = MetricState::Available(Percent::new(7.0).expect("valid"))
661 .into_stale(Duration::from_secs(9));
662 let rows = vec![
663 process(9, 9).cpu_state(stale).build(),
664 process(1, 1).cpu(7.0).build(),
665 ];
666 let order = ProcessSort::default().order(&rows);
669 assert_eq!(pids(&rows, &order), vec![1, 9]);
670 }
671
672 #[test]
673 fn cpu_sorts_by_magnitude_and_may_exceed_one_hundred_percent() {
674 let rows = vec![
675 process(1, 1).cpu(54.0).build(),
676 process(2, 2).cpu(287.0).build(),
677 process(3, 3).cpu(0.5).build(),
678 ];
679 let order = ProcessSort::default().order(&rows);
680 assert_eq!(pids(&rows, &order), vec![2, 1, 3]);
681 }
682
683 #[test]
684 fn memory_and_virtual_are_independent_columns() {
685 let rows = vec![
686 process(1, 1).rss(1_000).virtual_bytes(9_000_000).build(),
687 process(2, 2).rss(9_000).virtual_bytes(1_000).build(),
688 ];
689 assert_eq!(
690 pids(
691 &rows,
692 &ProcessSort::descending(ProcessSortKey::Memory).order(&rows)
693 ),
694 vec![2, 1]
695 );
696 assert_eq!(
697 pids(
698 &rows,
699 &ProcessSort::descending(ProcessSortKey::Virtual).order(&rows)
700 ),
701 vec![1, 2]
702 );
703 }
704
705 #[test]
706 fn read_and_write_rates_are_independent_columns() {
707 let rows = vec![
708 process(1, 1).read(18_000_000.0).write(1.0).build(),
709 process(2, 2).read(1.0).write(42_000_000.0).build(),
710 ];
711 assert_eq!(
712 pids(
713 &rows,
714 &ProcessSort::descending(ProcessSortKey::Read).order(&rows)
715 ),
716 vec![1, 2]
717 );
718 assert_eq!(
719 pids(
720 &rows,
721 &ProcessSort::descending(ProcessSortKey::Write).order(&rows)
722 ),
723 vec![2, 1]
724 );
725 }
726
727 #[test]
728 fn name_sorting_ignores_case_and_falls_back_to_the_command_line() {
729 let rows = vec![
730 process(1, 1).name("Zsh").build(),
731 process(2, 2).name("cargo").command("cargo test").build(),
732 process(3, 3).name("cargo").command("cargo build").build(),
733 process(4, 4).name("apache").build(),
734 ];
735 let order = ProcessSort::ascending(ProcessSortKey::Name).order(&rows);
736 assert_eq!(
737 pids(&rows, &order),
738 vec![4, 3, 2, 1],
739 "apache, cargo build, cargo test, Zsh"
740 );
741 }
742
743 #[test]
744 fn user_sorting_puts_unresolved_names_after_resolved_ones() {
745 let rows = vec![
746 process(1, 1).user(0, None).build(),
747 process(2, 2).user(501, Some("gabor")).build(),
748 process(3, 3).user(70, Some("_postgres")).build(),
749 process(4, 4)
750 .user_state(MetricState::PermissionDenied)
751 .build(),
752 ];
753 let order = ProcessSort::ascending(ProcessSortKey::User).order(&rows);
754 assert_eq!(
755 pids(&rows, &order),
756 vec![3, 2, 1, 4],
757 "_postgres, gabor, uid 0 (unnamed), then the unattributable row"
758 );
759 }
760
761 #[test]
762 fn state_sorting_puts_zombies_first_when_descending() {
763 let rows = vec![
764 process(1, 1).state(ProcessState::Sleeping).build(),
765 process(2, 2).state(ProcessState::Zombie).build(),
766 process(3, 3).state(ProcessState::Running).build(),
767 process(4, 4)
768 .state(ProcessState::UninterruptibleSleep)
769 .build(),
770 ];
771 let order = ProcessSort::descending(ProcessSortKey::State).order(&rows);
772 assert_eq!(pids(&rows, &order).first(), Some(&2));
773 assert_eq!(
774 pids(&rows, &order).last(),
775 Some(&4),
776 "D-state is the other extreme, one keypress away"
777 );
778 }
779
780 #[test]
781 fn pid_and_age_and_thread_columns_order_by_magnitude() {
782 let rows = vec![
783 process(900, 1).age(10).threads(2).build(),
784 process(100, 2).age(90).threads(64).build(),
785 ];
786 assert_eq!(
787 pids(
788 &rows,
789 &ProcessSort::ascending(ProcessSortKey::Pid).order(&rows)
790 ),
791 vec![100, 900]
792 );
793 assert_eq!(
794 pids(
795 &rows,
796 &ProcessSort::descending(ProcessSortKey::Age).order(&rows)
797 ),
798 vec![100, 900]
799 );
800 assert_eq!(
801 pids(
802 &rows,
803 &ProcessSort::descending(ProcessSortKey::Threads).order(&rows)
804 ),
805 vec![100, 900]
806 );
807 }
808
809 #[test]
810 fn sorting_an_empty_or_single_row_table_is_a_no_op() {
811 let mut empty: Vec<ProcessSnapshot> = Vec::new();
812 ProcessSort::default().sort(&mut empty);
813 assert!(empty.is_empty());
814 assert!(ProcessSort::default().order(&empty).is_empty());
815
816 let mut single = vec![process(1, 1).build()];
817 ProcessSort::default().sort(&mut single);
818 assert_eq!(single.len(), 1);
819 }
820
821 #[test]
822 fn sorting_works_on_borrowed_rows_too() {
823 let rows = [
824 process(1, 1).cpu(1.0).build(),
825 process(2, 2).cpu(2.0).build(),
826 ];
827 let mut borrowed: Vec<&ProcessSnapshot> = rows.iter().collect();
828 ProcessSort::default().sort(&mut borrowed);
829 assert_eq!(
830 borrowed.iter().map(|p| p.identity.pid).collect::<Vec<_>>(),
831 vec![2, 1]
832 );
833 }
834
835 #[test]
836 fn config_field_names_round_trip() {
837 for key in ProcessSortKey::ALL {
838 assert_eq!(
839 key.as_str().parse::<ProcessSortKey>(),
840 Ok(key),
841 "{key:?} does not round-trip"
842 );
843 assert!(ProcessSortKey::NAMES.contains(key.as_str()));
844 }
845 }
846
847 #[test]
848 fn field_name_parsing_accepts_documented_aliases_and_ignores_case() {
849 assert_eq!("MEM".parse(), Ok(ProcessSortKey::Memory));
850 assert_eq!("rss".parse(), Ok(ProcessSortKey::Memory));
851 assert_eq!(" Command ".parse(), Ok(ProcessSortKey::Name));
852 assert_eq!("thread-count".parse(), Ok(ProcessSortKey::Threads));
853 assert_eq!("VSZ".parse(), Ok(ProcessSortKey::Virtual));
854 }
855
856 #[test]
857 fn an_unknown_field_name_is_reported_with_the_offending_text() {
858 let error = "cpu%".parse::<ProcessSortKey>().expect_err("not a field");
859 assert_eq!(error.name(), "cpu%");
860 let message = error.to_string();
861 assert!(message.contains("cpu%"), "{message}");
862 assert!(message.contains("virtual"), "{message}");
863 }
864
865 #[test]
866 fn the_default_ordering_matches_the_documented_config_default() {
867 let default = ProcessSort::default();
869 assert_eq!(default.key, ProcessSortKey::Cpu);
870 assert!(default.direction.is_descending());
871 assert_eq!(
872 SortDirection::from_descending(false),
873 SortDirection::Ascending
874 );
875 }
876
877 #[test]
878 fn choosing_a_column_keeps_the_direction_and_reversing_keeps_the_column() {
879 let sort = ProcessSort::ascending(ProcessSortKey::Name);
880 assert_eq!(
881 sort.with_key(ProcessSortKey::Age),
882 ProcessSort::ascending(ProcessSortKey::Age)
883 );
884 assert_eq!(
885 sort.reversed(),
886 ProcessSort::descending(ProcessSortKey::Name)
887 );
888 assert_eq!(sort.reversed().reversed(), sort);
889 }
890
891 #[test]
892 fn the_selector_lists_every_key_exactly_once() {
893 let mut names: Vec<&str> = ProcessSortKey::ALL.iter().map(|key| key.as_str()).collect();
894 names.sort_unstable();
895 names.dedup();
896 assert_eq!(names.len(), ProcessSortKey::ALL.len());
897 for key in ProcessSortKey::ALL {
898 assert!(!key.label().is_empty());
899 }
900 }
901
902 #[test]
903 fn comparison_is_antisymmetric_and_only_identical_rows_are_equal() {
904 let left = process(1, 1).cpu(5.0).build();
905 let right = process(2, 2).cpu(5.0).build();
906 let sort = ProcessSort::default();
907 assert_eq!(sort.compare(&left, &right), Ordering::Less);
908 assert_eq!(sort.compare(&right, &left), Ordering::Greater);
909 assert_eq!(sort.compare(&left, &left), Ordering::Equal);
910 }
911
912 proptest! {
913 #[test]
916 fn ordering_is_independent_of_input_order(
917 rows in prop::collection::vec((1u32..40, 0u64..3, prop::option::of(0u32..4)), 1..24),
918 rotation in 0usize..24,
919 ) {
920 let table: Vec<ProcessSnapshot> = rows
921 .iter()
922 .enumerate()
923 .map(|(index, &(pid, start_key, cpu))| {
924 let unique = u64::try_from(index).unwrap_or(0);
927 let fixture = process(pid, start_key.wrapping_mul(1000) + unique);
928 match cpu {
929 Some(value) => fixture.cpu(f32::from(u16::try_from(value).unwrap_or(0))),
930 None => fixture,
931 }
932 .build()
933 })
934 .collect();
935
936 let mut rotated = table.clone();
937 let length = rotated.len();
938 if length > 0 {
939 rotated.rotate_left(rotation % length);
940 }
941
942 for key in ProcessSortKey::ALL {
943 for direction in [SortDirection::Ascending, SortDirection::Descending] {
944 let sort = ProcessSort::new(key, direction);
945 prop_assert_eq!(
946 identities(&table, &sort.order(&table)),
947 identities(&rotated, &sort.order(&rotated)),
948 "{:?} {:?} depends on input order",
949 key,
950 direction
951 );
952 }
953 }
954 }
955 }
956}