monatone-0.1.0.0: test/Test/IntegrationSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.IntegrationSpec (tests) where
import Test.Tasty
import Test.Tasty.HUnit
import Control.Monad.Except (runExceptT)
import System.FilePath ((</>))
import System.Directory (doesFileExist, copyFile, removeFile, getTemporaryDirectory)
import System.Process (callProcess)
import Control.Exception (catch, SomeException)
import Control.Monad (unless)
import System.OsPath hiding ((</>))
import qualified Data.Text as T
import Monatone.Common (parseMetadata)
import Monatone.Metadata
import Monatone.Writer
tests :: TestTree
tests = withResource ensureFixtures (const $ return ()) $ \_ ->
testGroup "Integration Tests"
[ testGroup "Reading Real Files"
[ testReadMinimalMP3
, testReadTaggedMP3
, testReadMinimalFLAC
]
, testGroup "Round-trip Tests"
[ testMP3RoundTrip
, testFLACRoundTrip
]
]
-- | Ensure test fixtures exist, generate them if missing
ensureFixtures :: IO ()
ensureFixtures = do
let files = [ fixturesDir </> "minimal.mp3"
, fixturesDir </> "tagged.mp3"
, fixturesDir </> "minimal.flac"
]
allExist <- and <$> mapM doesFileExist files
unless allExist $ do
putStrLn "Test fixtures not found. Attempting to generate with ffmpeg..."
generateTestFiles `catch` handleError
where
handleError :: SomeException -> IO ()
handleError _ = putStrLn "Warning: Could not generate test fixtures (ffmpeg not available). Integration tests will be skipped."
-- | Generate test files using ffmpeg
generateTestFiles :: IO ()
generateTestFiles = do
-- Generate 1 second of silence as raw PCM
callProcess "ffmpeg" ["-f", "lavfi", "-i", "anullsrc=r=44100:cl=stereo",
"-t", "1", "-f", "s16le", "-y", "/tmp/silence.raw"]
-- Create MP3 with metadata
callProcess "ffmpeg" ["-f", "s16le", "-ar", "44100", "-ac", "2", "-i", "/tmp/silence.raw",
"-codec:a", "libmp3lame", "-b:a", "128k",
"-metadata", "title=Test Title",
"-metadata", "artist=Test Artist",
"-metadata", "album=Test Album",
"-metadata", "date=2024",
"-metadata", "track=1/10",
"-metadata", "genre=Rock",
"-metadata", "comment=Test comment",
"-y", fixturesDir </> "tagged.mp3"]
-- Create minimal MP3
callProcess "ffmpeg" ["-f", "s16le", "-ar", "44100", "-ac", "2", "-i", "/tmp/silence.raw",
"-codec:a", "libmp3lame", "-b:a", "128k",
"-metadata", "title=Minimal Title",
"-y", fixturesDir </> "minimal.mp3"]
-- Create FLAC with metadata
callProcess "ffmpeg" ["-f", "s16le", "-ar", "44100", "-ac", "2", "-i", "/tmp/silence.raw",
"-codec:a", "flac",
"-metadata", "title=FLAC Test Track",
"-metadata", "artist=Test Band",
"-metadata", "album=FLAC Album",
"-metadata", "date=2024",
"-metadata", "track=3",
"-metadata", "comment=FLAC test comment",
"-y", fixturesDir </> "minimal.flac"]
-- Clean up
removeFile "/tmp/silence.raw" `catch` (\(_ :: SomeException) -> return ())
fixturesDir :: FilePath
fixturesDir = "test/fixtures"
-- Helper to convert FilePath to OsPath
toOsPath :: FilePath -> IO OsPath
toOsPath = encodeFS
testReadMinimalMP3 :: TestTree
testReadMinimalMP3 = testCase "Read minimal MP3" $ do
let path = fixturesDir </> "minimal.mp3"
exists <- doesFileExist path
unless exists $ assertFailure "Test skipped: fixture not available (run with ffmpeg to generate)"
osPath <- toOsPath path
result <- parseMetadata osPath
case result of
Left err -> assertFailure $ T.unpack $ "Failed to parse: " <> T.pack (show err)
Right metadata -> do
assertEqual "Format" MP3 (format metadata)
assertEqual "Title" (Just "Minimal Title") (title metadata)
testReadTaggedMP3 :: TestTree
testReadTaggedMP3 = testCase "Read MP3 with full metadata" $ do
let path = fixturesDir </> "tagged.mp3"
exists <- doesFileExist path
assertBool "Test file exists" exists
osPath <- toOsPath path
result <- parseMetadata osPath
case result of
Left err -> assertFailure $ T.unpack $ "Failed to parse: " <> T.pack (show err)
Right metadata -> do
assertEqual "Format" MP3 (format metadata)
assertEqual "Title" (Just "Test Title") (title metadata)
assertEqual "Artist" (Just "Test Artist") (artist metadata)
assertEqual "Album" (Just "Test Album") (album metadata)
assertEqual "Year" (Just 2024) (year metadata)
assertEqual "Track number" (Just 1) (trackNumber metadata)
assertEqual "Genre" (Just "Rock") (genre metadata)
assertEqual "Comment" (Just "Test comment") (comment metadata)
testReadMinimalFLAC :: TestTree
testReadMinimalFLAC = testCase "Read minimal FLAC" $ do
let path = fixturesDir </> "minimal.flac"
exists <- doesFileExist path
assertBool "Test file exists" exists
osPath <- toOsPath path
result <- parseMetadata osPath
case result of
Left err -> assertFailure $ T.unpack $ "Failed to parse: " <> T.pack (show err)
Right metadata -> do
assertEqual "Format" FLAC (format metadata)
assertEqual "Title" (Just "FLAC Test Track") (title metadata)
assertEqual "Artist" (Just "Test Band") (artist metadata)
-- Check audio properties from STREAMINFO
let props = audioProperties metadata
case sampleRate props of
Just sr -> assertEqual "Sample rate" 44100 sr
Nothing -> assertFailure "No sample rate found"
case channels props of
Just ch -> assertEqual "Channels" 2 ch
Nothing -> assertFailure "No channels found"
testMP3RoundTrip :: TestTree
testMP3RoundTrip = testCase "MP3 read-write-read round trip" $ do
-- Use system temp directory
tmpDir <- getTemporaryDirectory
let origPath = fixturesDir </> "tagged.mp3"
let tmpPath = tmpDir </> "monatone-test-mp3.mp3"
-- Verify source file exists
origExists <- doesFileExist origPath
assertBool (T.unpack $ "Source file exists: " <> T.pack origPath) origExists
-- Copy file to temp location
copyFile origPath tmpPath
-- Verify copy succeeded
tmpExists <- doesFileExist tmpPath
assertBool (T.unpack $ "Temp file created: " <> T.pack tmpPath) tmpExists
-- Read original metadata
osTmpPath <- toOsPath tmpPath
origResult <- parseMetadata osTmpPath
origMetadata <- case origResult of
Left err -> assertFailure $ T.unpack $ "Failed to read original: " <> T.pack (show err)
Right m -> return m
-- Modify metadata
let update = setTitle "Modified Title" $
setArtist "Modified Artist" $
setYear 2025 $
emptyUpdate
writeResult <- runExceptT $ updateMetadata osTmpPath update
case writeResult of
Left err -> assertFailure $ "Failed to write: " ++ show err
Right () -> return ()
-- Read modified metadata
modResult <- parseMetadata osTmpPath
case modResult of
Left err -> assertFailure $ "Failed to read modified: " ++ show err
Right modMetadata -> do
assertEqual "Modified title" (Just "Modified Title") (title modMetadata)
assertEqual "Modified artist" (Just "Modified Artist") (artist modMetadata)
assertEqual "Modified year" (Just 2025) (year modMetadata)
-- Unchanged fields should be preserved
assertEqual "Album preserved" (album origMetadata) (album modMetadata)
assertEqual "Genre preserved" (genre origMetadata) (genre modMetadata)
-- Clean up
removeFile tmpPath
testFLACRoundTrip :: TestTree
testFLACRoundTrip = testCase "FLAC read-write-read round trip" $ do
-- Use system temp directory
tmpDir <- getTemporaryDirectory
let origPath = fixturesDir </> "minimal.flac"
let tmpPath = tmpDir </> "monatone-test-flac.flac"
-- Verify source file exists
origExists <- doesFileExist origPath
assertBool (T.unpack $ "Source file exists: " <> T.pack origPath) origExists
-- Copy file to temp location
copyFile origPath tmpPath
-- Verify copy succeeded
tmpExists <- doesFileExist tmpPath
assertBool (T.unpack $ "Temp file created: " <> T.pack tmpPath) tmpExists
-- Read original metadata
osTmpPath <- toOsPath tmpPath
origResult <- parseMetadata osTmpPath
origMetadata <- case origResult of
Left err -> assertFailure $ T.unpack $ "Failed to read original: " <> T.pack (show err)
Right m -> return m
-- Modify metadata
let update = setTitle "New FLAC Title" $
setAlbum "New Album" $
setTrackNumber 5 $
setComment "Test comment" $
emptyUpdate
writeResult <- runExceptT $ updateMetadata osTmpPath update
case writeResult of
Left err -> assertFailure $ "Failed to write: " ++ show err
Right () -> return ()
-- Read modified metadata
modResult <- parseMetadata osTmpPath
case modResult of
Left err -> assertFailure $ "Failed to read modified: " ++ show err
Right modMetadata -> do
assertEqual "Modified title" (Just "New FLAC Title") (title modMetadata)
assertEqual "Modified album" (Just "New Album") (album modMetadata)
assertEqual "Modified track" (Just 5) (trackNumber modMetadata)
assertEqual "Modified comment" (Just "Test comment") (comment modMetadata)
-- Original artist should be preserved
assertEqual "Artist preserved" (artist origMetadata) (artist modMetadata)
-- Audio properties should remain unchanged
assertEqual "Audio props preserved"
(audioProperties origMetadata)
(audioProperties modMetadata)
-- Clean up
removeFile tmpPath