Skip to content

Commit f2c0e3a

Browse files
committed
Add search thread and Searchable trait
This is the start of allowing for searches to be run in a separate thread, using a reusable interface.
1 parent fb9a1c0 commit f2c0e3a

File tree

11 files changed

+655
-0
lines changed

11 files changed

+655
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ name = "core"
1818
anyhow = "1.0.68"
1919
bitflags = "1.3.2"
2020
captur = "0.1.0"
21+
crossbeam-channel = "0.5.6"
2122
if_chain = "1.0.2"
2223
lazy_static = "1.4.0"
2324
num-format = "0.4.4"

src/core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ mod license;
127127
mod module;
128128
mod modules;
129129
mod process;
130+
#[allow(dead_code)]
131+
mod search;
130132
#[cfg(test)]
131133
mod tests;
132134
#[cfg(test)]

src/core/src/search/action.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::fmt::{Debug, Formatter};
2+
3+
use crate::search::searchable::Searchable;
4+
5+
#[allow(clippy::exhaustive_enums)]
6+
pub(crate) enum Action {
7+
Cancel,
8+
Continue,
9+
End,
10+
SetSearchable(Box<dyn Searchable>),
11+
Start(String),
12+
}
13+
14+
impl Debug for Action {
15+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
16+
match *self {
17+
Self::Cancel => write!(f, "Cancel"),
18+
Self::Continue => write!(f, "Continue"),
19+
Self::End => write!(f, "End"),
20+
Self::SetSearchable(_) => write!(f, "SetSearchable(_)"),
21+
Self::Start(ref term) => write!(f, "Start({term})"),
22+
}
23+
}
24+
}
25+
26+
#[cfg(test)]
27+
mod tests {
28+
use rstest::rstest;
29+
30+
use super::*;
31+
use crate::search::{Interrupter, SearchResult};
32+
33+
struct TestSearchable;
34+
35+
impl Searchable for TestSearchable {
36+
fn reset(&mut self) {}
37+
38+
fn search(&mut self, _: Interrupter, _: &str) -> SearchResult {
39+
SearchResult::None
40+
}
41+
}
42+
43+
#[rstest]
44+
#[case::cancel(Action::Cancel, "Cancel")]
45+
#[case::cont(Action::Continue, "Continue")]
46+
#[case::end(Action::End, "End")]
47+
#[case::set_searchable(Action::SetSearchable(Box::new(TestSearchable {})), "SetSearchable(_)")]
48+
#[case::start(Action::Start(String::from("foo")), "Start(foo)")]
49+
fn debug(#[case] action: Action, #[case] expected: &str) {
50+
assert_eq!(format!("{action:?}"), expected);
51+
}
52+
}

src/core/src/search/interrupter.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::{
2+
ops::Add,
3+
time::{Duration, Instant},
4+
};
5+
6+
pub(crate) struct Interrupter {
7+
finish: Instant,
8+
}
9+
10+
impl Interrupter {
11+
pub(crate) fn new(duration: Duration) -> Self {
12+
Self {
13+
finish: Instant::now().add(duration),
14+
}
15+
}
16+
17+
pub(crate) fn should_continue(&self) -> bool {
18+
Instant::now() < self.finish
19+
}
20+
}
21+
22+
#[cfg(test)]
23+
mod test {
24+
use std::{ops::Sub, time::Duration};
25+
26+
use super::*;
27+
use crate::search::Interrupter;
28+
29+
#[test]
30+
fn should_continue_before_finish() {
31+
let interrupter = Interrupter::new(Duration::from_secs(60));
32+
assert!(interrupter.should_continue());
33+
}
34+
35+
#[test]
36+
fn should_continue_after_finish() {
37+
let interrupter = Interrupter {
38+
finish: Instant::now().sub(Duration::from_secs(60)),
39+
};
40+
assert!(!interrupter.should_continue());
41+
}
42+
}

src/core/src/search/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
mod action;
2+
mod interrupter;
3+
mod search_result;
4+
mod search_state;
5+
mod searchable;
6+
mod state;
7+
mod thread;
8+
9+
#[allow(unused_imports)]
10+
pub(crate) use self::{
11+
action::Action,
12+
interrupter::Interrupter,
13+
search_result::SearchResult,
14+
search_state::SearchState,
15+
searchable::Searchable,
16+
state::State,
17+
thread::Thread,
18+
};

