packages feed

dataframe-fastcsv-1.4.0.0: tests/Operations/Projection.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- | 'readColumns' semantics for the SIMD reader, pinned against the
pure reader. Every case asserts both readers agree: the two disagreed
over 'ProvideNames' for a long time, and a projection only one of them
honours is worse than no projection at all.
-}
module Operations.Projection (tests) where

import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

import Control.Exception (SomeException, evaluate, try)
import Data.List (isInfixOf)
import qualified DataFrame.IO.CSV as CSV
import qualified DataFrame.IO.CSV.Fast as Fast
import DataFrame.Internal.DataFrame (
    DataFrame,
    columnIndices,
    dataframeDimensions,
    forceDataFrame,
 )
import System.Directory (removeFile)
import Test.HUnit

withCsv :: String -> T.Text -> (FilePath -> IO a) -> IO a
withCsv label contents k = do
    let path = "./tests/data/unstable_csv/projection_" <> label <> ".csv"
    TIO.writeFile path contents
    r <- k path
    removeFile path
    pure r

{- | Read @contents@ through both readers with @opts@, check they agree,
then hand the frame to the assertion.
-}
bothReaders ::
    String -> CSV.ReadOptions -> T.Text -> (DataFrame -> Assertion) -> Test
bothReaders label opts contents check = TestLabel label $
    TestCase $
        withCsv label contents $ \path -> do
            pureDf <- forceDataFrame <$> CSV.readSeparated opts path
            fastDf <- forceDataFrame <$> Fast.fastReadCsvWithOpts opts path
            assertEqual (label <> ": readers agree") pureDf fastDf
            check pureDf

-- | Both readers must reject a name the file does not have.
bothReadersFail :: String -> CSV.ReadOptions -> T.Text -> String -> Test
bothReadersFail label opts contents needle = TestLabel label $
    TestCase $
        withCsv label contents $ \path -> do
            let expectFail which act = do
                    r <- try (act >>= evaluate . forceDataFrame)
                    case r of
                        Left (e :: SomeException) ->
                            assertBool
                                (label <> ": " <> which <> " should mention " <> show needle)
                                (needle `isInfixOf` show e)
                        Right _ ->
                            assertFailure (label <> ": " <> which <> " should have failed")
            expectFail "pure reader" (CSV.readSeparated opts path)
            expectFail "fast reader" (Fast.fastReadCsvWithOpts opts path)

sample :: T.Text
sample = "id,name,surname,dob\n1,Ada,Lovelace,1815\n2,Alan,Turing,1912\n"

select :: [T.Text] -> CSV.ReadOptions
select cs = CSV.defaultReadOptions{CSV.readColumns = Just cs}

tests :: [Test]
tests =
    [ bothReaders "prefix_subset" (select ["id", "name"]) sample $
        assertEqual "kept" (2, 2) . dataframeDimensions
    , bothReaders "non_prefix_subset" (select ["id", "dob"]) sample $ \df -> do
        assertEqual "kept" (2, 2) (dataframeDimensions df)
        assertEqual "names" [("dob", 1), ("id", 0)] (M.toList (columnIndices df))
    , bothReaders "requested_order_wins" (select ["dob", "id"]) sample $
        assertEqual "order" [("dob", 0), ("id", 1)] . M.toList . columnIndices
    , bothReaders "single_column" (select ["surname"]) sample $
        assertEqual "kept" (2, 1) . dataframeDimensions
    , bothReaders "no_selection_keeps_all" CSV.defaultReadOptions sample $
        assertEqual "kept" (2, 4) . dataframeDimensions
    , bothReadersFail
        "missing_column"
        (select ["id", "nope"])
        sample
        "Column not found"
    ]