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

WIP: experimentally add a filetree implementation #64

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
49 changes: 49 additions & 0 deletions src/filetree/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// Yields path components in reverse order.
///
/// This also takes care to normalize `.` and `..` components,
/// and it skips any trailing `..`.
pub fn reverse_components(path: &str) -> impl Iterator<Item = &str> {
let mut skip = 0;
let mut components = path.split('/').rev();

std::iter::from_fn(move || {
while let Some(next) = components.next() {
match next {
"." => continue,
".." => {
skip += 1;
continue;
}
_ if skip > 0 => {
skip -= 1;
continue;
}
component => return Some(component),
}
}
None
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_reverse_components() {
let components: Vec<_> = reverse_components("mod.rs").collect();
assert_eq!(components, &["mod.rs"]);

let components: Vec<_> = reverse_components("./foo/./bar/mod.rs").collect();
assert_eq!(components, &["mod.rs", "bar", "foo"]);

let components: Vec<_> = reverse_components("./foo/../bar/mod.rs").collect();
assert_eq!(components, &["mod.rs", "bar"]);

let components: Vec<_> = reverse_components("foo/../bar/../mod.rs").collect();
assert_eq!(components, &["mod.rs"]);

let components: Vec<_> = reverse_components("foo/bar/foobar/../../mod.rs").collect();
assert_eq!(components, &["mod.rs", "foo"]);
}
}
130 changes: 130 additions & 0 deletions src/filetree/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::collections::BTreeMap;

use components::reverse_components;

mod components;

#[derive(Default, Debug)]
struct Node {
full_paths: Vec<String>,
children: BTreeMap<String, Node>,
}

#[derive(Default, Debug)]
pub struct ReverseFileTree {
root: Node,
}

impl ReverseFileTree {
pub fn new() -> Self {
Self::default()
}

pub fn insert(&mut self, path: &str) {
let mut node = &mut self.root;

for component in reverse_components(path) {
node = node.children.entry(component.into()).or_default();
}

node.full_paths.push(path.into());
}

fn lookup(&self, path: &str, min_matches: Option<usize>) -> Vec<String> {
let mut matching_components = 0;
let mut components = reverse_components(path);
let mut node = &self.root;
let mut last_matching_paths = &vec![];

for component in &mut components {
match node.children.get(component) {
Some(child) => {
matching_components += 1;
node = child;
if !node.full_paths.is_empty() {
last_matching_paths = &node.full_paths;
}
}
None => break,
}
}

let mut results = last_matching_paths.clone();
if matching_components >= min_matches.map_or(1, |n| n + 1) {
// we have exhausted all the path components, but the tree might still have more children
// so we follow a straight branch down the tree if one exists, and extend the results with whatever we find
while node.children.len() == 1 {
node = node.children.first_key_value().unwrap().1;
if !node.full_paths.is_empty() {
results.extend_from_slice(&node.full_paths);
return results;
}
}
}

results
}
}

impl<T> FromIterator<T> for ReverseFileTree
where
T: AsRef<str>,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut tree = Self::new();
for path in iter {
tree.insert(path.as_ref());
}
tree
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_min_matches() {
let tree = ReverseFileTree::from_iter(&["x/y/z"]);

let cases: &[(usize, &str, &[&str])] = &[
// only the basename has to match
(0, "z", &["x/y/z"]),
(0, "R/z", &["x/y/z"]),
(0, "R/y/z", &["x/y/z"]),
(0, "x/y/z", &["x/y/z"]),
(0, "w/x/y/z", &["x/y/z"]),
// basename + one ancestor have to match
(1, "z", &[]),
(1, "R/z", &[]),
(1, "R/y/z", &["x/y/z"]),
(1, "x/y/z", &["x/y/z"]),
(1, "w/x/y/z", &["x/y/z"]),
// 3 components have to match
(2, "z", &[]),
(2, "R/z", &[]),
(2, "R/y/z", &[]),
(2, "x/y/z", &["x/y/z"]),
(2, "w/x/y/z", &["x/y/z"]),
];
for &(min_matches, lookup, result) in cases {
assert_eq!(tree.lookup(lookup, Some(min_matches)), result);
}
}

#[test]
fn test_lookup() {
let tree = ReverseFileTree::from_iter(&["mod.rs"]);
// exact lookup
assert_eq!(tree.lookup("mod.rs", None), &["mod.rs"]);
// no match
assert!(tree.lookup("not-found", None).is_empty());

let tree = ReverseFileTree::from_iter(&["foo/bar/mod.rs"]);

// the tree will follow unambiguous paths:
assert_eq!(tree.lookup("bar/mod.rs", None), &["foo/bar/mod.rs"]);
// it will also follow unambiguous partial matches:
assert_eq!(tree.lookup("qux/baz/bar/mod.rs", None), &["foo/bar/mod.rs"]);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use pyo3::prelude::*;

mod compute_name;
mod failure_message;
mod filetree;
mod junit;
mod testrun;

Expand Down
Loading