Initial commit
This commit is contained in:
191
tests/integration.rs
Normal file
191
tests/integration.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
use lofty::tag::TagExt;
|
||||
use shanty_db::{Database, queries};
|
||||
use shanty_index::{ScanConfig, run_scan};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Create a minimal valid MP3 file with ID3v2 tags using lofty.
|
||||
fn create_test_mp3(dir: &std::path::Path, filename: &str, title: &str, artist: &str, album: &str) {
|
||||
use lofty::config::WriteOptions;
|
||||
use lofty::tag::{Accessor, ItemKey, Tag, TagType};
|
||||
|
||||
let path = dir.join(filename);
|
||||
|
||||
// Write a minimal valid MPEG frame (silence, ~0.026s at 128kbps)
|
||||
// MPEG1 Layer 3, 128kbps, 44100Hz, stereo frame header + padding
|
||||
let frame_header: [u8; 4] = [0xFF, 0xFB, 0x90, 0x00];
|
||||
let frame_size = 417; // standard frame size for 128kbps/44100Hz
|
||||
let mut frame_data = vec![0u8; frame_size];
|
||||
frame_data[..4].copy_from_slice(&frame_header);
|
||||
|
||||
// Write a few frames so lofty recognizes it as valid audio
|
||||
let mut file = fs::File::create(&path).unwrap();
|
||||
for _ in 0..10 {
|
||||
file.write_all(&frame_data).unwrap();
|
||||
}
|
||||
drop(file);
|
||||
|
||||
// Now write tags using lofty
|
||||
let mut tag = Tag::new(TagType::Id3v2);
|
||||
tag.set_title(title.to_string());
|
||||
tag.set_artist(artist.to_string());
|
||||
tag.set_album(album.to_string());
|
||||
tag.set_track(1);
|
||||
tag.set_disk(1);
|
||||
tag.set_year(2024);
|
||||
tag.set_genre("Rock".to_string());
|
||||
tag.insert(lofty::tag::TagItem::new(
|
||||
ItemKey::AlbumArtist,
|
||||
lofty::tag::ItemValue::Text(artist.to_string()),
|
||||
));
|
||||
|
||||
tag.save_to_path(&path, WriteOptions::default()).unwrap();
|
||||
}
|
||||
|
||||
async fn test_db() -> Database {
|
||||
Database::new("sqlite::memory:")
|
||||
.await
|
||||
.expect("failed to create test database")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scan_indexes_music_files() {
|
||||
let db = test_db().await;
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
create_test_mp3(dir.path(), "song1.mp3", "Time", "Pink Floyd", "DSOTM");
|
||||
create_test_mp3(dir.path(), "song2.mp3", "Money", "Pink Floyd", "DSOTM");
|
||||
|
||||
let config = ScanConfig {
|
||||
root: dir.path().to_owned(),
|
||||
dry_run: false,
|
||||
concurrency: 2,
|
||||
};
|
||||
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_found, 2);
|
||||
assert_eq!(stats.files_indexed, 2);
|
||||
assert_eq!(stats.files_errored, 0);
|
||||
|
||||
// Verify tracks in DB
|
||||
let tracks = queries::tracks::list(db.conn(), 100, 0).await.unwrap();
|
||||
assert_eq!(tracks.len(), 2);
|
||||
|
||||
// Verify artist was created
|
||||
let artist = queries::artists::find_by_name(db.conn(), "Pink Floyd")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(artist.is_some());
|
||||
|
||||
// Verify album was created and linked to artist
|
||||
let albums = queries::albums::get_by_artist(db.conn(), artist.unwrap().id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(albums.len(), 1);
|
||||
assert_eq!(albums[0].name, "DSOTM");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_incremental_scan_skips_unchanged() {
|
||||
let db = test_db().await;
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
create_test_mp3(dir.path(), "song.mp3", "Time", "Pink Floyd", "DSOTM");
|
||||
|
||||
let config = ScanConfig {
|
||||
root: dir.path().to_owned(),
|
||||
dry_run: false,
|
||||
concurrency: 1,
|
||||
};
|
||||
|
||||
// First scan
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_indexed, 1);
|
||||
assert_eq!(stats.files_skipped, 0);
|
||||
|
||||
// Second scan — should skip since mtime unchanged
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_indexed, 0);
|
||||
assert_eq!(stats.files_skipped, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dry_run_does_not_write() {
|
||||
let db = test_db().await;
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
create_test_mp3(dir.path(), "song.mp3", "Time", "Pink Floyd", "DSOTM");
|
||||
|
||||
let config = ScanConfig {
|
||||
root: dir.path().to_owned(),
|
||||
dry_run: true,
|
||||
concurrency: 1,
|
||||
};
|
||||
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_found, 1);
|
||||
assert_eq!(stats.files_indexed, 1); // counted as indexed in dry-run
|
||||
|
||||
// But DB should be empty
|
||||
let tracks = queries::tracks::list(db.conn(), 100, 0).await.unwrap();
|
||||
assert!(tracks.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_metadata_still_indexed() {
|
||||
let db = test_db().await;
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
// Create a file with minimal valid audio but no tags
|
||||
let frame_header: [u8; 4] = [0xFF, 0xFB, 0x90, 0x00];
|
||||
let frame_size = 417;
|
||||
let mut frame_data = vec![0u8; frame_size];
|
||||
frame_data[..4].copy_from_slice(&frame_header);
|
||||
|
||||
let path = dir.path().join("untagged.mp3");
|
||||
let mut file = fs::File::create(&path).unwrap();
|
||||
for _ in 0..10 {
|
||||
file.write_all(&frame_data).unwrap();
|
||||
}
|
||||
drop(file);
|
||||
|
||||
let config = ScanConfig {
|
||||
root: dir.path().to_owned(),
|
||||
dry_run: false,
|
||||
concurrency: 1,
|
||||
};
|
||||
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_found, 1);
|
||||
assert_eq!(stats.files_indexed, 1);
|
||||
|
||||
// Track should exist but with NULL metadata
|
||||
let tracks = queries::tracks::list(db.conn(), 100, 0).await.unwrap();
|
||||
assert_eq!(tracks.len(), 1);
|
||||
assert!(tracks[0].title.is_none());
|
||||
assert!(tracks[0].artist.is_none());
|
||||
// But should still have file-level info
|
||||
assert!(tracks[0].file_size > 0);
|
||||
assert!(tracks[0].codec.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_non_music_files_ignored() {
|
||||
let db = test_db().await;
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(dir.path().join("readme.txt"), b"not music").unwrap();
|
||||
fs::write(dir.path().join("cover.jpg"), b"not music").unwrap();
|
||||
fs::write(dir.path().join("data.json"), b"{}").unwrap();
|
||||
|
||||
let config = ScanConfig {
|
||||
root: dir.path().to_owned(),
|
||||
dry_run: false,
|
||||
concurrency: 1,
|
||||
};
|
||||
|
||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||
assert_eq!(stats.files_found, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user