From d0fd30dedae37750a7c90db7b67ab353f1b5f0f7 Mon Sep 17 00:00:00 2001 From: Saatvik Shah Date: Sun, 4 Jul 2021 17:19:25 +0200 Subject: [PATCH] Add property based testing This patch uses hedgehogs property testing framework to validate timestamp extraction and log merging logic. Timestamp extraction is checked by ensuring that log lines synthetically created from a specific timestamp format and timestamp, on applying said function yield back the originally supplies ts. Log file merging is similarly checked by generating synthetic log files of different datetime formats and checking that the timestamps in the output file are in ascending order. --- README.md | 4 +-- loggy.cabal | 3 ++ test/Test/LoggyCore.hs | 63 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b37a2bd..227a59d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ Available options: - [x] With different formats - [x] Add unit tests(HUnit) - [x] Remove "error" on parse issues, replace with "Maybe" - - [ ] Add -Weverything flag + - [x] Adding quickcheck testing(how to model strftime/log file inputs) + - [x] Add -Werror flag - [ ] `fileembed` with actual data + setup of regression tests - [ ] Add Github CI (optional) - [ ] Checkpoint 3: Performance @@ -45,7 +46,6 @@ Available options: - [ ] Use heap for low memory consumption/Use streaming library - [ ] Benchmarks(optional) - [ ] Checkpoint 4: Stretch goals - - [ ] Adding quickcheck testing(how to model strftime/log file inputs) - [ ] Pull in remote files - [ ] Terminal coloring - [ ] Filename prefixing in output diff --git a/loggy.cabal b/loggy.cabal index 08bd62e..6a4a86a 100644 --- a/loggy.cabal +++ b/loggy.cabal @@ -19,6 +19,7 @@ common common-options -Wincomplete-uni-patterns -Wincomplete-record-updates -Wredundant-constraints + -Werror default-language: Haskell2010 library @@ -42,6 +43,8 @@ test-suite loggy-test other-modules: Test.LoggyCore build-depends: loggy , hspec ^>= 2.7.4 + , hspec-hedgehog + , hedgehog ghc-options: -threaded -rtsopts -with-rtsopts=-N diff --git a/test/Test/LoggyCore.hs b/test/Test/LoggyCore.hs index 0589522..3eb799d 100644 --- a/test/Test/LoggyCore.hs +++ b/test/Test/LoggyCore.hs @@ -1,7 +1,14 @@ module Test.LoggyCore (loggycore) where import Test.Hspec (shouldBe, Spec, describe, it) +import Test.Hspec.Hedgehog (hedgehog, (===), forAll, Gen) import Data.Time +import Data.Time.Clock.System +import qualified Hedgehog.Range as Range +import qualified Hedgehog.Gen as Gen +import Data.List (sort) +import Control.Monad (msum) +import Data.Maybe (catMaybes) import LoggyCore @@ -30,14 +37,21 @@ loggycore = describe "LoggyCoreTest" $ do mergeLogLinesTest extractTimestampTest :: Spec -extractTimestampTest = describe "LoggyCoreTest: extractTimestamp" $ do +extractTimestampTest = describe "LoggyCoreTest: extractTimestampTest" $ do it "simpleTimestamp" $ extractTsHelper inputTime `shouldBe` inputDiffTime it "simpleTimestampWithSpaces" $ extractTsHelper (inputTimeWith " ") `shouldBe` inputDiffTime it "simpleTimestampWithExtraChars" $ extractTsHelper (inputTimeWith " random extra chars") `shouldBe` inputDiffTime it "simpleTimestampWithRepeatedTimestamp" $ extractTsHelper (inputTimeWith $ " " ++ inputTime) `shouldBe` inputDiffTime it "invalidLogLine" $ extractTsHelper "invalid input" `shouldBe` Nothing - where - extractTsHelper tsLog = utctDayTime <$> extractTimestamp dateFormat tsLog + it "extractTimestampPropertyTest" $ hedgehog $ do + logText <- forAll $ Gen.string (Range.linear 0 1000) Gen.ascii + unixTimeSeconds <- forAll $ Gen.int64 (Range.linear 0 1000) + let utcTime = systemToUTCTime $ MkSystemTime unixTimeSeconds 0 + let utcTimeStr = formatTime defaultTimeLocale dateFormat utcTime + let logLine = utcTimeStr ++ " " ++ logText + Just utcTime === extractTimestamp dateFormat logLine + where + extractTsHelper tsLog = utctDayTime <$> extractTimestamp dateFormat tsLog mergeLogLinesTest :: Spec mergeLogLinesTest = describe "LoggyCoreTest: mergeLogLines" $ do @@ -64,4 +78,45 @@ mergeLogLinesTest = describe "LoggyCoreTest: mergeLogLines" $ do it "invalidLogLine" $ testMergeLogs dateFormat [[" invalid log line"]] `shouldBe` MkMergeResult [] InvalidLogLine it "invalidLogLines" $ testMergeLogs dateFormat [[" invalid log line", inputTimeWith " from file 1"]] - `shouldBe` MkMergeResult [inputTimeWith " from file 1"] InvalidLogLine \ No newline at end of file + `shouldBe` MkMergeResult [inputTimeWith " from file 1"] InvalidLogLine + it "mergeLogLinesPropertyTest" $ hedgehog $ do + numFiles <- forAll $ Gen.int (Range.linear 1 10) + fileDateFormats <- forAll $ genDateFormats numFiles + logFiles <- forAll $ mapM genFile fileDateFormats + let MkMergeResult mergedLines mergeStatus = mergeLogLines (zip fileDateFormats logFiles) + length mergedLines === sum (map length logFiles) + mergeStatus === NoWarning + let maybeTsPerLogLine = map (dateFormats `extractTimestamp'`) mergedLines + let tsPerLogLine = catMaybes maybeTsPerLogLine + length tsPerLogLine === length mergedLines + isSorted tsPerLogLine === True + where + dateFormats :: [DateFormat] + dateFormats = ["%H:%M:%S", "%H-%M-%S", "%H/%M/%S", "%H.%M.%S"] + genDateFormats ::Int -> Gen [DateFormat] + genDateFormats numFiles = Gen.list (Range.singleton numFiles) (Gen.element dateFormats) + genFile :: DateFormat -> Gen LogFileLines + genFile dFormat = do + numLogLines <- Gen.int (Range.linear 0 100) + logTxtLines <- Gen.list (Range.singleton numLogLines) (Gen.string (Range.linear 0 100) Gen.ascii) + logTsLst <- genSortedDates numLogLines + let logTsFormattedLst = map (formatTime defaultTimeLocale dFormat) logTsLst + let logLines = zipWith (\ts logLine -> ts ++ " " ++ logLine) logTsFormattedLst logTxtLines + return logLines + genSortedDates :: Int -> Gen [UTCTime] + genSortedDates numLines = do + unixTsLst <- Gen.list (Range.singleton numLines) (Gen.int64 (Range.linear 0 1000)) + let sortedUnixTsLst = sort unixTsLst + let utcTimeLst = map (\ts -> systemToUTCTime $ MkSystemTime ts 0) sortedUnixTsLst + return utcTimeLst + extractTimestamp' :: [DateFormat] -> LogLine -> Maybe UTCTime + -- Brute force try all formats till one matches + extractTimestamp' dFs logLine = tsForLogLine + where + tsForLogLine = msum tsPerDateFormat + tsPerDateFormat = map (`extractTimestamp` logLine) dFs + isSorted :: (Ord a) => [a] -> Bool + isSorted [] = True + isSorted [_] = True + isSorted (x:y:xs) = x <= y && isSorted (y:xs) +