packages feed

filediff-2.0.0: Tests/test-filediff.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Main (main) where

-- imports

import Test.Tasty
import Test.Tasty.HUnit

import Data.Monoid
import Control.Monad
import Control.Monad.Trans.Either
import Control.Monad.IO.Class (liftIO)

-- qualified imports

import qualified Data.Text as T

import qualified System.IO as IO
import qualified System.Directory as D

-- imported functions

import Data.List ((\\))

import System.Exit (exitSuccess)

import Data.Time.Clock (getCurrentTime, utctDay)
import Data.Time.Calendar (toGregorian)

import Control.Monad ((>>=), return, when)
import Control.Applicative ((<$>), (<*>))
import Control.Monad.IO.Class (liftIO)

import Data.Either.Combinators (isLeft, fromLeft)

import qualified Filediff as F
import qualified Filediff.Stats as F
import qualified Filediff.Types as F

-- helper functions

-- | Concatenates two filepaths, for example:
-- |
-- |     > "a/b" </> "c"
-- |     "a/b/c"
(</>) :: FilePath -> FilePath -> FilePath
a </> b = a ++ "/" ++ b

-- | Takes a list of filepaths, and removes "." and ".." from it.
removeDotDirs :: [FilePath] -> [FilePath]
removeDotDirs = flip (\\) $ [".", ".."]

-- | Removes the oldest ancestor from a path component, e.g.
-- |
-- |     > removeFirstPathComponent "a/b/c"
-- |     "b/c"
removeFirstPathComponent :: FilePath -> FilePath
removeFirstPathComponent = tail . dropUntil ((==) '/')

-- | Drops elements from the given list until the predicate function
-- | returns `True` (returned list includes element that passes test)
dropUntil :: (a -> Bool) -> [a] -> [a]
dropUntil _ [] = []
dropUntil f (x:xs) =
    if f x
        then (x:xs)
        else dropUntil f xs

areFilesEqual :: FilePath -> FilePath -> IO Bool
areFilesEqual a b = do
    if (removeFirstPathComponent a) /= (removeFirstPathComponent b)
        then return False
        else liftM2 (==) (IO.readFile a) (IO.readFile b)

-- | Gets paths to all files in or in subdirectories of the
-- | specified directory. Returned paths are relative to the
-- | given directory.
getDirectoryContentsRecursiveSafe :: FilePath -> IO [FilePath]
getDirectoryContentsRecursiveSafe directory = do
    contents <- getDirectoryContentsRecursiveSafe' directory

    let directoryWithTrailingSlash = if last directory == '/'
        then directory
        else directory </> ""
    let numPathComponents = length . filter ((==) '/') $ directoryWithTrailingSlash
    let removePathComponents = last . take (numPathComponents + 1) . iterate removeFirstPathComponent

    return . map removePathComponents $ contents

getDirectoryContentsRecursiveSafe' :: FilePath -> IO [FilePath]
getDirectoryContentsRecursiveSafe' directory = do
    exists <- D.doesDirectoryExist directory
    if not exists
        then return []
        else do
            relativeContents <- removeDotDirs <$> D.getDirectoryContents directory
            let contents = map ((</>) directory) relativeContents

            files <- filterM D.doesFileExist contents
            directories <- filterM D.doesDirectoryExist contents

            recFiles <- concat <$> mapM getDirectoryContentsRecursiveSafe' directories

            return $ files ++ recFiles

areDirectoriesEqual :: FilePath -> FilePath -> IO Bool
areDirectoriesEqual d1 d2 = do
    d1Contents <- map ((</>) d1) <$> getDirectoryContentsRecursiveSafe d1
    d2Contents <- map ((</>) d2) <$> getDirectoryContentsRecursiveSafe d2

    and <$> zipWithM areFilesEqual d1Contents d2Contents

-- set-up

-- | Runs a test in its own empty directory.
-- | Effectively, it isolates it from all other tests.
runTest :: Assertion -> Assertion
runTest t = do
    testDirectory <- getTestDirectory
    D.createDirectory testDirectory
    D.setCurrentDirectory testDirectory

    t

    D.setCurrentDirectory ".."
    D.removeDirectoryRecursive testDirectory
    where
        -- | Gives a name of a directory that is pretty much guaranteed to
        -- | exist, so it's free for creation.
        getTestDirectory :: IO FilePath
        getTestDirectory = (map formatChar . show) <$> getCurrentTime
            where
                -- | Some characters can't be in directory names.
                formatChar :: Char -> Char
                formatChar ' ' = '-'
                formatChar '.' = '-'
                formatChar ':' = '-'
                formatChar ch = ch

