Skip to content

Commit 07fdff6

Browse files
committed
refactor(issue558): extract flipRandomBitInRandomFile from prop_flipSnapshotBit
1 parent e6dd4e6 commit 07fdff6

File tree

3 files changed

+84
-55
lines changed

3 files changed

+84
-55
lines changed

test/Test/Database/LSMTree/Internal/Snapshot/FS.hs

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Control.Monad.IOSim (runSimOrThrow)
99
import Control.Tracer
1010
import Data.Bifunctor (Bifunctor (..))
1111
import Data.Maybe (fromJust)
12-
import qualified Data.Set as Set
1312
import qualified Data.Vector as V
1413
import Data.Word
1514
import Database.LSMTree.Extras (showPowersOf10)
@@ -28,10 +27,10 @@ import System.FS.Sim.Error hiding (genErrors)
2827
import qualified System.FS.Sim.MockFS as MockFS
2928
import Test.Database.LSMTree.Internal.Snapshot.Codec ()
3029
import Test.QuickCheck
31-
import Test.QuickCheck.Gen (genDouble)
3230
import Test.Tasty
3331
import Test.Tasty.QuickCheck
3432
import Test.Util.FS
33+
import Test.Util.QC (Choice)
3534

3635
tests :: TestTree
3736
tests = testGroup "Test.Database.LSMTree.Internal.Snapshot.FS" [
@@ -162,14 +161,6 @@ instance Arbitrary TestErrors where
162161
Snapshot corruption
163162
-------------------------------------------------------------------------------}
164163

165-
-- | A 'Double' in the @[0, 1)@ range.
166-
newtype Double_0_1 = Double_0_1 Double
167-
deriving stock (Show, Eq)
168-
169-
instance Arbitrary Double_0_1 where
170-
arbitrary = Double_0_1 <$> genDouble
171-
shrink (Double_0_1 x) = [Double_0_1 x' | x' <- shrink x, 0 <= x', x' < 1]
172-
173164
-- TODO: an alternative to generating doubles a priori is to run the monadic
174165
-- code in @PropertyM (IOSim s)@, and then we can do quantification inside the
175166
-- monadic property using @pick@. This complicates matters, however, because
@@ -179,14 +170,9 @@ instance Arbitrary Double_0_1 where
179170
prop_flipSnapshotBit ::
180171
Positive (Small Int)
181172
-> V.Vector (Word64, Entry Word64 Word64)
182-
-> Double_0_1 -- ^ Used to pick which file to corrupt
183-
-> Double_0_1 -- ^ Used to pick which bit to flip in the file we picked
173+
-> Choice -- ^ Used to pick which file/bit to corrupt.
184174
-> Property
185-
prop_flipSnapshotBit
186-
(Positive (Small bufferSize))
187-
es
188-
(Double_0_1 pickFile)
189-
(Double_0_1 pickBit) =
175+
prop_flipSnapshotBit (Positive (Small bufferSize)) es pickFileBit =
190176
runSimOrThrow $
191177
withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _fsVar ->
192178
withSession nullTracer hfs hbio root $ \s ->
@@ -195,42 +181,28 @@ prop_flipSnapshotBit
195181
updates resolve es' t
196182
createSnap t
197183

