packages feed

fast-myers-diff-0.0.0: test/Main.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Main (main) where

import Control.Monad
import Data.String
import qualified Data.Text as T
import Data.Vector (Vector)
import qualified Data.Vector as V
import Myers.Diff
import Test.Hspec
import Test.Hspec.QuickCheck

-- Just for this test
instance IsString (Vector Char) where
  fromString = V.fromList

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "getEditScript" $ do
    let exampleSpec :: Vector Char -> Vector Char -> Vector Edit -> Spec
        exampleSpec old new expected =
          it
            ( unwords
                [ "works for",
                  show old,
                  show new,
                  show expected
                ]
            )
            $ do
              let actual = getEditScript old new
              when (actual /= expected) $
                expectationFailure $
                  unlines
                    [ unwords ["actual:", show actual],
                      unwords ["expected:", show expected]
                    ]

    exampleSpec "" "" []
    exampleSpec "a" "a" []
    exampleSpec "a" "b" [Delete 0 1, Insert 1 0 1]
    exampleSpec "ab" "ac" [Delete 1 1, Insert 2 1 1]
    exampleSpec "b" "aba" [Insert 0 0 1, Insert 1 2 1]
    exampleSpec "abc" "acb" [Delete 1 1, Insert 3 2 1]
    exampleSpec "aaa" "aba" [Delete 0 1, Insert 2 1 1]
    exampleSpec "foofoo" "foo" [Delete 1 1, Delete 3 1, Delete 5 1]
    exampleSpec "foo" "foofoo" [Insert 1 1 1, Insert 2 3 1, Insert 3 5 1]

  describe "computeDiffFromEditScript" $ do
    let exampleSpec :: Vector Char -> Vector Char -> Vector Edit -> Vector (Diff Char) -> Spec
        exampleSpec old new editScript expected =
          it
            ( unwords
                [ "works for",
                  show old,
                  show new,
                  show editScript,
                  show expected
                ]
            )
            $ do
              let actual = computeDiffFromEditScript old new editScript
              when (actual /= expected) $
                expectationFailure $
                  unlines
                    [ unwords ["actual:", show actual],
                      unwords ["expected:", show expected]
                    ]

    exampleSpec "" "" [] []
    exampleSpec "a" "a" [] [Both 'a' 'a']
    exampleSpec "a" "b" [Delete 0 1, Insert 1 0 1] [First 'a', Second 'b']
    exampleSpec "ab" "ac" [Delete 1 1, Insert 2 1 1] [Both 'a' 'a', First 'b', Second 'c']
    exampleSpec "b" "aba" [Insert 0 0 1, Insert 1 2 1] [Second 'a', Both 'b' 'b', Second 'a']
    exampleSpec "abc" "acb" [Delete 1 1, Insert 3 2 1] [Both 'a' 'a', First 'b', Both 'c' 'c', Second 'b']
    exampleSpec "aaa" "aba" [Delete 0 1, Insert 2 1 1] [First 'a', Both 'a' 'a', Second 'b', Both 'a' 'a']
    exampleSpec "foofoo" "foo" [Delete 1 1, Delete 3 1, Delete 5 1] [Both 'f' 'f', First 'o', Both 'o' 'o', First 'f', Both 'o' 'o', First 'o']
    exampleSpec "foo" "foofoo" [Insert 1 1 1, Insert 2 3 1, Insert 3 5 1] [Both 'f' 'f', Second 'o', Both 'o' 'o', Second 'f', Both 'o' 'o', Second 'o']

  describe "getVectorDiff" $ do
    let exampleSpec :: Vector Char -> Vector Char -> Vector (Diff Char) -> Spec
        exampleSpec old new expected =
          it
            ( unwords
                [ "works for",
                  show old,
                  show new,
                  show expected
                ]
            )
            $ do
              let actual = getVectorDiff old new
              when (actual /= expected) $
                expectationFailure $
                  unlines
                    [ unwords ["actual:", show actual],
                      unwords ["expected:", show expected]
                    ]
    exampleSpec "" "" []
    exampleSpec "a" "a" [Both 'a' 'a']
    exampleSpec "a" "b" [First 'a', Second 'b']
    exampleSpec "ab" "ac" [Both 'a' 'a', First 'b', Second 'c']
    exampleSpec "aaa" "aba" [First 'a', Both 'a' 'a', Second 'b', Both 'a' 'a']
    exampleSpec "abgdef" "gh" [First 'a', First 'b', Both 'g' 'g', First 'd', First 'e', Second 'h', First 'f']
    exampleSpec "foofoo" "foo" [Both 'f' 'f', First 'o', Both 'o' 'o', First 'f', Both 'o' 'o', First 'o']

    prop "says that any list is entirely different from the empty list (left)" $ \ls ->
      let v = V.fromList (ls :: String)
       in getVectorDiff v "" `shouldBe` V.map First v

    prop "says that any list is entirely different from the empty list (right)" $ \ls ->
      let v = V.fromList (ls :: String)
       in getVectorDiff "" v `shouldBe` V.map Second v

    prop "does not find diffs in identical strings" $ \ls ->
      let v = V.fromList (ls :: String)
       in getVectorDiff v v `shouldBe` V.map (\a -> Both a a) v

    prop "only puts equal characters in 'Both'" $ \(ls1, ls2) ->
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
          diff = getVectorDiff v1 v2
          valid = \case
            First _ -> True
            Second _ -> True
            Both a b -> a == b
       in all valid diff

    let rebuildFirst :: Vector (PolyDiff a b) -> Vector a
        rebuildFirst = V.mapMaybe $ \case
          First a -> Just a
          Second _ -> Nothing
          Both a _ -> Just a
    prop "lets you rebuild the old vector" $ \(ls1, ls2) -> do
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
      rebuildFirst (getVectorDiff v1 v2) `shouldBe` v1

    let rebuildSecond :: Vector (PolyDiff a b) -> Vector b
        rebuildSecond = V.mapMaybe $ \case
          First _ -> Nothing
          Second b -> Just b
          Both _ b -> Just b
    prop "lets you rebuild the new vector" $ \(ls1, ls2) -> do
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
      rebuildSecond (getVectorDiff v1 v2) `shouldBe` v2

    let unrollGroupedDiff :: Vector (PolyDiff (Vector a) (Vector b)) -> Vector (PolyDiff a b)
        unrollGroupedDiff = foldMap unrollPolyDiff

        -- Not performant, only for testing
        unrollPolyDiff :: PolyDiff (Vector a) (Vector b) -> Vector (PolyDiff a b)
        unrollPolyDiff = \case
          First va -> V.map First va
          Second vb -> V.map Second vb
          Both va vb -> V.zipWith Both va vb

    prop "is the same thing as getGroupedVectorDiff, but unrolled (unroll grouped)" $ \(ls1, ls2) ->
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
          grouped = getGroupedVectorDiff v1 v2
          individual = getVectorDiff v1 v2
       in unrollGroupedDiff grouped `shouldBe` individual

  describe "getGroupedVectorDiff" $ do
    prop "says that any list is entirely different from the empty list (left)" $ \ls ->
      let v = V.fromList (ls :: String)
       in getGroupedVectorDiff v "" `shouldBe` (if V.null v then V.empty else V.singleton (First v))

    prop "says that any list is entirely different from the empty list (right)" $ \ls ->
      let v = V.fromList (ls :: String)
       in getGroupedVectorDiff "" v `shouldBe` (if V.null v then V.empty else V.singleton (Second v))

    prop "does not find diffs in identical strings" $ \ls ->
      let v = V.fromList (ls :: String)
       in getGroupedVectorDiff v v `shouldBe` (if V.null v then V.empty else V.singleton (Both v v))

    prop "only puts equal vectors in 'Both'" $ \(ls1, ls2) ->
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
          diff = getGroupedVectorDiff v1 v2
          valid = \case
            First _ -> True
            Second _ -> True
            Both a b -> a == b
       in all valid diff

    let rebuildFirst :: Vector (PolyDiff (Vector a) (Vector b)) -> Vector a
        rebuildFirst =
          V.concat
            . V.toList
            . V.mapMaybe
              ( \case
                  First a -> Just a
                  Second _ -> Nothing
                  Both a _ -> Just a
              )
    prop "lets you rebuild the old vector" $ \(ls1, ls2) -> do
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
      rebuildFirst (getGroupedVectorDiff v1 v2) `shouldBe` v1

    let rebuildSecond :: Vector (PolyDiff (Vector a) (Vector b)) -> Vector b
        rebuildSecond =
          V.concat
            . V.toList
            . V.mapMaybe
              ( \case
                  First _ -> Nothing
                  Second b -> Just b
                  Both _ b -> Just b
              )
    prop "lets you rebuild the new vector" $ \(ls1, ls2) -> do
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
      rebuildSecond (getGroupedVectorDiff v1 v2) `shouldBe` v2

    -- Not performant, only for testing.
    let rollupUngroupedDiff :: Vector (PolyDiff a b) -> Vector (PolyDiff (Vector a) (Vector b))
        rollupUngroupedDiff = V.fromList . go . V.toList
          where
            go = \case
              [] -> []
              (d : ds) -> case d of
                First _ ->
                  let (fs, rest) = goFirsts (d : ds)
                   in First (V.fromList fs) : go rest
                Second _ ->
                  let (ss, rest) = goSeconds (d : ds)
                   in Second (V.fromList ss) : go rest
                Both _ _ ->
                  let (bs, rest) = goBoths (d : ds)
                   in Both (V.fromList (map fst bs)) (V.fromList (map snd bs)) : go rest
            goFirsts = \case
              (First a : ds) ->
                let (fs, rest) = goFirsts ds
                 in (a : fs, rest)
              rest -> ([], rest)
            goSeconds = \case
              (Second a : ds) ->
                let (fs, rest) = goSeconds ds
                 in (a : fs, rest)
              rest -> ([], rest)
            goBoths = \case
              (Both a b : ds) ->
                let (fs, rest) = goBoths ds
                 in ((a, b) : fs, rest)
              rest -> ([], rest)

    let squishGroupedDiff :: Vector (PolyDiff (Vector a) (Vector b)) -> Vector (PolyDiff (Vector a) (Vector b))
        squishGroupedDiff = V.fromList . go . V.toList
          where
            go = \case
              [] -> []
              [pd] -> [pd]
              (First a : First b : rest) -> go (First (a <> b) : rest)
              (Second a : Second b : rest) -> go (Second (a <> b) : rest)
              (Both a b : Both c d : rest) -> go (Both (a <> c) (b <> d) : rest)
              (d : rest) -> d : go rest
    prop "is the same thing as getVectorDiff, but rolled up and squished (rollup individual)" $ \(ls1, ls2) ->
      let v1 = V.fromList (ls1 :: String)
          v2 = V.fromList (ls2 :: String)
          grouped = getGroupedVectorDiff v1 v2
          individual = getVectorDiff v1 v2
       in squishGroupedDiff grouped `shouldBe` rollupUngroupedDiff individual

  describe "getStringDiff" $ do
    it "can output a large diff quickly enough if there is no diff" $
      let s = replicate 10000 'a'
       in length (getStringDiff s s) `shouldBe` 10000

    it "can output a large diff quickly enough if it's only diff" $
      length (getStringDiff (replicate 10000 'a') "b") `shouldBe` 10001

  describe "getTextDiff" $ do
    it "can output a large diff quickly enough if there is no diff" $
      let s = T.pack $ replicate 10000 'a'
       in length (getTextDiff s s) `shouldBe` 1

    it "can output a large diff quickly enough if it's only diff" $
      length (getTextDiff (T.pack (replicate 10000 'a')) "b") `shouldBe` 15