phino-0.0.0.53: test/FilterSpec.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT
{- | Tests for the Filter module that provides include and exclude
functions for filtering phi-calculus programs by FQN expressions.
-}
module FilterSpec where
import AST
import Control.Monad (forM_, when)
import Data.Aeson
import Data.Yaml qualified as Yaml
import Encoding (Encoding (UNICODE))
import Filter qualified as F
import GHC.Generics (Generic)
import Lining (LineFormat (MULTILINE))
import Misc
import Parser (parseExpressionThrows, parseProgramThrows)
import Printer (printProgram')
import Sugar (SugarType (SALTY))
import System.FilePath
import Test.Hspec
data YamlPack = YamlPack
{ program :: String
, shown :: [String]
, hidden :: [String]
, result :: String
}
deriving (Generic, Show, FromJSON)
yamlPack :: FilePath -> IO YamlPack
yamlPack = Yaml.decodeFileThrow
spec :: Spec
spec = do
describe "filter packs" $ do
let resources = "test-resources/filter-packs"
packs <- runIO (allPathsIn resources)
forM_
packs
( \pth -> it (makeRelative resources pth) $ do
YamlPack{..} <- yamlPack pth
prog <- parseProgramThrows program
included <- traverse parseExpressionThrows shown
excluded <- traverse parseExpressionThrows hidden
res <- parseProgramThrows result
let [(prog', _)] = F.exclude (F.include [(prog, Nothing)] included) excluded
prog' `shouldBe` res
when
(prog' /= res)
( expectationFailure
( "Expected:\n"
++ printProgram' res (SALTY, UNICODE, MULTILINE)
++ "\nbut got:\n"
++ printProgram' prog' (SALTY, UNICODE, MULTILINE)
)
)
)
describe "exclude with empty expression list" $
it "returns programs unchanged" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let result = F.exclude [(prog, Nothing)] []
result `shouldBe` [(prog, Nothing)]
describe "exclude with empty program list" $
it "returns empty list" $ do
fqn <- parseExpressionThrows "Q.x"
let result = F.exclude [] [fqn]
result `shouldBe` []
describe "exclude with non-formation program" $
it "returns program unchanged" $ do
let prog = Program ExGlobal
fqn = ExDispatch ExGlobal (AtLabel "x")
result = F.exclude [(prog, Nothing)] [fqn]
result `shouldBe` [(prog, Nothing)]
describe "exclude with termination program" $
it "returns program unchanged" $ do
let prog = Program ExTermination
fqn = ExDispatch ExGlobal (AtLabel "x")
result = F.exclude [(prog, Nothing)] [fqn]
result `shouldBe` [(prog, Nothing)]
describe "exclude with non-dispatch FQN" $
it "handles non-dispatch expression gracefully" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let fqn = ExGlobal
[(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
prog' `shouldBe` prog
describe "exclude with xi FQN" $
it "handles xi expression gracefully" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let fqn = ExThis
[(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
prog' `shouldBe` prog
describe "exclude multiple programs" $
it "processes all programs in list" $ do
prog1 <- parseProgramThrows "{[[ x -> Q, y -> $ ]]}"
prog2 <- parseProgramThrows "{[[ a -> Q, b -> $ ]]}"
fqn <- parseExpressionThrows "Q.x"
let result = F.exclude [(prog1, Nothing), (prog2, Nothing)] [fqn]
length result `shouldBe` 2
describe "include with empty expression list" $
it "returns programs unchanged" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let result = F.include [(prog, Nothing)] []
result `shouldBe` [(prog, Nothing)]
describe "include with empty program list" $
it "returns empty list" $ do
fqn <- parseExpressionThrows "Q.x"
let result = F.include [] [fqn]
result `shouldBe` []
describe "include with non-formation program" $
it "returns empty formation" $ do
let prog = Program ExGlobal
fqn = ExDispatch ExGlobal (AtLabel "x")
[(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include with termination program" $
it "returns empty formation" $ do
let prog = Program ExTermination
fqn = ExDispatch ExGlobal (AtLabel "x")
[(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include with non-existent attribute" $
it "returns empty formation" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
fqn <- parseExpressionThrows "Q.nonexistent"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include with non-matching nested attribute" $
it "returns empty formation when path doesnt exist" $ do
prog <- parseProgramThrows "{[[ x -> [[ y -> Q ]] ]]}"
fqn <- parseExpressionThrows "Q.x.nonexistent"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include with non-dispatch FQN" $
it "handles non-dispatch expression gracefully" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let fqn = ExGlobal
[(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include with xi FQN" $
it "handles xi expression gracefully" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
let fqn = ExThis
[(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "include multiple programs" $
it "processes all programs in list" $ do
prog1 <- parseProgramThrows "{[[ x -> Q, y -> $ ]]}"
prog2 <- parseProgramThrows "{[[ a -> Q, b -> $ ]]}"
fqn <- parseExpressionThrows "Q.x"
let result = F.include [(prog1, Nothing), (prog2, Nothing)] [fqn]
length result `shouldBe` 2
describe "include with nested formation not matching" $
it "skips non-matching bindings to find target" $ do
prog <- parseProgramThrows "{[[ a -> Q, x -> [[ y -> Q ]] ]]}"
fqn <- parseExpressionThrows "Q.x.y"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn]
isFormation (Program (ExFormation _)) = True
isFormation _ = False
prog' `shouldSatisfy` isFormation
describe "include with deep nested path" $
it "follows multi-level FQN" $ do
prog <- parseProgramThrows "{[[ a -> [[ b -> [[ c -> Q ]] ]] ]]}"
fqn <- parseExpressionThrows "Q.a.b.c"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn]
isFormation (Program (ExFormation _)) = True
isFormation _ = False
prog' `shouldSatisfy` isFormation
describe "exclude with deep nested path" $
it "removes from multi-level FQN" $ do
prog <- parseProgramThrows "{[[ a -> [[ b -> [[ c -> Q, d -> $ ]] ]] ]]}"
fqn <- parseExpressionThrows "Q.a.b.c"
let [(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
isFormation (Program (ExFormation _)) = True
isFormation _ = False
prog' `shouldSatisfy` isFormation
describe "exclude preserves rule metadata" $
it "keeps Nothing rule through exclude" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
fqn <- parseExpressionThrows "Q.y"
let [(_, rule)] = F.exclude [(prog, Nothing)] [fqn]
rule `shouldBe` Nothing
describe "include preserves rule metadata" $
it "keeps Nothing rule through include" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
fqn <- parseExpressionThrows "Q.x"
let [(_, rule)] = F.include [(prog, Nothing)] [fqn]
rule `shouldBe` Nothing
describe "exclude with non-formation nested binding" $
it "handles tau with non-formation value" $ do
prog <- parseProgramThrows "{[[ x -> Q ]]}"
fqn <- parseExpressionThrows "Q.x.y"
let [(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
prog' `shouldBe` prog
describe "include with formation lacking target binding" $
it "returns empty formation when binding not found" $ do
prog <- parseProgramThrows "{[[ x -> [[ a -> Q ]] ]]}"
fqn <- parseExpressionThrows "Q.y.z"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn]
prog' `shouldBe` Program (ExFormation [BiVoid AtRho])
describe "exclude with void binding" $
it "handles void bindings correctly" $ do
prog <- parseProgramThrows "{[[ x -> ?, y -> Q ]]}"
fqn <- parseExpressionThrows "Q.x"
let [(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
Program (ExFormation bds) = prog'
length bds `shouldBe` 2
describe "exclude with meta binding" $
it "handles meta bindings correctly" $ do
let prog = Program (ExFormation [BiMeta "B", BiTau (AtLabel "x") ExGlobal])
fqn = ExDispatch ExGlobal (AtLabel "x")
[(prog', _)] = F.exclude [(prog, Nothing)] [fqn]
Program (ExFormation bds) = prog'
length bds `shouldBe` 1
describe "include uses only first FQN" $
it "ignores additional FQNs in list" $ do
prog <- parseProgramThrows "{[[ x -> Q, y -> $ ]]}"
fqn1 <- parseExpressionThrows "Q.x"
fqn2 <- parseExpressionThrows "Q.y"
let [(prog', _)] = F.include [(prog, Nothing)] [fqn1, fqn2]
Program (ExFormation bds) = prog'
names = [attr | BiTau attr _ <- bds]
AtLabel "x" `elem` names `shouldBe` True