tilia-0.1.0.0: tests/Tilia/DiffSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Finding what changed between two texts, and showing it.
module Tilia.DiffSpec (spec) where
import Data.Array (listArray, (!))
import Data.Text (Text)
import Data.Text qualified as T
import Test.Hspec
import Test.QuickCheck
import Tilia.Diff
import Tilia.Palette (Palette (..))
spec :: Spec
spec = do
describe "an edit script" $ do
it "takes the first sequence to the second" $
property $ \(Lines xs) (Lines ys) ->
followed (editScript xs ys) xs ys `shouldBe` True
it "keeps as many lines as the two have in common" $
property $ \(Lines xs) (Lines ys) ->
length (filter (== Context) (editScript xs ys)) `shouldBe` common xs ys
it "takes out the lines of every change before it puts any in" $
property $ \(Lines xs) (Lines ys) ->
editScript xs ys `shouldNotSatisfy` addsBeforeRemoving
it "takes a line out first where the search splits a change in two" $
editScript ["a", "b", "a"] ["d", "c", "b", "b"]
`shouldBe` [Removed, Added, Added, Added, Context, Removed]
it "keeps everything when nothing changed" $
editScript ["a", "b", "c"] ["a", "b", "c"] `shouldBe` [Context, Context, Context]
it "takes everything out and puts everything in when nothing is shared" $
editScript ["a", "b"] ["c"] `shouldBe` [Removed, Removed, Added]
describe "a unified diff" $ do
it "shows a changed line with the lines around it" $
diffInFull Plain "A.hs" "a\nb\nc\nd\ne\nf\ng\nh\n" "a\nb\nc\nd\nE\nf\ng\nh\n"
`shouldBe` T.intercalate
"\n"
[ "diff --git a/A.hs b/A.hs",
"--- a/A.hs",
"+++ b/A.hs",
"@@ -2,7 +2,7 @@",
" b",
" c",
" d",
"-e",
"+E",
" f",
" g",
" h"
]
it "keeps changes far apart in hunks of their own" $
length (filter (T.isPrefixOf "@@") (T.lines (diff Plain ("x", "y") far (T.replace "k" "K" (T.replace "b" "B" far)))))
`shouldBe` 2
where
far = T.unlines ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]
-- | A short run of lines out of a handful, so that two of them have much
-- in common.
newtype Lines = Lines [Text]
deriving (Show)
instance Arbitrary Lines where
arbitrary = Lines <$> resize 30 (listOf (elements ["a", "b", "c", "d"]))
shrink (Lines ls) = Lines <$> shrinkList (const []) ls
-- | Whether following the script consumes both sequences exactly, keeping
-- only lines they share.
followed :: [Mark] -> [Text] -> [Text] -> Bool
followed [] [] [] = True
followed (Context : ms) (x : xs) (y : ys) = x == y && followed ms xs ys
followed (Removed : ms) (_ : xs) ys = followed ms xs ys
followed (Added : ms) xs (_ : ys) = followed ms xs ys
followed _ _ _ = False
-- | Whether a line is put in somewhere right before one is taken out.
addsBeforeRemoving :: [Mark] -> Bool
addsBeforeRemoving marks = (Added, Removed) `elem` zip marks (drop 1 marks)
-- | The length of the longest common subsequence.
common :: [Text] -> [Text] -> Int
common xs ys = table ! (0, 0)
where
n = length xs
m = length ys
xa = listArray (0, n - 1) xs
ya = listArray (0, m - 1) ys
table =
listArray
((0, 0), (n, m))
[cell i j | i <- [0 .. n], j <- [0 .. m]]
cell i j
| i == n || j == m = 0
| xa ! i == ya ! j = 1 + table ! (i + 1, j + 1)
| otherwise = max (table ! (i + 1, j)) (table ! (i, j + 1))