diff --git a/Cargo.lock b/Cargo.lock index d102cbb..3bea8b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,26 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -20,6 +40,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "errno" version = "0.3.0" @@ -50,6 +85,24 @@ dependencies = [ "instant", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.1" @@ -71,7 +124,7 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "libc", "windows-sys", ] @@ -82,6 +135,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.140" @@ -98,12 +157,44 @@ checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" name = "musichoard" version = "0.1.0" dependencies = [ + "once_cell", "serde", "serde_json", + "structopt", "tempfile", "uuid", ] +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.54" @@ -168,7 +259,7 @@ checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] @@ -182,6 +273,47 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.11" @@ -206,12 +338,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "uuid" version = "1.3.0" @@ -221,6 +374,40 @@ dependencies = [ "serde", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 306828f..bbacf07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,9 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +structopt = "0.3" uuid = { version = "1.3", features = ["serde"] } [dev-dependencies] +once_cell = "1.17" tempfile = "3.5" diff --git a/src/database/json.rs b/src/database/json.rs index e2f7278..911045e 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -1,7 +1,7 @@ //! Module for storing MusicHoard data in a JSON file database. -use std::fs::{read_to_string, write}; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -56,8 +56,10 @@ pub struct DatabaseJsonFile { impl DatabaseJsonFile { /// Create a database instance that will read/write to the provided path. - pub fn new(path: PathBuf) -> Self { - DatabaseJsonFile { path } + pub fn new(path: &Path) -> Self { + DatabaseJsonFile { + path: path.to_path_buf(), + } } } @@ -65,11 +67,11 @@ impl DatabaseJsonBackend for DatabaseJsonFile { fn read(&self) -> Result { // Read entire file to memory as for now this is faster than a buffered read from disk: // https://github.com/serde-rs/json/issues/160 - read_to_string(&self.path) + fs::read_to_string(&self.path) } fn write(&mut self, json: &str) -> Result<(), std::io::Error> { - write(&self.path, json) + fs::write(&self.path, json) } } @@ -77,7 +79,7 @@ impl DatabaseJsonBackend for DatabaseJsonFile { mod tests { use super::*; - use crate::{tests::test_data, Artist}; + use crate::{tests::COLLECTION, Artist}; struct DatabaseJsonTest { json: String, @@ -154,7 +156,7 @@ mod tests { #[test] fn write() { - let write_data = test_data(); + let write_data = COLLECTION.to_owned(); let backend = DatabaseJsonTest { json: artists_to_json(&write_data), }; @@ -166,7 +168,7 @@ mod tests { #[test] fn read() { - let expected = test_data(); + let expected = COLLECTION.to_owned(); let backend = DatabaseJsonTest { json: artists_to_json(&expected), }; @@ -181,13 +183,13 @@ mod tests { #[test] fn reverse() { - let expected = test_data(); + let expected = COLLECTION.to_owned(); let backend = DatabaseJsonTest { json: artists_to_json(&expected), }; let mut database = DatabaseJson::new(Box::new(backend)); - let write_data = test_data(); + let write_data = COLLECTION.to_owned(); let mut read_data: Vec = vec![]; database.write(&write_data).unwrap(); database.read(&mut read_data).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index d471250..62cdb85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod library; pub type Mbid = Uuid; /// A single track on an album. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Track { pub number: u32, pub title: String, @@ -18,27 +18,27 @@ pub struct Track { } /// The album identifier. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct AlbumId { pub year: u32, pub title: String, } /// An album is a collection of tracks that were released together. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Album { pub id: AlbumId, pub tracks: Vec, } /// The artist identifier. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct ArtistId { pub name: String, } /// An artist. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Artist { pub id: ArtistId, pub albums: Vec, @@ -48,7 +48,9 @@ pub struct Artist { mod tests { use super::*; - pub fn test_data() -> Vec { + use once_cell::sync::Lazy; + + pub static COLLECTION: Lazy> = Lazy::new(|| { vec![ Artist { id: ArtistId { @@ -128,5 +130,5 @@ mod tests { }], }, ] - } + }); } diff --git a/src/library/beets.rs b/src/library/beets.rs index 1140210..bf8ff49 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -4,7 +4,9 @@ use std::{ collections::{HashMap, HashSet}, fmt::Display, + path::{Path, PathBuf}, process::Command, + str, }; use crate::{Album, AlbumId, Artist, ArtistId, Track}; @@ -30,7 +32,7 @@ impl QueryOptionArgBeets for QueryOption { Self::Exclude(value) => ("^", value), Self::None => return None, }; - Some(format!("{}{}{}", negate, option_name, value)) + Some(format!("{negate}{option_name}{value}")) } } @@ -41,7 +43,7 @@ impl QueryOptionArgBeets for QueryOption> { Self::Exclude(value) => ("^", value), Self::None => return None, }; - Some(format!("{}{}{}", negate, option_name, vec.join("; "))) + Some(format!("{negate}{option_name}{}", vec.join("; "))) } } @@ -117,7 +119,7 @@ impl Library for Beets { macro_rules! list_format_separator { () => { - "-*^-" + " -*^- " }; } @@ -151,8 +153,11 @@ impl LibraryPrivate for Beets { let mut album_ids = HashMap::>::new(); for line in list_output.iter() { - let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); + if line.is_empty() { + continue; + } + let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); if split.len() != 6 { return Err(Error::InvalidData(line.to_string())); } @@ -221,14 +226,21 @@ impl LibraryPrivate for Beets { /// Executor for executing beets commands on the local system. pub struct SystemExecutor { bin: String, + config: Option, } impl SystemExecutor { - pub fn new(bin: &str) -> SystemExecutor { + pub fn new(bin: &str) -> Self { SystemExecutor { bin: bin.to_string(), + config: None, } } + + pub fn config(mut self, path: Option<&Path>) -> Self { + self.config = path.map(|p| p.to_path_buf()); + self + } } impl Default for SystemExecutor { @@ -239,8 +251,18 @@ impl Default for SystemExecutor { impl BeetsExecutor for SystemExecutor { fn exec(&mut self, arguments: &[String]) -> Result, Error> { - let output = Command::new(&self.bin).args(arguments).output()?; - let output = std::str::from_utf8(&output.stdout)?; + let mut cmd = Command::new(&self.bin); + if let Some(ref path) = self.config { + cmd.arg("--config"); + cmd.arg(path); + } + let output = cmd.args(arguments).output()?; + if !output.status.success() { + return Err(Error::CmdExecError( + String::from_utf8_lossy(&output.stderr).to_string(), + )); + } + let output = str::from_utf8(&output.stdout)?; Ok(output.split('\n').map(|s| s.to_string()).collect()) } } @@ -249,7 +271,7 @@ impl BeetsExecutor for SystemExecutor { mod tests { use super::*; - use crate::tests::test_data; + use crate::tests::COLLECTION; struct TestExecutor { arguments: Option>, @@ -333,7 +355,7 @@ mod tests { #[test] fn test_list_ordered() { - let expected = test_data(); + let expected = COLLECTION.to_owned(); let output = artists_to_beets_string(&expected); let executor = TestExecutor { @@ -348,7 +370,7 @@ mod tests { #[test] fn test_list_unordered() { - let mut expected = test_data(); + let mut expected = COLLECTION.to_owned(); let mut output = artists_to_beets_string(&expected); let last = output.len() - 1; output.swap(0, last); @@ -377,7 +399,7 @@ mod tests { #[test] fn test_list_album_title_year_clash() { - let mut expected = test_data(); + let mut expected = COLLECTION.to_owned(); expected[0].albums[0].id.year = expected[1].albums[0].id.year; expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); diff --git a/src/library/mod.rs b/src/library/mod.rs index ed8eb25..6fa2634 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -98,6 +98,8 @@ impl Query { /// Error type for library calls. #[derive(Debug)] pub enum Error { + /// The underlying library failed to execute a command. + CmdExecError(String), /// The underlying library returned invalid data. InvalidData(String), /// The underlying library experienced an I/O error. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0cfcfc8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,52 @@ +use std::path::PathBuf; + +use structopt::StructOpt; + +use musichoard::{ + database::{ + json::{DatabaseJson, DatabaseJsonFile}, + DatabaseWrite, + }, + library::{ + beets::{Beets, SystemExecutor}, + Library, Query, + }, +}; + +#[derive(StructOpt)] +struct Opt { + #[structopt( + short = "b", + long = "beets-config", + name = "beets config file path", + parse(from_os_str) + )] + beets_config_file_path: Option, + + #[structopt( + short = "d", + long = "database", + name = "database file path", + default_value = "database.json", + parse(from_os_str) + )] + database_file_path: PathBuf, +} + +fn main() { + let opt = Opt::from_args(); + + let mut beets = Beets::new(Box::new( + SystemExecutor::default().config(opt.beets_config_file_path.as_deref()), + )); + + let collection = beets + .list(&Query::new()) + .expect("failed to query the library"); + + let mut database = DatabaseJson::new(Box::new(DatabaseJsonFile::new(&opt.database_file_path))); + + database + .write(&collection) + .expect("failed to write to the database"); +} diff --git a/tests/database/json.rs b/tests/database/json.rs new file mode 100644 index 0000000..8b22aef --- /dev/null +++ b/tests/database/json.rs @@ -0,0 +1,59 @@ +use std::fs; + +use musichoard::{ + database::{ + json::{DatabaseJson, DatabaseJsonFile}, + DatabaseRead, DatabaseWrite, + }, + Artist, +}; +use tempfile::NamedTempFile; + +use crate::COLLECTION; + +#[test] +fn write() { + let file = NamedTempFile::new().unwrap(); + + let backend = DatabaseJsonFile::new(file.path()); + let mut database = DatabaseJson::new(Box::new(backend)); + + let write_data = COLLECTION.to_owned(); + database.write(&write_data).unwrap(); + + let expected_path = fs::canonicalize("./tests/files/database/database.json").unwrap(); + let expected = fs::read_to_string(expected_path).unwrap(); + let actual = fs::read_to_string(file.path()).unwrap(); + + assert_eq!(actual, expected); +} + +#[test] +fn read() { + let file_path = fs::canonicalize("./tests/files/database/database.json").unwrap(); + + let backend = DatabaseJsonFile::new(&file_path); + let database = DatabaseJson::new(Box::new(backend)); + + let mut read_data: Vec = vec![]; + database.read(&mut read_data).unwrap(); + + let expected = COLLECTION.to_owned(); + assert_eq!(read_data, expected); +} + +#[test] +fn reverse() { + let file = NamedTempFile::new().unwrap(); + + let backend = DatabaseJsonFile::new(file.path()); + let mut database = DatabaseJson::new(Box::new(backend)); + + let write_data = COLLECTION.to_owned(); + database.write(&write_data).unwrap(); + + let mut read_data: Vec = vec![]; + database.read(&mut read_data).unwrap(); + + assert_eq!(write_data, read_data); +} diff --git a/tests/database/mod.rs b/tests/database/mod.rs new file mode 100644 index 0000000..cff0e90 --- /dev/null +++ b/tests/database/mod.rs @@ -0,0 +1 @@ +mod json; diff --git a/tests/files/.gitignore b/tests/files/.gitignore new file mode 100644 index 0000000..14d6358 --- /dev/null +++ b/tests/files/.gitignore @@ -0,0 +1 @@ +library diff --git a/tests/files/database/database.json b/tests/files/database/database.json new file mode 100644 index 0000000..e60452c --- /dev/null +++ b/tests/files/database/database.json @@ -0,0 +1 @@ +[{"id":{"name":"Аркона"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"number":1,"title":"Az’","artist":["Аркона"]},{"number":2,"title":"Arkaim","artist":["Аркона"]},{"number":3,"title":"Bol’no mne","artist":["Аркона"]},{"number":4,"title":"Leshiy","artist":["Аркона"]},{"number":5,"title":"Zakliatie","artist":["Аркона"]},{"number":6,"title":"Predok","artist":["Аркона"]},{"number":7,"title":"Nikogda","artist":["Аркона"]},{"number":8,"title":"Tam za tumanami","artist":["Аркона"]},{"number":9,"title":"Potomok","artist":["Аркона"]},{"number":10,"title":"Slovo","artist":["Аркона"]},{"number":11,"title":"Odna","artist":["Аркона"]},{"number":12,"title":"Vo moiom sadochke…","artist":["Аркона"]},{"number":13,"title":"Stenka na stenku","artist":["Аркона"]},{"number":14,"title":"Zimushka","artist":["Аркона"]}]}]},{"id":{"name":"Eluveitie"},"albums":[{"id":{"year":2008,"title":"Slania"},"tracks":[{"number":1,"title":"Samon","artist":["Eluveitie"]},{"number":2,"title":"Primordial Breath","artist":["Eluveitie"]},{"number":3,"title":"Inis Mona","artist":["Eluveitie"]},{"number":4,"title":"Gray Sublime Archon","artist":["Eluveitie"]},{"number":5,"title":"Anagantios","artist":["Eluveitie"]},{"number":6,"title":"Bloodstained Ground","artist":["Eluveitie"]},{"number":7,"title":"The Somber Lay","artist":["Eluveitie"]},{"number":8,"title":"Slanias Song","artist":["Eluveitie"]},{"number":9,"title":"Giamonios","artist":["Eluveitie"]},{"number":10,"title":"Tarvos","artist":["Eluveitie"]},{"number":11,"title":"Calling the Rain","artist":["Eluveitie"]},{"number":12,"title":"Elembivos","artist":["Eluveitie"]}]},{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"number":1,"title":"Verja Urit an Bitus","artist":["Eluveitie"]},{"number":2,"title":"Uis Elveti","artist":["Eluveitie"]},{"number":3,"title":"Ôrô","artist":["Eluveitie"]},{"number":4,"title":"Lament","artist":["Eluveitie"]},{"number":5,"title":"Druid","artist":["Eluveitie"]},{"number":6,"title":"Jêzaïg","artist":["Eluveitie"]}]}]},{"id":{"name":"Frontside"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"number":1,"title":"Intro = Chaos","artist":["Frontside"]},{"number":2,"title":"Modlitwa","artist":["Frontside"]},{"number":3,"title":"Długa droga z piekła","artist":["Frontside"]},{"number":4,"title":"Synowie ognia","artist":["Frontside"]},{"number":5,"title":"1902","artist":["Frontside"]},{"number":6,"title":"Krew za krew","artist":["Frontside"]},{"number":7,"title":"Kulminacja","artist":["Frontside"]},{"number":8,"title":"Judasz","artist":["Frontside"]},{"number":9,"title":"Więzy","artist":["Frontside"]},{"number":10,"title":"Zagubione dusze","artist":["Frontside"]},{"number":11,"title":"Linia życia","artist":["Frontside"]}]}]},{"id":{"name":"Metallica"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"number":1,"title":"Fight Fire with Fire","artist":["Metallica"]},{"number":2,"title":"Ride the Lightning","artist":["Metallica"]},{"number":3,"title":"For Whom the Bell Tolls","artist":["Metallica"]},{"number":4,"title":"Fade to Black","artist":["Metallica"]},{"number":5,"title":"Trapped under Ice","artist":["Metallica"]},{"number":6,"title":"Escape","artist":["Metallica"]},{"number":7,"title":"Creeping Death","artist":["Metallica"]},{"number":8,"title":"The Call of Ktulu","artist":["Metallica"]}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"number":1,"title":"The Ecstasy of Gold","artist":["Metallica"]},{"number":2,"title":"The Call of Ktulu","artist":["Metallica"]},{"number":3,"title":"Master of Puppets","artist":["Metallica"]},{"number":4,"title":"Of Wolf and Man","artist":["Metallica"]},{"number":5,"title":"The Thing That Should Not Be","artist":["Metallica"]},{"number":6,"title":"Fuel","artist":["Metallica"]},{"number":7,"title":"The Memory Remains","artist":["Metallica"]},{"number":8,"title":"No Leaf Clover","artist":["Metallica"]},{"number":9,"title":"Hero of the Day","artist":["Metallica"]},{"number":10,"title":"Devil’s Dance","artist":["Metallica"]},{"number":11,"title":"Bleeding Me","artist":["Metallica"]},{"number":12,"title":"Nothing Else Matters","artist":["Metallica"]},{"number":13,"title":"Until It Sleeps","artist":["Metallica"]},{"number":14,"title":"For Whom the Bell Tolls","artist":["Metallica"]},{"number":15,"title":"−Human","artist":["Metallica"]},{"number":16,"title":"Wherever I May Roam","artist":["Metallica"]},{"number":17,"title":"Outlaw Torn","artist":["Metallica"]},{"number":18,"title":"Sad but True","artist":["Metallica"]},{"number":19,"title":"One","artist":["Metallica"]},{"number":20,"title":"Enter Sandman","artist":["Metallica"]},{"number":21,"title":"Battery","artist":["Metallica"]}]}]}] \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..c749926 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,442 @@ +mod database; +mod library; + +use musichoard::{Album, AlbumId, Artist, ArtistId, Track}; +use once_cell::sync::Lazy; + +static COLLECTION: Lazy> = Lazy::new(|| { + vec![ + Artist { + id: ArtistId { + name: String::from("Аркона"), + }, + albums: vec![Album { + id: AlbumId { + year: 2011, + title: String::from("Slovo"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("Az’"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 02, + title: String::from("Arkaim"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 03, + title: String::from("Bol’no mne"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 04, + title: String::from("Leshiy"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 05, + title: String::from("Zakliatie"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 06, + title: String::from("Predok"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 07, + title: String::from("Nikogda"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 08, + title: String::from("Tam za tumanami"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 09, + title: String::from("Potomok"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 10, + title: String::from("Slovo"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 11, + title: String::from("Odna"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 12, + title: String::from("Vo moiom sadochke…"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 13, + title: String::from("Stenka na stenku"), + artist: vec![String::from("Аркона")], + }, + Track { + number: 14, + title: String::from("Zimushka"), + artist: vec![String::from("Аркона")], + }, + ], + }], + }, + Artist { + id: ArtistId { + name: String::from("Eluveitie"), + }, + albums: vec![ + Album { + id: AlbumId { + year: 2008, + title: String::from("Slania"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("Samon"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 02, + title: String::from("Primordial Breath"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 03, + title: String::from("Inis Mona"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 04, + title: String::from("Gray Sublime Archon"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 05, + title: String::from("Anagantios"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 06, + title: String::from("Bloodstained Ground"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 07, + title: String::from("The Somber Lay"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 08, + title: String::from("Slanias Song"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 09, + title: String::from("Giamonios"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 10, + title: String::from("Tarvos"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 11, + title: String::from("Calling the Rain"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 12, + title: String::from("Elembivos"), + artist: vec![String::from("Eluveitie")], + }, + ], + }, + Album { + id: AlbumId { + year: 2004, + title: String::from("Vên [re‐recorded]"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("Verja Urit an Bitus"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 02, + title: String::from("Uis Elveti"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 03, + title: String::from("Ôrô"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 04, + title: String::from("Lament"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 05, + title: String::from("Druid"), + artist: vec![String::from("Eluveitie")], + }, + Track { + number: 06, + title: String::from("Jêzaïg"), + artist: vec![String::from("Eluveitie")], + }, + ], + }, + ], + }, + Artist { + id: ArtistId { + name: String::from("Frontside"), + }, + albums: vec![Album { + id: AlbumId { + year: 2001, + title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("Intro = Chaos"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 02, + title: String::from("Modlitwa"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 03, + title: String::from("Długa droga z piekła"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 04, + title: String::from("Synowie ognia"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 05, + title: String::from("1902"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 06, + title: String::from("Krew za krew"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 07, + title: String::from("Kulminacja"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 08, + title: String::from("Judasz"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 09, + title: String::from("Więzy"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 10, + title: String::from("Zagubione dusze"), + artist: vec![String::from("Frontside")], + }, + Track { + number: 11, + title: String::from("Linia życia"), + artist: vec![String::from("Frontside")], + }, + ], + }], + }, + Artist { + id: ArtistId { + name: String::from("Metallica"), + }, + albums: vec![ + Album { + id: AlbumId { + year: 1984, + title: String::from("Ride the Lightning"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("Fight Fire with Fire"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 02, + title: String::from("Ride the Lightning"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 03, + title: String::from("For Whom the Bell Tolls"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 04, + title: String::from("Fade to Black"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 05, + title: String::from("Trapped under Ice"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 06, + title: String::from("Escape"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 07, + title: String::from("Creeping Death"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 08, + title: String::from("The Call of Ktulu"), + artist: vec![String::from("Metallica")], + }, + ], + }, + Album { + id: AlbumId { + year: 1999, + title: String::from("S&M"), + }, + tracks: vec![ + Track { + number: 01, + title: String::from("The Ecstasy of Gold"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 02, + title: String::from("The Call of Ktulu"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 03, + title: String::from("Master of Puppets"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 04, + title: String::from("Of Wolf and Man"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 05, + title: String::from("The Thing That Should Not Be"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 06, + title: String::from("Fuel"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 07, + title: String::from("The Memory Remains"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 08, + title: String::from("No Leaf Clover"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 09, + title: String::from("Hero of the Day"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 10, + title: String::from("Devil’s Dance"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 11, + title: String::from("Bleeding Me"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 12, + title: String::from("Nothing Else Matters"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 13, + title: String::from("Until It Sleeps"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 14, + title: String::from("For Whom the Bell Tolls"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 15, + title: String::from("−Human"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 16, + title: String::from("Wherever I May Roam"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 17, + title: String::from("Outlaw Torn"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 18, + title: String::from("Sad but True"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 19, + title: String::from("One"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 20, + title: String::from("Enter Sandman"), + artist: vec![String::from("Metallica")], + }, + Track { + number: 21, + title: String::from("Battery"), + artist: vec![String::from("Metallica")], + }, + ], + }, + ], + }, + ] +}); diff --git a/tests/library/beets.rs b/tests/library/beets.rs new file mode 100644 index 0000000..f176fe7 --- /dev/null +++ b/tests/library/beets.rs @@ -0,0 +1,80 @@ +use std::fs; + +use musichoard::{ + library::{ + beets::{Beets, SystemExecutor}, + Library, Query, QueryOption, + }, + Artist, +}; + +use crate::COLLECTION; + +#[test] +fn test_no_config_list() { + let executor = SystemExecutor::default(); + + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + let expected: Vec = vec![]; + assert_eq!(output, expected); +} + +#[test] +fn test_full_list() { + let executor = SystemExecutor::default().config(Some( + &fs::canonicalize("./tests/files/library/config.yml").unwrap(), + )); + + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + let expected: Vec = COLLECTION.to_owned(); + assert_eq!(output, expected); +} + +#[test] +fn test_album_artist_query() { + let executor = SystemExecutor::default().config(Some( + &fs::canonicalize("./tests/files/library/config.yml").unwrap(), + )); + + let mut beets = Beets::new(Box::new(executor)); + let output = beets + .list(&Query::default().album_artist(QueryOption::Include(String::from("Аркона")))) + .unwrap(); + + let expected: Vec = COLLECTION[0..1].to_owned(); + assert_eq!(output, expected); +} + +#[test] +fn test_album_title_query() { + let executor = SystemExecutor::default().config(Some( + &fs::canonicalize("./tests/files/library/config.yml").unwrap(), + )); + + let mut beets = Beets::new(Box::new(executor)); + let output = beets + .list(&Query::default().album_title(QueryOption::Include(String::from("Slovo")))) + .unwrap(); + + let expected: Vec = COLLECTION[0..1].to_owned(); + assert_eq!(output, expected); +} + +#[test] +fn test_exclude_query() { + let executor = SystemExecutor::default().config(Some( + &fs::canonicalize("./tests/files/library/config.yml").unwrap(), + )); + + let mut beets = Beets::new(Box::new(executor)); + let output = beets + .list(&Query::default().album_artist(QueryOption::Exclude(String::from("Аркона")))) + .unwrap(); + + let expected: Vec = COLLECTION[1..].to_owned(); + assert_eq!(output, expected); +} diff --git a/tests/library/mod.rs b/tests/library/mod.rs new file mode 100644 index 0000000..035ac9f --- /dev/null +++ b/tests/library/mod.rs @@ -0,0 +1 @@ +mod beets;