createFileWithContents :: FilePath -> String -> IO ()
createFileWithContents filepath contents = do
    handle <- IO.openFile filepath IO.WriteMode
    IO.hPutStr handle contents
    IO.hClose handle

-- sequence tests

testListDiffEdgeCase1 :: Assertion
testListDiffEdgeCase1 = do
    return $ F.diffLists "" "wabxyze"
    True @?= True -- no exception: considered success for this test

testListDiffEdgeCase2 :: Assertion
testListDiffEdgeCase2 = do
    return $ F.diffLists "wabxyze" ""
    True @?= True -- no exception: considered success for this test

testListDiffEdgeCase3 :: Assertion
testListDiffEdgeCase3 = do
    return $ F.diffLists "" ""
    True @?= True -- no exception: considered success for this test

-- file diff tests

testFileDiff :: Assertion
testFileDiff = do
    createFileWithContents "BASE" "a\nb\nc\nd\ne\nf\ng"
    createFileWithContents "COMP" "w\na\nb\nx\ny\nz\ne"

    let expectedFilediff = F.Filediff "BASE" "COMP" (F.Mod $ F.ListDiff [(2, T.pack "c"),(3, T.pack "d"),(5, T.pack "f"),(6, T.pack "g")] [(0, T.pack "w"),(3, T.pack "x"),(4, T.pack "y"),(5, T.pack "z")])
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testFileDiffEmptyFiles :: Assertion
testFileDiffEmptyFiles = do
    createFileWithContents "BASE" ""
    createFileWithContents "COMP" ""

    let expectedFilediff = F.Filediff "BASE" "COMP" mempty
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testFileDiffEmptyBase :: Assertion
testFileDiffEmptyBase = do
    createFileWithContents "BASE" ""
    createFileWithContents "COMP" "w\na\nb\nx\ny\nz\ne"

    let expectedFilediff = F.Filediff "BASE" "COMP" (F.Mod $ F.ListDiff [] [(0, T.pack "w"),(1, T.pack "a"),(2, T.pack "b"),(3, T.pack "x"),(4, T.pack "y"),(5, T.pack "z"),(6, T.pack "e")])
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testFileDiffEmptyComp :: Assertion
testFileDiffEmptyComp = do
    createFileWithContents "BASE" "a\nb\nc\nd\ne\nf\ng"
    createFileWithContents "COMP" ""

    let expectedFilediff = F.Filediff "BASE" "COMP" (F.Mod $ F.ListDiff [(0, T.pack "a"),(1, T.pack "b"),(2, T.pack "c"),(3, T.pack "d"),(4, T.pack "e"),(5, T.pack "f"),(6, T.pack "g")] [])
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testNonexistentFileDiff1 :: Assertion
testNonexistentFileDiff1 = do
    createFileWithContents "BASE" "a\nb\nc\nd\ne\nf\ng"

    let expectedFilediff = F.Filediff "BASE" "COMP" (F.Del $ F.ListDiff [(0, T.pack "a"),(1, T.pack "b"),(2, T.pack "c"),(3, T.pack "d"),(4, T.pack "e"),(5, T.pack "f"),(6, T.pack "g")] [])
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testNonexistentFileDiff2 :: Assertion
testNonexistentFileDiff2 = do
    createFileWithContents "COMP" "w\na\nb\nx\ny\nz\ne"

    let expectedFilediff = F.Filediff "BASE" "COMP" (F.Add $ F.ListDiff [] [(0, T.pack "w"),(1, T.pack "a"),(2, T.pack "b"),(3, T.pack "x"),(4, T.pack "y"),(5, T.pack "z"),(6, T.pack "e")])
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testNonexistentFileDiff3 :: Assertion
testNonexistentFileDiff3 = do
    let expectedFilediff = F.Filediff "BASE" "COMP" mempty
    (F.diffFiles "BASE" "COMP") >>= (@=?) expectedFilediff

testIdentityFileDiff :: Assertion
testIdentityFileDiff = do
    createFileWithContents "COMP" "w\na\nb\nx\ny\nz\ne"

    let expectedFilediff = F.Filediff "COMP" "COMP" mempty
    (F.diffFiles "COMP" "COMP") >>= (@=?) expectedFilediff

