Multiple integration tests can call beets at the same time #19

Merged
wojtek merged 2 commits from 18---multiple-integration-tests-can-call-beets-at-the-same-time into main 2023-04-10 20:48:45 +02:00
5 changed files with 74 additions and 39 deletions

View File

@ -17,13 +17,14 @@ pub trait DatabaseJsonBackend {
fn write(&mut self, json: &str) -> Result<(), std::io::Error>; fn write(&mut self, json: &str) -> Result<(), std::io::Error>;
} }
/// A JSON file database. /// JSON database.
pub struct DatabaseJson { pub struct DatabaseJson {
backend: Box<dyn DatabaseJsonBackend>, backend: Box<dyn DatabaseJsonBackend + Send>,
} }
impl DatabaseJson { impl DatabaseJson {
pub fn new(backend: Box<dyn DatabaseJsonBackend>) -> Self { /// Create a new JSON database with the provided executor, e.g. [DatabaseJsonFile].
pub fn new(backend: Box<dyn DatabaseJsonBackend + Send>) -> Self {
DatabaseJson { backend } DatabaseJson { backend }
} }
} }
@ -50,6 +51,7 @@ impl DatabaseWrite for DatabaseJson {
} }
} }
/// JSON database that uses a local file for persistent storage.
pub struct DatabaseJsonFile { pub struct DatabaseJsonFile {
path: PathBuf, path: PathBuf,
} }

View File

