File: Test.hs

package info (click to toggle)
haskell-diff 1.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 92 kB
  • sloc: haskell: 524; makefile: 2
file content (287 lines) | stat: -rw-r--r-- 11,456 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import Test.Framework (defaultMain, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck
import Data.Algorithm.Diff
import Data.Algorithm.DiffContext
import Data.Algorithm.DiffOutput
import qualified Data.Array as A
import Data.Foldable
import Data.Semigroup (Arg(..))
import Text.PrettyPrint

import System.IO
import System.Exit
import System.IO.Unsafe (unsafePerformIO)
import Debug.Trace (trace)
import System.Environment (getArgs)
import Data.Maybe (mapMaybe, catMaybes)
import System.Process (readProcessWithExitCode)
import System.Directory (getTemporaryDirectory)


main :: IO ()
main = defaultMain [ testGroup "sub props" [
                        slTest "empty in subs" prop_emptyInSubs,
                        slTest "self in subs"  prop_selfInSubs,
                        slTest "count subs"    prop_countSubs,
                        slTest "every sub is a sub" prop_everySubIsSub,
                        slTest2 "sub prop" prop_sub
                     ],
                     testGroup "diff props" [
                        slTest "lcsEmpty" prop_lcsEmpty,
                        slTest "lcsSelf" prop_lcsSelf,
                        slTest2 "lcsBoth" prop_lcsBoth,
                        slTest2 "recover first" prop_recoverFirst,
                        slTest2 "recover second" prop_recoverSecond,
                        slTest2 "lcs" prop_lcs,
                        testProperty "compare random with reference" prop_compare_with_reference
                     ],
                     testGroup "output props" [
                        testProperty "self generates empty" $ forAll shortLists  prop_ppDiffEqual,
                        --testProperty "compare our lists with diff" $ forAll2 shortLists  prop_ppDiffShort,
                        testProperty "compare random with diff" prop_ppDiffR,
                        testProperty "compare with diff, issue #5" $ prop_ppDiffR
                          (DiffInput
                            { diLeft = ["1","2","3","4","","5","6","7"]
                            , diRight = ["1","2","3","q","b","u","l","","XXX6",""]
                            }),
                        testProperty "test parse" prop_parse
                     ],
                     testGroup "context props" [
                        testProperty "test context" $ prop_ppContextDiffUnitTest
                          (DiffInput
                            { diLeft = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]
                            , diRight = ["a", "b", "d", "e", "f", "g", "h", "i", "j"]
                            })
                          "--- a\n+++ b\n@@ -1,5 +1,4 @@\n a\n b\n-c\n d\n e\n@@ -9,3 +8,2 @@\n i\n j\n-k\n",
                        testProperty "compare with empty" $ prop_ppContextDiffUnitTest
                          (DiffInput
                            { diLeft = []
                            , diRight = ["1","2","3"]
                            })
                          "--- a\n+++ b\n@@ --0,0 +1,3 @@\n+1\n+2\n+3\n",
                        testProperty "compare with empty" $ prop_ppContextDiffUnitTest
                          (DiffInput
                            { diLeft = ["1","2","3"]
                            , diRight = []
                            })
                          "--- a\n+++ b\n@@ -1,3 +-0,0 @@\n-1\n-2\n-3\n"
                     ]
                   ]
slTest s t = testProperty s $ forAll shortLists   (t :: [Bool] -> Bool)
slTest2 s t = testProperty s $ forAll2 shortLists (t :: [Bool] -> [Bool] -> Bool)

-- We need some quick and dirty subsequence stuff for the diff tests,
-- so we build that and some tests for it.

-- | Determines whether one list is a subsequence of another.
isSub :: (Eq a) => [a] -> [a] -> Bool
isSub [] _ = True
isSub (_:_) [] = False
isSub (x:xs) (y:ys) | x == y = isSub xs ys
                    | otherwise = isSub (x:xs) ys

-- | Lists the subsequences of a list.
subs :: [a] -> [[a]]
subs [] = [[]]
subs (x:rest) = map (x:) restss ++ restss
  where restss = subs rest

prop_emptyInSubs = elem [] . subs
prop_selfInSubs xs = elem xs (subs xs)
prop_countSubs xs = length (subs xs) == 2^(length xs)
prop_sub xs ys = isSub xs ys == elem xs (subs ys)
prop_everySubIsSub xs = all (flip isSub xs) (subs xs)