-- directory diff tests

testDirDiff :: Assertion
testDirDiff = do
    D.createDirectory "a"
    D.createDirectory "b"

    D.createDirectory "a/common"
    D.createDirectory "b/common"

    D.createDirectory "a/aonly"
    D.createDirectory "b/bonly"

    createFileWithContents "a/common/x" "x\na\nx"
    createFileWithContents "b/common/x" "x\nb\nx"

    createFileWithContents "a/aonly/afile" "a\na\na"
    createFileWithContents "b/bonly/bfile" "b\nb\nb"

    actualDiff <- F.diffDirectories "a" "b"
    let expectedDiff = F.Diff {
        F.filediffs =
            [ F.Filediff
                { F.base = "common/x"
                , F.comp = "common/x"
                , F.change = F.Mod $ F.ListDiff [(1, T.pack "a")] [(1, T.pack "b")] }
            , F.Filediff
                    { F.base = "aonly/afile"
                    , F.comp = "aonly/afile"
                    , F.change = F.Del $ F.ListDiff [(0, T.pack "a"),(1, T.pack "a"),(2, T.pack "a")] [] }
            , F.Filediff
                { F.base = "bonly/bfile"
                , F.comp = "bonly/bfile"
                , F.change = F.Add $ F.ListDiff [] [(0, T.pack "b"),(1, T.pack "b"),(2, T.pack "b")] } ]
        }
    actualDiff @?= expectedDiff

testDirDiffIgnoreFiles :: Assertion
testDirDiffIgnoreFiles = do
    D.createDirectory "a"
    D.createDirectory "b"

    D.createDirectory "a/common"
    D.createDirectory "a/common2"
    D.createDirectory "b/common"
    D.createDirectory "b/common2"

    D.createDirectory "a/aonly"
    D.createDirectory "a/aonly2"
    D.createDirectory "b/bonly"
    D.createDirectory "b/bonly2"

    createFileWithContents "a/common/x" "x\na\nx"
    createFileWithContents "a/common2/x" "x\na\nx"
    createFileWithContents "b/common/x" "x\nb\nx"
    createFileWithContents "b/common2/x" "x\nb\nx"

    createFileWithContents "a/aonly/afile" "a\na\na"
    createFileWithContents "a/aonly2/afile" "a\na\na"
    createFileWithContents "b/bonly/bfile" "b\nb\nb"
    createFileWithContents "b/bonly2/bfile" "b\nb\nb"

    actualDiff <- F.diffDirectoriesWithIgnoredSubdirs "a" "b" ["aonly2", "common2"] ["bonly2", "common2"]
    let expectedDiff = F.Diff {
        F.filediffs =
            [ F.Filediff
                { F.base = "common/x"
                , F.comp = "common/x"
                , F.change = F.Mod $ F.ListDiff [(1, T.pack "a")] [(1, T.pack "b")] }
            , F.Filediff
                    { F.base = "aonly/afile"
                    , F.comp = "aonly/afile"
                    , F.change = F.Del $ F.ListDiff [(0, T.pack "a"),(1, T.pack "a"),(2, T.pack "a")] [] }
            , F.Filediff
                { F.base = "bonly/bfile"
                , F.comp = "bonly/bfile"
                , F.change = F.Add $ F.ListDiff [] [(0, T.pack "b"),(1, T.pack "b"),(2, T.pack "b")] } ]
        }
    actualDiff @?= expectedDiff

testDirDiffEmptyDirectories :: Assertion
testDirDiffEmptyDirectories = do
    D.createDirectory "a"
    D.createDirectory "b"

    actualDiff <- F.diffDirectories "a" "b"
    let expectedDiff = mempty
    actualDiff @?= expectedDiff

testDirDiffNoFiles :: Assertion
testDirDiffNoFiles = do
    D.createDirectory "a"
    D.createDirectory "b"

    D.createDirectory "a/common"
    D.createDirectory "b/common"

    D.createDirectory "a/aonly"
    D.createDirectory "b/bonly"

    actualDiff <- F.diffDirectories "a" "b"
    let expectedDiff = mempty
    actualDiff @?= expectedDiff

testIdentityDirDiff :: Assertion
testIdentityDirDiff = do
    createFileWithContents "BASE" "a\nb\nc\nd\ne\nf\ng"

    let expectedDiff = mempty
    (F.diffDirectories "." ".") >>= (@=?) expectedDiff

