File: Test.hs

package info (click to toggle)
haskell-zlib 0.6.3.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,300 kB
  • sloc: ansic: 7,058; haskell: 1,081; makefile: 6
file content (352 lines) | stat: -rw-r--r-- 14,219 bytes parent folder | download | duplicates (2)
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
{-# LANGUAGE CPP #-}
{-# LANGUAGE RankNTypes #-}
module Main where

import Codec.Compression.Zlib.Internal
import qualified Codec.Compression.Zlib     as Zlib
import qualified Codec.Compression.GZip     as GZip
import qualified Codec.Compression.Zlib.Raw as Raw

import Test.Codec.Compression.Zlib.Internal ()
import Test.Codec.Compression.Zlib.Stream ()

import Test.Tasty
import Test.Tasty.QuickCheck
import Utils ()

import Control.Monad
import Control.Monad.ST.Lazy (ST)
import Control.Exception
import qualified Data.ByteString.Char8 as BS.Char8
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString      as BS
#if !MIN_VERSION_bytestring(0,11,0)
import qualified Data.ByteString.Internal as BS
#endif
import System.IO
#if !(MIN_VERSION_base(4,6,0))
import Prelude hiding (catch)
#endif


main :: IO ()
main = defaultMain $
  testGroup "zlib tests" [
    testGroup "property tests" [
      testProperty "decompress . compress = id (standard)"           prop_decompress_after_compress,
      testProperty "decompress . compress = id (Zlib -> GZipOrZLib)" prop_gziporzlib1,
      testProperty "decompress . compress = id (GZip -> GZipOrZlib)" prop_gziporzlib2,
      testProperty "concatenated gzip members"                       prop_gzip_concat,
      testProperty "multiple gzip members, boundaries (all 2-chunks)" prop_multiple_members_boundary2,
      testProperty "multiple gzip members, boundaries (all 3-chunks)" prop_multiple_members_boundary3,
      testProperty "prefixes of valid stream detected as truncated"  prop_truncated,
      testProperty "compress works with BSes with non-zero offset"   prop_compress_nonzero_bs_offset
    ],
    testGroup "unit tests" [
      testProperty "simple gzip case"          test_simple_gzip,
      testProperty "detect bad crc"            test_bad_crc,
      testProperty "detect non-gzip"           test_non_gzip,
      testProperty "detect custom dictionary"  test_custom_dict,
      testProperty "dectect inflate with wrong dict"   test_wrong_dictionary,
      testProperty "dectect inflate with right dict"   test_right_dictionary,
      testProperty "handle trailing data"      test_trailing_data,
      testProperty "multiple gzip members"     test_multiple_members,
      testProperty "check small input chunks"  test_small_chunks,
      testProperty "check empty input"         test_empty,
      testProperty "check exception raised"    test_exception
    ]
  ]


prop_decompress_after_compress :: Format
                               -> CompressParams
                               -> DecompressParams
                               -> Property
prop_decompress_after_compress w cp dp =
   (w /= zlibFormat || decompressWindowBits dp >= compressWindowBits cp) &&
   (decompressWindowBits dp > compressWindowBits cp) &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress w dp . compress w cp) id


prop_gziporzlib1 :: CompressParams
                 -> DecompressParams
                 -> Property
prop_gziporzlib1 cp dp =
   decompressWindowBits dp > compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress gzipOrZlibFormat dp . compress zlibFormat cp) id


prop_gziporzlib2 :: CompressParams
                 -> DecompressParams
                 -> Property
prop_gziporzlib2 cp dp =
   decompressWindowBits dp >= compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   liftM2 (==) (decompress gzipOrZlibFormat dp . compress gzipFormat cp) id

prop_gzip_concat :: CompressParams
                 -> DecompressParams
                 -> BL.ByteString
                 -> Property
prop_gzip_concat cp dp input =
   decompressWindowBits dp >= compressWindowBits cp &&
   decompressBufferSize dp > 0 && compressBufferSize cp > 0 ==>
   let catComp = BL.concat (replicate 5 (compress gzipFormat cp input))
       compCat = compress gzipFormat cp (BL.concat (replicate 5 input))

    in decompress gzipFormat dp { decompressAllMembers = True } catComp
    == decompress gzipFormat dp { decompressAllMembers = True } compCat

prop_multiple_members_boundary2 :: Property
prop_multiple_members_boundary2 =
    forAll shortStrings $ \bs ->
      all (\c -> decomp c == BL.append bs bs)
          (twoChunkSplits (comp bs `BL.append` comp bs))
  where
    comp   = compress gzipFormat defaultCompressParams
    decomp = decompress gzipFormat defaultDecompressParams

    shortStrings = fmap BL.pack $ listOf arbitrary

