packages feed

ktx-codec-0.0.2.1: test/Spec.hs

module Main where

import Codec.Compression.Zstd.FFI qualified as Zstd
import Control.Exception (bracket)
import Control.Monad (guard, unless, void)
import Data.ByteString qualified as BS
import Data.ByteString.Builder (toLazyByteString)
import Data.ByteString.Lazy qualified as BSL
import Data.Foldable (for_)
import Data.List (isSuffixOf)
import Data.Traversable (for)
import Data.Vector qualified as Vector
import Debug.Trace (traceM)
import Foreign qualified
import Shower qualified
import System.Directory (listDirectory)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import Test.Tasty (testGroup, defaultMain)
import Test.Tasty.HUnit (testCase)

import Codec.Ktx qualified as Ktx1
import Codec.Ktx.KeyValue qualified as KeyValue
import Codec.Ktx2.DFD (DFD(..))
import Codec.Ktx2.DFD.Khronos.BasicV2 qualified as BasicV2
import Codec.Ktx2.Header qualified as Header
import Codec.Ktx2.Level qualified as Level
import Codec.Ktx2.Read qualified as Read
import Codec.Ktx2.Write qualified as Write

assetsPath :: FilePath
assetsPath = ".." </> "assets"

main :: IO ()
main = do
  assets <- listDirectory assetsPath
  defaultMain $
    testGroup "ktx-codec"
      [ testGroup "KTX 1" do
          fp <- assets
          guard $ ".ktx" `isSuffixOf` fp
          pure . testCase fp $
            testKtx1 (assetsPath </> fp)
      , testGroup "KTX 2 (files)" do
          fp <- assets
          guard $ ".ktx2" `isSuffixOf` fp
          pure . testCase fp $
            testKtx2File (assetsPath </> fp)
      , testGroup "KTX 2 (bytes)" do
          fp <- assets
          guard $ ".ktx2" `isSuffixOf` fp
          pure . testCase fp $
            testKtx2Bytes (assetsPath </> fp)
      ]

testKtx1 :: FilePath -> IO ()
testKtx1 source =
  Ktx1.fromFile source >>= \case
    Left (offset, err) -> do
      traceM err
      fail $ unwords
        [ "KTX-1 load error in"
        , source
        , "at"
        , show offset
        ]
    Right ktx -> do
      printer ktx
      let encodedBytes = toLazyByteString (Ktx1.toBuilder ktx)
      originalBytes <- BSL.readFile source
      unless (encodedBytes == originalBytes) $
        fail $ "Encoded KTX does not match original for " <> source

testKtx2File :: FilePath -> IO ()
testKtx2File source =
  bracket (Read.open source) Read.close $
    void . testKtx2Context source

testKtx2Bytes :: FilePath -> IO ()
testKtx2Bytes source = do
  contents <- BS.readFile source
  context <- Read.bytes contents

  chunks <- testKtx2Context source context
  -- BSL.writeFile (source <> ".out") chunks

  tripping <- Read.bytes (BSL.toStrict chunks)
  let
    readHeader = Read.header context
    writeHeader = Read.header tripping
  unless (readHeader == writeHeader) $
    fail . mappend "Header tripping failed:\n" $ show
      ( readHeader
      , writeHeader
      )

  readLevels <- Read.levels context
  writeLevels <- Read.levels tripping
  unless (readLevels == writeLevels) $
    fail . mappend "Level index tripping failed:\n" $ show
      ( readLevels
      , writeLevels
      )

  readDfd <- Read.dataFormatDescriptor context
  writeDfd <- Read.dataFormatDescriptor tripping
  unless (readDfd == writeDfd) $
    fail . mappend "DFD tripping failed:\n" $ show
      ( readDfd
      , writeDfd
      )

  readKvd <- Read.keyValueData context
  writeKvd <- Read.keyValueData tripping
  unless (readKvd == writeKvd) $
    fail . mappend "KeyValue tripping failed:\n" $ show
      ( readKvd
      , writeKvd
      )

  for_ readLevels \level -> do
    readLevel <- Read.levelData context level
    writeLevel <- Read.levelData tripping level
    unless (readLevel == writeLevel) $
      fail . mappend "Level tripping failed:\n" $ show
        ( level
        , BS.take 16 readLevel
        , BS.take 16 writeLevel
        )
testKtx2Context
  :: ( Read.ReadChunk a
     , Read.ReadLevel a
     , Show (Read.Context a)
     )
  => FilePath
  -> Read.Context a
  -> IO BSL.ByteString
testKtx2Context source ctx = do
  printer ctx

  levels <- Read.levels ctx
  printer levels

  DFD{dfdBlocks} <- Read.dataFormatDescriptor ctx
  Vector.forM_ dfdBlocks \block -> do
    case BasicV2.fromBlock block of
      Nothing -> do
        printer block
        fail $ "Unexpected block type in test suite in " <> source
      Just basic -> do
        -- printer basic
        let back = BasicV2.toBlock basic
        unless (back == block) do
          printer (back, block)
          fail "BasicV2 DFD tripping failed"

  kvd <- Read.keyValueData ctx
  printer $ KeyValue.textual kvd

  sgd <- Read.supercompressionGlobalData ctx
  printer sgd

  levelBuffers <- for (Vector.toList levels) \level -> do
    bytes <- Read.levelData ctx level
    unless (BS.length bytes == fromIntegral (Level.byteLength level)) $
      fail "Uncompressed level data does not match with index!"

    Foreign.allocaBytesAligned (fromIntegral $ Level.uncompressedByteLength level) 16 \dst -> do
      case Header.supercompressionScheme (Read.header ctx) of
        Header.SC_NONE -> do
          -- XXX: Write to destination directly
          ok <- Read.levelToPtr ctx level dst
          unless ok $
            fail $ "Data read failed for " <> show level

        Header.SC_ZSTANDARD ->
          -- XXX: Prepare an intermediate buffer for decompression
          Foreign.allocaBytesAligned (fromIntegral $ Level.byteLength level) 16 \stage -> do
            ok <- Read.levelToPtr ctx level stage
            unless ok $
              fail $ "Data read failed for " <> show level

            let expected = fromIntegral $ Level.uncompressedByteLength level
            res <-
              Zstd.checkError $
                Zstd.decompress
                  dst
                  expected
                  stage
                  (fromIntegral $ Level.byteLength level)
            case res of
              Right decoded | decoded == expected ->
                printer (decoded, level)
              Right mismatch ->
                fail $ "Zstd decoded unexpected number of bytes (" <> show mismatch <> ") for level " <> show level
              Left err ->
                fail $ "Zstd error: " <> err

        huh ->
          fail $ "Unexpected supercompression: " <> show huh
    pure
      ( if Header.supercompressionScheme (Read.header ctx) == Header.SC_NONE then
          Nothing
        else
          Just . fromIntegral $ Level.uncompressedByteLength level
      , bytes
      )

  pure $
    Write.toChunks
      (Read.header ctx)
      dfdBlocks
      kvd
      sgd
      levelBuffers

printer :: Show a => a -> IO ()
printer x =
  lookupEnv "TEST_DUMP" >>= \case
    Nothing ->
      pure ()
    Just _ ->
      Shower.printer x