Skip to content

Commit 5a9b237

Browse files
authored
Add limit option (#40)
* Add test for limit option * Add limit option * Adjust test to use number instad of NonZero limit * Make limit usize instead of NonZero * Make limit option work * Fix which loop gets break * Ignore send error inside thread * Add crossbeam-channel * Formatting * Add ThreadPool.stop and stop when limit reached * Improve limit tests to have multiple cases * Fix limit condition (it was backwards)
1 parent e53a8ae commit 5a9b237

File tree

7 files changed

+118
-15
lines changed

7 files changed

+118
-15
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ documentation = "https://docs.rs/grepdef/latest/grepdef"
1111
[dependencies]
1212
clap = { version = "4.5.9", features = ["derive"] }
1313
colored = "2.1.0"
14+
crossbeam-channel = "0.5.14"
1415
exitcode = "1.1.2"
1516
ignore = "0.4.22"
1617
memchr = "2.7.4"

src/lib.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ pub struct Args {
112112
#[arg(long = "no-color")]
113113
pub no_color: bool,
114114

115+
/// Limit the number of results
116+
#[arg(short = 'l', long = "limit")]
117+
pub limit: Option<usize>,
118+
115119
/// (Advanced) Print debugging information
116120
#[arg(long = "debug")]
117121
pub debug: bool,
@@ -192,6 +196,9 @@ struct Config {
192196
/// Output debugging info during search if true
193197
debug: bool,
194198

199+
/// Limit the number of results
200+
limit: Option<usize>,
201+
195202
/// Explicitly disable color output if true
196203
no_color: bool,
197204

@@ -242,6 +249,7 @@ impl Config {
242249
no_color: args.no_color,
243250
color,
244251
search_method: args.search_method.unwrap_or_default(),
252+
limit: args.limit,
245253
num_threads,
246254
format: args.format.unwrap_or_default(),
247255
};
@@ -438,10 +446,13 @@ impl Searcher {
438446
/// Perform the search and return formatted strings
439447
pub fn search_and_format(&self) -> Result<Vec<String>, Box<dyn Error>> {
440448
let results = self.search()?;
441-
Ok(results.iter().map(|result| match self.config.format {
442-
SearchResultFormat::Grep => result.to_grep(),
443-
SearchResultFormat::JsonPerMatch => result.to_json_per_match(),
444-
}).collect())
449+
Ok(results
450+
.iter()
451+
.map(|result| match self.config.format {
452+
SearchResultFormat::Grep => result.to_grep(),
453+
SearchResultFormat::JsonPerMatch => result.to_json_per_match(),
454+
})
455+
.collect())
445456
}
446457

447458
/// Perform the search and run a callback for each formatted string
@@ -468,7 +479,7 @@ impl Searcher {
468479
};
469480
let re = query_regex::get_regex_for_query(&self.config.query, &self.config.file_type);
470481
let file_type_re = file_type::get_regexp_for_file_type(&self.config.file_type);
471-
let mut pool = threads::ThreadPool::new(self.config.num_threads);
482+
let mut pool = threads::ThreadPool::new(self.config.num_threads, self.config.debug);
472483

473484
match self.config.color {
474485
ColorOption::ALWAYS => colored::control::set_override(true),
@@ -517,10 +528,12 @@ impl Searcher {
517528
&re1,
518529
&path1,
519530
&config1,
520-
// NOTE: it would be nice to have better error handling for if this
521-
// message send fails, but since error handling would happen through
522-
// message sending, I don't know what else to do other than panic.
523-
move |file_results: Vec<SearchResult>| tx1.send(file_results).unwrap(),
531+
move |file_results: Vec<SearchResult>| {
532+
// NOTE: it would be nice to have better error handling for if this
533+
// message send fails, but since normal error handling would happen through
534+
// message sending, I don't know what else to do.
535+
let _ = tx1.send(file_results);
536+
},
524537
);
525538
})
526539
}
@@ -529,15 +542,25 @@ impl Searcher {
529542
};
530543

531544
self.debug("Listening to searcher results");
532-
for received_results in rx {
545+
let mut result_counter: usize = 0;
546+
'all_results: for received_results in rx {
533547
for received_result in received_results {
548+
result_counter += 1;
534549
callback(received_result);
535550
// Don't try to even calculate elapsed time if we are not going to print it
536551
if let (true, Some(start)) = (self.config.debug, start) {
537552
self.debug(
538553
format!("Found a result in {} ms", start.elapsed().as_millis()).as_str(),
539554
);
540555
}
556+
if let Some(i) = self.config.limit {
557+
self.debug(format!("This is result {}; limit {}", result_counter, i).as_str());
558+
if result_counter >= i {
559+
self.debug("Limit reached");
560+
pool.stop();
561+
break 'all_results;
562+
}
563+
}
541564
}
542565
}
543566

src/threads.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use colored::Colorize;
12
use std::num::NonZero;
23
use std::sync::mpsc;
34
use std::sync::Arc;
45
use std::sync::Mutex;
56
use std::thread;
67

78
type WorkerReceiver = Arc<Mutex<mpsc::Receiver<Job>>>;
9+
type ThreadControllerReceiver = crossbeam_channel::Receiver<usize>;
810
type Job = Box<dyn FnOnce() + Send + 'static>;
911

1012
struct Worker {
@@ -13,21 +15,44 @@ struct Worker {
1315
}
1416

1517
impl Worker {
16-
pub fn new(id: usize, receiver: WorkerReceiver) -> Worker {
18+
pub fn new(
19+
id: usize,
20+
debug: bool,
21+
receiver: WorkerReceiver,
22+
controller: ThreadControllerReceiver,
23+
) -> Worker {
1724
let thread = thread::spawn(move || loop {
25+
// If the controller says to stop, ignore remaining jobs on the receiver and just stop.
26+
if ! controller.is_empty() {
27+
if debug {
28+
println!("thread {}: {}", id, "Controller said to stop".red());
29+
}
30+
break;
31+
}
32+
1833
// recv will block until the next job is sent.
34+
if debug {
35+
println!("thread {}: {}", id, "Waiting for job".green());
36+
}
1937
let message = receiver
2038
.lock()
2139
.expect("Worker thread could not get message from main thread")
2240
.recv();
2341

2442
match message {
2543
Ok(job) => {
44+
if debug {
45+
println!("thread {}: {}", id, "Job received; running".green());
46+
}
47+
// Run the job inside this thread.
2648
job();
2749
}
2850
// The thread will stop when the job channel is sent an Err, which will happen when
2951
// the channel is closed.
3052
Err(_) => {
53+
if debug {
54+
println!("thread {}: {}", id, "No more jobs; stopping".red());
55+
}
3156
break;
3257
}
3358
}
@@ -42,24 +67,33 @@ impl Worker {
4267
pub struct ThreadPool {
4368
workers: Vec<Worker>,
4469
sender: Option<mpsc::Sender<Job>>,
70+
thread_control_tx: crossbeam_channel::Sender<usize>,
4571
}
4672

4773
impl ThreadPool {
48-
pub fn new(count: NonZero<usize>) -> ThreadPool {
74+
pub fn new(count: NonZero<usize>, debug: bool) -> ThreadPool {
4975
// This channel is used to send Jobs to each thread.
5076
let (sender, receiver) = mpsc::channel();
77+
// This channel is used to send a stop signal to each thread.
78+
let (thread_control_tx, thread_control_rx) = crossbeam_channel::unbounded();
5179

5280
let receiver = Arc::new(Mutex::new(receiver));
5381

5482
let mut workers = Vec::with_capacity(count.into());
5583

5684
for id in 0..count.into() {
57-
workers.push(Worker::new(id, Arc::clone(&receiver)));
85+
workers.push(Worker::new(
86+
id,
87+
debug,
88+
Arc::clone(&receiver),
89+
thread_control_rx.clone(),
90+
));
5891
}
5992

6093
ThreadPool {
6194
workers,
6295
sender: Some(sender),
96+
thread_control_tx,
6397
}
6498
}
6599

@@ -87,6 +121,12 @@ impl ThreadPool {
87121
}
88122
}
89123
}
124+
125+
pub fn stop(&mut self) {
126+
self.thread_control_tx
127+
.send(1)
128+
.expect("Unable to send stop message to threads");
129+
}
90130
}
91131

92132
impl Drop for ThreadPool {

tests/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub fn make_args(
1313
},
1414
file_type: file_type_string,
1515
line_number: true,
16+
limit: None,
1617
search_method: None,
1718
debug: false,
1819
no_color: false,

tests/fixtures/by-language/php-fixture.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ public function similarMethod() {}
5050
class ClassWithNonUniqueMethod2 {
5151
public function similarMethod() {}
5252
}
53+
class ClassWithNonUniqueMethod3 {
54+
public function similarMethod() {}
55+
}

tests/integration_test.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,11 +406,11 @@ fn search_returns_expected_line_number_for_file_type(
406406
}
407407

408408
#[rstest]
409-
#[case::php_non_unique_method(String::from("similarMethod"), String::from("php"), [48, 51])]
409+
#[case::php_non_unique_method(String::from("similarMethod"), String::from("php"), [48, 51, 54])]
410410
fn search_returns_expected_line_number_group_for_file_type(
411411
#[case] query: String,
412412
#[case] file_type_string: String,
413-
#[case] line_numbers: [usize;2],
413+
#[case] line_numbers: [usize;3],
414414
) {
415415
let file_path =
416416
common::get_default_fixture_for_file_type_string(file_type_string.as_str()).unwrap();
@@ -426,6 +426,31 @@ fn search_returns_expected_line_number_group_for_file_type(
426426
}
427427
}
428428

429+
#[rstest]
430+
#[case::php_non_unique_method_limit_1(String::from("similarMethod"), String::from("php"), [48, 51, 54], 1)]
431+
#[case::php_non_unique_method_limit_2(String::from("similarMethod"), String::from("php"), [48, 51, 54], 2)]
432+
#[case::php_non_unique_method_limit_3(String::from("similarMethod"), String::from("php"), [48, 51, 54], 3)]
433+
fn search_returns_limited_expected_line_number_group_for_file_type(
434+
#[case] query: String,
435+
#[case] file_type_string: String,
436+
#[case] line_numbers: [usize;3],
437+
#[case] limit: usize,
438+
) {
439+
let file_path =
440+
common::get_default_fixture_for_file_type_string(file_type_string.as_str()).unwrap();
441+
let mut args = common::make_args(query, Some(file_path), Some(file_type_string));
442+
args.limit = Some(limit);
443+
let actual = common::do_search(args);
444+
assert_eq!(limit, actual.len(), "Did not find expected number of matches");
445+
for (i, result) in actual.iter().enumerate() {
446+
assert_eq!(
447+
line_numbers[i],
448+
result.line_number.unwrap(),
449+
"Match found but it has the wrong line number"
450+
);
451+
}
452+
}
453+
429454
#[rstest]
430455
fn search_returns_matching_js_function_line_for_recursive() {
431456
let file_path = String::from("./tests");

0 commit comments

Comments
 (0)