Implement Week 3 virtual tree with path resolver and FUSE integration

This commit is contained in:
Alexander
2026-05-12 18:25:24 +02:00
parent d664439746
commit d9e5e06166
7 changed files with 777 additions and 37 deletions
+1
View File
@@ -406,6 +406,7 @@ version = "0.1.0"
dependencies = [
"fuser",
"libc",
"musicfs-cache",
"musicfs-core",
"tokio",
"tracing",
+4
View File
@@ -1,5 +1,9 @@
mod db;
mod metadata;
mod tree;
pub use db::Database;
pub use metadata::MetadataCache;
pub use tree::{
DirNode, FileNode, Inode, RefreshPolicy, TreeBuilder, VirtualNode, VirtualTree, ROOT_INODE,
};
+444
View File
@@ -0,0 +1,444 @@
use musicfs_core::{FileId, FileMeta, VirtualPath};
use std::collections::{BTreeMap, HashMap};
use std::ffi::{OsStr, OsString};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
pub type Inode = u64;
pub const ROOT_INODE: Inode = 1;
#[derive(Debug)]
pub enum VirtualNode {
Directory(DirNode),
File(FileNode),
}
impl VirtualNode {
pub fn inode(&self) -> Inode {
match self {
VirtualNode::Directory(d) => d.inode,
VirtualNode::File(f) => f.inode,
}
}
pub fn name(&self) -> &OsStr {
match self {
VirtualNode::Directory(d) => &d.name,
VirtualNode::File(f) => &f.name,
}
}
pub fn is_dir(&self) -> bool {
matches!(self, VirtualNode::Directory(_))
}
}
#[derive(Debug)]
pub struct DirNode {
pub inode: Inode,
pub parent: Inode,
pub name: OsString,
pub children: BTreeMap<OsString, Inode>,
pub mtime: SystemTime,
}
#[derive(Debug)]
pub struct FileNode {
pub inode: Inode,
pub name: OsString,
pub file_id: FileId,
pub size: u64,
pub mtime: SystemTime,
}
#[derive(Debug, Clone)]
pub struct RefreshPolicy {
pub ttl: Duration,
pub refresh_on_access: bool,
pub background_interval: Option<Duration>,
}
impl Default for RefreshPolicy {
fn default() -> Self {
Self {
ttl: Duration::from_secs(300),
refresh_on_access: false,
background_interval: None,
}
}
}
pub struct VirtualTree {
nodes: HashMap<Inode, VirtualNode>,
path_to_inode: HashMap<VirtualPath, Inode>,
next_inode: AtomicU64,
last_refresh: RwLock<SystemTime>,
refresh_policy: RefreshPolicy,
}
impl VirtualTree {
pub fn new() -> Self {
Self::with_policy(RefreshPolicy::default())
}
pub fn with_policy(policy: RefreshPolicy) -> Self {
let mut tree = Self {
nodes: HashMap::new(),
path_to_inode: HashMap::new(),
next_inode: AtomicU64::new(ROOT_INODE + 1),
last_refresh: RwLock::new(SystemTime::now()),
refresh_policy: policy,
};
tree.nodes.insert(
ROOT_INODE,
VirtualNode::Directory(DirNode {
inode: ROOT_INODE,
parent: ROOT_INODE,
name: OsString::from(""),
children: BTreeMap::new(),
mtime: SystemTime::now(),
}),
);
tree.path_to_inode
.insert(VirtualPath::new("/"), ROOT_INODE);
tree
}
fn alloc_inode(&self) -> Inode {
self.next_inode.fetch_add(1, Ordering::SeqCst)
}
pub fn get(&self, inode: Inode) -> Option<&VirtualNode> {
self.nodes.get(&inode)
}
pub fn get_by_path(&self, path: &VirtualPath) -> Option<&VirtualNode> {
self.path_to_inode
.get(path)
.and_then(|ino| self.nodes.get(ino))
}
pub fn lookup(&self, parent_inode: Inode, name: &OsStr) -> Option<Inode> {
if let Some(VirtualNode::Directory(dir)) = self.nodes.get(&parent_inode) {
dir.children.get(name).copied()
} else {
None
}
}
pub fn readdir(&self, inode: Inode) -> Option<Vec<(OsString, Inode, bool)>> {
if let Some(VirtualNode::Directory(dir)) = self.nodes.get(&inode) {
Some(
dir.children
.iter()
.map(|(name, &ino)| {
let is_dir = self.nodes.get(&ino).map(|n| n.is_dir()).unwrap_or(false);
(name.clone(), ino, is_dir)
})
.collect(),
)
} else {
None
}
}
pub fn get_parent(&self, inode: Inode) -> Option<Inode> {
match self.nodes.get(&inode) {
Some(VirtualNode::Directory(dir)) => Some(dir.parent),
Some(VirtualNode::File(_)) => self.find_parent_by_path_lookup(inode),
None => None,
}
}
fn find_parent_by_path_lookup(&self, inode: Inode) -> Option<Inode> {
for (path, &ino) in &self.path_to_inode {
if ino == inode {
return std::path::Path::new(path.as_str())
.parent()
.and_then(|p| {
self.path_to_inode
.get(&VirtualPath::new(p.to_string_lossy().into_owned()))
.copied()
});
}
}
None
}
pub fn insert_file(&mut self, meta: &FileMeta) -> Inode {
let path = &meta.virtual_path;
let parent_inode = self.ensure_parents(path);
let inode = self.alloc_inode();
let name = std::path::Path::new(path.as_str())
.file_name()
.unwrap_or_default()
.to_os_string();
let file_node = FileNode {
inode,
name: name.clone(),
file_id: meta.id,
size: meta.size,
mtime: meta.mtime,
};
self.nodes.insert(inode, VirtualNode::File(file_node));
self.path_to_inode.insert(path.clone(), inode);
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&parent_inode) {
dir.children.insert(name, inode);
}
inode
}
fn ensure_parents(&mut self, path: &VirtualPath) -> Inode {
let path_str = path.as_str();
let components: Vec<&str> = path_str
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if components.len() <= 1 {
return ROOT_INODE;
}
let mut current_inode = ROOT_INODE;
let mut current_path = String::from("/");
for component in &components[..components.len() - 1] {
current_path.push_str(component);
let vpath = VirtualPath::new(&current_path);
if let Some(&existing) = self.path_to_inode.get(&vpath) {
current_inode = existing;
} else {
let new_inode = self.alloc_inode();
let name = OsString::from(*component);
let dir_node = DirNode {
inode: new_inode,
parent: current_inode,
name: name.clone(),
children: BTreeMap::new(),
mtime: SystemTime::now(),
};
self.nodes
.insert(new_inode, VirtualNode::Directory(dir_node));
self.path_to_inode.insert(vpath, new_inode);
if let Some(VirtualNode::Directory(parent)) = self.nodes.get_mut(&current_inode) {
parent.children.insert(name, new_inode);
}
current_inode = new_inode;
}
current_path.push('/');
}
current_inode
}
pub fn remove_file(&mut self, path: &VirtualPath) -> Option<FileId> {
let inode = self.path_to_inode.remove(path)?;
if let Some(VirtualNode::File(file)) = self.nodes.remove(&inode) {
let parent_path = std::path::Path::new(path.as_str())
.parent()
.map(|p| VirtualPath::new(p.to_string_lossy().into_owned()))
.unwrap_or_else(|| VirtualPath::new("/"));
if let Some(&parent_inode) = self.path_to_inode.get(&parent_path) {
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&parent_inode) {
dir.children.remove(&file.name);
}
}
Some(file.file_id)
} else {
None
}
}
pub fn file_count(&self) -> usize {
self.nodes
.values()
.filter(|n| matches!(n, VirtualNode::File(_)))
.count()
}
pub fn dir_count(&self) -> usize {
self.nodes
.values()
.filter(|n| matches!(n, VirtualNode::Directory(_)))
.count()
}
pub fn needs_refresh(&self) -> bool {
let last = *self.last_refresh.read().unwrap();
last.elapsed().unwrap_or(Duration::MAX) > self.refresh_policy.ttl
}
pub fn force_refresh(&mut self) {
self.nodes.retain(|&ino, _| ino == ROOT_INODE);
self.path_to_inode.retain(|p, _| p.as_str() == "/");
if let Some(VirtualNode::Directory(root)) = self.nodes.get_mut(&ROOT_INODE) {
root.children.clear();
}
*self.last_refresh.write().unwrap() = SystemTime::now();
}
pub fn mark_refreshed(&self) {
*self.last_refresh.write().unwrap() = SystemTime::now();
}
pub fn refresh_policy(&self) -> &RefreshPolicy {
&self.refresh_policy
}
}
impl Default for VirtualTree {
fn default() -> Self {
Self::new()
}
}
pub struct TreeBuilder {
tree: VirtualTree,
}
impl TreeBuilder {
pub fn new() -> Self {
Self {
tree: VirtualTree::new(),
}
}
pub fn with_policy(policy: RefreshPolicy) -> Self {
Self {
tree: VirtualTree::with_policy(policy),
}
}
pub fn add_file(&mut self, meta: &FileMeta) {
self.tree.insert_file(meta);
}
pub fn build(self) -> VirtualTree {
self.tree
}
}
impl Default for TreeBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use musicfs_core::{OriginId, RealPath};
use std::path::PathBuf;
fn make_file_meta(id: i64, vpath: &str) -> FileMeta {
FileMeta {
id: FileId(id),
virtual_path: VirtualPath::new(vpath),
real_path: RealPath {
origin_id: OriginId::from("test"),
path: PathBuf::from("/test"),
},
size: 1000,
mtime: SystemTime::now(),
content_hash: None,
audio: None,
}
}
#[test]
fn test_tree_creation() {
let tree = VirtualTree::new();
assert!(tree.get(ROOT_INODE).is_some());
}
#[test]
fn test_insert_file() {
let mut tree = VirtualTree::new();
let meta = make_file_meta(1, "/Artist/Album/Track.flac");
tree.insert_file(&meta);
assert!(tree.get_by_path(&VirtualPath::new("/Artist")).is_some());
assert!(tree
.get_by_path(&VirtualPath::new("/Artist/Album"))
.is_some());
assert!(tree
.get_by_path(&VirtualPath::new("/Artist/Album/Track.flac"))
.is_some());
}
#[test]
fn test_readdir() {
let mut tree = VirtualTree::new();
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track1.flac"));
tree.insert_file(&make_file_meta(2, "/Artist/Album/Track2.flac"));
let root_children = tree.readdir(ROOT_INODE).unwrap();
assert_eq!(root_children.len(), 1);
assert_eq!(root_children[0].0, "Artist");
}
#[test]
fn test_lookup() {
let mut tree = VirtualTree::new();
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track.flac"));
let artist_inode = tree.lookup(ROOT_INODE, OsStr::new("Artist")).unwrap();
assert!(tree.lookup(artist_inode, OsStr::new("Album")).is_some());
}
#[test]
fn test_file_and_dir_count() {
let mut tree = VirtualTree::new();
tree.insert_file(&make_file_meta(1, "/A/B/Track1.flac"));
tree.insert_file(&make_file_meta(2, "/A/B/Track2.flac"));
tree.insert_file(&make_file_meta(3, "/A/C/Track3.flac"));
assert_eq!(tree.file_count(), 3);
assert_eq!(tree.dir_count(), 4);
}
#[test]
fn test_remove_file() {
let mut tree = VirtualTree::new();
let path = VirtualPath::new("/Artist/Album/Track.flac");
tree.insert_file(&make_file_meta(1, path.as_str()));
assert!(tree.get_by_path(&path).is_some());
let removed_id = tree.remove_file(&path);
assert_eq!(removed_id, Some(FileId(1)));
assert!(tree.get_by_path(&path).is_none());
}
#[test]
fn test_tree_builder() {
let mut builder = TreeBuilder::new();
builder.add_file(&make_file_meta(1, "/A/Track1.flac"));
builder.add_file(&make_file_meta(2, "/A/Track2.flac"));
let tree = builder.build();
assert_eq!(tree.file_count(), 2);
}
}
+2
View File
@@ -1,7 +1,9 @@
pub mod error;
pub mod events;
pub mod resolver;
pub mod types;
pub use error::{Error, Result};
pub use events::{Event, EventBus};
pub use resolver::{PathResolver, PathTemplate};
pub use types::*;
+174
View File
@@ -0,0 +1,174 @@
use crate::{AudioMeta, VirtualPath};
#[derive(Debug, Clone)]
pub struct PathTemplate {
pub pattern: String,
pub fallback_artist: String,
pub fallback_album: String,
pub fallback_title: String,
pub fallback_year: String,
}
impl Default for PathTemplate {
fn default() -> Self {
Self {
pattern: "$artist/$album ($year) [$format_upper]/$track - $title.$format".to_string(),
fallback_artist: "Unknown Artist".to_string(),
fallback_album: "Unknown Album".to_string(),
fallback_title: "Unknown Track".to_string(),
fallback_year: "Unknown".to_string(),
}
}
}
pub struct PathResolver {
template: PathTemplate,
}
impl PathResolver {
pub fn new(template: PathTemplate) -> Self {
Self { template }
}
pub fn resolve(&self, meta: &AudioMeta, extension: &str) -> VirtualPath {
let artist = meta
.artist
.as_deref()
.unwrap_or(&self.template.fallback_artist);
let album = meta
.album
.as_deref()
.unwrap_or(&self.template.fallback_album);
let title = meta
.title
.as_deref()
.unwrap_or(&self.template.fallback_title);
let year = meta
.year
.map(|y| y.to_string())
.unwrap_or_else(|| self.template.fallback_year.clone());
let track = meta.track.unwrap_or(0);
let disc = meta.disc.unwrap_or(1);
let genre = meta.genre.as_deref().unwrap_or("Unknown");
let format = extension.to_lowercase();
let format_upper = extension.to_uppercase();
let artist = sanitize_path_component(artist);
let album = sanitize_path_component(album);
let title = sanitize_path_component(title);
let genre = sanitize_path_component(genre);
let path = self
.template
.pattern
.replace("$artist", &artist)
.replace("$album", &album)
.replace("$title", &title)
.replace("$track", &format!("{:02}", track))
.replace("$disc", &disc.to_string())
.replace("$year", &year)
.replace("$genre", &genre)
.replace("$format_upper", &format_upper)
.replace("$format", &format);
VirtualPath::new(path)
}
}
impl Default for PathResolver {
fn default() -> Self {
Self::new(PathTemplate::default())
}
}
fn sanitize_path_component(s: &str) -> String {
s.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_',
c => c,
})
.collect::<String>()
.trim()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AudioFormat;
#[test]
fn test_resolve_complete_metadata() {
let resolver = PathResolver::default();
let meta = AudioMeta {
artist: Some("Metallica".to_string()),
album: Some("Master of Puppets".to_string()),
title: Some("Battery".to_string()),
track: Some(1),
year: Some(1986),
format: AudioFormat::Flac,
..Default::default()
};
let path = resolver.resolve(&meta, "flac");
assert_eq!(
path.as_str(),
"Metallica/Master of Puppets (1986) [FLAC]/01 - Battery.flac"
);
}
#[test]
fn test_resolve_missing_album() {
let resolver = PathResolver::default();
let meta = AudioMeta {
artist: Some("Artist".to_string()),
title: Some("Track".to_string()),
track: Some(5),
..Default::default()
};
let path = resolver.resolve(&meta, "mp3");
assert_eq!(
path.as_str(),
"Artist/Unknown Album (Unknown) [MP3]/05 - Track.mp3"
);
}
#[test]
fn test_sanitize_special_chars() {
let resolver = PathResolver::default();
let meta = AudioMeta {
artist: Some("AC/DC".to_string()),
album: Some("Who Made Who?".to_string()),
title: Some("Test:Track".to_string()),
track: Some(1),
year: Some(1986),
..Default::default()
};
let path = resolver.resolve(&meta, "flac");
assert!(!path.as_str().contains(':'));
assert!(!path.as_str().contains('?'));
assert!(path.as_str().contains("AC_DC"));
}
#[test]
fn test_custom_template() {
let template = PathTemplate {
pattern: "$genre/$artist - $album/$track $title.$format".to_string(),
..Default::default()
};
let resolver = PathResolver::new(template);
let meta = AudioMeta {
artist: Some("Artist".to_string()),
album: Some("Album".to_string()),
title: Some("Song".to_string()),
genre: Some("Rock".to_string()),
track: Some(3),
..Default::default()
};
let path = resolver.resolve(&meta, "flac");
assert_eq!(path.as_str(), "Rock/Artist - Album/03 Song.flac");
}
}
+1
View File
@@ -5,6 +5,7 @@ edition.workspace = true
[dependencies]
musicfs-core = { path = "../musicfs-core" }
musicfs-cache = { path = "../musicfs-cache" }
fuser.workspace = true
tokio.workspace = true
tracing.workspace = true
+151 -37
View File
@@ -1,23 +1,28 @@
use fuser::{
FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen,
Request, FUSE_ROOT_ID,
Request,
};
use musicfs_core::{Error, Result};
use musicfs_cache::{VirtualNode, VirtualTree, ROOT_INODE};
use musicfs_core::Result;
use std::ffi::OsStr;
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use tracing::{debug, info};
const TTL: Duration = Duration::from_secs(1);
const BLOCK_SIZE: u32 = 512;
pub struct MusicFs {
tree: Arc<RwLock<VirtualTree>>,
uid: u32,
gid: u32,
}
impl MusicFs {
pub fn new() -> Self {
pub fn new(tree: Arc<RwLock<VirtualTree>>) -> Self {
Self {
tree,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}
@@ -33,38 +38,51 @@ impl MusicFs {
fuser::MountOption::AllowOther,
];
fuser::mount2(self, mountpoint, &options).map_err(Error::Io)?;
fuser::mount2(self, mountpoint, &options).map_err(musicfs_core::Error::Io)?;
Ok(())
}
fn root_attr(&self) -> FileAttr {
FileAttr {
ino: FUSE_ROOT_ID,
size: 0,
blocks: 0,
atime: UNIX_EPOCH,
mtime: UNIX_EPOCH,
ctime: UNIX_EPOCH,
crtime: UNIX_EPOCH,
kind: FileType::Directory,
perm: 0o755,
nlink: 2,
uid: self.uid,
gid: self.gid,
rdev: 0,
blksize: 512,
flags: 0,
fn node_to_attr(&self, node: &VirtualNode) -> FileAttr {
match node {
VirtualNode::Directory(dir) => FileAttr {
ino: dir.inode,
size: 0,
blocks: 0,
atime: dir.mtime,
mtime: dir.mtime,
ctime: dir.mtime,
crtime: dir.mtime,
kind: FileType::Directory,
perm: 0o755,
nlink: 2,
uid: self.uid,
gid: self.gid,
rdev: 0,
blksize: BLOCK_SIZE,
flags: 0,
},
VirtualNode::File(file) => FileAttr {
ino: file.inode,
size: file.size,
blocks: (file.size + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64,
atime: file.mtime,
mtime: file.mtime,
ctime: file.mtime,
crtime: file.mtime,
kind: FileType::RegularFile,
perm: 0o644,
nlink: 1,
uid: self.uid,
gid: self.gid,
rdev: 0,
blksize: BLOCK_SIZE,
flags: 0,
},
}
}
}
impl Default for MusicFs {
fn default() -> Self {
Self::new()
}
}
impl Filesystem for MusicFs {
fn init(
&mut self,
@@ -81,14 +99,28 @@ impl Filesystem for MusicFs {
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
debug!("lookup(parent={}, name={:?})", parent, name);
let tree = self.tree.read().unwrap();
if let Some(inode) = tree.lookup(parent, name) {
if let Some(node) = tree.get(inode) {
let attr = self.node_to_attr(node);
reply.entry(&TTL, &attr, 0);
return;
}
}
reply.error(libc::ENOENT);
}
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
debug!("getattr(ino={})", ino);
if ino == FUSE_ROOT_ID {
reply.attr(&TTL, &self.root_attr());
let tree = self.tree.read().unwrap();
if let Some(node) = tree.get(ino) {
let attr = self.node_to_attr(node);
reply.attr(&TTL, &attr);
} else {
reply.error(libc::ENOENT);
}
@@ -104,13 +136,46 @@ impl Filesystem for MusicFs {
) {
debug!("readdir(ino={}, offset={})", ino, offset);
if ino == FUSE_ROOT_ID {
if offset == 0 {
let _ = reply.add(FUSE_ROOT_ID, 1, FileType::Directory, ".");
let tree = self.tree.read().unwrap();
if let Some(children) = tree.readdir(ino) {
let parent_ino = tree.get_parent(ino).unwrap_or(ROOT_INODE);
let entries: Vec<(u64, FileType, &str)> = vec![
(ino, FileType::Directory, "."),
(parent_ino, FileType::Directory, ".."),
];
let child_entries: Vec<(u64, FileType, String)> = children
.iter()
.map(|(name, child_ino, is_dir)| {
let kind = if *is_dir {
FileType::Directory
} else {
FileType::RegularFile
};
(*child_ino, kind, name.to_string_lossy().to_string())
})
.collect();
for (i, (inode, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
if reply.add(*inode, (i + 1) as i64, *kind, name) {
reply.ok();
return;
}
}
if offset <= 1 {
let _ = reply.add(FUSE_ROOT_ID, 2, FileType::Directory, "..");
let base_offset = entries.len();
for (i, (inode, kind, name)) in child_entries.iter().enumerate() {
let entry_offset = base_offset + i;
if entry_offset < offset as usize {
continue;
}
if reply.add(*inode, (entry_offset + 1) as i64, *kind, name) {
break;
}
}
reply.ok();
} else {
reply.error(libc::ENOENT);
@@ -126,7 +191,13 @@ impl Filesystem for MusicFs {
return;
}
reply.error(libc::ENOENT);
let tree = self.tree.read().unwrap();
if tree.get(ino).is_some() {
reply.opened(0, 0);
} else {
reply.error(libc::ENOENT);
}
}
fn read(
@@ -141,7 +212,14 @@ impl Filesystem for MusicFs {
reply: ReplyData,
) {
debug!("read(ino={}, offset={}, size={})", ino, offset, size);
reply.error(libc::ENOENT);
let tree = self.tree.read().unwrap();
if let Some(VirtualNode::File(_file)) = tree.get(ino) {
reply.data(&[]);
} else {
reply.error(libc::ENOENT);
}
}
fn release(
@@ -275,3 +353,39 @@ impl Filesystem for MusicFs {
reply.error(libc::EROFS);
}
}
#[cfg(test)]
mod tests {
use super::*;
use musicfs_cache::TreeBuilder;
use musicfs_core::{FileId, FileMeta, OriginId, RealPath, VirtualPath};
use std::path::PathBuf;
fn make_file_meta(id: i64, vpath: &str, size: u64) -> FileMeta {
FileMeta {
id: FileId(id),
virtual_path: VirtualPath::new(vpath),
real_path: RealPath {
origin_id: OriginId::from("test"),
path: PathBuf::from("/test"),
},
size,
mtime: SystemTime::now(),
content_hash: None,
audio: None,
}
}
#[test]
fn test_tree_integration() {
let mut builder = TreeBuilder::new();
builder.add_file(&make_file_meta(1, "/Artist/Album/Track.flac", 30_000_000));
let tree = Arc::new(RwLock::new(builder.build()));
let _fs = MusicFs::new(tree.clone());
let tree_read = tree.read().unwrap();
assert!(tree_read.get(ROOT_INODE).is_some());
assert!(tree_read.get_by_path(&VirtualPath::new("/Artist")).is_some());
}
}