prop_multiple_members_boundary3 :: Property
prop_multiple_members_boundary3 =
    forAll shortStrings $ \bs ->
      all (\c -> decomp c == BL.append bs bs)
          (threeChunkSplits (comp bs `BL.append` comp bs))
  where
    comp   = compress gzipFormat defaultCompressParams
    decomp = decompress gzipFormat defaultDecompressParams

    shortStrings = sized $ \sz -> resize (sz `div` 10) $
                   fmap BL.pack $ listOf arbitrary

prop_truncated :: Format -> Property
prop_truncated format =
   forAll shortStrings $ \bs ->
     all (truncated decomp)
         (init (BL.inits (comp bs)))
  -- All the initial prefixes of a valid compressed stream should be detected
  -- as truncated.
  where
    comp   = compress format defaultCompressParams
    decomp = decompressST format defaultDecompressParams
    truncated :: (forall s. DecompressStream (ST s)) -> BL.ByteString -> Bool
    truncated = foldDecompressStreamWithInput (\_ r -> r) (\_ -> False)
                  (\err -> case err of TruncatedInput -> True; _ -> False)

    shortStrings = sized $ \sz -> resize (sz `div` 6) arbitrary

prop_compress_nonzero_bs_offset :: BS.ByteString
                                -> Int
                                -> Property
prop_compress_nonzero_bs_offset original to_drop =
   to_drop > 0 &&
   BS.length original > to_drop ==>
   let input = BS.drop to_drop original
#if MIN_VERSION_bytestring(0,11,0)
       dropped = to_drop
#else
       (BS.PS _ptr dropped _length) = input
#endif
       input' = BL.pack $ BS.unpack input -- BL.fromStrict is only available since bytestring-0.10.4.0
       compressed = compress gzipFormat defaultCompressParams input'
       decompressed = decompress gzipFormat defaultDecompressParams compressed
   in  dropped == to_drop && decompressed == input'


test_simple_gzip :: Property
test_simple_gzip = ioProperty $
  withSampleData "hello.gz" $ \hnd ->
    let decomp = decompressIO gzipFormat defaultDecompressParams
     in assertDecompressOk hnd decomp

test_bad_crc :: Property
test_bad_crc = ioProperty $
  withSampleData "bad-crc.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams
    assertDecompressError hnd (assertDataFormatError "incorrect data check") decomp

test_non_gzip :: Property
test_non_gzip = conjoin
  [ ioProperty $ withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams
    assertDecompressError hnd (assertDataFormatError "incorrect header check") decomp

  , ioProperty $ withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams
    assertDecompressError hnd (assertDataFormatError "incorrect header check") decomp

  , ioProperty $ withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO rawFormat defaultDecompressParams
        checkError err = disjoin
          -- The majority of platforms throw this:
          [ assertDataFormatError "invalid code lengths set" err
          -- But on z15+ mainframes zlib employs CPU instruction DFLTCC,
          -- which returns error code with the same meaning.
          -- See http://publibfp.dhe.ibm.com/epubs/pdf/a227832c.pdf, page 26-37
          -- and https://github.com/haskell/zlib/issues/46
          , assertDataFormatError "Operation-Ending-Supplemental Code is 0x27" err
          ]
    assertDecompressError hnd checkError decomp

  , ioProperty $ withSampleData "not-gzip" $ \hnd -> do
    let decomp = decompressIO gzipOrZlibFormat defaultDecompressParams
    assertDecompressError hnd (assertDataFormatError "incorrect header check") decomp
  ]

test_custom_dict :: Property
test_custom_dict = ioProperty $
  withSampleData "custom-dict.zlib" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams
    assertDecompressError hnd (=== DictionaryRequired) decomp

test_wrong_dictionary :: Property
test_wrong_dictionary = ioProperty $
  withSampleData "custom-dict.zlib" $ \hnd -> do
    let decomp = decompressIO zlibFormat defaultDecompressParams {
                                           decompressDictionary = -- wrong dict!
                                             Just (BS.pack [65,66,67])
                                         }

    assertDecompressError hnd (=== DictionaryMismatch) decomp

test_right_dictionary :: Property
test_right_dictionary = ioProperty $
  withSampleData "custom-dict.zlib" $ \hnd -> do
    dict <- readSampleData "custom-dict.zlib-dict"
    let decomp = decompressIO zlibFormat defaultDecompressParams {
                                           decompressDictionary =
                                             Just (toStrict dict)
                                         }
    assertDecompressOk hnd decomp

test_trailing_data :: Property
test_trailing_data = ioProperty $
  withSampleData "two-files.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams {
                   decompressAllMembers = False
                 }
        checkChunks chunks = case chunks of
          [chunk] -> chunk === BS.Char8.pack "Test 1"
          _       -> counterexample "expected single chunk" False
    assertDecompressOkChunks hnd checkChunks decomp