src/core/src/search/search_result.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2+
pub(crate) enum SearchResult {
3+
None,
4+
Complete,
5+
Updated,
6+
}

src/core/src/search/search_state.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2+
pub(crate) enum SearchState {
3+
Inactive,
4+
Active,
5+
Complete,
6+
}

src/core/src/search/searchable.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use crate::search::{Interrupter, SearchResult};
2+
3+
pub(crate) trait Searchable: Send {
4+
fn reset(&mut self);
5+
6+
fn search(&mut self, interrupter: Interrupter, term: &str) -> SearchResult;
7+
}

src/core/src/search/state.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use std::{
2+
sync::{
3+
atomic::{AtomicBool, Ordering},
4+
Arc,
5+
},
6+
time::Duration,
7+
};
8+
9+
use crossbeam_channel::RecvTimeoutError;
10+
11+
use crate::search::action::Action;
12+
13+
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(500);
14+
15+
#[derive(Clone, Debug)]
16+
pub(crate) struct State {
17+
ended: Arc<AtomicBool>,
18+
paused: Arc<AtomicBool>,
19+
update_receiver: crossbeam_channel::Receiver<Action>,
20+
update_sender: crossbeam_channel::Sender<Action>,
21+
}
22+
23+
impl State {
24+
pub(crate) fn new() -> Self {
25+
let (update_sender, update_receiver) = crossbeam_channel::unbounded();
26+
Self {
27+
ended: Arc::new(AtomicBool::from(false)),
28+
paused: Arc::new(AtomicBool::from(false)),
29+
update_receiver,
30+
update_sender,
31+
}
32+
}
33+
34+
pub(crate) fn receive_update(&self) -> Action {
35+
self.update_receiver
36+
.recv_timeout(RECEIVE_TIMEOUT)
37+
.unwrap_or_else(|e: RecvTimeoutError| {
38+
match e {
39+
RecvTimeoutError::Timeout => Action::Continue,
40+
RecvTimeoutError::Disconnected => Action::End,
41+
}
42+
})
43+
}
44+
45+
pub(crate) fn send_update(&self, action: Action) {
46+
let _result = self.update_sender.send(action);
47+
}
48+
49+
pub(crate) fn is_paused(&self) -> bool {
50+
self.paused.load(Ordering::Acquire)
51+
}
52+
53+
pub(crate) fn is_ended(&self) -> bool {
54+
self.ended.load(Ordering::Acquire)
55+
}
56+
57+
pub(crate) fn pause(&self) {
58+
self.paused.store(true, Ordering::Release);
59+
}
60+
61+
pub(crate) fn resume(&self) {
62+
self.paused.store(false, Ordering::Release);
63+
}
64+
65+
pub(crate) fn end(&self) {
66+
self.ended.store(true, Ordering::Release);
67+
}
68+
}
69+
70+
#[cfg(test)]
71+
mod tests {
72+
73+
use super::*;
74+
75+
#[test]
76+
fn send_recv_update() {
77+
let state = State::new();
78+
state.send_update(Action::Start(String::from("test")));
79+
assert!(matches!(state.receive_update(), Action::Start(_)));
80+
}
81+
82+
#[test]
83+
fn send_recv_update_timeout() {
84+
let state = State::new();
85+
assert!(matches!(state.receive_update(), Action::Continue));
86+
}
87+
88+
#[test]
89+
fn send_recv_disconnect() {
90+
let (update_sender, _update_receiver) = crossbeam_channel::unbounded();
91+
let mut state = State::new();
92+
state.update_sender = update_sender; // replace last reference to sender, to force a disconnect
93+
assert!(matches!(state.receive_update(), Action::End));
94+
}
95+
96+
#[test]
97+
fn paused() {
98+
let state = State::new();
99+
state.pause();
100+
assert!(state.is_paused());
101+
}
102+
103+
#[test]
104+
fn resumed() {
105+
let state = State::new();
106+
state.resume();
107+
assert!(!state.is_paused());
108+
}
109+
110+
#[test]
111+
fn ended() {
112+
let state = State::new();
113+
state.end();
114+
assert!(state.is_ended());
115+
}
116+
}

0 commit comments

Comments
 (0)