-- | Obtains a longest common subsequence of two lists using their
-- diff. Note that there is an @lcs@ function in the
-- 'Data.Algorithm.Diff' module, but it's not exported. It's trivial
-- to reconstruct the LCS though, just by taking the 'B' elements.
diffLCS :: (Eq a) => [a] -> [a] -> [a]
diffLCS xs ys = recoverLCS $ getDiff xs ys

-- | Recovers the (longest) common subsequence from a diff.
recoverLCS :: [Diff a] -> [a]
recoverLCS (Both x _ : xs) = x : recoverLCS xs
recoverLCS (_ : xs) = recoverLCS xs
recoverLCS [] = []

-- | Recovers the first list from a diff.
recoverFirst :: [Diff a] -> [a]
recoverFirst (First x  : xs) = x : recoverFirst xs
recoverFirst (Both x _ : xs) = x : recoverFirst xs
recoverFirst (_ : xs) = recoverFirst xs
recoverFirst [] = []

-- | Recovers the second list from a diff.
recoverSecond :: [Diff a] -> [a]
recoverSecond (Second x  : xs) = x : recoverSecond xs
recoverSecond (Both x _ : xs) = x : recoverSecond xs
recoverSecond (_ : xs) = recoverSecond xs
recoverSecond [] = []

-- | Indicates whether a list is a longest common subsequence of two
-- lists.
isLCS :: (Eq a) => [a] -> [a] -> [a] -> Bool
isLCS ss xs ys = isSub ss ys && isSub ss ys && length ss == lenLCS xs ys

-- | Computes the length of the longest common subsequence of two
-- lists. This is a naive and inefficient recursive implementation
-- that doesn't memoize repeated sub-calls, so don't use it with large
-- lists.
lenLCS :: (Eq a) => [a] -> [a] -> Int
lenLCS [] _ = 0
lenLCS _ [] = 0
lenLCS (x:xs) (y:ys) | x == y = 1 + lenLCS xs ys
                     | otherwise = max (lenLCS (x:xs) ys) (lenLCS xs (y:ys))


prop_recoverFirst xs ys = recoverFirst (getDiff xs ys) == xs
prop_recoverSecond xs ys = recoverSecond (getDiff xs ys) == ys
prop_lcs xs ys = isLCS (diffLCS xs ys) xs ys
prop_lcsEmpty xs = diffLCS xs [] == [] && diffLCS [] xs == []
prop_lcsSelf xs = diffLCS xs xs == xs
prop_lcsBoth xs ys = all areMatch $ getDiff xs ys
    where areMatch (Both x y) = x == y
          areMatch _ = True

-- | Lists of no more than twelve elements.
shortLists :: (Arbitrary a) => Gen [a]
shortLists = sized $ \n -> resize (min n 12) $ listOf arbitrary

-- | 'forAll' where the generator is used twice.
forAll2 :: (Show a, Testable prop) => Gen a -> (a -> a -> prop) -> Property
forAll2 gen f = forAll gen $ \x -> forAll gen (f x)

prop_ppDiffEqual xs=ppDiff (getGroupedDiff xs xs)=="\n"

-- | truly random tests
prop_ppDiffR :: DiffInput -> Property
prop_ppDiffR (DiffInput le ri) =
    let haskDiff=ppDiff $ getGroupedDiff le ri
        utilDiff= unsafePerformIO (runDiff (unlines le) (unlines ri))
    in  cover 90 (haskDiff == utilDiff) "exact match" $
                classify (haskDiff == utilDiff) "exact match"
                        (div ((length (lines haskDiff))*100) (length (lines utilDiff)) < 110) -- less than 10% bigger
    where
      runDiff left right =
          do leftFile <- writeTemp left
             rightFile <- writeTemp right
             (ecode, out, err) <-
                 readProcessWithExitCode "diff" [leftFile, rightFile] ""
             -- putStrLn ("OUT:\n" ++ out)
             -- putStrLn ("ERR:\n" ++ err)
             -- putStrLn ("ECODE:\n" ++ show ecode)
             case ecode of
               ExitSuccess -> return out
               ExitFailure 1 -> return out
               ExitFailure i -> error ("'diff " ++ leftFile ++ " " ++ rightFile ++
                                       "' failed with exit code " ++ show i ++
                                       ": " ++ show err)
      writeTemp s =
          do dir <- getTemporaryDirectory
             (fp, h) <- openTempFile dir "HTF-diff.txt"
             hPutStr h s
             hClose h
             return fp