@ -91,7 +91,7 @@ pub trait BeetsExecutor {
/// Struct for interacting with the music library via beets. /// Struct for interacting with the music library via beets.
pub struct Beets { pub struct Beets {
executor: Box<dyn BeetsExecutor>, executor: Box<dyn BeetsExecutor + Send>,
} }
trait LibraryPrivate { trait LibraryPrivate {
@ -106,7 +106,8 @@ trait LibraryPrivate {
} }
impl Beets { impl Beets {
pub fn new(executor: Box<dyn BeetsExecutor>) -> Beets { /// Create a new beets library instance with the provided executor, e.g. [SystemExecutor].
pub fn new(executor: Box<dyn BeetsExecutor + Send>) -> Beets {
Beets { executor } Beets { executor }
} }
} }
@ -236,28 +237,48 @@ impl LibraryPrivate for Beets {
} }
/// Executor for executing beets commands on the local system. /// Executor for executing beets commands on the local system.
///
/// # Safety
///
/// The beets executable is not safe to call concurrently for operations on the same
/// database/library. Therefore, all functions that create a [SystemExecutor] or modify which
/// library it works with are marked unsafe. It is the caller's responsibility to make sure the
/// library is not being concurrently accessed from anywhere else.
pub struct SystemExecutor { pub struct SystemExecutor {
bin: String, bin: String,
config: Option<PathBuf>, config: Option<PathBuf>,
} }
impl SystemExecutor { impl SystemExecutor {
pub fn new(bin: &str) -> Self { /// Create a new [SystemExecutor] that uses the provided beets executable.
///
/// # Safety
///
/// The caller must ensure the library is not being concurrently accessed from anywhere else.
pub unsafe fn new(bin: &str) -> Self {
SystemExecutor { SystemExecutor {
bin: bin.to_string(), bin: bin.to_string(),
config: None, config: None,
} }
} }
pub fn config(mut self, path: Option<&Path>) -> Self { /// Create a new [SystemExecutor] that uses the system's default beets executable.
self.config = path.map(|p| p.to_path_buf()); ///
self /// # Safety
} ///
/// The caller must ensure the library is not being concurrently accessed from anywhere else.
pub unsafe fn default() -> Self {
SystemExecutor::new("beet")
} }
impl Default for SystemExecutor { /// Update the configuration file for the beets executable.
fn default() -> Self { ///
SystemExecutor::new("beet") /// # Safety
///
/// The caller must ensure the library is not being concurrently accessed from anywhere else.
pub unsafe fn config(mut self, path: Option<&Path>) -> Self {
self.config = path.map(|p| p.to_path_buf());
self
} }
} }

View File

@ -37,7 +37,7 @@ fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
let mut beets = Beets::new(Box::new( let mut beets = Beets::new(Box::new(
SystemExecutor::default().config(opt.beets_config_file_path.as_deref()), unsafe { SystemExecutor::default().config(opt.beets_config_file_path.as_deref()) },
)); ));
let collection = beets let collection = beets

View File

@ -1,4 +1,4 @@
use std::fs; use std::{fs, path::PathBuf};
use musichoard::{ use musichoard::{
database::{ database::{
@ -7,10 +7,14 @@ use musichoard::{
}, },
Artist, Artist,
}; };
use once_cell::sync::Lazy;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::COLLECTION; use crate::COLLECTION;
static DATABASE_TEST_FILE: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/database/database.json").unwrap());
#[test] #[test]
fn write() { fn write() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
@ -21,8 +25,7 @@ fn write() {
let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
database.write(&write_data).unwrap(); database.write(&write_data).unwrap();
let expected_path = fs::canonicalize("./tests/files/database/database.json").unwrap(); let expected = fs::read_to_string(&*DATABASE_TEST_FILE).unwrap();
let expected = fs::read_to_string(expected_path).unwrap();
let actual = fs::read_to_string(file.path()).unwrap(); let actual = fs::read_to_string(file.path()).unwrap();
assert_eq!(actual, expected); assert_eq!(actual, expected);
@ -30,9 +33,7 @@ fn write() {
#[test] #[test]
fn read() { fn read() {
let file_path = fs::canonicalize("./tests/files/database/database.json").unwrap(); let backend = DatabaseJsonFile::new(&*DATABASE_TEST_FILE);
let backend = DatabaseJsonFile::new(&file_path);
let database = DatabaseJson::new(Box::new(backend)); let database = DatabaseJson::new(Box::new(backend));
let mut read_data: Vec<Artist> = vec![]; let mut read_data: Vec<Artist> = vec![];

View File

@ -1,4 +1,9 @@
use std::fs; use std::{
fs,
sync::{Arc, Mutex},
};
use once_cell::sync::Lazy;
use musichoard::{ use musichoard::{
library::{ library::{
@ -10,11 +15,25 @@ use musichoard::{
use crate::COLLECTION; use crate::COLLECTION;
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<Beets>>> = Lazy::new(|| {
Arc::new(Mutex::new(Beets::new(Box::new(unsafe {
SystemExecutor::default()
}))))
});
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<Beets>>> = Lazy::new(|| {
Arc::new(Mutex::new(Beets::new(Box::new(unsafe {
SystemExecutor::default().config(Some(
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
))
}))))
});
#[test] #[test]
fn test_no_config_list() { fn test_no_config_list() {
let executor = SystemExecutor::default(); let beets_arc = BEETS_EMPTY_CONFIG.clone();
let beets = &mut beets_arc.lock().unwrap();
let mut beets = Beets::new(Box::new(executor));
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
let expected: Vec<Artist> = vec![]; let expected: Vec<Artist> = vec![];
@ -23,11 +42,9 @@ fn test_no_config_list() {
#[test] #[test]
fn test_full_list() { fn test_full_list() {
let executor = SystemExecutor::default().config(Some( let beets_arc = BEETS_TEST_CONFIG.clone();
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), let beets = &mut beets_arc.lock().unwrap();
));
let mut beets = Beets::new(Box::new(executor));
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
let expected: Vec<Artist> = COLLECTION.to_owned(); let expected: Vec<Artist> = COLLECTION.to_owned();
@ -36,11 +53,9 @@ fn test_full_list() {
#[test] #[test]
fn test_album_artist_query() { fn test_album_artist_query() {
let executor = SystemExecutor::default().config(Some( let beets_arc = BEETS_TEST_CONFIG.clone();
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), let beets = &mut beets_arc.lock().unwrap();
));
let mut beets = Beets::new(Box::new(executor));
let output = beets let output = beets
.list(&Query::default().album_artist(QueryOption::Include(String::from("Аркона")))) .list(&Query::default().album_artist(QueryOption::Include(String::from("Аркона"))))
.unwrap(); .unwrap();
@ -51,11 +66,9 @@ fn test_album_artist_query() {
#[test] #[test]
fn test_album_title_query() { fn test_album_title_query() {
let executor = SystemExecutor::default().config(Some( let beets_arc = BEETS_TEST_CONFIG.clone();
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), let beets = &mut beets_arc.lock().unwrap();
));
let mut beets = Beets::new(Box::new(executor));
let output = beets let output = beets
.list(&Query::default().album_title(QueryOption::Include(String::from("Slovo")))) .list(&Query::default().album_title(QueryOption::Include(String::from("Slovo"))))
.unwrap(); .unwrap();
@ -66,11 +79,9 @@ fn test_album_title_query() {
#[test] #[test]
fn test_exclude_query() { fn test_exclude_query() {
let executor = SystemExecutor::default().config(Some( let beets_arc = BEETS_TEST_CONFIG.clone();
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), let beets = &mut beets_arc.lock().unwrap();
));
let mut beets = Beets::new(Box::new(executor));
let output = beets let output = beets
.list(&Query::default().album_artist(QueryOption::Exclude(String::from("Аркона")))) .list(&Query::default().album_artist(QueryOption::Exclude(String::from("Аркона"))))
.unwrap(); .unwrap();