identicon-0.2.3: tests/Spec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
module Main (main) where
import Codec.Picture
import Control.Monad
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.Function (on)
import Data.Proxy
import Data.Word (Word8)
import Graphics.Identicon
import Graphics.Identicon.Primitive
import Test.Hspec
import Test.QuickCheck hiding (oneof)
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
describe "renderIdenticon" renderIdenticonSpec
ω gen0 [0x00, 0x00, 0x00, 0x00] "data-examples/identicon-00.png"
ω gen0 [0x8f, 0x55, 0x6e, 0x93] "data-examples/identicon-01.png"
ω gen0 [0x31, 0xa8, 0x29, 0x5b] "data-examples/identicon-02.png"
ω
gen1
[0x3e, 0xf1, 0xde, 0x08, 0x85, 0x0b, 0x9c, 0x81, 0x25, 0xf0, 0x53, 0x0b]
"data-examples/identicon-10.png"
ω
gen1
[0xa9, 0xf7, 0x66, 0xf0, 0xd7, 0xf9, 0xb0, 0x8e, 0x57, 0x21, 0xc5, 0x06]
"data-examples/identicon-11.png"
ω
gen1
[0x23, 0x29, 0x2d, 0x29, 0x2f, 0x05, 0x28, 0x11, 0x1e, 0x0e, 0x0d, 0x06]
"data-examples/identicon-12.png"
ω
gen2
[0xcf, 0xe7, 0xb9, 0x49, 0x93, 0xb1, 0x01]
"data-examples/identicon-20.png"
ω
gen2
[0xc8, 0xa4, 0xda, 0xa1, 0xe9, 0x93, 0x86]
"data-examples/identicon-21.png"
ω
gen2
[0xf9, 0x9b, 0xb7, 0x11, 0x5b, 0xca, 0x00]
"data-examples/identicon-22.png"
describe "Semigroup and Monoid instances of Layer" $ do
it "mempty always returns black pixel" $
property $ \w h x y ->
let (Layer f) = mempty
in f w h x y `shouldBe` PixelRGB8 0 0 0
it "mappend combines layers" $
property $ \w'' h'' x'' y'' ->
let w = w'' `mod` 10
h = h'' `mod` 10
x = x'' `mod` 10
y = y'' `mod` 10
(Layer f) = Layer a `mappend` Layer b
a w' h' x' y' = PixelRGB8 (g $ w' + h') (g $ h' + x') (g $ x' + y')
b w' h' x' y' = PixelRGB8 (g $ w' + y') (g $ h' + w') (g $ x' + w')
g = fromIntegral
in f w h x y
`shouldBe` PixelRGB8
(g $ w + h + w + y)
(g $ h + x + h + w)
(g $ x + y + x + w)
renderIdenticonSpec :: Spec
renderIdenticonSpec = do
context "when we pass too short byte string" $
it "returns Nothing" $
shouldBeNothing (gen0 100 100 "aaa")
context "when we pass nonsential width value" $
it "returns Nothing" $
shouldBeNothing (gen0 0 100 "aaaa")
context "when we pass nonsential height value" $
it "returns Nothing" $
shouldBeNothing (gen0 100 0 "aaaa")
----------------------------------------------------------------------------
-- Identicon generators
type Gen0 = Identicon 4 :+ Consumer 4
gen0 :: Int -> Int -> ByteString -> Maybe (Image PixelRGB8)
gen0 = renderIdenticon (Proxy :: Proxy Gen0) i
where
i = Identicon :+ a
a r g b n =
rsym $
onGrid 4 4 n $
circle $
gradientLR (edge . mid) black (PixelRGB8 r g b)
type Gen1 = Identicon 12 :+ Consumer 4 :+ Consumer 4 :+ Consumer 4
gen1 :: Int -> Int -> ByteString -> Maybe (Image PixelRGB8)
gen1 = renderIdenticon (Proxy :: Proxy Gen1) i
where
i = Identicon :+ a0 :+ a1 :+ a2
a0 r g b n =
hsym $
onGrid 3 3 n $
gradientTLBR id black (PixelRGB8 r g b)
a1 r g b n =
vsym $
onGrid 4 4 n $
gradientXY id black (PixelRGB8 r g b)
a2 r g b n =
hvsym $
onGrid 5 5 n $
circle $
gradientTRBL mid (PixelRGB8 r g b) black
type Gen2 = Identicon 7 :+ Consumer 3 :+ Consumer 4
gen2 :: Int -> Int -> ByteString -> Maybe (Image PixelRGB8)
gen2 = renderIdenticon (Proxy :: Proxy Gen2) i
where
i = Identicon :+ a0 :+ a1
a0 r g b = gradientTB edge (PixelRGB8 r g b) black
a1 r g b n = oneof [gradientXY id black, color] n (PixelRGB8 r g b)
----------------------------------------------------------------------------
-- Helpers
-- | A helper to check that 'Nothing' is returned without requiring that
-- argument is an instance of 'Show' or 'Eq' type class.
shouldBeNothing :: Maybe a -> Expectation
shouldBeNothing m =
case m of
Nothing -> return ()
Just _ ->
expectationFailure "it returned not Nothing"
-- | A shorthand for test cases.
ω ::
-- | Identicon generator
(Int -> Int -> ByteString -> Maybe (Image PixelRGB8)) ->
-- | Input to use for identicon generation
[Word8] ->
-- | Where to get image to compare with
FilePath ->
Spec
ω f bs path =
describe path $
it ("reproduces " ++ path) $
compareWithFile f (B.pack bs) path
-- | Take function that produces identicon, binary input for it, path to
-- already rendered identicon and compare them. Fail with informative
-- message if they differ.
compareWithFile ::
-- | Identicon generator
(Int -> Int -> ByteString -> Maybe (Image PixelRGB8)) ->
-- | Input to use for identicon generation
ByteString ->
-- | Where to get image to compare with
FilePath ->
Expectation
compareWithFile f bs path = do
(Right (ImageRGB8 img)) <- readImage path
let mimg = f (imageWidth img) (imageHeight img) bs
case mimg of
Nothing -> expectationFailure "failed to generate an image"
Just img' ->
unless (imageEq img img') $
expectationFailure ("generated image is different from " ++ path)
-- | Since 'Image' for some reason is not an instance of 'Eq', we use this
-- to compare 'Image's.
imageEq :: Image PixelRGB8 -> Image PixelRGB8 -> Bool
imageEq a b =
((==) `on` imageWidth) a b
&& ((==) `on` imageHeight) a b
&& ((==) `on` imageData) a b