-- composition tests

testListDiffComposition :: Assertion
testListDiffComposition = do
    let a = "abcdefg"
    let b = "wabxyze"
    let c = "#x##ye"

    let ab = F.diffLists a b
    let bc = F.diffLists b c
    let ac = F.diffLists a c

    ab `mappend` bc @?= ac

testListDiffCompositionEdgeCase1 :: Assertion
testListDiffCompositionEdgeCase1 = do
    let a = ""
    let b = "bbb"
    let c = ""

    let ab = F.diffLists a b
    let bc = F.diffLists b c
    let ac = F.diffLists a c

    ab `mappend` bc @?= ac

testListDiffCompositionEdgeCase2 :: Assertion
testListDiffCompositionEdgeCase2 = do
    let ab = F.ListDiff {F.dels = [], F.adds = [(0,"a")]}
    let bc = F.ListDiff {F.dels = [], F.adds = [(1,"b")]}

    let ac = F.ListDiff {F.dels = [], F.adds = [(0,"a"),(1,"b")]}

    ab `mappend` bc @?= ac


testFileDiffComposition :: Assertion
testFileDiffComposition = do
    createFileWithContents "a" "a\nb\nc\nd\ne\nf\ng"
    createFileWithContents "b" "w\na\nb\nx\ny\nz\ne"
    createFileWithContents "c" "#\nx\n#\n#\ny\ne"

    ab <- F.diffFiles "a" "b"
    bc <- F.diffFiles "b" "c"
    ac <- F.diffFiles "a" "c"

    ab `mappend` bc @?= ac

testDirectoryDiffComposition :: Assertion
testDirectoryDiffComposition = do
    D.createDirectory "a"
    D.createDirectory "b"
    D.createDirectory "c"

    D.createDirectory "a/common"
    D.createDirectory "b/common"
    D.createDirectory "c/common"

    D.createDirectory "a/aonly"
    D.createDirectory "b/bonly"
    D.createDirectory "c/conly"

    createFileWithContents "a/common/file" "a\nb\nc\nd\ne\nf\ng"
    createFileWithContents "b/common/file" "w\na\nb\nx\ny\nz\ne"
    createFileWithContents "c/common/file" "#\nx\n#\n#\ny\ne"

    createFileWithContents "a/aonly/afile" "a\na\na"
    createFileWithContents "b/bonly/bfile" "b\nb\nb"
    createFileWithContents "c/conly/cfile" "c\nc\nc"

    ab <- F.diffDirectories "a" "b"
    bc <- F.diffDirectories "b" "c"
    ac <- F.diffDirectories "a" "c"

    ab `mappend` bc @?= ac

-- sequence apply tests

testListApplyEdgeCase1 :: Assertion
testListApplyEdgeCase1 = do
    let base = ""
    let comp = "abcde"

    let listdiff = F.diffLists base comp
    let applied = F.applyListDiff listdiff base
    applied @?= Right comp

-- file apply tests

testFileApply :: Assertion
testFileApply = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"
    let compContents = "w\na\nb\nx\ny\nz\ne"

    createFileWithContents "BASE" baseContents
    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

testFileApplyEdgeCase1 :: Assertion
testFileApplyEdgeCase1 = do
    let baseContents = ""
    let compContents = "w\na\nb\nx\ny\nz\ne"

    createFileWithContents "BASE" baseContents
    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

testFileApplyEdgeCase2 :: Assertion
testFileApplyEdgeCase2 = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"
    let compContents = ""

    createFileWithContents "BASE" baseContents
    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

-- tests deletion of a file
testFileApplyEdgeCase3 :: Assertion
testFileApplyEdgeCase3 = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"

    createFileWithContents "BASE" baseContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")

    exists <- D.doesFileExist "BASE"
    assertBool "File should be deleted." (not exists)

-- tests creation of a file
testFileApplyEdgeCase4 :: Assertion
testFileApplyEdgeCase4 = do
    let compContents = "w\na\nb\nx\ny\nz\ne"

    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"

    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

testFileApplyEdgeCase5 :: Assertion
testFileApplyEdgeCase5 = do
    let baseContents = ""
    let compContents = "\n"

    createFileWithContents "BASE" baseContents
    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

