packages feed

yxdb-utils-0.1.0.1: Tests/Database/Alteryx.hs

{-# LANGUAGE OverloadedStrings #-}

module Tests.Database.Alteryx (yxdbTests) where

import qualified Database.Alteryx.CLI.Csv2Yxdb as C2Y
import qualified Database.Alteryx.CLI.Yxdb2Csv as Y2C
import Database.Alteryx
import Tests.Database.Alteryx.Arbitrary
import Tests.Database.Utils

import Prelude hiding (readFile)

import Control.Applicative
import Control.Lens hiding (elements)
import Control.Monad (replicateM, when)
import Control.Monad.State (evalStateT)
import Control.Monad.Trans.Resource
import Data.Array.IArray (listArray)
import Data.Binary
import Data.Binary.Get (runGet)
import Data.Binary.Put (runPut)
import Data.ByteString as BS
import Data.ByteString.Char8 as BSC
import Data.ByteString.Lazy as BSL
import Data.Conduit
import Data.Conduit.Combinators
import Data.Conduit.Lift
import Data.Conduit.List as CL
import qualified Data.Conduit.Text as CT
import qualified Data.CSV.Conduit as CSVT
import Data.Text as T
import Data.Text.Encoding as T
import Data.Text.IO as T

import Test.Framework
import Test.Framework.Providers.HUnit (testCase)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.HUnit
import Test.QuickCheck

assertEq :: (Eq a, Show a) => a -> a -> Property
assertEq a b = let
    aStr = show a
    bStr = show b
    in printTestCase ("\nA: " ++ aStr ++ "\nB: " ++ bStr) (a == b)

numBytes :: Binary a => a -> Int
numBytes x = fromIntegral $ BSL.length $ runPut $ put x

prop_HeaderLength :: Header -> Property
prop_HeaderLength header =
    assertEq headerPageSize (numBytes header)

prop_MetadataLength :: YxdbFile -> Property
prop_MetadataLength yxdb =
    assertEq (numMetadataBytesHeader $ yxdb ^. yxdbFileHeader)
             (numMetadataBytesActual $ yxdb ^. yxdbFileMetadata)

-- prop_BlocksLength :: YxdbFile -> Property
-- prop_BlocksLength yxdb =
--     assertEq (numBlocksBytesHeader $ header yxdb) (numBlocksBytesActual $ blocks yxdb)

prop_ValueGetAndPutAreInverses :: PairedValue -> Property
prop_ValueGetAndPutAreInverses (PairedValue field value) =
    assertEq (runGet (getValue field) (runPut $ putValue field value)) value

prop_BlockIndexGetAndPutAreInverses :: BlockIndex -> Property
prop_BlockIndexGetAndPutAreInverses x = assertEq (decode $ encode x) x

prop_MetadataGetAndPutAreInverses :: RecordInfo -> Property
prop_MetadataGetAndPutAreInverses x = assertEq (decode $ encode x) x

prop_HeaderGetAndPutAreInverses :: Header -> Property
prop_HeaderGetAndPutAreInverses x = assertEq (decode $ encode x) x

prop_BlocksGetAndPutAreInverses :: Block -> Property
prop_BlocksGetAndPutAreInverses x = assertEq (decode $ encode x) x

prop_YxdbFileGetAndPutAreInverses :: YxdbFile -> Property
prop_YxdbFileGetAndPutAreInverses x = assertEq (decode $ encode x) x

test_LoadingSmallModule :: Assertion
test_LoadingSmallModule = do
  parsed <- decodeFileOrFail "test-data/samples_of_various_field_types.yxdb"
  case parsed of
    Left (bytes, msg) -> assertFailure (msg ++ " at " ++ show bytes ++ " bytes")
    Right yxdbFile -> const (return ()) (yxdbFile :: YxdbFile)

csv2yxdb2csv :: C2Y.Settings -> FilePath -> FilePath -> IO T.Text
csv2yxdb2csv settings inputFilename outputFilename = do
  let c2ySettings = settings & C2Y.settingFilename .~ inputFilename
                             & C2Y.settingOutput .~ outputFilename
  let y2cSettings = Y2C.defaultSettings & Y2C.settingFilename .~ outputFilename
  evalStateT C2Y.runCsv2Yxdb c2ySettings
  newCsv <- T.decodeUtf8 <$> (captureStdout $ evalStateT Y2C.runYxdb2Csv y2cSettings)

  return newCsv

readCsvRecords :: Maybe Text -> FilePath -> IO [Record]
readCsvRecords header filename =
    runResourceT $
      sourceCsvRecords filename header alteryxCsvSettings $$
      sinkList

test_csv2yxdb2csvIsIdentity :: Assertion
test_csv2yxdb2csvIsIdentity = do
  let inputFilename = "test-data/samples.csv"
  let outputFilename = "test-data/samples.yxdb"
  newCsv <- csv2yxdb2csv C2Y.defaultSettings inputFilename outputFilename
  originalCsv <- T.readFile inputFilename
  assertEqual "CSV to YXDB to CSV" originalCsv newCsv

test_csv2yxdbQuoteParsing :: Assertion
test_csv2yxdbQuoteParsing = do
  let inputFilename = "test-data/quote-parsing.csv"
  let outputFilename = "test-data/quote-parsing.yxdb"
  oldCsv <- T.readFile inputFilename
  newCsv <- csv2yxdb2csv C2Y.defaultSettings inputFilename outputFilename
  assertEqual "" oldCsv newCsv

test_csv2yxdbDateFormat :: Assertion
test_csv2yxdbDateFormat = do
  let inputFilename = "test-data/date-format.csv"
      outputFilename = "test-data/date-format.yxdb"
  oldCsv <- T.readFile inputFilename
  newCsv <- csv2yxdb2csv C2Y.defaultSettings inputFilename outputFilename
  assertEqual "" oldCsv newCsv

nonAnsiCodepointFieldValue :: Maybe FieldValue
nonAnsiCodepointFieldValue = Just $ FVString "plátano"

nonAnsiCodepointField :: Field
nonAnsiCodepointField = Field {
                          _fieldName = "derp",
                          _fieldType = FTString,
                          _fieldSize = Just 10,
                          _fieldScale = Nothing
                        }

test_renderNonAnsiCodepoint :: Assertion
test_renderNonAnsiCodepoint = do
  let fieldValue = nonAnsiCodepointFieldValue
      field = nonAnsiCodepointField
      bs = runPut (putValue field fieldValue)
      parsedValue = runGet (getValue field) bs
  assertEqual "Converting a non-ANSI character lost information" fieldValue parsedValue

test_renderNonAnsiCodepointRecord :: Assertion
test_renderNonAnsiCodepointRecord = do
    let fieldValue = nonAnsiCodepointFieldValue
        field = nonAnsiCodepointField

        record = Record [ fieldValue ]
        recordInfo = RecordInfo [ field ]

        record2recordConduit = record2csv recordInfo =$=
                         csv2bytes =$=
                         CT.decode CT.utf8 =$=
                         csv2records alteryxCsvSettings
    newRecords <- runResourceT $ CL.sourceList [ record ] =$= record2recordConduit $$ sinkList

    let result = Prelude.head newRecords
    assertEqual "Converting a non-ANSI character lost information" record result

test_renderNonAnsiCodepointCSVRecord :: Assertion
test_renderNonAnsiCodepointCSVRecord =
  let csvText = "derp:string(10)\nplátano\n"
      recordInfo = RecordInfo [ nonAnsiCodepointField ]
      csv2csvConduit =
          csv2records alteryxCsvSettings =$=
          record2csv recordInfo =$=
          csv2bytes =$=
          CT.decode CT.utf8

  in do
    newCsv <- runResourceT $ CL.sourceList [ csvText ] =$= csv2csvConduit $$ sinkList
    assertEqual "" csvText $ T.concat newCsv

test_renderNonAnsiCodepointCSV2Record2Block2Record2CSV :: Assertion
test_renderNonAnsiCodepointCSV2Record2Block2Record2CSV =
  let csvText = "derp:string(10)\nplátano\n"
      recordInfo = RecordInfo [ nonAnsiCodepointField ]
      csv2csvConduit =
          csv2records alteryxCsvSettings =$=
          evalStateLC defaultStatistics (recordsToBlocks recordInfo) =$=
          blocksToRecords recordInfo =$=
          record2csv recordInfo =$=
          csv2bytes =$=
          CT.decode CT.utf8
  in do
    newCsv <- runResourceT $ CL.sourceList [ csvText ] =$= csv2csvConduit $$ sinkList
    assertEqual "" csvText $ T.concat newCsv


test_recordParsing :: Assertion
test_recordParsing = do
  let inputFilename = "test-data/samples.csv"
      outputFilename = "test-data/samples.yxdb"
      secondOutputFilename = "test-data/samples.yxdb.csv"
      expectedRecord =
          [
           Record [
            Just (FVByte 1),
            Just (FVInt16 256),
            Just (FVInt32 32768),
            Just (FVInt64 4294967296),
            Just (FVString "1.10000"),
            Just (FVFloat 1.1),
            Just (FVDouble 1.1),
            Just (FVString "plátano"),
            Just (FVWString "香蕉"),
            Just (FVString "plátano"),
            Just (FVWString "香蕉"),
            Just (FVString "2013-12-31"),
            Just (FVString "23:34:56"),
            Just (FVString "2013-12-31 23:34:56")
           ]
          ]
  records <- readCsvRecords Nothing inputFilename
  assertEqual "Parse from CSV" expectedRecord records

  newCsv <- csv2yxdb2csv C2Y.defaultSettings inputFilename outputFilename
  yxdbFile <- decodeFile outputFilename :: IO YxdbFile

  T.writeFile secondOutputFilename newCsv
  newRecords <- readCsvRecords Nothing secondOutputFilename

  assertEqual "Parse from YXDB" expectedRecord (yxdbFile ^. yxdbFileRecords)
  assertEqual "New CSV doesn't match old" expectedRecord newRecords

test_signedRecordParsing :: Assertion
test_signedRecordParsing = do
  let inputFilename = "test-data/signed.csv"
      outputFilename = "test-data/signed.yxdb"
      secondOutputFilename = "test-data/signed.yxdb.csv"
      expectedRecord =
          [
           Record [
            Just (FVByte $ -1),
            Just (FVInt16 $ -256),
            Just (FVInt32 $ -32768),
            Just (FVInt64 $ -4294967296),
            Just (FVString "1.10000"),
            Just (FVFloat $ -1.1),
            Just (FVDouble $ -1.1)
           ]
          ]
  records <- readCsvRecords Nothing inputFilename
  assertEqual "Parse from CSV" expectedRecord records

  newCsv <- csv2yxdb2csv C2Y.defaultSettings inputFilename outputFilename
  yxdbFile <- decodeFile outputFilename :: IO YxdbFile

  T.writeFile secondOutputFilename newCsv
  newRecords <- readCsvRecords Nothing secondOutputFilename

  assertEqual "Parse from YXDB" expectedRecord (yxdbFile ^. yxdbFileRecords)
  assertEqual "New CSV doesn't match old" expectedRecord newRecords

yxdbTests :: Test.Framework.Test
yxdbTests =
    testGroup "YXDB" [
        testProperty "Header length" prop_HeaderLength,
        testProperty "Metadata length" prop_MetadataLength,
--        testProperty "Blocks length" prop_BlocksLength, --
        testProperty "Block Index get & put inverses" prop_BlockIndexGetAndPutAreInverses,
        testProperty "Metadata get and put inverses" prop_MetadataGetAndPutAreInverses,
        testProperty "Header get & put inverses" prop_HeaderGetAndPutAreInverses,
        testProperty "Value get & put inverses" prop_ValueGetAndPutAreInverses,
        testProperty "Blocks get & put inverses" prop_BlocksGetAndPutAreInverses,
--        testProperty "Yxdb get & put inverses" prop_YxdbFileGetAndPutAreInverses,
        testCase "Loading small module" test_LoadingSmallModule,
        testCase "CSV to YXDB to CSV is the identity" test_csv2yxdb2csvIsIdentity,
        testCase "Quotes are not used to escape" test_csv2yxdbQuoteParsing,
        testCase "Can parse ISO style dates" test_csv2yxdbDateFormat,
        testCase "Example CSV parses into correct structures" test_recordParsing,
        testCase "Example CSV with signed values" test_signedRecordParsing,
        testCase "Rendering a non-ANSI codepoint" test_renderNonAnsiCodepoint,
        testCase "Rendering a non-ANSI codepoint at record level" test_renderNonAnsiCodepointRecord,
        testCase "Rendering a non-ANSI codepoint from CSV to CSV" test_renderNonAnsiCodepointCSVRecord,
        testCase "Rendering a non-ANSI codepoint from CSV to YXDB blocks to CSV" test_renderNonAnsiCodepointCSV2Record2Block2Record2CSV
    ]