packages feed

dawgdic-1.0.0: test/Data/DAWG/RankedCompleterSpec.hs

{-# LANGUAGE BlockArguments #-}
module Data.DAWG.RankedCompleterSpec where

import Data.DAWG.Internal.BaseType

import Data.DAWG.RankedCompleter (keyToString, start, next)

import qualified Data.DAWG.RankedCompleter as C
import qualified Data.DAWG.RankedGuide as G
import qualified Data.DAWG.DAWG as DAWG
import qualified Data.DAWG.Dictionary as Dict

import Control.Monad (forM_, when)
import Data.Char (ord)
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe, isJust)
import Test.Hspec
import Test.QuickCheck (chooseEnum, chooseInt, forAll, property, shuffle, vectorOf, withMaxSuccess)
import Text.Read (readMaybe)

import qualified Data.Binary as Binary
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Set as Set
import qualified Data.Vector as Vector

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "RankedCompleter" do
    it "Builds a completer from a lexicon" do
      db <- DAWG.new
      contents <- readFile "data/lexicon"
      forM_ (lines contents) \l -> do
        let (w, strVal) = break (== '\t') l
            mVal = readMaybe . drop 1 $ strVal
            value = fromMaybe maxBound mVal :: ValueType
        DAWG.insert (Vector.fromList w) (Just value) db
      dawg <- DAWG.freeze db
      -- it must not fail since the DictionarySpec covers it
      dict <- Dict.build' dawg
      mGuide <- G.build dawg dict
      isJust mGuide `shouldBe` True
      forM_ mGuide \guide -> G.write "ranked-guide.dic" guide
    
    it "Completes keys from a lexicon" do
      let completerResultFile = "ranked-completer-result"
          goNext w c = case next c of
            Nothing -> pure ()
            Just !nc -> do
              appendFile completerResultFile $ concat
                [ " ", w, keyToString nc, " = ", show $ C.value nc ]
              goNext w nc

          go :: BaseType -> String -> String -> G.RankedGuide -> IO ()
          go _dictIx _fullWord [] _guide = pure ()
          go dictIx fw w g = do
            let d = G.rankedGuideDictionary g
            case Dict.followPrefixLength w (fromIntegral $ length w) dictIx d of
              Nothing -> pure ()
              Just nextDictIx -> do
                let !nc = start nextDictIx "" g
                goNext fw nc
                go nextDictIx fw w g

      writeFile completerResultFile ""
      guide <- G.read "ranked-guide.dic"
      
      contents <- readFile "data/query"
      forM_ (lines contents) \l -> do
        appendFile completerResultFile $ concat [ l, ":" ]
        go Dict.root l l guide
        appendFile completerResultFile "\n"

      True `shouldBe` True

    it "Checks results" do
      let completerResultFile = "ranked-completer-result"
      testResult <- readFile completerResultFile
      testExpectation <- readFile "data/ranked-completer-answer"
      testResult `shouldBe` testExpectation


    it "Completes randomly linked keys and values" do
      let n = 65536
          keyLength = 6

          whenJust x action = case x of
            Nothing -> pure ()
            Just val -> action val

          goNext c valueRef = do
            mPrevValue <- readIORef valueRef
            case next c of
              Nothing -> pure ()
              Just !nc -> do
                let v = C.value nc
                -- putStrLn $ concat [keyToString nc, ": ", show v]

                -- 2. Ranked value should be equal or less than the previous one
                -- (i.e. in descending order)
                whenJust mPrevValue \prevValue -> do
                  v `shouldSatisfy` (<= prevValue)

                writeIORef valueRef (Just v)

          wordGen = vectorOf keyLength $ chooseEnum ('A', 'Z')
          shuffledListN = do
            vals <- vectorOf n $ chooseInt (0, 100)
            keys <- vectorOf n wordGen
            let keys' = Set.toList . Set.fromList $ keys
            vals' <- shuffle vals
            pure $ zip keys' (fromIntegral <$> vals')
          prop_rankedCompleterCompletes = forAll shuffledListN \lexicon -> do
            db <- DAWG.new
            forM_ lexicon \(k, v) -> do
              -- putStrLn $ concat [k, "\t", show v]
              DAWG.insert (Vector.fromList k) (Just v) db
            dawg <- DAWG.freeze db
            dict <- Dict.build' dawg

            -- 1. Dictionary should contain value associated with a key
            forM_ lexicon \(k, v) -> do
              Dict.lookup k dict `shouldBe` Just v

            guide <- G.build' dawg dict
            globalRef <- newIORef (-1)
            forM_ ['A' .. 'Z'] \c -> do
              let firstLabel = fromIntegral $ ord c
              case Dict.followChar firstLabel Dict.root dict of
                Nothing -> pure ()
                Just dictIx -> do
                  prevValueRef <- newIORef Nothing
                  let !nc = start dictIx [c] guide
                  goNext nc prevValueRef
                  mLatestVal <- readIORef prevValueRef
                  globalVal <- readIORef globalRef
                  whenJust mLatestVal \latestVal -> do
                    when (globalVal < latestVal) do
                      writeIORef globalRef latestVal

            -- 3. maximum value should always be picked up during full dictionary traversal
            let maxVal = maximum (snd <$> lexicon)
            globalVal <- readIORef globalRef
            maxVal `shouldBe` globalVal
            
      withMaxSuccess 10 $ property prop_rankedCompleterCompletes

    it "Ensures binary compatibility with C++" do
      cppContents <- BSL.readFile "data/lexicon.ranked.dic"
      let cppGuide = Binary.decode cppContents
      guide <- G.read "ranked-guide.dic"
      cppGuide `shouldBe` guide
      Binary.encode guide `shouldBe` cppContents