198-
-- Pick a random file from the named snapshot directory
199-
files <- listDirectoryRecursiveFiles hfs (getNamedSnapshotDir namedSnapDir)
200-
let i = round (fromIntegral (Set.size files - 1) * pickFile)
201-
let file = Set.elemAt i files
202-
let path = getNamedSnapshotDir namedSnapDir </> file
203-
-- Pick a random bit from the file that we want to corrupt
204-
n <- withFile hfs path ReadMode $ hGetSize hfs
205-
let j = round (fromIntegral (n * 8 - 1) * pickBit)
206-
207-
-- Some info for the test output
208-
let
209-
tabCorruptedFile = tabulate "Corrupted file" [show path]
210-
counterCorruptedFile = counterexample ("Corrupted file: " ++ show path)
211-
tabFlippedBit = tabulate "Flipped bit" [showPowersOf10 j]
212-
counterFlippedBit = counterexample ("Flipped bit: " ++ show j)
213-
214-
-- TODO: check forgotten refs
215-
if n <= 0 then -- file is empty
216-
pure $ tabulate "Result" ["No corruption applied"] True
217-
else do -- file is non-empty
218-
219-
-- Flip a bit and try to open the snapshot
220-
flipFileBit hfs path j
221-
t' <- try @_ @SomeException $ bracket (openSnap s) close $ \_ -> pure ()
222-
223-
pure $
224-
tabCorruptedFile $ counterCorruptedFile $ tabFlippedBit $ counterFlippedBit $
225-
case t' of
226-
-- If we find an error, we detected corruption. Success!
227-
Left e ->
228-
tabulate
229-
"Result"
230-
["Corruption detected: " <> getConstructorName e]
231-
True
232-
-- The corruption was not detected. Failure!
233-
Right _ -> property False
184+
-- Corrupt the snapshot
185+
flipRandomBitInRandomFile hfs pickFileBit (getNamedSnapshotDir namedSnapDir) >>= \case
186+
Nothing -> pure $ property False
187+
Just (path, j) -> do
188+
-- Some info for the test output
189+
let tabCorruptedFile = tabulate "Corrupted file" [show path]
190+
counterCorruptedFile = counterexample ("Corrupted file: " ++ show path)
191+
tabFlippedBit = tabulate "Flipped bit" [showPowersOf10 j]
192+
counterFlippedBit = counterexample ("Flipped bit: " ++ show j)
193+
194+
t' <- try @_ @SomeException $ bracket (openSnap s) close $ \_ -> pure ()
195+
pure $
196+
tabCorruptedFile $ counterCorruptedFile $ tabFlippedBit $ counterFlippedBit $
197+
case t' of
198+
-- If we find an error, we detected corruption. Success!
199+
Left e ->
200+
tabulate
201+
"Result"
202+
["Corruption detected: " <> getConstructorName e]
203+
True
204+
-- The corruption was not detected. Failure!
205+
Right _ -> property False
234206
where
235207
root = FS.mkFsPath []
236208
namedSnapDir = namedSnapshotDir (SessionRoot root) snapName

