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 = ")"
-- 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
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@.
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
bimap f g = BiAlongside . bimap (second f) (second g) . runBiAlongside

View file

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

View file

@ -1,8 +1,11 @@
resolver: lts-18.15
resolver: lts-24.25
allow-newer: false
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
import Control.Exception
import Control.Monad (filterM)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.List (sort)
import Data.Maybe (maybe)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.Directory
@ -23,22 +25,78 @@ import Test.Tasty.HUnit
data BuildSystem
= Cabal
| 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 #-}
buildSystem :: BuildSystem
buildSystem = unsafePerformIO $ do
exeCabal <- findExecutable "cabal"
exeStack <- findExecutable "stack"
newbuild <- doesDirectoryExist "dist-newstyle"
stack <- doesDirectoryExist ".stack-work"
if newbuild then pure Cabal
else if stack then pure Stack
else error "Unknown build system."
v1build <- doesDirectoryExist "dist"
stack <- doesDirectoryExist ".stack-work"
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 #-}
unitTestsDisabled :: Bool
unitTestsDisabled =
case buildSystem of
Cabal -> False
Stack -> unsafePerformIO $ do
Cabal -> False
NixBuild _ -> False
Stack -> unsafePerformIO $ do
(ret, _, _) <- readProcessWithExitCode "stack" ["exec","--","ghc", "-e", "Reanimate.duration Reanimate.Builtin.Documentation.drawCircle"] ""
case ret of
ExitFailure{} -> pure True
@ -64,16 +122,26 @@ unitTestFolder path = do
genGolden :: FilePath -> IO LBS.ByteString
genGolden path = do
(inh, outh, errh, pid) <- case buildSystem of
Stack -> runInteractiveProcess "stack" ["runhaskell", path, "test"]
Stack -> runInteractiveProcess "stack" ["runhaskell", path, "test"]
Nothing Nothing
Cabal -> runInteractiveProcess "cabal" ["v2-exec", "runhaskell", path, "test"]
Cabal -> runInteractiveProcess "cabal" ["v2-exec", "runhaskell", path, "test"]
Nothing Nothing
NixBuild nixbuild -> do
nixIncludeDir <- includeBuild nixbuild
runInteractiveProcess "ghc"
( ["-isrc", "-iunix"]
++ nixIncludeDir
++ nixGHCOptions
++ ["--run", path, "--", "test"]
) Nothing Nothing
-- hSetBinaryMode outh True
-- hSetNewlineMode outh universalNewlineMode
-- hSetNewlineMode outh universalNewlineMode
hClose inh
out <- BS.hGetContents outh
err <- T.hGetContents errh
code <- waitForProcess pid
case code of
ExitSuccess -> return $ LBS.fromChunks [out]
ExitFailure{} -> error $ "Failed to run: " ++ T.unpack err
@ -87,9 +155,17 @@ compileTestFolder path = do
[ testCase file $ do
(ret, _stdout, err) <-
case buildSystem of
Stack -> readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) ""
Cabal -> readProcessWithExitCode "cabal"
Stack -> readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) ""
Cabal -> readProcessWithExitCode "cabal"
(["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)
case ret of
ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err
@ -100,7 +176,7 @@ compileTestFolder path = do
, notElem (replaceExtension file "golden") goldenFiles
]
where
ghcOpts = ["-fno-code", "-O0", "-Werror", "-Wall"]
ghcOpts = ["-fno-code", "-O0", "-Wall", "-Werror"]
compileVideoFolder :: FilePath -> IO TestTree
compileVideoFolder _ | unitTestsDisabled = return $ testGroup "videos (disabled)" []
@ -113,8 +189,16 @@ compileVideoFolder path = do
[ testCase dir $ do
(ret, _stdout, err) <-
case buildSystem of
Stack -> readProcessWithExitCode "stack" (["ghc","--", "-i"++path</>dir, fullPath] ++ ghcOpts) ""
Cabal -> readProcessWithExitCode "cabal" (["v2-exec", "ghc","--", "-package", "reanimate", "-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) ""
NixBuild nixbuild -> do
nixIncludeDir <- includeBuild nixbuild
readProcessWithExitCode "ghc"
( ["-isrc", "-iunix", "-i"++path</>dir]
++ nixIncludeDir
++ nixGHCOptions
++ [fullPath] ++ ghcOpts
) ""
_ <- evaluate (length err)
case ret of
ExitFailure{} -> assertFailure $ "Failed to compile:\n" ++ err