packages feed

proto3-suite-0.9.4: tests/Main.hs

{-# LANGUAGE AllowAmbiguousTypes   #-}
{-# LANGUAGE BlockArguments        #-}
{-# LANGUAGE CPP                   #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE ImplicitParams        #-}
{-# LANGUAGE MagicHash             #-}
{-# LANGUAGE OverloadedLists       #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE PolyKinds             #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE UndecidableInstances  #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

module Main where

import           ArbitraryGeneratedTestTypes ()
import qualified Data.ByteString             as B
import qualified Data.ByteString.Char8       as BC
import qualified Data.ByteString.Lazy        as BL
import qualified Data.ByteString.Short       as BS
import           Data.Either                 (isRight)
import           Data.Functor.Identity       (Identity(..))
import           Data.Int                    (Int8, Int16, Int32, Int64)
import qualified Data.List.NonEmpty          as NE
import           Data.Proxy                  (Proxy(..))
import           Data.String
import qualified Data.Text.Short
import           Data.Typeable               (Typeable, showsTypeRep, typeRep)
import           Data.Word                   (Word8, Word16, Word32, Word64)
import qualified Data.Vector                 as V
import           GHC.Exts                    (fromList, Proxy#)
import           GHC.TypeLits                (Nat, SomeNat(..), Symbol, TypeError, someNatVal)
import           Proto3.Suite
import qualified Proto3.Suite.Form           as Form
import qualified Proto3.Suite.Form.Encode    as FormE
import           Proto3.Suite.Haskell.Parser (Logger, initLogger)
import qualified Proto3.Suite.JSONPB.Class   as JSONPB
import           Proto3.Wire.Decode          (ParseError)
import qualified Proto3.Wire.Decode          as Decode
import qualified Proto3.Wire.Reverse         as RB
import           Proto3.Wire.Types           as P
import qualified Test.DocTest
import           Test.QuickCheck             (Arbitrary, Property, (.&&.), arbitrary, choose,
                                              counterexample, forAll, oneof, property)
import           Test.Tasty
import           Test.Tasty.HUnit            (Assertion, assertBool, assertEqual,
                                              testCase, (@=?), (@?=))
import           Test.Tasty.QuickCheck       (testProperty, (===))
import           TestCodeGen
import qualified TestProto                   as TP
import qualified TestProtoOneof              as TPO
import qualified TestProtoOneofImport        as TPOI

#ifdef DHALL
import           TestDhall
#endif

import qualified Test.Proto.Generate.Name
import qualified Test.Proto.Parse.Option
import qualified Test.Proto.Interval
import           Test.Proto.ToEncoder        (Iterator(Forward, Vector),
                                              Stripping(Keep, Strip), ToEncoder(..))

-- -----------------------------------------------------------------------------

main :: IO ()
main = do
  logger <- initLogger
  defaultMain (tests logger)

tests :: Logger -> TestTree
tests logger = testGroup "Tests"
  [ docTests
  , qcProperties
  , encodeUnitTests
  , decodeUnitTests
  , parserUnitTests
  , dotProtoUnitTests
  , codeGenTests logger
  , Test.Proto.Generate.Name.testTree
  , Test.Proto.Parse.Option.testTree
  , Test.Proto.Interval.testTree

#ifdef DHALL
  , dhallTests
#endif
  ]

-- -----------------------------------------------------------------------------
-- Doctests

docTests :: TestTree
docTests = testCase "doctests" $ do
  putStrLn "Running all doctests..."
  Test.DocTest.doctest
    [ "--verbose"
    , "-package"
    , "ghc-lib-parser"
    , "-isrc"
    , "-XBlockArguments"
#ifdef SWAGGER
#ifdef SWAGGER_WRAPPER_FORMAT
    , "-isrc/swagger-wrapper-format"
    , "-DSWAGGER_WRAPPER_FORMAT"
#else
    , "-isrc/no-swagger-wrapper-format"
#endif
    , "-DSWAGGER"
#endif
#ifdef DHALL
    , "-DDHALL"
#endif
#ifdef LARGE_RECORDS
    , "-DLARGE_RECORDS"
#endif
    , "src/Proto3/Suite/DotProto/Internal.hs"
    , "src/Proto3/Suite/DotProto/Generate.hs"
    , "src/Proto3/Suite/JSONPB/Class.hs"
    , "src/Proto3/Suite/Tutorial.hs"
    ]

--------------------------------------------------------------------------------
-- QuickCheck properties

type MsgProp a = a -> Property

qcProperties :: TestTree
qcProperties = testGroup "QuickCheck properties"
  [ qcPropDecEncId
  ]

-- | Verifies that @decode . encode = id@ for various message types
qcPropDecEncId :: TestTree
qcPropDecEncId = testGroup "Property: (decode . encode = id) for various message types"
  [ testProperty "Trivial"             (prop :: MsgProp TP.Trivial)
  , testProperty "MultipleFields"      (prop :: MsgProp TP.MultipleFields)
  , testProperty "WithEnum"            (prop :: MsgProp TP.WithEnum)
  , testProperty "WithNesting"         (prop :: MsgProp TP.WithNesting)
  , testProperty "WithRepetition"      (prop :: MsgProp TP.WithRepetition)
  , testProperty "WithRepeatedSigned"  (prop :: MsgProp TP.WithRepeatedSigned)
  , testProperty "WithFixed"           (prop :: MsgProp TP.WithFixed)
  , testProperty "WithBytes"           (prop :: MsgProp TP.WithBytes)
  , testProperty "AllPackedTypes"      (prop :: MsgProp TP.AllPackedTypes)
  , testProperty "SignedInts"          (prop :: MsgProp TP.SignedInts)
  , testProperty "WithNestingRepeated" (prop :: MsgProp TP.WithNestingRepeated)
  , deeplyNest prop 1000
  ]
  where
    prop :: (Message a, Arbitrary a, Eq a, Show a) => MsgProp a
    prop msg = msg === (dec . enc) msg
      where
        dec = either (error . ("error parsing: " <>) . show) id . fromByteString
        enc = BL.toStrict . toLazyByteString

    deeplyNest :: MsgProp TP.Wrapped -> Int -> TestTree
    deeplyNest pf 0 = testProperty "Deeply nested" pf
    deeplyNest pf n = deeplyNest (pf . TP.Wrapped . Just) (n-1)


--------------------------------------------------------------------------------
-- Encoding

encodeUnitTests :: TestTree
encodeUnitTests = testGroup "Encoder unit tests"
  [ encoderMatchesGoldens
  , encoderPromotionsAndAuto
  , encodeBytesFromBuilder
  , encodeMessageReflection
  , encodeCachedSubmessage
  , testShowMessageEncoder
  , testShowMessageEncoding
  , testToJSONPBMessageEncoder
  , testToJSONPBMessageEncoding
  , testFromJSONPBMessageEncoder
  , testFromJSONPBMessageEncoding
  ]

-- TODO: We should consider generating the reference encodings
-- (test-files/make_reference_encodings.py) as a part of running the test suite
-- rather than having them in the repository.
encoderMatchesGoldens :: TestTree
encoderMatchesGoldens = testGroup "Encoder matches golden encodings"
  [ check "trivial.bin"               $ TP.Trivial 123
  , check "trivial_negative.bin"      $ TP.Trivial (-1)
  , check "multiple_fields.bin"       $ TP.MultipleFields 1.23 (-0.5) 123 1234567890 "Hello, world!" True
  , check "signedints.bin"            $ TP.SignedInts (-42) (-84)
  , check "with_nesting.bin"          $ TP.WithNesting $ Just $ TP.WithNesting_Nested "123abc" 123456 [] []
  , check "with_enum0.bin"            $ TP.WithEnum $ Enumerated $ Right $ TP.WithEnum_TestEnumENUM1
  , check "with_enum1.bin"            $ TP.WithEnum $ Enumerated $ Right $ TP.WithEnum_TestEnumENUM2
  , check "with_repetition.bin"       $ TP.WithRepetition [1..5]
  , check "with_repeated_signed.bin"  $ TP.WithRepeatedSigned
      [ 0, -1, 1, -2, 2,
        0x3FFFFFFF, -0x40000000, 0x40000000, -0x40000001,
        0x7FFFFFFF, -0x7FFFFFFF - 1 ]
      [ 0, -1, 1, -2, 2,
        0x3FFFFFFFFFFFFFFF, -0x4000000000000000, 0x4000000000000000, -0x4000000000000001,
        0x7FFFFFFFFFFFFFFF, -0x7FFFFFFFFFFFFFFF - 1 ]
  , check "with_bytes.bin"            $ TP.WithBytes (BC.pack "abc") (fromList $ map BC.pack ["abc","123"])
  , check "with_nesting_repeated.bin" $ TP.WithNestingRepeated
                                          [ TP.WithNestingRepeated_Nested "123abc" 123456 [1,2,3,4] [5,6,7,8]
                                          , TP.WithNestingRepeated_Nested "abc123" 654321 [0,9,8,7] [6,5,4,3]
                                          ]
  ]
  where
    check fp v = testCase fp $ do
      goldenEncoding <- BL.readFile (testFilesPfx <> fp)
      assertEqual (show fp ++ ": encoding from intermediate representation")
        goldenEncoding (toLazyByteString v)
      -- NOTE: We skip the 'Identity' iterator in this test because we expect it
      -- will create longer output for packed fields than the golden result when
      -- there is more than one element of the repetition--in which case our test
      -- code misuses 'Identity' by using it repeatedly, once for each element.
      -- Though suboptimal, the resulting encoding remains decodable by parsers
      -- that adhere to the protobuf specification.  We check that decodability
      -- in other tests that pair a Haskell encoder with a Python decoder.
      assertEqual (show fp ++ ": direct encoding, Forward iterator, keep wrappers")
        goldenEncoding $ FormE.messageEncoderToLazyByteString $
          let ?iterator = Forward
              ?stripping = Keep
          in toEncoder v
      assertEqual (show fp ++ ": direct encoding, Forward iterator, strip wrappers")
        goldenEncoding $ FormE.messageEncoderToLazyByteString $
          let ?iterator = Forward
              ?stripping = Strip
          in toEncoder v
      assertEqual (show fp ++ ": direct encoding, Vector iterator, keep wrappers")
        goldenEncoding $ FormE.messageEncoderToLazyByteString $
          let ?iterator = Vector
              ?stripping = Keep
          in toEncoder v
      assertEqual (show fp ++ ": direct encoding, Vector iterator, strip wrappers")
        goldenEncoding $ FormE.messageEncoderToLazyByteString $
          let ?iterator = Vector
              ?stripping = Strip
          in toEncoder v

-- Simulated protobuf message type having a single field named @myBytes@ of
-- type @bytes@ with field number @8@ and the specified 'Form.Cardinality'.
data MyWithBytes (r :: Form.Cardinality)

type instance Form.NamesOf (MyWithBytes r) = '["x"]

type instance Form.NumberOf (MyWithBytes r) name = MyWithBytes_NumberOf r name

type family MyWithBytes_NumberOf (r :: Form.Cardinality) (name :: Symbol) :: Nat
  where
    MyWithBytes_NumberOf _ "myBytes" = 8
    MyWithBytes_NumberOf r name = TypeError (Form.FieldNotFound (MyWithBytes r) name)

type instance Form.ProtoTypeOf (MyWithBytes r) name = MyWithBytes_ProtoTypeOf r name

type family MyWithBytes_ProtoTypeOf (r :: Form.Cardinality) (name :: Symbol) :: Form.ProtoType
  where
    MyWithBytes_ProtoTypeOf _ "myBytes" = 'Form.Bytes
    MyWithBytes_ProtoTypeOf r name = TypeError (Form.FieldNotFound (MyWithBytes r) name)

type instance Form.OneOfOf (MyWithBytes r) name = MyWithBytes_OneOfOf r name

type family MyWithBytes_OneOfOf (r :: Form.Cardinality) (name :: Symbol) :: Symbol
  where
    MyWithBytes_OneOfOf 'Form.Optional "myBytes" = "pickOne"
    MyWithBytes_OneOfOf 'Form.Optional "pickOne" = "pickOne"
    MyWithBytes_OneOfOf r "myBytes" = ""
    MyWithBytes_OneOfOf r name = TypeError (Form.FieldOrOneOfNotFound (MyWithBytes r) name)

type instance Form.CardinalityOf (MyWithBytes r) name = MyWithBytes_CardinalityOf r name

type family MyWithBytes_CardinalityOf (r :: Form.Cardinality) (name :: Symbol) :: Form.Cardinality
  where
    MyWithBytes_CardinalityOf r "myBytes" = r
    MyWithBytes_CardinalityOf 'Form.Optional "pickOne" = 'Form.Optional
    MyWithBytes_CardinalityOf r name = TypeError (Form.FieldOrOneOfNotFound (MyWithBytes r) name)

encodeBytesFromBuilder :: TestTree
encodeBytesFromBuilder = testCase "bytes from builder" $ do
    assertEqual "implicitly empty"
      (enc @'Form.Implicit (mempty :: RB.BuildR)) ""
    assertEqual "implicit but nonempty"
      (enc @'Form.Implicit (RB.byteString "xyz")) "B\ETXxyz"
    assertEqual "unset optional"
      (enc @'Form.Optional (Nothing :: Maybe RB.BuildR)) ""
    assertEqual "set empty optional"
      (enc @'Form.Optional (Just (mempty :: RB.BuildR))) "B\NUL"
    assertEqual "set nonempty optional"
      (enc @'Form.Optional (Just (RB.byteString "xyz"))) "B\ETXxyz"
    assertEqual "zero repetitions"
      (enc @('Form.Repeated 'Form.Unpacked)
           ([] :: [RB.BuildR]))
      ""
    assertEqual "single empty"
      (enc @('Form.Repeated 'Form.Unpacked)
           ([mempty] :: [RB.BuildR]))
      "B\NUL"
    assertEqual "double empty"
      (enc @('Form.Repeated 'Form.Unpacked)
           ([mempty, mempty] :: [RB.BuildR]))
      "B\NULB\NUL"
    assertEqual "empty nonempty"
      (enc @('Form.Repeated 'Form.Unpacked)
           ([mempty, RB.byteString "xyz"] :: [RB.BuildR]))
      "B\NULB\ETXxyz"
    assertEqual "nonempty empty"
      (enc @('Form.Repeated 'Form.Unpacked)
           (V.fromList [RB.byteString "uv", mempty]))
      "B\STXuvB\NUL"
    assertEqual "double nonempty"
      (enc @('Form.Repeated 'Form.Unpacked)
           (V.fromList [RB.byteString "uv", RB.byteString "xyz"]))
      "B\STXuvB\ETXxyz"
  where
    enc ::
      forall r a .
      ( FormE.Distinct (MyWithBytes r) (FormE.Occupy (MyWithBytes r) "myBytes" '[])
      , FormE.Field "myBytes" a (MyWithBytes r)
      ) =>
      a ->
      BL.ByteString
    enc =
      FormE.messageEncoderToLazyByteString .
      FormE.fieldsToMessage .
      FormE.field @"myBytes" @a @(MyWithBytes r)

encodeMessageReflection :: TestTree
encodeMessageReflection = testCase "messageReflection" $ do
  let msg = TP.Trivial 123
  FormE.messageEncoderToLazyByteString (FormE.messageReflection msg) @?= toLazyByteString msg

encodeCachedSubmessage :: TestTree
encodeCachedSubmessage = testGroup "Cached Submessages"
  [ testOptional
  , testRepeated
  , testOneof
  ]
  where
    testOptional :: TestTree
    testOptional = testCase "cached optional submessage" $ do
      let trivial = TP.Trivial 123
          wrappedTrivial = FormE.fieldsToMessage @TP.WrappedTrivial $
            FormE.field @"trivial" (Just (FormE.messageCache trivial))
      FormE.messageEncoderToLazyByteString wrappedTrivial @?=
        toLazyByteString (TP.WrappedTrivial (Just trivial))

    testRepeated :: TestTree
    testRepeated = testCase "cached repeated submessage" $ do
      let trivials :: V.Vector TP.MapTestEmulation_Trivial
          trivials =
            [ TP.MapTestEmulation_Trivial 15 (Just (TP.WrappedTrivial (Just (TP.Trivial 465))))
            , TP.MapTestEmulation_Trivial 25 (Just (TP.WrappedTrivial (Just (TP.Trivial 789))))
            ]
          mapTestEmulation = FormE.fieldsToMessage @TP.MapTestEmulation $
            FormE.field @"trivial" (V.map FormE.messageCache trivials)
      FormE.messageEncoderToLazyByteString mapTestEmulation @?=
        toLazyByteString (TP.MapTestEmulation mempty trivials mempty)

    testOneof :: TestTree
    testOneof = testCase "cached submessage within oneof" $ do
      let withOneof = TPOI.WithOneof (Just (TPOI.WithOneofPickOneB 123))
          withImported = FormE.fieldsToMessage @TPO.WithImported $
            FormE.field @"withOneof" (Just (FormE.messageCache withOneof))
      FormE.messageEncoderToLazyByteString withImported @?=
        toLazyByteString (TPO.WithImported (Just (TPO.WithImportedPickOneWithOneof withOneof)))

testShowMessageEncoding :: TestTree
testShowMessageEncoding = testCase "Show MessageEncoding" $ do
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoding :: FormE.MessageEncoding TP.Trivial
      encoding = FormE.messageCache trivial
      bogus :: FormE.MessageEncoding TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoding badForm
  -- Test desired behavior for valid encoding:
  show (Just encoding) @?=
    "Just (Proto3.Suite.Form.Encode.messageCache " ++ showsPrec 11 trivial ")"
  -- Test fallback behavior for invalid encoding:
  show (Just bogus) @?=
    "Just (Proto3.Suite.Form.Encode.unsafeByteStringToMessageEncoding " ++ showsPrec 11 badForm ")"

testShowMessageEncoder :: TestTree
testShowMessageEncoder = testCase "Show MessageEncoder" $ do
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoder :: FormE.MessageEncoder TP.Trivial
      encoder = FormE.messageReflection trivial
      bogus :: FormE.MessageEncoder TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoder badForm
  -- Test desired behavior for valid encoder:
  show (Just encoder) @?=
    "Just (Proto3.Suite.Form.Encode.messageReflection " ++ showsPrec 11 trivial ")"
  -- Test fallback behavior for invalid encoder:
  show (Just bogus) @?=
    "Just (Proto3.Suite.Form.Encode.unsafeByteStringToMessageEncoder " ++ showsPrec 11 badForm ")"

testToJSONPBMessageEncoding :: TestTree
testToJSONPBMessageEncoding = testProperty "ToJSONPB MessageEncoding" $ \opts ->
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoding :: FormE.MessageEncoding TP.Trivial
      encoding = FormE.messageCache trivial
      bogus :: FormE.MessageEncoding TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoding badForm
  in
    -- Test desired behavior for valid encoding:
    JSONPB.toJSONPB encoding opts === JSONPB.toJSONPB trivial opts
    .&&.
    JSONPB.toEncodingPB encoding opts === JSONPB.toEncodingPB trivial opts
    .&&.
    -- Test fallback behavior for invalid encoding:
    JSONPB.toJSONPB bogus opts === JSONPB.toJSONPB badForm opts
    .&&.
    JSONPB.toEncodingPB bogus opts === JSONPB.toEncodingPB badForm opts

testToJSONPBMessageEncoder :: TestTree
testToJSONPBMessageEncoder = testProperty "ToJSONPB MessageEncoder" $ \opts ->
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoder :: FormE.MessageEncoder TP.Trivial
      encoder = FormE.messageReflection trivial
      bogus :: FormE.MessageEncoder TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoder badForm
  in
    -- Test desired behavior for valid encoder:
    JSONPB.toJSONPB encoder opts === JSONPB.toJSONPB trivial opts
    .&&.
    JSONPB.toEncodingPB encoder opts === JSONPB.toEncodingPB trivial opts
    .&&.
    -- Test fallback behavior for invalid encoder:
    JSONPB.toJSONPB bogus opts === JSONPB.toJSONPB badForm opts
    .&&.
    JSONPB.toEncodingPB bogus opts === JSONPB.toEncodingPB badForm opts

testFromJSONPBMessageEncoding :: TestTree
testFromJSONPBMessageEncoding = testProperty "FromJSONPB MessageEncoding" $ \opts ->
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoding :: FormE.MessageEncoding TP.Trivial
      encoding = FormE.messageCache trivial
      bogus :: FormE.MessageEncoding TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoding badForm
      edec :: BL.ByteString -> Either String (FormE.MessageEncoding TP.Trivial)
      edec = JSONPB.eitherDecode
  in
    -- Test desired behavior for valid encoder:
    fmap (fromByteString . FormE.messageEncodingToByteString) (edec (JSONPB.encode opts encoding))
      === Right (Right trivial)
    .&&.
    -- Test fallback behavior for invalid encoder:
    fmap FormE.messageEncodingToByteString (edec (JSONPB.encode opts bogus)) === Right badForm

testFromJSONPBMessageEncoder :: TestTree
testFromJSONPBMessageEncoder = testProperty "FromJSONPB MessageEncoder" $ \opts ->
  let trivial = TP.Trivial 123
      badForm = "abc"
      encoder :: FormE.MessageEncoder TP.Trivial
      encoder = FormE.messageReflection trivial
      bogus :: FormE.MessageEncoder TP.Trivial
      bogus = FormE.unsafeByteStringToMessageEncoder badForm
      edec :: BL.ByteString -> Either String (FormE.MessageEncoder TP.Trivial)
      edec = JSONPB.eitherDecode
  in
    -- Test desired behavior for valid encoder:
    fmap (fromByteString . FormE.messageEncoderToByteString) (edec (JSONPB.encode opts encoder))
      === Right (Right trivial)
    .&&.
    -- Test fallback behavior for invalid encoder:
    fmap FormE.messageEncoderToByteString (edec (JSONPB.encode opts bogus)) === Right badForm

data TestMessage
       (num :: Nat)
       (repetition :: Maybe Form.Cardinality)
       (protoType :: Form.ProtoType)
  -- ^ Omitting the repetition means that field @"name"@ is in a @oneof@ named @"pickOne"@.

type instance Form.NamesOf (TestMessage num maybeRepetition protoType) = '[ "name" ]

type instance Form.NumberOf (TestMessage num maybeRepetition protoType) "name" = num

type instance Form.ProtoTypeOf (TestMessage num maybeRepetition protoType) "name" = protoType

type instance Form.OneOfOf (TestMessage num 'Nothing protoType) "name" = "pickOne"
type instance Form.OneOfOf (TestMessage num 'Nothing protoType) "pickOne" = "pickOne"
type instance Form.OneOfOf (TestMessage num ('Just _) protoType) "name" = ""

type instance Form.CardinalityOf (TestMessage num 'Nothing protoType) "name" = 'Form.Optional
type instance Form.CardinalityOf (TestMessage num 'Nothing protoType) "pickOne" = 'Form.Optional
type instance Form.CardinalityOf (TestMessage num ('Just repetition) protoType) "name" = repetition

encoderPromotionsAndAuto :: TestTree
encoderPromotionsAndAuto = testGroup "Encoder promotes types correctly and Auto-selection works"
  [ testGroup "Encoder promotes types correctly"
      [ check @'Form.Int32 @Int8 @Int32 "int32" (fromIntegral @Int8 @Int32)
      , check @'Form.Int32 @Word8 @Int32 "int32" (fromIntegral @Word8 @Int32)
      , check @'Form.Int32 @Int16 @Int32 "int32" (fromIntegral @Int16 @Int32)
      , check @'Form.Int32 @Word16 @Int32 "int32" (fromIntegral @Word16 @Int32)
      , check @'Form.Int64 @Int8 @Int64 "int64" (fromIntegral @Int8 @Int64)
      , check @'Form.Int64 @Word8 @Int64 "int64" (fromIntegral @Word8 @Int64)
      , check @'Form.Int64 @Int16 @Int64 "int64"(fromIntegral @Int16 @Int64)
      , check @'Form.Int64 @Word16 @Int64 "int64"(fromIntegral @Word16 @Int64)
      , check @'Form.Int64 @Int32 @Int64 "int64"(fromIntegral @Int32 @Int64)
      , check @'Form.Int64 @Word32 @Int64 "int64"(fromIntegral @Word32 @Int64)
      , check @'Form.SInt32 @Int8 @Int32 "sint32" (fromIntegral @Int8 @Int32)
      , check @'Form.SInt32 @Word8 @Int32 "sint32" (fromIntegral @Word8 @Int32)
      , check @'Form.SInt32 @Int16 @Int32 "sint32" (fromIntegral @Int16 @Int32)
      , check @'Form.SInt32 @Word16 @Int32 "sint32" (fromIntegral @Word16 @Int32)
      , check @'Form.SInt64 @Int8 @Int64 "sint64" (fromIntegral @Int8 @Int64)
      , check @'Form.SInt64 @Word8 @Int64 "sint64" (fromIntegral @Word8 @Int64)
      , check @'Form.SInt64 @Int16 @Int64 "sint64" (fromIntegral @Int16 @Int64)
      , check @'Form.SInt64 @Word16 @Int64 "sint64" (fromIntegral @Word16 @Int64)
      , check @'Form.SInt64 @Int32 @Int64 "sint64" (fromIntegral @Int32 @Int64)
      , check @'Form.SInt64 @Word32 @Int64 "sint64" (fromIntegral @Word32 @Int64)
      , check @'Form.UInt32 @Word8 @Word32 "uint32" (fromIntegral @Word8 @Word32)
      , check @'Form.UInt32 @Word16 @Word32 "uint32" (fromIntegral @Word16 @Word32)
      , check @'Form.UInt64 @Word8 @Word64 "uint64" (fromIntegral @Word8 @Word64)
      , check @'Form.UInt64 @Word16 @Word64 "uint64" (fromIntegral @Word16 @Word64)
      , check @'Form.UInt64 @Word32 @Word64 "uint64" (fromIntegral @Word32 @Word64)
      , check @'Form.Fixed32 @Word8 @Word32 "fixed32" (fromIntegral @Word8 @Word32)
      , check @'Form.Fixed32 @Word16 @Word32 "fixed32" (fromIntegral @Word16 @Word32)
      , check @'Form.Fixed64 @Word8 @Word64 "fixed64" (fromIntegral @Word8 @Word64)
      , check @'Form.Fixed64 @Word16 @Word64 "fixed64" (fromIntegral @Word16 @Word64)
      , check @'Form.Fixed64 @Word32 @Word64 "fixed64" (fromIntegral @Word32 @Word64)
      , check @'Form.SFixed32 @Int8 @Int32 "sfixed32" (fromIntegral @Int8 @Int32)
      , check @'Form.SFixed32 @Word8 @Int32 "sfixed32" (fromIntegral @Word8 @Int32)
      , check @'Form.SFixed32 @Int16 @Int32 "sfixed32" (fromIntegral @Int16 @Int32)
      , check @'Form.SFixed32 @Word16 @Int32 "sfixed32" (fromIntegral @Word16 @Int32)
      , check @'Form.SFixed64 @Int8 @Int64 "sfixed64" (fromIntegral @Int8 @Int64)
      , check @'Form.SFixed64 @Word8 @Int64 "sfixed64" (fromIntegral @Word8 @Int64)
      , check @'Form.SFixed64 @Int16 @Int64 "sfixed64" (fromIntegral @Int16 @Int64)
      , check @'Form.SFixed64 @Word16 @Int64 "sfixed64" (fromIntegral @Word16 @Int64)
      , check @'Form.SFixed64 @Int32 @Int64 "sfixed64" (fromIntegral @Int32 @Int64)
      , check @'Form.SFixed64 @Word32 @Int64 "sfixed64" (fromIntegral @Word32 @Int64)
      , check @'Form.Double @Float @Double "double" (realToFrac @Float @Double)
      ]
  , testGroup "Encoder Auto wrapper does not change format"
      [ check @'Form.Int32 @Int32 @(FormE.Auto Int32) "int32" FormE.Auto
      , check @'Form.Int64 @Int64 @(FormE.Auto Int64) "int64" FormE.Auto
      , check @'Form.SInt32 @Int32 @(FormE.Auto Int32) "sint32" FormE.Auto
      , check @'Form.SInt64 @Int64 @(FormE.Auto Int64) "sint64" FormE.Auto
      , check @'Form.UInt32 @Word32 @(FormE.Auto Word32) "uint32" FormE.Auto
      , check @'Form.UInt64 @Word64 @(FormE.Auto Word64) "uint64" FormE.Auto
      , check @'Form.Fixed32 @Word32 @(FormE.Auto Word32) "fixed32" FormE.Auto
      , check @'Form.Fixed64 @Word64 @(FormE.Auto Word64) "fixed64" FormE.Auto
      , check @'Form.SFixed32 @Int32 @(FormE.Auto Int32) "sfixed32" FormE.Auto
      , check @'Form.SFixed64 @Int64 @(FormE.Auto Int64) "sfixed64" FormE.Auto
      , check @'Form.Bool @Bool @(FormE.Auto Bool) "bool" FormE.Auto
      , check @'Form.Float @Float @(FormE.Auto Float) "float" FormE.Auto
      , check @'Form.Double @Double @(FormE.Auto Double) "double" FormE.Auto
      , checkNotPacked @'Form.String @Data.Text.Short.ShortText @(FormE.Auto Data.Text.Short.ShortText) "string" FormE.Auto
      , checkNotPacked @'Form.Bytes @BS.ShortByteString @(FormE.Auto BS.ShortByteString) "bytes" FormE.Auto
      ]
  ]
  where
    check ::
      forall (protoType :: Form.ProtoType) a b .
      ( Typeable a
      , Typeable b
      , Arbitrary a
      , Show a
      , FormE.FieldForm 'Form.Implicit protoType a
      , FormE.FieldForm 'Form.Implicit protoType b
      , FormE.FieldForm 'Form.Optional protoType (Maybe a)
      , FormE.FieldForm 'Form.Optional protoType (Maybe b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [a]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [b]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector b)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (Identity a)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (Identity b)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType [a]
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType [b]
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (V.Vector a)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (V.Vector b)
      ) =>
      String ->
      (a -> b) ->
      TestTree
    check protoTypeName convert =
      testProperty (showString protoTypeName $ showString ": " $
                    showsType @a $ showString " -> " $ showsType @b "") $
        propNotPacked @protoType convert .&&.
        propPacked @protoType convert

    checkNotPacked ::
      forall (protoType :: Form.ProtoType) a b .
      ( Typeable a
      , Typeable b
      , Arbitrary a
      , Show a
      , FormE.FieldForm 'Form.Implicit protoType a
      , FormE.FieldForm 'Form.Implicit protoType b
      , FormE.FieldForm 'Form.Optional protoType (Maybe a)
      , FormE.FieldForm 'Form.Optional protoType (Maybe b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [a]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [b]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector b)
      ) =>
      String ->
      (a -> b) ->
      TestTree
    checkNotPacked protoTypeName convert =
      testProperty (showString protoTypeName $ showString ": " $
                    showsType @a $ showString " -> " $ showsType @b "") $
        propNotPacked @protoType convert

    propNotPacked ::
      forall (protoType :: Form.ProtoType) a b .
      ( Typeable a
      , Typeable b
      , Arbitrary a
      , Show a
      , FormE.FieldForm 'Form.Implicit protoType a
      , FormE.FieldForm 'Form.Implicit protoType b
      , FormE.FieldForm 'Form.Optional protoType (Maybe a)
      , FormE.FieldForm 'Form.Optional protoType (Maybe b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (Identity b)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [a]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType [b]
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector a)
      , FormE.FieldForm ('Form.Repeated 'Form.Unpacked) protoType (V.Vector b)
      ) =>
      (a -> b) ->
      Property
    propNotPacked convert =
      forAll (choose (1, 536870911)) $ \fieldNum ->
      case someNatVal fieldNum of
        Nothing ->
          property False  -- Should never happen.
        Just (SomeNat (_ :: Proxy num)) ->
          forAll arbitrary $ \a ->
          forAll arbitrary $ \(as :: [a])->
          let b :: b
              b = convert a
              bs :: [b]
              bs = map convert as
              asVec :: V.Vector a
              asVec = V.fromList as
              bsVec :: V.Vector b
              bsVec = V.fromList bs
          in
          counterexample "Implicit"
            (check1 @num @('Just 'Form.Implicit) @protoType a b) .&&.
          counterexample "Optional - Nothing"
            (check1 @num @('Just 'Form.Optional) @protoType (Nothing @a) (Nothing @b)) .&&.
          counterexample "Optional - Just"
            (check1 @num @('Just 'Form.Optional) @protoType (Just a) (Just b)) .&&.
          counterexample "inside oneof"
            (check1 @num @'Nothing @protoType (Just a) (Just b)) .&&.
          counterexample "Unpacked Identity"
            (check1 @num @('Just ('Form.Repeated 'Form.Unpacked)) @protoType (Identity a) (Identity b)) .&&.
          counterexample "Unpacked Forward"
            (check1 @num @('Just ('Form.Repeated 'Form.Unpacked)) @protoType as bs) .&&.
          counterexample "Unpacked Vector"
            (check1 @num @('Just ('Form.Repeated 'Form.Unpacked)) @protoType asVec bsVec)

    propPacked ::
      forall (protoType :: Form.ProtoType) a b .
      ( Typeable a
      , Typeable b
      , Arbitrary a
      , Show a
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (Identity a)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (Identity b)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType [a]
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType [b]
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (V.Vector a)
      , FormE.FieldForm ('Form.Repeated 'Form.Packed) protoType (V.Vector b)
      ) =>
      (a -> b) ->
      Property
    propPacked convert =
      forAll (choose (1, 536870911)) $ \fieldNum ->
      case someNatVal fieldNum of
        Nothing ->
          property False  -- Should never happen.
        Just (SomeNat (_ :: Proxy num)) ->
          forAll arbitrary $ \a ->
          forAll arbitrary $ \(as :: [a])->
          let b :: b
              b = convert a
              bs :: [b]
              bs = map convert as
              asVec :: V.Vector a
              asVec = V.fromList as
              bsVec :: V.Vector b
              bsVec = V.fromList bs
          in
          counterexample "Packed Identity"
            (check1 @num @('Just ('Form.Repeated 'Form.Packed)) @protoType (Identity a) (Identity b)) .&&.
          counterexample "Packed Forward"
            (check1 @num @('Just ('Form.Repeated 'Form.Packed)) @protoType as bs) .&&.
          counterexample "Packed Vector"
            (check1 @num @('Just ('Form.Repeated 'Form.Packed)) @protoType asVec bsVec)

    check1 ::
      forall (num :: Nat) (maybeRepetition :: Maybe Form.Cardinality)
             (protoType :: Form.ProtoType) a b .
      ( FormE.Field "name" a (TestMessage num maybeRepetition protoType)
      , FormE.Field "name" b (TestMessage num maybeRepetition protoType)
      , FormE.Distinct (TestMessage num maybeRepetition protoType)
                       (FormE.Occupy (TestMessage num maybeRepetition protoType) "name" '[])
      ) =>
      a ->
      b ->
      Property
    check1 a b =
      FormE.messageEncoderToLazyByteString
        (FormE.fieldsToMessage (FormE.field @"name" @a @(TestMessage num maybeRepetition protoType) a))
      ===
      FormE.messageEncoderToLazyByteString
        (FormE.fieldsToMessage (FormE.field @"name" @b @(TestMessage num maybeRepetition protoType) b))

    showsType :: forall a . Typeable a => ShowS
    showsType = showsTypeRep (typeRep (Proxy :: Proxy a))

--------------------------------------------------------------------------------
-- Decoding

decodeUnitTests :: TestTree
decodeUnitTests = testGroup "Decoder unit tests"
  [ decodeFromGoldens
  ]

decodeFromGoldens :: TestTree
decodeFromGoldens = testGroup "Decode golden encodings into key/value lists"
  [ check "trivial.bin"
  , check "trivial_negative.bin"
  , check "multiple_fields.bin"
  , check "signedints.bin"
  , check "with_nesting.bin"
  , check "with_enum0.bin"
  , check "with_enum1.bin"
  , check "with_repetition.bin"
  , check "with_bytes.bin"
  , check "with_nesting_repeated.bin"
  ]
  where
    check fp = testCase fp $ do
      kvs <- Decode.decodeWire <$> B.readFile (testFilesPfx <> fp)
      assertBool ("parsing " <> fp <> " into a key-value list succeeds") (isRight kvs)

--------------------------------------------------------------------------------
-- Parser

parserUnitTests :: TestTree
parserUnitTests = testGroup "Parser unit tests"
  [ parseFromGoldens
  ]

parseFromGoldens :: TestTree
parseFromGoldens = testGroup "Parse golden encodings"
  [ check "trivial.bin"               $ TP.Trivial 123
  , check "multiple_fields.bin"       $ TP.MultipleFields 1.23 (-0.5) 123 1234567890 "Hello, world!" True
  , check "signedints.bin"            $ TP.SignedInts (-42) (-84)
  , check "with_nesting.bin"          $ TP.WithNesting $ Just $ TP.WithNesting_Nested "123abc" 123456 [] []
  , check "with_enum0.bin"            $ TP.WithEnum $ Enumerated $ Right $ TP.WithEnum_TestEnumENUM1
  , check "with_enum1.bin"            $ TP.WithEnum $ Enumerated $ Right $ TP.WithEnum_TestEnumENUM2
  , check "with_repetition.bin"       $ TP.WithRepetition [1..5]
  , check "with_repeated_signed.bin"  $ TP.WithRepeatedSigned
      [ 0, -1, 1, -2, 2,
        0x3FFFFFFF, -0x40000000, 0x40000000, -0x40000001,
        0x7FFFFFFF, -0x7FFFFFFF - 1 ]
      [ 0, -1, 1, -2, 2,
        0x3FFFFFFFFFFFFFFF, -0x4000000000000000, 0x4000000000000000, -0x4000000000000001,
        0x7FFFFFFFFFFFFFFF, -0x7FFFFFFFFFFFFFFF - 1 ]
  , check "with_fixed.bin"            $ TP.WithFixed 16 (-123) 4096 (-4096)
  , check "with_bytes.bin"            $ TP.WithBytes (BC.pack "abc") (fromList $ map BC.pack ["abc","123"])
  , check "with_packing.bin"          $ TP.WithPacking [1,2,3] [1,2,3]
  , check "all_packed_types.bin"      $ TP.AllPackedTypes
                                          [1,2,3]
                                          [1,2,3]
                                          [-1,-2,-3]
                                          [-1,-2,-3]
                                          (fromList [1..3])
                                          (fromList [1..3])
                                          [1.0,2.0]
                                          [1.0,-1.0]
                                          (fromList [1,2,3])
                                          (fromList [1,2,3])
                                          [False,True]
                                          (Enumerated . Right <$> [TP.EFLD0, TP.EFLD1])
                                          (Enumerated . Right <$> [TP.EFLD0, TP.EFLD1])
  , check "with_nesting_repeated.bin" $ TP.WithNestingRepeated
                                          [ TP.WithNestingRepeated_Nested "123abc" 123456 [1,2,3,4] [5,6,7,8]
                                          , TP.WithNestingRepeated_Nested "abc123" 654321 [0,9,8,7] [6,5,4,3]
                                          ]
  , -- Checks parsing repeated embedded messages when one is expected (i.e.,
    -- this tests correct merging; this value was encoded as a
    -- WithNestingRepeated).
    check "with_nesting_repeated.bin" $ TP.WithNesting $ Just $ TP.WithNesting_Nested "abc123" 654321 [1,2,3,4,0,9,8,7] [5,6,7,8,6,5,4,3]
  , -- Checks that embedded message merging works correctly when fields have
    -- default values; this value was encoded as a WithNestingRepeatedInts
    check "with_nesting_ints.bin"     $ TP.WithNestingInts $ Just $ TP.NestedInts 2 2
  ]
  where
    check fp = testCase fp . testParser (testFilesPfx <> fp) fromByteString

testParser :: (Show a, Eq a)
           => FilePath -> (B.ByteString -> Either ParseError a) -> a -> IO ()
testParser fp p reference = do
  bs <- B.readFile fp
  case p bs of
    Left err        -> error $ "Got error: " ++ show err
    Right ourResult -> ourResult @?= reference

testDotProtoParse :: FilePath -> DotProto -> Assertion
testDotProtoParse file ast = do
  contents <- readFile file
  let path = metaModulePath $ protoMeta ast
  case parseProto path contents of
    Left err     -> error $ show err
    Right result -> ast @=? result

testDotProtoPrint :: DotProto -> String -> Assertion
testDotProtoPrint ast expected = expected @=? toProtoFileDef ast

testDotProtoRoundtrip :: DotProto -> Assertion
testDotProtoRoundtrip ast =
  let path = metaModulePath $ protoMeta ast in
  Right ast @=? parseProto path (toProtoFileDef ast)

dotProtoUnitTests :: TestTree
dotProtoUnitTests = testGroup ".proto parsing tests"
  [ dotProtoParseTrivial
  , dotProtoPrintTrivial
  , dotProtoParseEmptyStatement
  , dotProtoRoundtripTrivial
  , dotProtoRoundtripSimpleMessage
  , dotProtoRoundtripExtend
  , qcDotProtoRoundtrip
  ]

trivialDotProto :: DotProto
trivialDotProto = DotProto [] [] DotProtoNoPackage [] (DotProtoMeta (Path $ "test-files" NE.:| ["test_trivial"]))

dotProtoParseTrivial :: TestTree
dotProtoParseTrivial = testCase
  "Parse a content-less file" $
  testDotProtoParse (testFilesPfx <> "trivial.proto") trivialDotProto

dotProtoPrintTrivial :: TestTree
dotProtoPrintTrivial = testCase
  "Print a content-less DotProto" $
  testDotProtoPrint trivialDotProto "syntax = \"proto3\";"

dotProtoParseEmptyStatement :: TestTree
dotProtoParseEmptyStatement =
  testCase "Parse empty statements" (testDotProtoParse filePath dotProtoAST)
  where
    filePath :: FilePath
    filePath = testFilesPfx <> "test_proto_empty_field.proto"

    dotProtoAST :: DotProto
    dotProtoAST =
      DotProto [] []
        (DotProtoPackageSpec (Single "TestProtoEmptyField"))
        [ DotProtoEnum
            ""
            (Single "EnumWithEmptyField")
            [ DotProtoEnumField (Single "enum_foo") 0 []
            , DotProtoEnumField (Single "enum_bar") 1 []
            ]
        , DotProtoMessage
            ""
            (Single "MessageWithEmptyField")
            [ DotProtoMessageField (DotProtoField
                { dotProtoFieldNumber = 1
                , dotProtoFieldType = Prim UInt32
                , dotProtoFieldName = Single "foo"
                , dotProtoFieldOptions = []
                , dotProtoFieldComment = ""
                })
            , DotProtoMessageField (DotProtoField
                { dotProtoFieldNumber = 2
                , dotProtoFieldType = Prim UInt32
                , dotProtoFieldName = Single "bar"
                , dotProtoFieldOptions = []
                , dotProtoFieldComment = ""
                })
            ]
        ]
        (DotProtoMeta (Path (testFilesPfx NE.:| ["test_proto_empty_field.proto"])))

dotProtoRoundtripTrivial :: TestTree
dotProtoRoundtripTrivial = testCase
  "Printing then parsing a content-less DotProto yields an empty DotProto" $
  testDotProtoRoundtrip trivialDotProto

dotProtoSimpleMessage :: DotProto
dotProtoSimpleMessage = DotProto [] [] DotProtoNoPackage
  [ DotProtoMessage "" (Single "MessageTest")
      [ DotProtoMessageField $
          DotProtoField (fieldNumber 1) (Prim Int32) (Single "testfield") [] ""
      ]
  ]
  (DotProtoMeta (Path ("test-files" NE.:| ["simple"])))

dotProtoRoundtripSimpleMessage :: TestTree
dotProtoRoundtripSimpleMessage = testCase
  "Round-trip for a single flat message" $
  testDotProtoRoundtrip dotProtoSimpleMessage

qcDotProtoRoundtrip :: TestTree
qcDotProtoRoundtrip = testProperty
  "Round-trip for a randomly-generated .proto AST" roundtrip
  where
    roundtrip :: DotProto -> Property
    roundtrip ast = let generated = toProtoFileDef ast
                    in case parseProto fakePath generated of
                      Left err     -> error $ formatParseError err generated
                      Right result -> counterexample (formatMismatch ast generated result ) (ast == result)

    formatMismatch initial generated result = "AST changed during reparsing\n\nInitial AST:\n\n"
                                           ++ show initial
                                           ++ "\n\nGenerated .proto file:\n\n"
                                           ++ generated
                                           ++ "\n\nReparsed AST:\n\n"
                                           ++ show result
                                           ++ "\n\nRegenerated .proto file:\n\n"
                                           ++ (toProtoFileDef result)
    formatParseError err generated = "Parsec error:\n\n"
                                  ++ show err
                                  ++ "\n\nWhen attempting to parse:\n\n"
                                  ++ generated
                                  ++ "\n\nInitial AST:\n\n"

dotProtoRoundtripExtend :: TestTree
dotProtoRoundtripExtend =
  let expected :: DotProto
      expected = DotProto [] [] DotProtoNoPackage [] (DotProtoMeta (Path ("test-files" NE.:| ["test_proto_extend"])))
   in testCase
        "Round-trip for a message extension"
        (testDotProtoRoundtrip expected)

--------------------------------------------------------------------------------
-- Helpers

dotProtoFor :: (Named a, Message a) => Proxy# a -> DotProto
dotProtoFor proxy = DotProto [] [] DotProtoNoPackage
  [ DotProtoMessage
      "" (Single (nameOf proxy)) (DotProtoMessageField <$> dotProto proxy)
  ]
  (DotProtoMeta (Path $ "mypath" NE.:| []))

showDotProtoFor :: (Named a, Message a) => Proxy# a -> IO ()
showDotProtoFor proxy = putStrLn . toProtoFileDef $ dotProtoFor proxy

instance Arbitrary WireType where
  arbitrary = oneof $ map return [Varint, P.Fixed32, P.Fixed64, LengthDelimited]

testFilesPfx :: IsString a => a
testFilesPfx = "test-files/"