test/Test/Util/FS.hs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module Test.Util.FS (
3030
, listDirectoryRecursive
3131
, listDirectoryRecursiveFiles
3232
-- * Corruption
33+
, flipRandomBitInRandomFile
3334
, flipFileBit
3435
, hFlipBit
3536
-- * Errors
@@ -56,14 +57,16 @@ import Control.Monad.IOSim (runSimOrThrow)
5657
import Control.Monad.Primitive (PrimMonad)
5758
import Data.Bit (MVector (..), flipBit)
5859
import Data.Char (isAscii, isDigit, isLetter)
59-
import Data.Foldable (foldlM)
60+
import Data.Foldable (Foldable (..), foldlM)
6061
import Data.List.NonEmpty (NonEmpty (..))
6162
import qualified Data.List.NonEmpty as NE
6263
import Data.Primitive.ByteArray (newPinnedByteArray, setByteArray)
6364
import Data.Primitive.Types (sizeOf)
6465
import Data.Set (Set)
6566
import qualified Data.Set as Set
6667
import qualified Data.Text as T
68+
import Data.Traversable (for)
69+
import Data.Word (Word64)
6770
import GHC.Stack
6871
import System.FS.API as FS
6972
import System.FS.BlockIO.API
@@ -79,6 +82,7 @@ import System.FS.Sim.Stream (InternalInfo (..), Stream (..))
7982
import System.IO.Temp
8083
import Test.QuickCheck
8184
import Test.QuickCheck.Instances ()
85+
import Test.Util.QC (Choice, getChoice)
8286
import Text.Printf
8387

8488
{-------------------------------------------------------------------------------
@@ -347,6 +351,43 @@ listDirectoryFiles hfs = go Set.empty
347351
Corruption
348352
-------------------------------------------------------------------------------}
349353

354+
-- | Flip a random bit in a random file in a given directory.
355+
flipRandomBitInRandomFile ::
356+
(PrimMonad m, MonadThrow m)
357+
=> HasFS m h
358+
-> Choice
359+
-> FsPath
360+
-> m (Maybe (FsPath, Int))
361+
flipRandomBitInRandomFile hfs bitChoice dir = do
362+
-- List all files and their sizes
363+
files <- fmap (dir </>) . toList <$> listDirectoryRecursiveFiles hfs dir
364+
-- Handle the situation where there are no files
365+
if null files then pure Nothing else do
366+
filesAndFileSizeBits <-
367+
for files $ \file -> do
368+
fileSizeBytes <- withFile hfs file ReadMode (hGetSize hfs)
369+
pure (file, fileSizeBytes * 8)
370+
let totalFileSizeBits = sum (snd <$> filesAndFileSizeBits)
371+
-- Handle the situation where there are no non-empty files
372+
if totalFileSizeBits == 0 then pure Nothing else do
373+
assert (totalFileSizeBits > 0) $ pure ()
374+
-- Interpret `index` to point to a bit between `0` and `totalFileSize - 1`
375+
let bitIndex = getChoice bitChoice (0, totalFileSizeBits - 1)
376+
-- Flip the bit
377+
Just <$> flipFileBitAt hfs bitIndex filesAndFileSizeBits
378+
379+
-- | Internal helper: flip a single bit in a given list of files and sizes.
380+
flipFileBitAt :: (MonadThrow m, PrimMonad m) => HasFS m h -> Word64 -> [(FsPath, Word64)] -> m (FsPath, Int)
381+
flipFileBitAt _hfs bitIndex [] =
382+
error $ printf "flipFileBitAt: bit index out of bounds (%d)" bitIndex
383+
flipFileBitAt hfs bitIndex ((file, fileSize) : filesAndSizes)
384+
| bitIndex < fileSize = do
385+
let bitIndexAsInt = fromIntegral bitIndex
386+
flipFileBit hfs file bitIndexAsInt
387+
pure (file, bitIndexAsInt)
388+
| otherwise = do
389+
flipFileBitAt hfs (bitIndex - fileSize) filesAndSizes
390+
350391
-- | Flip a single bit in the given file.
351392
flipFileBit :: (MonadThrow m, PrimMonad m) => HasFS m h -> FsPath -> Int -> m ()
352393
flipFileBit hfs p bitOffset =

test/Test/Util/QC.hs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ module Test.Util.QC (
22
testClassLaws
33
, testClassLawsWith
44
, Proxy (..)
5+
, Choice
6+
, getChoice
57
) where
68

79
import Data.Proxy (Proxy (..))
10+
import Data.Word (Word64)
811
import Test.QuickCheck.Classes (Laws (..))
912
import Test.Tasty (TestTree, testGroup)
10-
import Test.Tasty.QuickCheck (Property, testProperty)
13+
import Test.Tasty.QuickCheck (Property, Arbitrary (..), testProperty)
1114

1215
testClassLaws :: String -> Laws -> TestTree
1316
testClassLaws typename laws = testClassLawsWith typename laws testProperty
@@ -22,3 +25,16 @@ testClassLawsWith typename Laws {lawsTypeclass, lawsProperties} k =
2225
| (name, prop) <- lawsProperties ]
2326

2427

28+
-- | A 'Choice' of a uniform random number in a range where shrinking picks smaller numbers.
29+
newtype Choice = Choice Word64
30+
deriving stock (Show, Eq)
31+
deriving newtype (Arbitrary)
32+
33+
getChoice :: (Integral a) => Choice -> (a, a) -> a
34+
getChoice (Choice n) (l, u) = fromIntegral (((ni * (ui - li)) `div` mi) + li)
35+
where
36+
ni = toInteger n
37+
li = toInteger l
38+
ui = toInteger u
39+
mi = toInteger (maxBound :: Word64)
40+

0 commit comments

Comments
 (0)