testFileApplyEdgeCase6 :: Assertion
testFileApplyEdgeCase6 = do
    let baseContents = "\n"
    let compContents = ""

    createFileWithContents "BASE" baseContents
    createFileWithContents "COMP" compContents

    fileDiff <- F.diffFiles "BASE" "COMP"
    applied <- runEitherT (F.applyFileDiff fileDiff "BASE")
    applied @?= Right (T.lines . T.pack $ compContents)
    join $ (liftM2 (@?=)) (readFile "BASE") (readFile "COMP")

testListDiffApplyFailureDeletionCase :: Assertion
testListDiffApplyFailureDeletionCase = do
    let base = "abcdefg"
    let faultyBase = "ab*defg"
    let comp = "wabxyze"

    let listDiff = F.diffLists base comp
    let eitherResult = F.applyListDiff listDiff faultyBase
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires removing an element where the diff calls for a different element residing at that index)."

testListDiffApplyFailureAdditionCase :: Assertion
testListDiffApplyFailureAdditionCase = do
    let base = "abcdefg"
    let faultyBase = "abcde"
    let comp = "wabxyzefgq"

    let listDiff = F.diffLists base comp
    let eitherResult = F.applyListDiff listDiff faultyBase
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires inserting at an index larger than the length of the list to which to apply the diff)."
    return ()

testFileDiffApplyFailureDeletionCase :: Assertion
testFileDiffApplyFailureDeletionCase = do
    let baseContents = "abcdefg"
    let faultyBaseContents = "ab*defg"
    let compContents = "wabxyze"

    D.createDirectory "a"
    D.createDirectory "b"
    createFileWithContents "a/BASE" baseContents
    createFileWithContents "a/FAULTY-BASE" faultyBaseContents
    createFileWithContents "b/COMP" compContents

    fileDiff <- F.diffFiles "a/BASE" "b/COMP"
    eitherResult <- runEitherT (F.applyFileDiff fileDiff "a/FAULTY-BASE")
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires removing an element where the diff calls for a different element residing at that index)."

testFileDiffApplyFailureAdditionCase :: Assertion
testFileDiffApplyFailureAdditionCase = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"
    let faultyBaseContents = "a\nb\nc\nd\ne"
    let compContents = "w\na\nb\nx\ny\nz\ne\nf\ng\nq"

    D.createDirectory "a"
    D.createDirectory "b"
    createFileWithContents "a/BASE" baseContents
    createFileWithContents "a/FAULTY-BASE" faultyBaseContents
    createFileWithContents "b/COMP" compContents

    fileDiff <- F.diffFiles "a/BASE" "b/COMP"
    eitherResult <- runEitherT (F.applyFileDiff fileDiff "a/FAULTY-BASE")
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires inserting at an index larger than the length of the list to which to apply the diff)."

testDirectoryDiffApplyFailureDeletionCase :: Assertion
testDirectoryDiffApplyFailureDeletionCase = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"
    let faultyBaseContents = "a\nb\n*\nd\ne\nf\ng"
    let compContents = "wabxyze"

    D.createDirectory "a"
    D.createDirectory "faulty-a"
    D.createDirectory "b"
    createFileWithContents "a/file" baseContents
    createFileWithContents "faulty-a/file" faultyBaseContents
    createFileWithContents "b/file" compContents

    dirDiff <- F.diffDirectories "a" "b"
    eitherResult <- runEitherT (F.applyDirectoryDiff dirDiff "faulty-a")
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires removing an element where the diff calls for a different element residing at that index)."

testDirectoryDiffApplyFailureAdditionCase :: Assertion
testDirectoryDiffApplyFailureAdditionCase = do
    let baseContents = "a\nb\nc\nd\ne\nf\ng"
    let faultyBaseContents = "a\nb\nc\nd\ne"
    let compContents = "w\na\nb\nx\ny\nz\ne\nf\ng\nq"

    D.createDirectory "a"
    D.createDirectory "faulty-a"
    D.createDirectory "b"
    createFileWithContents "a/file" baseContents
    createFileWithContents "faulty-a/file" faultyBaseContents
    createFileWithContents "b/file" compContents

    dirDiff <- F.diffDirectories "a" "b"
    eitherResult <- runEitherT (F.applyDirectoryDiff dirDiff "faulty-a")
    eitherResult @?= Left "Fatal: couldn't apply list diff (application requires inserting at an index larger than the length of the list to which to apply the diff)."

