setup unit tests for Nix

Both for a `nix-build`, where `cabal` executable is not present, but
also just for using Stack with nix integration. With nix integration,
Stack will make a `nix-shell` where `stack` executable is not found.

We use "bare-bones" GHC to solve this. This GHC doesn't exactly respect
our Stack or Cabal configurations, so we need to avoid `cubicbezier`'s
`matrices` dependency so that `Data.Matrix` resolves to `matrix`
package.

Also, we don't error on compiler warnings when compiling test files
through Nix. I don't know if these warnings would show up on a regular
Stack or Cabal config (I'm on NixOS), so it's unaffected for those.

TL;DR: Added testing support for Nix build environments; Cabal and Stack
environments should be unchanged.
This commit is contained in:
Ai-Ya-Ya 2025-12-26 23:16:11 +00:00
commit a9b758ea0a
5 changed files with 137 additions and 16 deletions

View file

@ -60,6 +60,10 @@ op2LaTeX LeftParen = "("
op2LaTeX RightParen = ")" op2LaTeX RightParen = ")"
-- Some supportive types and instances. -- Some supportive types and instances.
instance Functor (Expr a) where
fmap f (Var x) = Var (f x)
fmap f (BinOp x op y) = BinOp (fmap f x) op (fmap f y)
fmap f (Paren l x r) = Paren l (fmap f x) r
instance Bifunctor Expr where instance Bifunctor Expr where
bimap _ g (Var x) = Var (g x) bimap _ g (Var x) = Var (g x)
@ -91,6 +95,9 @@ instance Bitraversable p => Traversable (Both p) where
-- | Make a bifunctor @f (a, -) (b, -)@ (namely @BiAlongside a b f@) out of @f@. -- | Make a bifunctor @f (a, -) (b, -)@ (namely @BiAlongside a b f@) out of @f@.
newtype BiAlongside a b f x y = BiAlongside { runBiAlongside :: f (a, x) (b, y) } newtype BiAlongside a b f x y = BiAlongside { runBiAlongside :: f (a, x) (b, y) }
instance Bifunctor p => Functor (BiAlongside a b p c) where
fmap f (BiAlongside x) = BiAlongside (bimap id (fmap f) x)
instance Bifunctor p => Bifunctor (BiAlongside a b p) where instance Bifunctor p => Bifunctor (BiAlongside a b p) where
bimap f g = BiAlongside . bimap (second f) (second g) . runBiAlongside bimap f g = BiAlongside . bimap (second f) (second g) . runBiAlongside

View file

@ -6,6 +6,7 @@
module Main (main) where module Main (main) where
import Codec.Picture import Codec.Picture
import Control.Monad (forM_, when, zipWithM_)
import Control.Monad.ST import Control.Monad.ST
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.Text (Text) import Data.Text (Text)

View file

@ -1,8 +1,11 @@
resolver: lts-18.15 resolver: lts-24.25
allow-newer: false allow-newer: false
packages: packages:
- . - .
extra-deps: [] extra-deps:
- geojson-4.1.1@sha256:f450e81d4fe1993f97c7cdb2b9743d54d2621c37b67d8a89536e5e09d33fd737,4233
- reanimate-svg-0.13.0.1@sha256:260957e61e2e290a6cbca1d129fa0da66d624a224d0016d3bdc76fa210c70cbd,2950

26
stack.yaml.lock Normal file
View file

@ -0,0 +1,26 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages:
- completed:
hackage: geojson-4.1.1@sha256:f450e81d4fe1993f97c7cdb2b9743d54d2621c37b67d8a89536e5e09d33fd737,4233
pantry-tree:
sha256: 0a1c1e2c6533b27efd472de9931e99d5cd05e0db568840e6e6dc014c8a2591cd
size: 2376
original:
hackage: geojson-4.1.1@sha256:f450e81d4fe1993f97c7cdb2b9743d54d2621c37b67d8a89536e5e09d33fd737,4233
- completed:
hackage: reanimate-svg-0.13.0.1@sha256:260957e61e2e290a6cbca1d129fa0da66d624a224d0016d3bdc76fa210c70cbd,2950
pantry-tree:
sha256: 93a217570c21431d5c9cf369058c8227aa1dee3a506edfed324550969c55a685
size: 39432
original:
hackage: reanimate-svg-0.13.0.1@sha256:260957e61e2e290a6cbca1d129fa0da66d624a224d0016d3bdc76fa210c70cbd,2950
snapshots:
- completed:
sha256: c0060a2ed91942bd6426f00617125fe9f0fef613dad0acc5f0ea10e8d499f799
size: 726335
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/25.yaml
original: lts-24.25

View file

@ -5,9 +5,11 @@ module UnitTests
) where ) where
import Control.Exception import Control.Exception
import Control.Monad (filterM)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.List (sort) import Data.List (sort)
import Data.Maybe (maybe)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as T import qualified Data.Text.IO as T
import System.Directory import System.Directory
@ -23,22 +25,78 @@ import Test.Tasty.HUnit
data BuildSystem data BuildSystem
= Cabal = Cabal
| Stack | Stack
| NixBuild NixBuildSystem
data NixBuildSystem
= NixCabal
| NixStack
-- required for nix build
nixGHCOptions :: [String]
nixGHCOptions =
[ "-XPackageImports"
, "-XPatternSynonyms"
, "-Wall"
, "-fno-ignore-asserts"
, "-DNO_HGEOMETRY"
, "-hide-package"
, "matrices"
]
findStackAutogen :: IO (Maybe FilePath)
findStackAutogen = do
let base = ".stack-work" </> "dist"
baseExists <- doesDirectoryExist base
if not baseExists
then return Nothing
else do
subDirs <- listDirectories base
case subDirs of
[] -> return Nothing
(arch:_) -> do
let archPath = base </> arch
cabalDirs <- listDirectories archPath
case cabalDirs of
[] -> return Nothing
(ver:_) -> return $ Just (archPath </> ver </> "build" </> "autogen")
includeBuild :: NixBuildSystem -> IO [String]
includeBuild NixCabal = return ["-idist/build/autogen"]
includeBuild NixStack = do
mbPath <- findStackAutogen
case mbPath of
Nothing -> return []
Just p -> return ["-i" ++ p]
listDirectories :: FilePath -> IO [FilePath]
listDirectories path = do
contents <- listDirectory path
filterM (\f -> doesDirectoryExist (path </> f)) contents
{-# NOINLINE buildSystem #-} {-# NOINLINE buildSystem #-}
buildSystem :: BuildSystem buildSystem :: BuildSystem
buildSystem = unsafePerformIO $ do buildSystem = unsafePerformIO $ do
exeCabal <- findExecutable "cabal"
exeStack <- findExecutable "stack"
newbuild <- doesDirectoryExist "dist-newstyle" newbuild <- doesDirectoryExist "dist-newstyle"
stack <- doesDirectoryExist ".stack-work" v1build <- doesDirectoryExist "dist"
if newbuild then pure Cabal stack <- doesDirectoryExist ".stack-work"
else if stack then pure Stack
else error "Unknown build system." return $ case (exeCabal, exeStack) of
(Nothing, _) | v1build -> NixBuild NixCabal
(_, Nothing) | stack -> NixBuild NixStack
(Just _, _) | newbuild -> Cabal
(_, Just _) | stack -> Stack
_ -> error "Unknown build system."
{-# NOINLINE unitTestsDisabled #-} {-# NOINLINE unitTestsDisabled #-}
unitTestsDisabled :: Bool unitTestsDisabled :: Bool
unitTestsDisabled = unitTestsDisabled =
case buildSystem of case buildSystem of
Cabal -> False Cabal -> False
Stack -> unsafePerformIO $ do NixBuild _ -> False
Stack -> unsafePerformIO $ do
(ret, _, _) <- readProcessWithExitCode "stack" ["exec","--","ghc", "-e", "Reanimate.duration Reanimate.Builtin.Documentation.drawCircle"] "" (ret, _, _) <- readProcessWithExitCode "stack" ["exec","--","ghc", "-e", "Reanimate.duration Reanimate.Builtin.Documentation.drawCircle"] ""
case ret of case ret of
ExitFailure{} -> pure True ExitFailure{} -> pure True
@ -64,16 +122,26 @@ unitTestFolder path = do
genGolden :: FilePath -> IO LBS.ByteString genGolden :: FilePath -> IO LBS.ByteString
genGolden path = do genGolden path = do
(inh, outh, errh, pid) <- case buildSystem of (inh, outh, errh, pid) <- case buildSystem of
Stack -> runInteractiveProcess "stack" ["runhaskell", path, "test"] Stack -> runInteractiveProcess "stack" ["runhaskell", path, "test"]
Nothing Nothing Nothing Nothing
Cabal -> runInteractiveProcess "cabal" ["v2-exec", "runhaskell", path, "test"] Cabal -> runInteractiveProcess "cabal" ["v2-exec", "runhaskell", path, "test"]
Nothing Nothing Nothing Nothing
NixBuild nixbuild -> do
nixIncludeDir <- includeBuild nixbuild
runInteractiveProcess "ghc"
( ["-isrc", "-iunix"]
++ nixIncludeDir
++ nixGHCOptions
++ ["--run", path, "--", "test"]
) Nothing Nothing
-- hSetBinaryMode outh True -- hSetBinaryMode outh True
-- hSetNewlineMode outh universalNewlineMode -- hSetNewlineMode outh universalNewlineMode
hClose inh hClose inh
out <- BS.hGetContents outh out <- BS.hGetContents outh
err <- T.hGetContents errh err <- T.hGetContents errh
code <- waitForProcess pid code <- waitForProcess pid
case code of case code of
ExitSuccess -> return $ LBS.fromChunks [out] ExitSuccess -> return $ LBS.fromChunks [out]
ExitFailure{} -> error $ "Failed to run: " ++ T.unpack err ExitFailure{} -> error $ "Failed to run: " ++ T.unpack err
@ -87,9 +155,17 @@ compileTestFolder path = do
[ testCase file $ do [ testCase file $ do
(ret, _stdout, err) <- (ret, _stdout, err) <-
case buildSystem of case buildSystem of
Stack -> readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) "" Stack -> readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) ""
Cabal -> readProcessWithExitCode "cabal" Cabal -> readProcessWithExitCode "cabal"
(["v2-exec","ghc","--", "-package", "reanimate", fullPath] ++ ghcOpts) "" (["v2-exec","ghc","--", "-package", "reanimate", fullPath] ++ ghcOpts) ""
NixBuild nixbuild -> do
nixIncludeDir <- includeBuild nixbuild
readProcessWithExitCode "ghc"
( ["-isrc", "-iunix"]
++ nixIncludeDir
++ nixGHCOptions
++ [fullPath] ++ (init ghcOpts)
) ""
_ <- evaluate (length err) _ <- evaluate (length err)
case ret of case ret of
ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err
@ -100,7 +176,7 @@ compileTestFolder path = do
, notElem (replaceExtension file "golden") goldenFiles , notElem (replaceExtension file "golden") goldenFiles
] ]
where where
ghcOpts = ["-fno-code", "-O0", "-Werror", "-Wall"] ghcOpts = ["-fno-code", "-O0", "-Wall", "-Werror"]
compileVideoFolder :: FilePath -> IO TestTree compileVideoFolder :: FilePath -> IO TestTree
compileVideoFolder _ | unitTestsDisabled = return $ testGroup "videos (disabled)" [] compileVideoFolder _ | unitTestsDisabled = return $ testGroup "videos (disabled)" []
@ -113,8 +189,16 @@ compileVideoFolder path = do
[ testCase dir $ do [ testCase dir $ do
(ret, _stdout, err) <- (ret, _stdout, err) <-
case buildSystem of case buildSystem of
Stack -> readProcessWithExitCode "stack" (["ghc","--", "-i"++path</>dir, fullPath] ++ ghcOpts) "" Stack -> readProcessWithExitCode "stack" (["ghc","--", "-i"++path</>dir, fullPath] ++ ghcOpts) ""
Cabal -> readProcessWithExitCode "cabal" (["v2-exec", "ghc","--", "-package", "reanimate", "-i"++path</>dir, fullPath] ++ ghcOpts) "" Cabal -> readProcessWithExitCode "cabal" (["v2-exec", "ghc","--", "-package", "reanimate", "-i"++path</>dir, fullPath] ++ ghcOpts) ""
NixBuild nixbuild -> do
nixIncludeDir <- includeBuild nixbuild
readProcessWithExitCode "ghc"
( ["-isrc", "-iunix", "-i"++path</>dir]
++ nixIncludeDir
++ nixGHCOptions
++ [fullPath] ++ ghcOpts
) ""
_ <- evaluate (length err) _ <- evaluate (length err)
case ret of case ret of
ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err