From 200b89c8c1ef72a4e1054be00e0f53a3965f8623 Mon Sep 17 00:00:00 2001 From: Joseph Micheli Date: Thu, 10 Oct 2024 23:19:08 -0500 Subject: [PATCH] Resurrect core integration tests --- .cargo/config.toml | 1 - .gitignore | 2 - apps/server/src/utils/http.rs | 2 +- core/integration-tests/Cargo.toml | 18 - core/integration-tests/tests/epub.rs | 114 ------ core/integration-tests/tests/lib.rs | 5 - core/integration-tests/tests/rar.rs | 82 ---- core/integration-tests/tests/scanner.rs | 128 ------ core/integration-tests/tests/utils.rs | 366 ------------------ core/integration-tests/tests/zip.rs | 46 --- core/package.json | 2 +- core/src/context.rs | 2 +- core/src/db/client.rs | 2 +- core/src/db/entity/mod.rs | 2 +- core/src/filesystem/image/mod.rs | 10 +- core/src/filesystem/media/mod.rs | 12 +- core/src/job/scheduler.rs | 1 + core/tests/core_tests.rs | 34 ++ .../data/book-image-format-test.zip | Bin .../data/book.epub | Bin .../data/book.rar | Bin .../data/book.zip | Bin .../data/example.avif | Bin .../data/example.jpeg | Bin .../data/example.jxl | Bin .../data/example.png | Bin .../data/example.webp | Bin .../data/mock-stump.toml | 0 .../data/nested-macos-compressed.cbz | Bin .../data/rust_book.pdf | Bin .../data/science_comics_001.cbz | Bin core/tests/scanner_tests.rs | 104 +++++ core/tests/utils/mod.rs | 22 ++ core/tests/utils/temp_core.rs | 58 +++ core/tests/utils/temp_library.rs | 201 ++++++++++ scripts/lib | 1 - 36 files changed, 436 insertions(+), 779 deletions(-) delete mode 100644 core/integration-tests/Cargo.toml delete mode 100644 core/integration-tests/tests/epub.rs delete mode 100644 core/integration-tests/tests/lib.rs delete mode 100644 core/integration-tests/tests/rar.rs delete mode 100644 core/integration-tests/tests/scanner.rs delete mode 100644 core/integration-tests/tests/utils.rs delete mode 100644 core/integration-tests/tests/zip.rs create mode 100644 core/tests/core_tests.rs rename core/{integration-tests => tests}/data/book-image-format-test.zip (100%) rename core/{integration-tests => tests}/data/book.epub (100%) rename core/{integration-tests => tests}/data/book.rar (100%) rename core/{integration-tests => tests}/data/book.zip (100%) rename core/{integration-tests => tests}/data/example.avif (100%) rename core/{integration-tests => tests}/data/example.jpeg (100%) rename core/{integration-tests => tests}/data/example.jxl (100%) rename core/{integration-tests => tests}/data/example.png (100%) rename core/{integration-tests => tests}/data/example.webp (100%) rename core/{integration-tests => tests}/data/mock-stump.toml (100%) rename core/{integration-tests => tests}/data/nested-macos-compressed.cbz (100%) rename core/{integration-tests => tests}/data/rust_book.pdf (100%) rename core/{integration-tests => tests}/data/science_comics_001.cbz (100%) create mode 100644 core/tests/scanner_tests.rs create mode 100644 core/tests/utils/mod.rs create mode 100644 core/tests/utils/temp_core.rs create mode 100644 core/tests/utils/temp_library.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 8ca26ee57..ae70621ef 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,5 @@ [alias] prisma = "run --package prisma-cli --" -integration-tests = "test --package integration-tests -- --test-threads 1" doc-tests = "cargo test --doc -- --show-output" build-server = "build --package stump_server --bin stump_server --release --" codegen = "run --package codegen --bin codegen" diff --git a/.gitignore b/.gitignore index 8825b4b71..aa2c4a621 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,6 @@ target/ .env docker-compose.yaml -core/integration-tests/.* -core/integration-tests/*libraries* static apps/server/client/* !apps/server/client/.placeholder diff --git a/apps/server/src/utils/http.rs b/apps/server/src/utils/http.rs index ed805d581..8cc6d2a50 100644 --- a/apps/server/src/utils/http.rs +++ b/apps/server/src/utils/http.rs @@ -244,7 +244,7 @@ mod tests { async fn test_named_file_response() { let response = NamedFile::open( PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../core/integration-tests/data/example.jpeg"), + .join("../../core/tests/data/example.jpeg"), ) .await .unwrap(); diff --git a/core/integration-tests/Cargo.toml b/core/integration-tests/Cargo.toml deleted file mode 100644 index b173d52ef..000000000 --- a/core/integration-tests/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "integration-tests" -version = { workspace = true } -autotests = false -autobenches = false -edition = "2021" - -[[test]] -name = "integration_tests" -path = "tests/lib.rs" -harness = true - -[dev-dependencies] -serde = { workspace = true } -prisma-client-rust = { workspace = true } -tokio = { workspace = true } -stump_core = { path = ".." } -tempfile = { workspace = true } \ No newline at end of file diff --git a/core/integration-tests/tests/epub.rs b/core/integration-tests/tests/epub.rs deleted file mode 100644 index 980f8a9b5..000000000 --- a/core/integration-tests/tests/epub.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::path::PathBuf; - -use crate::utils::{init_test, TempLibrary}; - -use stump_core::{ - db::models::Epub, - fs::media_file::epub::{ - get_epub_chapter, get_epub_resource, normalize_resource_path, - }, - prelude::{ContentType, CoreResult, Ctx}, - prisma::media, -}; - -#[tokio::test] -async fn can_make_epub_struct() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - - let _ret = TempLibrary::epub_library(ctx.get_db()).await?; - - let fetch_epub = ctx - .db - .media() - .find_first(vec![media::extension::equals("epub".to_string())]) - .exec() - .await?; - - assert!(fetch_epub.is_some()); - - let media = fetch_epub.unwrap(); - let epub = Some(Epub::try_from(media)?); - - assert!(epub.is_some()); - - Ok(()) -} - -#[tokio::test] -async fn can_get_resource() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - - let _ret = TempLibrary::epub_library(ctx.get_db()).await?; - - let fetch_epub = ctx - .db - .media() - .find_first(vec![media::extension::equals("epub".to_string())]) - .exec() - .await?; - - assert!(fetch_epub.is_some()); - - let media = fetch_epub.unwrap(); - let media_path = media.path.clone(); - - let epub = Epub::try_from(media)?; - - let first_resource = epub.resources.into_iter().next().unwrap(); - - let got_resource = get_epub_resource(&media_path, &first_resource.0); - - assert!(got_resource.is_ok()); - - let got_resource = got_resource.unwrap(); - - assert_eq!( - got_resource.0, - ContentType::from(first_resource.1 .1.as_str()) - ); - - Ok(()) -} - -#[test] -fn canonical_correction() { - let invalid = PathBuf::from("OEBPS/../Styles/style.css"); - - let expected = PathBuf::from("OEBPS/Styles/style.css"); - - let result = normalize_resource_path(invalid, "OEBPS"); - - assert_eq!(result, expected); -} - -#[tokio::test] -async fn can_get_chapter() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - let _ret = TempLibrary::epub_library(ctx.get_db()).await?; - - let fetch_epub = ctx - .db - .media() - .find_first(vec![media::extension::equals("epub".to_string())]) - .exec() - .await?; - - assert!(fetch_epub.is_some()); - - let media = fetch_epub.unwrap(); - - let get_chapter_result = get_epub_chapter(&media.path, 4); - assert!(get_chapter_result.is_ok()); - - let get_chapter_result = get_chapter_result.unwrap(); - - assert!(!get_chapter_result.1.is_empty()); - - Ok(()) -} diff --git a/core/integration-tests/tests/lib.rs b/core/integration-tests/tests/lib.rs deleted file mode 100644 index 8e2557d4c..000000000 --- a/core/integration-tests/tests/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod epub; -mod rar; -mod scanner; -mod utils; -mod zip; diff --git a/core/integration-tests/tests/rar.rs b/core/integration-tests/tests/rar.rs deleted file mode 100644 index 9390850ac..000000000 --- a/core/integration-tests/tests/rar.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::utils::{init_test, make_tmp_file, TempLibrary}; - -use stump_core::{ - db::models::{LibraryPattern, LibraryScanMode}, - fs::{ - checksum, - media_file::rar::{convert_to_zip, sample_size}, - }, - prelude::{CoreResult, Ctx}, - prisma::media, -}; - -// TODO: fix these tests... - -#[test] -// TODO: don't ignore, need to figure out best way to do this... something like the -// tempfile crate maybe? -#[ignore] -fn test_rar_to_zip() -> CoreResult<()> { - let tmp_file = make_tmp_file("book.rar")?; - - let path = tmp_file.path(); - - let result = convert_to_zip(path); - assert!(result.is_ok()); - - let zip_path = result.unwrap(); - assert!(zip_path.exists()); - - // TODO: more? - - Ok(()) -} - -#[tokio::test] -#[ignore] -async fn digest_rars_synchronous() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - - let _ret = TempLibrary::create( - ctx.get_db(), - LibraryPattern::SeriesBased, - LibraryScanMode::Batched, - ) - .await?; - - let rars = ctx - .db - .media() - .find_many(vec![media::extension::in_vec(vec![ - "rar".to_string(), - "cbr".to_string(), - ])]) - .exec() - .await?; - - // TODO: uncomment once I create rar test data - // assert_ne!(rars.len(), 0); - - // TODO: remove this check once I create rar test data - if rars.is_empty() { - println!("STINKY: could not run digest_rars_synchronous test until aaron fixes his stuff"); - return Ok(()); - } - - for rar in rars { - let rar_sample_result = sample_size(&rar.path); - assert!(rar_sample_result.is_ok()); - - let rar_sample = rar_sample_result.unwrap(); - - let digest_result = checksum::digest(&rar.path, rar_sample); - assert!(digest_result.is_ok()); - - let checksum = digest_result.unwrap(); - assert_ne!(checksum.len(), 0); - } - - Ok(()) -} diff --git a/core/integration-tests/tests/scanner.rs b/core/integration-tests/tests/scanner.rs deleted file mode 100644 index ebf8cc0c3..000000000 --- a/core/integration-tests/tests/scanner.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::utils::{init_test, run_test_scan, TempLibrary}; - -use stump_core::{ - db::models::{LibraryPattern, LibraryScanMode}, - prelude::{CoreResult, Ctx}, - prisma::{library, PrismaClient}, -}; - -async fn check_library_post_scan( - client: &PrismaClient, - id: &str, - series_count: usize, - media_count: usize, -) -> CoreResult<()> { - let library = client - .library() - .find_unique(library::id::equals(id.to_string())) - .include(library::include!({ - series: select { - id - name - media: select { - id - name - } - } - })) - .exec() - .await? - .expect("Error during test, library not found"); - - let library_series = library.series; - assert_eq!(library_series.len(), series_count); - - let library_media_count = library_series - .into_iter() - .flat_map(|series| series.media) - .count(); - assert_eq!(library_media_count, media_count); - - Ok(()) -} - -#[tokio::test] -async fn series_based_library_batch_scan() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - let client = &ctx.db; - - let (library, _library_config, _tmp) = - TempLibrary::create(client, LibraryPattern::SeriesBased, LibraryScanMode::None) - .await?; - - let scan_result = run_test_scan(&ctx, &library, LibraryScanMode::Sync).await; - - assert!( - scan_result.is_ok(), - "Failed to scan library: {:?}", - scan_result - ); - - let completed_tasks = scan_result.unwrap(); - assert_eq!(completed_tasks, 3); - - check_library_post_scan(client, &library.id, 3, 3).await?; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn collection_based_library_batch_scan() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - let client = &ctx.db; - - let (library, _library_config, _tmp) = TempLibrary::create( - client, - LibraryPattern::CollectionBased, - LibraryScanMode::None, - ) - .await?; - - let scan_result = run_test_scan(&ctx, &library, LibraryScanMode::Batched).await; - - assert!( - scan_result.is_ok(), - "Failed to scan library: {:?}", - scan_result - ); - - let completed_tasks = scan_result.unwrap(); - assert_eq!(completed_tasks, 3); - - check_library_post_scan(client, &library.id, 1, 3).await?; - - Ok(()) -} - -// Note: This test is ignored because it kind of needs to be run with multiple threads, -// but the test runner for stump will only run one test at a time. It would just take too long -// otherwise. -#[tokio::test(flavor = "multi_thread")] -#[ignore] -async fn massive_library_batch_scan() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - let client = &ctx.db; - - let temp_library = TempLibrary::massive_library(10000, LibraryPattern::SeriesBased)?; - let (library, _options) = temp_library.insert(client, LibraryScanMode::None).await?; - let scan_result = run_test_scan(&ctx, &library, LibraryScanMode::Batched).await; - - assert!( - scan_result.is_ok(), - "Failed to scan library: {:?}", - scan_result - ); - - let completed_tasks = scan_result.unwrap(); - assert_eq!(completed_tasks, 10000); - - check_library_post_scan(client, &library.id, 10000, 10000).await?; - - Ok(()) -} diff --git a/core/integration-tests/tests/utils.rs b/core/integration-tests/tests/utils.rs deleted file mode 100644 index 428eb566a..000000000 --- a/core/integration-tests/tests/utils.rs +++ /dev/null @@ -1,366 +0,0 @@ -extern crate stump_core; - -use std::{fs, path::PathBuf}; -use tempfile::{Builder, NamedTempFile, TempDir}; - -use stump_core::{ - db::{ - migration::run_migrations, - models::{LibraryPattern, LibraryScanMode}, - }, - fs::scanner::scan, - job::{persist_new_job, runner::RunnerCtx, LibraryScanJob}, - prelude::{CoreResult, Ctx}, - prisma::{library, library_config, PrismaClient}, -}; - -// https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/second-edition/ch11-03-test-organization.html - -// Note: this struct is used to hold the TempDir that points to the temporary directory -// used throughout the tests. It is done this way so that when the object goes out of -// scope, the directory is deleted. -pub struct TempLibrary { - pub _dir: TempDir, - pub root: PathBuf, - pub library_root: PathBuf, - pub pattern: LibraryPattern, -} - -impl TempLibrary { - /// Creates the root temporary libray for the [`TempLibrary`] struct. Places it - /// as a child of CARGO_MANIFEST_DIR. - fn root() -> TempDir { - Builder::new() - .tempdir_in(get_manifest_dir()) - .expect("Failed to create temp dir") - } - - /// Builds a temporary directory structure for a collection-based library: - /// - /// ```md - /// . - /// └── LIBRARY-ROOT - ///    └── collection-1 - ///    ├── collection-1-nested1 - ///    │   ├── collection-1-nested1-nested2 - ///    │   │   └── book.zip - ///    │   └── book.zip - ///    └── book.epub - /// ``` - pub fn collection_library() -> CoreResult { - let root = TempLibrary::root(); - let root_path = root.path().to_path_buf(); - - let collection_based_library = root_path.clone(); - let cbl_1 = collection_based_library.join("collection-1"); - let cbl_1_epub = cbl_1.join("book.epub"); - let cbl_nested_1 = cbl_1.join("collection-1-nested1"); - let cbl_nested_2 = cbl_nested_1.join("collection-1-nested1-nested2"); - - fs::create_dir_all(&cbl_nested_2)?; - - fs::write(&cbl_1_epub, get_test_file_contents("book.epub"))?; - fs::write( - &cbl_nested_2.join("book.zip"), - get_test_file_contents("book.zip"), - )?; - fs::write( - &cbl_nested_1.join("book.zip"), - get_test_file_contents("book.zip"), - )?; - - Ok(TempLibrary { - _dir: root, - root: root_path, - library_root: collection_based_library, - pattern: LibraryPattern::CollectionBased, - }) - } - - /// Builds a temporary directory structure for a series-based library: - /// - /// ```md - /// . - /// └── LIBRARY-ROOT - ///    ├── book.zip - ///    ├── series-1 - ///    │   └── space-book.zip - ///    └── series-2 - ///    └── duck-book.zip - /// ``` - pub fn series_library() -> CoreResult { - let root = TempLibrary::root(); - let root_path = root.path().to_path_buf(); - - let series_based_library = root_path.clone(); - let series_based_library_root_book = series_based_library.join("book.zip"); - let sbl_1 = series_based_library.join("series-1"); - let sbl_1_book = sbl_1.join("space-book.cbz"); - let sbl_2 = series_based_library.join("series-2"); - let sbl_2_book = sbl_2.join("duck-book.zip"); - - fs::create_dir_all(&sbl_1)?; - fs::create_dir_all(&sbl_2)?; - - fs::write( - &series_based_library_root_book, - get_test_file_contents("book.zip"), - )?; - fs::write(&sbl_1_book, get_test_file_contents("space-book.cbz"))?; - fs::write(&sbl_2_book, get_test_file_contents("duck-book.zip"))?; - - Ok(TempLibrary { - _dir: root, - root: root_path, - library_root: series_based_library, - pattern: LibraryPattern::SeriesBased, - }) - } - - pub fn massive_library( - num_dirs: i32, - pattern: LibraryPattern, - ) -> CoreResult { - let root = TempLibrary::root(); - let root_path = root.path().to_path_buf(); - - let massive_library = root_path.clone(); - // Create num_dirs directories in the root, all containing `book.zip`. So, - // if num_dirs is 1000, since `book.zip` is 31kb, a 31mb temporary directory is created. - for i in 0..num_dirs { - let dir = massive_library.join(format!("dir-{}", i)); - fs::create_dir_all(&dir)?; - fs::write(&dir.join("book.zip"), get_test_file_contents("book.zip"))?; - } - - Ok(TempLibrary { - _dir: root, - root: root_path, - library_root: massive_library, - pattern, - }) - } - - /// Builds a temporary directory structure, and inserts it into the database. - /// Returns the temporary directory, the library, and the library options. - pub async fn create( - client: &PrismaClient, - pattern: LibraryPattern, - scan_mode: LibraryScanMode, - ) -> CoreResult<(library::Data, library_config::Data, TempLibrary)> { - let temp_library = match pattern { - LibraryPattern::CollectionBased => TempLibrary::collection_library()?, - LibraryPattern::SeriesBased => TempLibrary::series_library()?, - }; - - let (library, options) = temp_library.insert(client, scan_mode).await?; - - Ok((library, options, temp_library)) - } - - /// A helper to create a collection based library used in the epub tests. - pub async fn epub_library( - client: &PrismaClient, - ) -> CoreResult<(library::Data, library_config::Data, TempLibrary)> { - let _tmp = TempLibrary::collection_library()?; - - let (library, options) = _tmp.insert(client, LibraryScanMode::Batched).await?; - - Ok((library, options, _tmp)) - } - - /// Gets the name of the library from the directory name. - pub fn get_name(&self) -> &str { - self.library_root - .file_name() - .unwrap() - .to_str() - .expect("Failed to get library name") - } - - /// Inserts a library into the database based on the temp library - pub async fn insert( - &self, - client: &PrismaClient, - scan_mode: LibraryScanMode, - ) -> CoreResult<(library::Data, library_config::Data)> { - let (library, options) = create_library( - client, - self.get_name(), - self.library_root.to_str().unwrap(), - self.pattern.clone(), - scan_mode, - ) - .await?; - - Ok((library, options)) - } -} - -// FIXME: not sure why this caused the tests to fail... I'd rather not create a -// database for each test, but I'm not sure how to get around this for now. -// static INIT: Once = Once::new(); - -/// Deletes the test database if it exists, then runs migrations and creates a test user. -/// Meant to create a clean slate for each test that needs it. -pub async fn init_db() { - let db_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test.db"); - - // remove existing test database - if let Err(err) = std::fs::remove_file(&db_path) { - // If the file doesn't exist, that's fine, but if it does exist and we can't - // remove it, that's a problem. - if err.kind() != std::io::ErrorKind::NotFound { - panic!("Failed to remove existing test database: {}", err); - } - } - - let test_ctx = Ctx::mock().await; - - let client = test_ctx.get_db(); - - // TODO: once migration engine is built into pcr, replace with commented out code below - // client._db_push().await.expect("Failed to push database schema"); - let migration_result = run_migrations(client).await; - - assert!( - migration_result.is_ok(), - "Failed to run migrations: {:?}", - migration_result - ); - - // create test user - let user_result = client - .user() - .create("oromei".into(), "1234".into(), vec![]) - .exec() - .await; - - assert!( - user_result.is_ok(), - "Failed to create test user: {:?}", - user_result - ); -} - -pub async fn init_test() { - // if INIT.is_completed() { - // return; - // } - init_db().await; - // INIT.call_once(|| {}); -} - -pub fn make_tmp_file(test_file: &str) -> CoreResult { - let contents = get_test_file_contents(test_file); - let tmp_file = - NamedTempFile::new_in(&get_manifest_dir()).expect("Failed to create temp file"); - - fs::write(tmp_file.path(), contents)?; - - Ok(tmp_file) -} - -pub fn get_manifest_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) -} - -pub fn get_test_data_dir() -> PathBuf { - get_manifest_dir().join("data") -} - -pub fn get_test_file_contents(name: &str) -> Vec { - let path = get_test_data_dir().join(name); - fs::read(path).unwrap_or_else(|_| panic!("Failed to read test file: {}", name)) -} - -pub async fn persist_test_job( - id: &str, - ctx: &Ctx, - library: &library::Data, - scan_mode: LibraryScanMode, -) -> CoreResult<()> { - let job = LibraryScanJob { - path: library.path.clone(), - scan_mode, - }; - - persist_new_job(ctx, id.to_string(), &job).await?; - - Ok(()) -} - -/// Runs a library scan job. If the scan mode is `None`, no scan is performed. -pub async fn run_test_scan( - ctx: &Ctx, - library: &library::Data, - scan_mode: LibraryScanMode, -) -> CoreResult { - persist_test_job(&library.id, ctx, library, scan_mode).await?; - - let fake_runner_ctx = RunnerCtx::new(ctx.get_ctx(), library.id.clone()); - - if scan_mode == LibraryScanMode::None { - return Ok(0); - } - - scan( - fake_runner_ctx, - library.path.clone(), - library.id.clone(), - scan_mode, - ) - .await -} - -/// Creates a library with the given name, path, and pattern. If the scan mode is -/// // not `None`, a scan is performed. -pub async fn create_library( - client: &PrismaClient, - name: &str, - library_path: &str, - pattern: LibraryPattern, - scan_mode: LibraryScanMode, -) -> CoreResult<(library::Data, library_config::Data)> { - let library_config_result = client - .library_config() - .create(vec![library_config::library_pattern::set( - pattern.to_string(), - )]) - .exec() - .await; - - assert!( - library_config_result.is_ok(), - "Failed to create library options: {:?}", - library_config_result - ); - - let library_config = library_config_result.unwrap(); - - let library = client - .library() - .create( - name.into(), - library_path.into(), - library_config::id::equals(library_config.id.clone()), - vec![], - ) - .exec() - .await; - - assert!(library.is_ok(), "Failed to create library: {:?}", library); - - let library = library.unwrap(); - - if scan_mode != LibraryScanMode::None { - let ctx = Ctx::mock().await; - run_test_scan(&ctx, &library, scan_mode) - .await - .expect("Failed to scan library"); - } - - // println!("Created library at {:?}", library_path); - - Ok((library, library_config)) -} diff --git a/core/integration-tests/tests/zip.rs b/core/integration-tests/tests/zip.rs deleted file mode 100644 index 600bb7cc2..000000000 --- a/core/integration-tests/tests/zip.rs +++ /dev/null @@ -1,46 +0,0 @@ -use stump_core::{ - db::models::{LibraryPattern, LibraryScanMode}, - fs::{checksum, media_file::zip}, - prelude::{CoreResult, Ctx}, - prisma::media, -}; - -use crate::utils::{init_test, TempLibrary}; - -#[tokio::test] -async fn digest_zips() -> CoreResult<()> { - init_test().await; - - let ctx = Ctx::mock().await; - - let _ret = TempLibrary::create( - ctx.get_db(), - LibraryPattern::SeriesBased, - LibraryScanMode::Batched, - ) - .await?; - - let zips = ctx - .db - .media() - .find_many(vec![media::extension::in_vec(vec![ - "zip".to_string(), - "cbz".to_string(), - ])]) - .exec() - .await?; - - assert_ne!(zips.len(), 0); - - for zip in zips { - let zip_sample = zip::sample_size(&zip.path); - - let digest_result = checksum::digest(&zip.path, zip_sample); - assert!(digest_result.is_ok()); - - let checksum = digest_result.unwrap(); - assert_ne!(checksum.len(), 0); - } - - Ok(()) -} diff --git a/core/package.json b/core/package.json index cbc3c1652..662eba769 100644 --- a/core/package.json +++ b/core/package.json @@ -10,7 +10,7 @@ "studio": "yarn dlx prisma studio", "build": "cargo build --release", "format": "cargo fmt --package stump_core", - "integration-tests": "cargo integration-tests", + "run-tests": "cargo test", "nuke": "rimraf node_modules" } } diff --git a/core/src/context.rs b/core/src/context.rs index 468ab6da6..291afe664 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -65,7 +65,7 @@ impl Ctx { // - https://github.com/rust-lang/cargo/issues/8379 /// Creates a [Ctx] instance for testing **only**. The prisma client is created - /// pointing to the `integration-tests` crate relative to the `core` crate. + /// pointing to the `tests` crate relative to the `core` crate. /// /// **This should not be used in production.** pub async fn integration_test_mock() -> Ctx { diff --git a/core/src/db/client.rs b/core/src/db/client.rs index 27ee76deb..03eee8a70 100644 --- a/core/src/db/client.rs +++ b/core/src/db/client.rs @@ -43,7 +43,7 @@ pub async fn create_client_with_url(url: &str) -> prisma::PrismaClient { } pub async fn create_test_client() -> prisma::PrismaClient { - let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("integration-tests"); + let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); create_client_with_url(&format!("file:{}/test.db", test_dir.to_str().unwrap())).await } diff --git a/core/src/db/entity/mod.rs b/core/src/db/entity/mod.rs index 0304b8ce8..37cb24d85 100644 --- a/core/src/db/entity/mod.rs +++ b/core/src/db/entity/mod.rs @@ -34,7 +34,7 @@ pub use user::*; pub use common::{ AccessRole, Cursor, EntityVisibility, FileStatus, LayoutMode, ReactTableColumnSort, - ReactTableGlobalSort, + ReactTableGlobalSort, ReadingDirection, ReadingImageScaleFit, ReadingMode, }; pub mod utils { diff --git a/core/src/filesystem/image/mod.rs b/core/src/filesystem/image/mod.rs index 26064b5fd..ec83ae966 100644 --- a/core/src/filesystem/image/mod.rs +++ b/core/src/filesystem/image/mod.rs @@ -21,28 +21,28 @@ mod tests { pub fn get_test_webp_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/example.webp") + .join("tests/data/example.webp") .to_string_lossy() .to_string() } pub fn get_test_jpg_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/example.jpeg") + .join("tests/data/example.jpeg") .to_string_lossy() .to_string() } pub fn get_test_png_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/example.png") + .join("tests/data/example.png") .to_string_lossy() .to_string() } // pub fn get_test_avif_path() -> String { // PathBuf::from(env!("CARGO_MANIFEST_DIR")) - // .join("integration-tests/data/example.avif") + // .join("tests/data/example.avif") // .to_string_lossy() // .to_string() // } @@ -50,5 +50,5 @@ mod tests { // TODO(339): Avif + Jxl support // pub fn get_test_jxl_path() -> String { // PathBuf::from(env!("CARGO_MANIFEST_DIR")) - // .join("integration-tests/data/example.jxl") + // .join("tests/data/example.jxl") } diff --git a/core/src/filesystem/media/mod.rs b/core/src/filesystem/media/mod.rs index e10c15849..84c6a1ea6 100644 --- a/core/src/filesystem/media/mod.rs +++ b/core/src/filesystem/media/mod.rs @@ -16,14 +16,14 @@ pub(crate) mod tests { pub fn get_test_zip_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/book.zip") + .join("tests/data/book.zip") .to_string_lossy() .to_string() } pub fn get_test_rar_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/book.rar") + .join("tests/data/book.rar") .to_string_lossy() .to_string() } @@ -36,21 +36,21 @@ pub(crate) mod tests { pub fn get_test_epub_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/book.epub") + .join("tests/data/book.epub") .to_string_lossy() .to_string() } pub fn get_test_pdf_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/rust_book.pdf") + .join("tests/data/rust_book.pdf") .to_string_lossy() .to_string() } pub fn get_test_cbz_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/science_comics_001.cbz") + .join("tests/data/science_comics_001.cbz") .to_string_lossy() .to_string() } @@ -59,7 +59,7 @@ pub(crate) mod tests { // ignored by the processor. Commenting the sizes for posterity. pub fn get_nested_macos_compressed_cbz_path() -> String { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/nested-macos-compressed.cbz") + .join("tests/data/nested-macos-compressed.cbz") .to_string_lossy() .to_string() } diff --git a/core/src/job/scheduler.rs b/core/src/job/scheduler.rs index 7c97d1b15..8bd7ab49b 100644 --- a/core/src/job/scheduler.rs +++ b/core/src/job/scheduler.rs @@ -12,6 +12,7 @@ use crate::{ // 1. Schedule multiple job types (complex config) // 2. Last run timestamp, so on boot we don't immediately trigger the scheduled tasks +#[derive(Debug)] pub struct JobScheduler { pub scheduler_handle: Option>, } diff --git a/core/tests/core_tests.rs b/core/tests/core_tests.rs new file mode 100644 index 000000000..d0ea3fdf0 --- /dev/null +++ b/core/tests/core_tests.rs @@ -0,0 +1,34 @@ +mod utils; + +use stump_core::StumpCore; +use tempfile::TempDir; + +#[tokio::test] +async fn test_create_stump_core() { + let temp_dir = TempDir::new().unwrap(); + + let config_dir = temp_dir.path().to_string_lossy().to_string(); + let config = StumpCore::init_config(config_dir).unwrap(); + + let _core = StumpCore::new(config).await; +} + +#[tokio::test] +pub async fn test_create_user() { + let (core, _temp_dir) = utils::get_temp_core().await; + let ctx = core.get_context(); + + // create test user + let user_result = ctx + .db + .user() + .create("oromei".into(), "1234".into(), vec![]) + .exec() + .await; + + assert!( + user_result.is_ok(), + "Failed to create test user: {:?}", + user_result + ); +} diff --git a/core/integration-tests/data/book-image-format-test.zip b/core/tests/data/book-image-format-test.zip similarity index 100% rename from core/integration-tests/data/book-image-format-test.zip rename to core/tests/data/book-image-format-test.zip diff --git a/core/integration-tests/data/book.epub b/core/tests/data/book.epub similarity index 100% rename from core/integration-tests/data/book.epub rename to core/tests/data/book.epub diff --git a/core/integration-tests/data/book.rar b/core/tests/data/book.rar similarity index 100% rename from core/integration-tests/data/book.rar rename to core/tests/data/book.rar diff --git a/core/integration-tests/data/book.zip b/core/tests/data/book.zip similarity index 100% rename from core/integration-tests/data/book.zip rename to core/tests/data/book.zip diff --git a/core/integration-tests/data/example.avif b/core/tests/data/example.avif similarity index 100% rename from core/integration-tests/data/example.avif rename to core/tests/data/example.avif diff --git a/core/integration-tests/data/example.jpeg b/core/tests/data/example.jpeg similarity index 100% rename from core/integration-tests/data/example.jpeg rename to core/tests/data/example.jpeg diff --git a/core/integration-tests/data/example.jxl b/core/tests/data/example.jxl similarity index 100% rename from core/integration-tests/data/example.jxl rename to core/tests/data/example.jxl diff --git a/core/integration-tests/data/example.png b/core/tests/data/example.png similarity index 100% rename from core/integration-tests/data/example.png rename to core/tests/data/example.png diff --git a/core/integration-tests/data/example.webp b/core/tests/data/example.webp similarity index 100% rename from core/integration-tests/data/example.webp rename to core/tests/data/example.webp diff --git a/core/integration-tests/data/mock-stump.toml b/core/tests/data/mock-stump.toml similarity index 100% rename from core/integration-tests/data/mock-stump.toml rename to core/tests/data/mock-stump.toml diff --git a/core/integration-tests/data/nested-macos-compressed.cbz b/core/tests/data/nested-macos-compressed.cbz similarity index 100% rename from core/integration-tests/data/nested-macos-compressed.cbz rename to core/tests/data/nested-macos-compressed.cbz diff --git a/core/integration-tests/data/rust_book.pdf b/core/tests/data/rust_book.pdf similarity index 100% rename from core/integration-tests/data/rust_book.pdf rename to core/tests/data/rust_book.pdf diff --git a/core/integration-tests/data/science_comics_001.cbz b/core/tests/data/science_comics_001.cbz similarity index 100% rename from core/integration-tests/data/science_comics_001.cbz rename to core/tests/data/science_comics_001.cbz diff --git a/core/tests/scanner_tests.rs b/core/tests/scanner_tests.rs new file mode 100644 index 000000000..d9ddcb539 --- /dev/null +++ b/core/tests/scanner_tests.rs @@ -0,0 +1,104 @@ +mod utils; + +use std::time::Duration; + +use stump_core::{ + db::entity::{ + IgnoreRules, LibraryConfig, ReadingDirection, ReadingImageScaleFit, ReadingMode, + }, + filesystem::scanner::LibraryScanJob, + prisma::media, +}; +use utils::temp_library::TempLibrary; + +#[tokio::test] +async fn test_scan_collection_library() { + let (core, temp_dir) = utils::get_temp_core().await; + let ctx = core.get_context(); + + let temp_lib = TempLibrary::collection_library(temp_dir.path()).unwrap(); + let (lib_data, _) = utils::temp_library::create_library( + ctx.db.clone(), + temp_lib.get_name(), + temp_lib.library_root.to_str().unwrap(), + LibraryConfig { + id: None, + convert_rar_to_zip: false, + hard_delete_conversions: false, + generate_file_hashes: true, + process_metadata: true, + library_pattern: temp_lib.pattern.clone(), + thumbnail_config: None, + default_reading_dir: ReadingDirection::LeftToRight, + default_reading_mode: ReadingMode::Paged, + default_reading_image_scale_fit: ReadingImageScaleFit::None, + ignore_rules: IgnoreRules::new(vec![]).unwrap(), + library_id: None, + }, + ) + .await; + + ctx.enqueue_job(LibraryScanJob::new(lib_data.id, lib_data.path)) + .unwrap(); + /// TODO - something less heinous, there must be a way to wait on a job + tokio::time::sleep(Duration::from_secs(1)).await; + + let items = ctx + .db + .media() + .find_many(vec![media::extension::in_vec(vec![ + "zip".to_string(), + "cbz".to_string(), + ])]) + .exec() + .await + .unwrap(); + + assert_ne!(items.len(), 0); +} + +#[tokio::test] +async fn test_scan_series_library() { + let (core, temp_dir) = utils::get_temp_core().await; + let ctx = core.get_context(); + + let temp_lib = TempLibrary::series_library(temp_dir.path()).unwrap(); + let (lib_data, _) = utils::temp_library::create_library( + ctx.db.clone(), + temp_lib.get_name(), + temp_lib.library_root.to_str().unwrap(), + LibraryConfig { + id: None, + convert_rar_to_zip: false, + hard_delete_conversions: false, + generate_file_hashes: true, + process_metadata: true, + library_pattern: temp_lib.pattern.clone(), + thumbnail_config: None, + default_reading_dir: ReadingDirection::LeftToRight, + default_reading_mode: ReadingMode::Paged, + default_reading_image_scale_fit: ReadingImageScaleFit::None, + ignore_rules: IgnoreRules::new(vec![]).unwrap(), + library_id: None, + }, + ) + .await; + + ctx.enqueue_job(LibraryScanJob::new(lib_data.id, lib_data.path)) + .unwrap(); + /// TODO - something less heinous, there must be a way to wait on a job + tokio::time::sleep(Duration::from_secs(1)).await; + + let items = ctx + .db + .media() + .find_many(vec![media::extension::in_vec(vec![ + "zip".to_string(), + "cbz".to_string(), + ])]) + .exec() + .await + .unwrap(); + + assert_ne!(items.len(), 0); +} diff --git a/core/tests/utils/mod.rs b/core/tests/utils/mod.rs new file mode 100644 index 000000000..665b59a32 --- /dev/null +++ b/core/tests/utils/mod.rs @@ -0,0 +1,22 @@ +mod temp_core; +pub mod temp_library; + +use std::{fs, path::PathBuf}; + +pub use temp_core::get_temp_core; + +#[allow(dead_code)] +pub fn get_manifest_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +#[allow(dead_code)] +pub fn get_test_data_dir() -> PathBuf { + get_manifest_dir().join("tests/data") +} + +#[allow(dead_code)] +pub fn get_test_file_contents(name: &str) -> Vec { + let path = get_test_data_dir().join(name); + fs::read(path).unwrap_or_else(|_| panic!("Failed to read test file: {}", name)) +} diff --git a/core/tests/utils/temp_core.rs b/core/tests/utils/temp_core.rs new file mode 100644 index 000000000..3bc0cd049 --- /dev/null +++ b/core/tests/utils/temp_core.rs @@ -0,0 +1,58 @@ +use tempfile::TempDir; + +use stump_core::StumpCore; + +pub async fn get_temp_core() -> (StumpCore, TempDir) { + let temp_dir = TempDir::new().unwrap(); + + let config_dir = temp_dir.path().to_string_lossy().to_string(); + let config = StumpCore::init_config(config_dir).unwrap(); + let core = StumpCore::new(config).await; + + let migration_res = core.run_migrations().await; + assert!( + migration_res.is_ok(), + "Failed to run migrations: {:?}", + migration_res + ); + + let job_init_res = core.get_job_controller().initialize().await; + assert!( + job_init_res.is_ok(), + "Failed to initialize job controller: {:?}", + job_init_res + ); + + // Initialize the server configuration. If it already exists, nothing will happen. + let init_config_res = core.init_server_config().await; + assert!( + init_config_res.is_ok(), + "Failed to initialize server config: {:?}", + init_config_res + ); + + // Initialize the encryption key, if it doesn't exist + let init_encryption_res = core.init_encryption().await; + assert!( + init_encryption_res.is_ok(), + "Failed to initialize encryption: {:?}", + init_encryption_res + ); + + let init_journal_res = core.init_journal_mode().await; + assert!( + init_journal_res.is_ok(), + "Failed to initialize journal mode: {:?}", + init_journal_res + ); + + // Initialize the scheduler + let scheduler_res = core.init_scheduler().await; + assert!( + scheduler_res.is_ok(), + "Failed to initialize scheduler: {:?}", + scheduler_res + ); + + (core, temp_dir) +} diff --git a/core/tests/utils/temp_library.rs b/core/tests/utils/temp_library.rs new file mode 100644 index 000000000..bb3e1a0ea --- /dev/null +++ b/core/tests/utils/temp_library.rs @@ -0,0 +1,201 @@ +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, +}; + +use prisma_client_rust::QueryError; +use stump_core::{ + db::entity::{LibraryConfig, LibraryPattern}, + prisma::{self, PrismaClient}, + CoreResult, +}; + +use super::get_test_file_contents; + +// Note: this struct is used to hold the TempDir that points to the temporary directory +// used throughout the tests. It is done this way so that when the object goes out of +// scope, the directory is deleted. +#[allow(dead_code)] +pub struct TempLibrary { + pub library_root: PathBuf, + pub pattern: LibraryPattern, +} + +impl TempLibrary { + /// Builds a temporary directory structure for a collection-based library: + /// + /// ```md + /// . + /// └── LIBRARY-ROOT + ///    └── collection-1 + ///    ├── collection-1-nested1 + ///    │   ├── collection-1-nested1-nested2 + ///    │   │   └── book.zip + ///    │   └── book.zip + ///    └── book.epub + /// ``` + #[allow(dead_code)] + pub fn collection_library(root: &Path) -> CoreResult { + let library_root = root.join("collection_library"); + let cbl_1 = library_root.join("collection-1"); + let cbl_1_epub = cbl_1.join("book.epub"); + let cbl_nested_1 = cbl_1.join("collection-1-nested1"); + let cbl_nested_2 = cbl_nested_1.join("collection-1-nested1-nested2"); + + fs::create_dir_all(&cbl_nested_2)?; + + fs::write(&cbl_1_epub, get_test_file_contents("book.epub"))?; + fs::write( + cbl_nested_2.join("book.zip"), + get_test_file_contents("book.zip"), + )?; + fs::write( + cbl_nested_1.join("book.zip"), + get_test_file_contents("book.zip"), + )?; + + Ok(TempLibrary { + library_root, + pattern: LibraryPattern::CollectionBased, + }) + } + + /// Builds a temporary directory structure for a series-based library: + /// + /// ```md + /// . + /// └── LIBRARY-ROOT + ///    ├── book.zip + ///    ├── series-1 + ///    │   └── space-book.zip + ///    └── series-2 + ///    └── duck-book.zip + /// ``` + #[allow(dead_code)] + pub fn series_library(root: &Path) -> CoreResult { + let library_root = root.join("series_library"); + let series_based_library_root_book = library_root.join("book.zip"); + let sbl_1 = library_root.join("series-1"); + let sbl_1_book = sbl_1.join("science_comics_001.cbz"); + let sbl_2 = library_root.join("series-2"); + let sbl_2_book = sbl_2.join("book.zip"); + + fs::create_dir_all(&sbl_1)?; + fs::create_dir_all(&sbl_2)?; + + fs::write( + &series_based_library_root_book, + get_test_file_contents("book.zip"), + )?; + fs::write( + &sbl_1_book, + get_test_file_contents("science_comics_001.cbz"), + )?; + fs::write(&sbl_2_book, get_test_file_contents("book.zip"))?; + + Ok(TempLibrary { + library_root, + pattern: LibraryPattern::SeriesBased, + }) + } + + /// Gets the name of the library from the directory name. + #[allow(dead_code)] + pub fn get_name(&self) -> &str { + self.library_root + .file_name() + .unwrap() + .to_str() + .expect("Failed to get library name") + } +} + +#[allow(dead_code)] +pub async fn create_library( + db: Arc, + library_name: &str, + library_path: &str, + library_config: LibraryConfig, +) -> (prisma::library::Data, prisma::library_config::Data) { + let result: Result<(_, _), QueryError> = db + ._transaction() + .with_timeout(10 * 1000) + .run(|client| async move { + let ignore_rules = (!library_config.ignore_rules.is_empty()) + .then(|| library_config.ignore_rules.as_bytes()) + .transpose() + .expect("Ignore rules should exist"); + let thumbnail_config = library_config + .thumbnail_config + .map(|options| options.as_bytes()) + .transpose() + .expect("Thumbnail config should exist"); + + let library_config = client + .library_config() + .create(vec![ + prisma::library_config::convert_rar_to_zip::set( + library_config.convert_rar_to_zip, + ), + prisma::library_config::hard_delete_conversions::set( + library_config.hard_delete_conversions, + ), + prisma::library_config::process_metadata::set( + library_config.process_metadata, + ), + prisma::library_config::generate_file_hashes::set( + library_config.generate_file_hashes, + ), + prisma::library_config::default_reading_dir::set( + library_config.default_reading_dir.to_string(), + ), + prisma::library_config::default_reading_image_scale_fit::set( + library_config.default_reading_image_scale_fit.to_string(), + ), + prisma::library_config::default_reading_mode::set( + library_config.default_reading_mode.to_string(), + ), + prisma::library_config::library_pattern::set( + library_config.library_pattern.to_string(), + ), + prisma::library_config::thumbnail_config::set(thumbnail_config), + prisma::library_config::ignore_rules::set(ignore_rules), + ]) + .exec() + .await + .unwrap(); + + let library = client + .library() + .create( + library_name.to_string(), + library_path.to_string(), + prisma::library_config::id::equals(library_config.id.clone()), + vec![prisma::library::description::set(None)], + ) + .exec() + .await + .unwrap(); + + let library_config = client + .library_config() + .update( + prisma::library_config::id::equals(library_config.id), + vec![ + prisma::library_config::library::connect( + prisma::library::id::equals(library.id.clone()), + ), + prisma::library_config::library_id::set(Some(library.id.clone())), + ], + ) + .exec() + .await + .unwrap(); + + Ok((library, library_config)) + }) + .await; + + result.unwrap() +} diff --git a/scripts/lib b/scripts/lib index 939329039..860f7a6bb 100644 --- a/scripts/lib +++ b/scripts/lib @@ -20,7 +20,6 @@ prisma_sed_correction() { workspaces_sed_correction() { set -ex; \ - sed -i '/core\/integration-tests/d' Cargo.toml; \ sed -i '/apps\/desktop\/src-tauri/d' Cargo.toml; \ sed -i '/crates\/prisma-cli/d' Cargo.toml }