-- directory apply tests

testDirApply :: Assertion
testDirApply = do
    D.createDirectory "a"
    D.createDirectory "b"

    D.createDirectory "a/common"
    D.createDirectory "b/common"

    D.createDirectory "a/aonly"
    D.createDirectory "b/bonly"

    createFileWithContents "a/common/x" "x\na\nx"
    createFileWithContents "b/common/x" "x\nb\nx"

    createFileWithContents "a/aonly/afile" "a\na\na"
    createFileWithContents "b/bonly/bfile" "b\nb\nb"

    diff <- F.diffDirectories "a" "b"
    runEitherT (F.applyDirectoryDiff diff "a")

    -- removing files and removing directories
    -- also addition of bfile to `a` adds \n

    directoriesEqual <- areDirectoriesEqual "a" "b"
    (@?) directoriesEqual "Directories not equal after application"

-- assorted

setUpRelativePathTest :: IO ()
setUpRelativePathTest = do
    D.createDirectory "a"
    D.createDirectory "_"
    D.createDirectory "_/b"

    D.setCurrentDirectory "a"
    createFileWithContents "a" "a"
    D.setCurrentDirectory ".."

    D.setCurrentDirectory "_/b"
    createFileWithContents "a" "b"
    D.setCurrentDirectory "../.."

relativePathExpectedDiff :: F.Diff
relativePathExpectedDiff = F.Diff {
    F.filediffs =
        [ F.Filediff
            { F.base = "a"
            , F.comp = "a"
            , F.change = F.Mod $ F.ListDiff [(0, T.pack "a")] [(0, T.pack "b")] } ]
    }

testRelativePathness :: Assertion
testRelativePathness = do
    setUpRelativePathTest

    let expectedDiff = relativePathExpectedDiff
    actualDiff <- F.diffDirectories "a" "_/b"
    actualDiff @?= expectedDiff

testRelativePathnessEdgeCases :: Assertion
testRelativePathnessEdgeCases = do
    setUpRelativePathTest
    test "a"  "_/b"
    test "a/" "_/b"
    test "a"  "_/b/"
    test "a/" "_/b/"
    test "./a"  "_/b"
    test "./a/" "_/b"
    test "./a"  "_/b/"
    test "./a/" "_/b/"
    test "a"  "./_/b"
    test "a/" "./_/b"
    test "a"  "./_/b/"
    test "a/" "./_/b/"
    test "./a"  "./_/b"
    test "./a/" "./_/b"
    test "./a"  "./_/b/"
    test "./a/" "./_/b/"
    test "./a/" "./_/b/"
    where
        test :: FilePath -> FilePath -> Assertion
        test a b = do
            actualDiff <- F.diffDirectories a b
            actualDiff @?= relativePathExpectedDiff

testDiffStats :: Assertion
testDiffStats = do
    D.createDirectory "a"
    D.createDirectory "b"

    createFileWithContents "a/common" "x\na\nx"
    createFileWithContents "b/common" "x\nb\nx"

    createFileWithContents "a/afile" "a\na"
    createFileWithContents "b/bfile" "b\nb\nb"

    diff <- F.diffDirectories "a" "b"

    F.numFilesAffected diff @?= 3
    F.numAddedLines diff @?= 1 + 3
    F.numDeletedLines diff @?= 1 + 2

testSameListConcatenated :: Assertion
testSameListConcatenated = do
    let diff = F.diffLists "abc" "abcabc"

    diff @?= F.ListDiff {F.dels = [], F.adds = [(3,'a'),(4,'b'),(5,'c')]}

testSameListConcatenatedWithIntermediate :: Assertion
testSameListConcatenatedWithIntermediate = do
    let diff = F.diffLists "abc" "abc*abc"

    diff @?= F.ListDiff {F.dels = [], F.adds = [(3, '*'), (4,'a'),(5,'b'),(6,'c')]}