test_multiple_members :: Property
test_multiple_members = ioProperty $
  withSampleData "two-files.gz" $ \hnd -> do
    let decomp = decompressIO gzipFormat defaultDecompressParams {
                   decompressAllMembers = True
                 }
        checkChunks chunks = case chunks of
          [chunk1, chunk2] ->
            chunk1 === BS.Char8.pack "Test 1" .&&. chunk2 === BS.Char8.pack "Test 2"
          _ -> counterexample "expected two chunks" False
    assertDecompressOkChunks hnd checkChunks decomp

test_small_chunks :: Property
test_small_chunks = ioProperty $ do
  uncompressedFile <- readSampleData "not-gzip"
  compressedFile   <- readSampleData "hello.gz"
  return $ conjoin
    [ GZip.compress (smallChunks uncompressedFile) === GZip.compress uncompressedFile
    , Zlib.compress (smallChunks uncompressedFile) === Zlib.compress uncompressedFile
    , Raw.compress  (smallChunks uncompressedFile) === Raw.compress uncompressedFile

    , GZip.decompress (smallChunks (GZip.compress uncompressedFile)) === uncompressedFile
    , Zlib.decompress (smallChunks (Zlib.compress uncompressedFile)) === uncompressedFile
    , Raw.decompress  (smallChunks (Raw.compress  uncompressedFile)) === uncompressedFile

    , (GZip.decompress . smallChunks) compressedFile === GZip.decompress compressedFile
    ]

test_empty :: Property
test_empty = ioProperty $ do
  -- Regression test to make sure we only ask for input once in the case of
  -- initially empty input. We previously asked for input twice before
  -- returning the error.
  let decomp = decompressIO zlibFormat defaultDecompressParams
  case decomp of
    DecompressInputRequired next -> do
      decomp' <- next BS.empty
      case decomp' of
        DecompressStreamError TruncatedInput -> return $ property True
        _ -> return $ counterexample "expected truncated error" False

    _ -> return $ counterexample "expected input" False

test_exception :: Property
test_exception = ioProperty $ do
  compressedFile <- readSampleData "bad-crc.gz"
  len <- try (evaluate (BL.length (GZip.decompress compressedFile)))
  return $ case len of
    Left err -> assertDataFormatError "incorrect data check" err
    Right{} -> counterexample "expected exception" False

toStrict :: BL.ByteString -> BS.ByteString
#if MIN_VERSION_bytestring(0,10,0)
toStrict = BL.toStrict
#else
toStrict = BS.concat . BL.toChunks
#endif

-----------------------
-- Chunk boundary utils

smallChunks :: BL.ByteString -> BL.ByteString
smallChunks = BL.fromChunks . map (\c -> BS.pack [c]) . BL.unpack

twoChunkSplits :: BL.ByteString -> [BL.ByteString]
twoChunkSplits bs = zipWith (\a b -> BL.fromChunks [a,b]) (BS.inits sbs) (BS.tails sbs)
  where
    sbs = toStrict bs

threeChunkSplits :: BL.ByteString -> [BL.ByteString]
threeChunkSplits bs =
    [ BL.fromChunks [a,b,c]
    | (a,x) <- zip (BS.inits sbs) (BS.tails sbs)
    , (b,c) <- zip (BS.inits x) (BS.tails x) ]
  where
    sbs = toStrict bs

--------------
-- HUnit Utils

readSampleData :: FilePath -> IO BL.ByteString
readSampleData file = BL.readFile ("test/data/" ++ file)

withSampleData :: FilePath -> (Handle -> IO a) -> IO a
withSampleData file = withFile ("test/data/" ++ file) ReadMode

expected :: String -> String -> Property
expected e g = counterexample ("expected: " ++ e ++ "\nbut got: " ++ g) False

assertDecompressOk :: Handle -> DecompressStream IO -> IO Property
assertDecompressOk hnd =
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\_ r -> r)
      (\_ -> return $ property True)
      (\err -> return $ expected "decompress ok" (show err))

assertDecompressOkChunks :: Handle -> ([BS.ByteString] -> Property) -> DecompressStream IO -> IO Property
assertDecompressOkChunks hnd callback = fmap (either id callback) .
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\chunk -> liftM (liftM (chunk:)))
      (\_ -> return $ Right [])
      (\err -> return $ Left $ expected "decompress ok" (show err))

assertDecompressError :: Handle -> (DecompressError -> Property) -> DecompressStream IO -> IO Property
assertDecompressError hnd callback =
    foldDecompressStream
      (BS.hGet hnd 4000 >>=)
      (\_ r -> r)
      (\_ -> return $ expected "StreamError" "StreamEnd")
      (return . callback)

assertDataFormatError :: String -> DecompressError -> Property
assertDataFormatError expect (DataFormatError actual) = expect === actual
assertDataFormatError _ _ = counterexample "expected DataError" False