packages feed

cue-sheet-2.0.2: tests/Text/CueSheet/ParserSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Text.CueSheet.ParserSpec (spec) where

import Control.Monad.Catch
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.List.NonEmpty as NE
import Test.Hspec
import Test.Hspec.Megaparsec
import Text.CueSheet.Parser
import Text.CueSheet.Types
import Text.Megaparsec

spec :: Spec
spec =
  describe "parseCueSheet" $ do
    it "correctly deals with whitespace" $ do
      bs <- B.readFile "cue-sheet-samples/parser-whitespace.cue"
      sheet <- testSheet
      parseCueSheet "" bs `shouldParse` sheet
    it "parses commands case-insensitively" $ do
      bs <- B.readFile "cue-sheet-samples/parser-case-insensitivity.cue"
      sheet <- testSheet
      parseCueSheet "" bs `shouldParse` sheet
    it "handles comments properly" $ do
      bs <- B.readFile "cue-sheet-samples/parser-comments.cue"
      sheet <- testSheet
      parseCueSheet "" bs `shouldParse` sheet
    it "at least one file should be declared" $ do
      bs <- B.readFile "cue-sheet-samples/parser-missing-file.cue"
      let es =
            foldMap
              etoks
              [ "CATALOG",
                "CDTEXTFILE",
                "FILE",
                "PERFORMER",
                "REM",
                "SONGWRITER",
                "TITLE"
              ]
      parseCueSheet "" bs
        `shouldFailWith` err 67 (ueof <> es)
    it "at least one track should be declared" $ do
      bs <- B.readFile "cue-sheet-samples/parser-missing-track.cue"
      parseCueSheet "" bs
        `shouldFailWith` err 109 (ueof <> etoks "REM" <> etoks "TRACK")
    it "at least one non-zero index should be declared" $ do
      bs <- B.readFile "cue-sheet-samples/parser-missing-index.cue"
      let e = wrappedTrivial (ueof <> etoks "INDEX" <> etoks "REM")
      parseCueSheet "" bs
        `shouldFailWith` errFancy 174 (cstm (Eec (Just 1) e))
    it "parses a normal CUE file without issues" $ do
      bs <- B.readFile "cue-sheet-samples/parser-normal.cue"
      sheet <- normalCueSheet
      parseCueSheet "" bs `shouldParse` sheet
    it "rejects invalid catalog number" $ do
      bs <- B.readFile "cue-sheet-samples/parser-invalid-catalog.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          8
          (cstm (Eec Nothing (CueParserInvalidCatalog "123")))
    it "rejects invalid CUE text" $ do
      bs <- B.readFile "cue-sheet-samples/parser-invalid-text.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          10
          (cstm (Eec Nothing (CueParserInvalidCueText "")))
    it "detects and reports tracks that appear out of order" $ do
      bs <- B.readFile "cue-sheet-samples/parser-track-out-of-order.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          182
          (cstm (Eec Nothing CueParserTrackOutOfOrder))
    it "rejects invalid ISRC values" $ do
      bs <- B.readFile "cue-sheet-samples/parser-invalid-isrc.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          161
          (cstm (Eec (Just 3) (CueParserInvalidTrackIsrc "123")))
    it "rejects invalid number of seconds" $ do
      bs <- B.readFile "cue-sheet-samples/parser-invalid-seconds.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          168
          (cstm (Eec (Just 1) (CueParserInvalidSeconds 61)))
    it "rejects invalid number of frames" $ do
      bs <- B.readFile "cue-sheet-samples/parser-invalid-frames.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          171
          (cstm (Eec (Just 1) (CueParserInvalidFrames 77)))
    it "detects and reports indices that appear out of order" $ do
      bs <- B.readFile "cue-sheet-samples/parser-index-out-of-order.cue"
      parseCueSheet "" bs
        `shouldFailWith` errFancy
          162
          (cstm (Eec (Just 1) CueParserTrackIndexOutOfOrder))
    it "rejects duplicate CATALOG command" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-catalog.cue"
      let es =
            utoks "CATA"
              <> foldMap
                etoks
                [ "CDTEXTFILE",
                  "FILE",
                  "PERFORMER",
                  "REM",
                  "SONGWRITER",
                  "TITLE"
                ]
      parseCueSheet "" bs `shouldFailWith` err 22 es
    it "rejects duplicate CDTEXTFILE command" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-cdtextfile.cue"
      let es =
            utoks "CDTE"
              <> foldMap
                etoks
                [ "CATALOG",
                  "FILE",
                  "PERFORMER",
                  "REM",
                  "SONGWRITER",
                  "TITLE"
                ]
      parseCueSheet "" bs `shouldFailWith` err 23 es
    it "rejects duplicate PERFORMER command" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-performer.cue"
      let es =
            utoks "PERF"
              <> foldMap
                etoks
                [ "CATALOG",
                  "CDTEXTFILE",
                  "FILE",
                  "REM",
                  "SONGWRITER",
                  "TITLE"
                ]
      parseCueSheet "" bs `shouldFailWith` err 30 es
    it "rejects duplicate TITLE command" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-title.cue"
      let es =
            utoks "TITL"
              <> foldMap
                etoks
                [ "CATALOG",
                  "CDTEXTFILE",
                  "FILE",
                  "PERFORMER",
                  "REM",
                  "SONGWRITER"
                ]
      parseCueSheet "" bs `shouldFailWith` err 17 es
    it "rejects duplicate SONGWRITER command" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-songwriter.cue"
      let es =
            utoks "SONG"
              <> foldMap
                etoks
                [ "CATALOG",
                  "CDTEXTFILE",
                  "FILE",
                  "PERFORMER",
                  "REM",
                  "TITLE"
                ]
      parseCueSheet "" bs `shouldFailWith` err 18 es
    it "rejects duplicate FLAGS command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-flags.cue"
      let es =
            wrappedTrivial $
              utoks "FLAGS"
                <> foldMap
                  etoks
                  [ "INDEX",
                    "ISRC",
                    "PERFORMER",
                    "PREGAP",
                    "REM",
                    "SONGWRITER",
                    "TITLE"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 144 (cstm (Eec (Just 1) es))
    it "rejects duplicate ISRC command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-isrc.cue"
      let es =
            wrappedTrivial $
              utoks "ISRC "
                <> foldMap
                  etoks
                  [ "FLAGS",
                    "INDEX",
                    "PERFORMER",
                    "PREGAP",
                    "REM",
                    "SONGWRITER",
                    "TITLE"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 152 (cstm (Eec (Just 1) es))
    it "rejects duplicate PERFORMER command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-performer.cue"
      let es =
            wrappedTrivial $
              utoks "PERFO"
                <> foldMap
                  etoks
                  [ "FLAGS",
                    "INDEX",
                    "ISRC",
                    "PREGAP",
                    "REM",
                    "SONGWRITER",
                    "TITLE"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 156 (cstm (Eec (Just 1) es))
    it "rejects duplicate TITLE command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-title.cue"
      let es =
            wrappedTrivial $
              utoks "TITLE"
                <> foldMap
                  etoks
                  [ "FLAGS",
                    "INDEX",
                    "ISRC",
                    "PERFORMER",
                    "PREGAP",
                    "REM",
                    "SONGWRITER"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 146 (cstm (Eec (Just 1) es))
    it "rejects duplicate SONGWRITER command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-songwriter.cue"
      let es =
            wrappedTrivial $
              utoks "SONGW"
                <> foldMap
                  etoks
                  [ "FLAGS",
                    "INDEX",
                    "ISRC",
                    "PERFORMER",
                    "PREGAP",
                    "REM",
                    "TITLE"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 150 (cstm (Eec (Just 1) es))
    it "rejects duplicate PREGAP command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-pregap.cue"
      let es =
            wrappedTrivial $
              utoks "PREGA"
                <> foldMap
                  etoks
                  [ "FLAGS",
                    "INDEX",
                    "ISRC",
                    "PERFORMER",
                    "REM",
                    "SONGWRITER",
                    "TITLE"
                  ]
      parseCueSheet "" bs
        `shouldFailWith` errFancy 176 (cstm (Eec (Just 1) es))
    it "rejects duplicate POSTGAP command (in track)" $ do
      bs <- B.readFile "cue-sheet-samples/parser-duplicate-track-postgap.cue"
      let es =
            foldMap
              etoks
              [ "FILE",
                "REM",
                "TRACK"
              ]
      parseCueSheet "" bs
        `shouldFailWith` err 199 (utok 80 <> es <> eeof)

testSheet :: MonadThrow m => m CueSheet
testSheet = do
  performer <- mkCueText "Faithless"
  title <- mkCueText "Live in Berlin"
  trackTitle <- mkCueText "Reverence"
  return
    CueSheet
      { cueCatalog = Nothing,
        cueCdTextFile = Nothing,
        cuePerformer = Just performer,
        cueTitle = Just title,
        cueSongwriter = Nothing,
        cueFirstTrackNumber = 1,
        cueFiles =
          NE.fromList
            [ CueFile
                { cueFileName = "Faithless - Live in Berlin.mp3",
                  cueFileType = MP3,
                  cueFileTracks =
                    NE.fromList
                      [ CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just trackTitle,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices =
                              NE.fromList
                                [CueTime 3, CueTime 151],
                            cueTrackPostgap = Nothing
                          }
                      ]
                }
            ]
      }

normalCueSheet :: MonadThrow m => m CueSheet
normalCueSheet = do
  performer <- mkCueText "Faithless"
  catalog <- mkMcn "1112223334445"
  title <- mkCueText "Live in Berlin"
  track1Title <- mkCueText "Reverence"
  track2Title <- mkCueText "She's My Baby"
  track3Title <- mkCueText "Take the Long Way Home"
  track4Title <- mkCueText "Insomnia"
  track5Title <- mkCueText "Bring the Family Back"
  track6Title <- mkCueText "Salva Mea"
  track7Title <- mkCueText "Dirty Old Man"
  track8Title <- mkCueText "God Is a DJ"
  return
    CueSheet
      { cueCatalog = Just catalog,
        cueCdTextFile = Just "blah-blah.txt",
        cuePerformer = Just performer,
        cueTitle = Just title,
        cueSongwriter = Nothing,
        cueFirstTrackNumber = 1,
        cueFiles =
          NE.fromList
            [ CueFile
                { cueFileName = "Faithless - Live in Berlin.mp3",
                  cueFileType = MP3,
                  cueFileTracks =
                    NE.fromList
                      [ CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track1Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Just (CueTime 75),
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 0],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track2Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 30150],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track3Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 49050],
                            cueTrackPostgap = Just (CueTime 76)
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track4Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices =
                              NE.fromList
                                [CueTime 76800, CueTime 76805],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = True,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track5Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 115800],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track6Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 138750],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track7Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 172800],
                            cueTrackPostgap = Nothing
                          },
                        CueTrack
                          { cueTrackDigitalCopyPermitted = False,
                            cueTrackFourChannelAudio = False,
                            cueTrackPreemphasisEnabled = False,
                            cueTrackSerialCopyManagement = False,
                            cueTrackType = CueTrackAudio,
                            cueTrackIsrc = Nothing,
                            cueTrackTitle = Just track8Title,
                            cueTrackPerformer = Just performer,
                            cueTrackSongwriter = Nothing,
                            cueTrackPregap = Nothing,
                            cueTrackPregapIndex = Nothing,
                            cueTrackIndices = NE.fromList [CueTime 191625],
                            cueTrackPostgap = Nothing
                          }
                      ]
                }
            ]
      }

-- | Construct 'CueParserFailure' from given @'ET' 'Word8'@ thing.
wrappedTrivial :: ET ByteString -> CueParserFailure
wrappedTrivial xs =
  case err 0 xs of
    TrivialError _ us es -> CueParserTrivialError us es
    _ -> error "Ooops!"

-- | Quickly make a custom error component.
cstm :: e -> EF e
cstm = fancy . ErrorCustom
{-# INLINE cstm #-}