prop_ppContextDiffUnitTest :: DiffInput -> String -> Property
prop_ppContextDiffUnitTest (DiffInput le ri) expected =
  show diff === expected
  where
    hunks = getContextDiff (Just 2) le ri
    diff = prettyContextDiff (text "a") (text "b") (text . unnumber) hunks

-- | Check pretty printed DiffOperations can be parsed again
prop_parse :: DiffInput -> Bool
prop_parse (DiffInput le ri) =
    let difflrs = diffToLineRanges $ getGroupedDiff le ri
        output = render (prettyDiffs difflrs) ++ "\n"
        parsed = parsePrettyDiffs output
    in difflrs == parsed

data DiffInput = DiffInput { diLeft :: [String], diRight :: [String] }
               deriving (Show)

leftDiffInput = ["1", "2", "3", "4", "", "5", "6", "7"]

instance Arbitrary DiffInput where
    arbitrary =
        do let leftLines = leftDiffInput
           rightLinesLines <- mapM modifyLine (leftLines ++ [""])
           return $ DiffInput leftLines
                              (concat rightLinesLines)
      where
        randomString =
            do c <- elements ['a' .. 'z']
               return [c]
        modifyLine :: String -> Gen [String]
        modifyLine str =
            do prefixLen <- frequency [(20-i, return i) | i <- [0..5]]
               prefix <- mapM (const randomString) [1..prefixLen]
               frequency [ (5, return (prefix ++ [str]))
                         , (3, return (prefix ++ ["XXX" ++ str]))
                         , (2, return prefix)
                         , (2, return [str])]

-- | Reference implementation, very slow.
naiveGetDiffBy :: forall a b. (a -> b -> Bool) -> [a] -> [b] -> [PolyDiff a b]
naiveGetDiffBy eq as bs = reverse $ (\(Arg _ ds) -> ds) $ tbl A.! (length us, length vs)
  where
    us = A.listArray (0, length as - 1) as
    vs = A.listArray (0, length bs - 1) bs

    -- Indices run up to length us/vs *inclusive*
    tbl :: A.Array (Int, Int) (Arg Word [PolyDiff a b])
    tbl = A.listArray ((0, 0), (length us, length vs))
      [ gen ui vi | ui <- [0..length us], vi <- [0..length vs] ]

    gen :: Int -> Int -> Arg Word [PolyDiff a b]
    gen ui vi
      | ui == 0, vi == 0 = Arg 0 []
      | ui == 0
      = left'
      | vi == 0
      = top'
      | otherwise
      = if eq u v
        then min (min left' top') diag'
        else min left' top'
      where
        Arg leftL leftP = tbl A.! (ui, vi - 1)
        Arg diagL diagP = tbl A.! (ui - 1, vi - 1)
        Arg topL topP  = tbl A.! (ui - 1, vi)

        u = us A.! (ui - 1)
        v = vs A.! (vi - 1)

        left' = Arg (leftL + 1) (Second v : leftP)
        top' = Arg (topL + 1) (First u  : topP)
        diag' = Arg diagL (Both u v : diagP)

prop_compare_with_reference :: Positive Word -> [(Int, Int)] -> Property
prop_compare_with_reference (Positive x) ixs' =
  counterexample (show (as, bs, d1, d2)) $
    length (notBoth d1) === length (notBoth d2)
  where
    as = [0 .. max 100 x]
    len = length as
    ixs = filter (uncurry (/=)) $ map (\(i, j) -> (i `mod` len, j `mod` len)) $ take 100 ixs'
    bs = foldl' applySwap as ixs
    d1 = getDiffBy (==) as bs
    d2 = naiveGetDiffBy (==) as bs

    applySwap xs (i, j) = zipWith
      (\k x -> (if k == i then xs !! j else if k == j then xs !! i else x))
      [0..]
      xs

    notBoth = filter $ \case
      Both{} -> False
      _ -> True