tests :: TestTree
tests = testGroup "unit tests"
    [ -- diffing
      --    files
      testCase
        "Testing diffing individual files"
        (runTest testFileDiff)
    , testCase
        "Testing diffing the same file (identity diff)"
        (runTest testIdentityFileDiff)
    , testCase
        "Testing diffing individual files (nonexistent file case 1)"
        (runTest testNonexistentFileDiff1)
    , testCase
        "Testing diffing individual files (nonexistent file case 2)"
        (runTest testNonexistentFileDiff2)
    , testCase
        "Testing diffing individual files (nonexistent file case 3)"
        (runTest testNonexistentFileDiff3)
    , testCase
        "Testing diffing individual files (empty file case 1)"
        (runTest testFileDiffEmptyFiles)
    , testCase
        "Testing diffing individual files (empty file case 2)"
        (runTest testFileDiffEmptyBase)
    , testCase
        "Testing diffing individual files (empty file case 3)"
        (runTest testFileDiffEmptyComp)

    --     directories
    , testCase
        "Testing diffing the same directory (identity diff)"
        (runTest testIdentityDirDiff)
    , testCase
        "Testing diffing empty directories"
        (runTest testDirDiffEmptyDirectories)
    , testCase
        "Testing diffing for directories without files in them"
        (runTest testDirDiffNoFiles)
    , testCase
        "Testing diffing for directories"
        (runTest testDirDiff)
    , testCase
        "Testing diffing for directories, with ignoring subdirs"
        (runTest testDirDiffIgnoreFiles)

    -- patching
    , testCase
        "Testing patching individual files"
        (runTest testFileApply)
    , testCase
        "Testing patching individual files (edge case 1)"
        (runTest testFileApplyEdgeCase1)
    , testCase
        "Testing patching individual files (edge case 2)"
        (runTest testFileApplyEdgeCase2)
    , testCase
        "Testing patching individual files (edge case 3)"
        (runTest testFileApplyEdgeCase3)
    , testCase
        "Testing patching individual files (edge case 4)"
        (runTest testFileApplyEdgeCase4)
    -- this edge case is kind of tricky to get working. It'll
    -- basically never get hit in practice, and doesn't have that
    -- much of a consequence. We'll ignore it for now.
    --, testCase
    --    "Testing patching individual files (edge case 5)"
    --    (runTest testFileApplyEdgeCase5)
    , testCase
        "Testing patching individual files (edge case 6)"
        (runTest testFileApplyEdgeCase6)
    , testCase
        "Testing patching directories"
        (runTest testDirApply)

    -- alg
    -- edge cases
    , testCase
        "Testing list diffing (edge case 1)"
        (testListDiffEdgeCase1)
    , testCase
        "Testing list diffing (edge case 2)"
        (testListDiffEdgeCase2)
    , testCase
        "Testing list diffing (edge case 3)"
        (testListDiffEdgeCase3)
    , testCase
        "Testing sequence patching (edge case 1)"
        (testListApplyEdgeCase1)

    --     composition
    , testCase
        "Testing list diffing composition"
        (testListDiffComposition)
    , testCase
        "Testing list diffing composition (edge case 1)"
        (testListDiffCompositionEdgeCase1)
    , testCase
        "Testing list diffing composition (edge case 2)"
        (testListDiffCompositionEdgeCase2)
    , testCase
        "Testing file diffing composition"
        (runTest testFileDiffComposition)
    , testCase
        "Testing directory diffing composition"
        (runTest testDirectoryDiffComposition)

    -- assorted
    , testCase
        "Testing that paths are relative"
        (runTest testRelativePathness)
    , testCase
        "Testing that paths are relative (edge cases)"
        (runTest testRelativePathnessEdgeCases)
    , testCase
        "Testing diff statistics"
        (runTest testDiffStats)
    , testCase
        "Testing same list concatenated with itself (case 1)"
        testSameListConcatenated
    , testCase
        "Testing same list concatenated with itself (case 2)"
        testSameListConcatenatedWithIntermediate

    -- can apply?
    , testCase
        "Testing application failure for list diffs (case 1)"
        (runTest testListDiffApplyFailureDeletionCase)
    , testCase
        "Testing application failure for list diffs (case 2)"
        (runTest testListDiffApplyFailureAdditionCase)
    , testCase
        "Testing application failure for file diffs (case 1)"
        (runTest testFileDiffApplyFailureDeletionCase)
    , testCase
        "Testing application failure for file diffs (case 2)"
        (runTest testFileDiffApplyFailureAdditionCase)
    , testCase
        "Testing application failure for directory diffs (case 1)"
        (runTest testDirectoryDiffApplyFailureDeletionCase)
    , testCase
        "Testing application failure for directory diffs (case 2)"
        (runTest testDirectoryDiffApplyFailureAdditionCase)
    ]

main :: IO ()
main = defaultMain tests