packages feed

aihc-parser-1.0.0.2: test/Test/Parser/Suite.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Parser.Suite
  ( parserGoldenTests,
  )
where

import Control.Monad (unless, when)
import Data.Text qualified as T
import ParserEquivalent qualified as PE
import ParserGolden qualified as PG
import System.FilePath (takeExtension)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, testCaseInfo)

parserGoldenTests :: IO TestTree
parserGoldenTests = do
  goldenTests <- parserGoldenGroup
  equivalentTests <- parserEquivalentGroup
  pure
    ( testGroup
        "parser-fixtures"
        [ goldenTests,
          equivalentTests
        ]
    )

parserGoldenGroup :: IO TestTree
parserGoldenGroup = do
  exprCases <- PG.loadExprCases
  importCases <- PG.loadImportCases
  moduleCases <- PG.loadModuleCases
  patternCases <- PG.loadPatternCases
  pragmaCases <- PG.loadPragmaCases

  exprChecks <- mapM mkExprCaseTest exprCases
  importChecks <- mapM mkModuleCaseTest importCases
  moduleChecks <- mapM mkModuleCaseTest moduleCases
  patternChecks <- mapM mkPatternCaseTest patternCases
  pragmaChecks <- mapM mkModuleCaseTest pragmaCases

  exprSummary <- mkSummaryTest "expr" PG.evaluateExprCase exprCases
  importSummary <- mkSummaryTest "import" PG.evaluateModuleCase importCases
  moduleSummary <- mkSummaryTest "module" PG.evaluateModuleCase moduleCases
  patternSummary <- mkSummaryTest "pattern" PG.evaluatePatternCase patternCases
  pragmaSummary <- mkSummaryTest "pragma" PG.evaluateModuleCase pragmaCases
  combinedSummary <- mkCombinedSummary exprCases importCases moduleCases patternCases pragmaCases

  pure
    ( testGroup
        "parser-golden"
        [ fixtureValidationTests,
          testGroup "expr" (exprChecks <> [exprSummary]),
          testGroup "import" (importChecks <> [importSummary]),
          testGroup "module" (moduleChecks <> [moduleSummary]),
          testGroup "pattern" (patternChecks <> [patternSummary]),
          testGroup "pragma" (pragmaChecks <> [pragmaSummary]),
          combinedSummary
        ]
    )

parserEquivalentGroup :: IO TestTree
parserEquivalentGroup = do
  exprCases <- PE.loadExprCases
  moduleCases <- PE.loadModuleCases
  declCases <- PE.loadDeclCases
  patternCases <- PE.loadPatternCases

  exprChecks <- mapM mkEquivalentExprCaseTest exprCases
  moduleChecks <- mapM mkEquivalentModuleCaseTest moduleCases
  declChecks <- mapM mkEquivalentDeclCaseTest declCases
  patternChecks <- mapM mkEquivalentPatternCaseTest patternCases

  exprSummary <- mkEquivalentSummaryTest "expr" PE.evaluateExprCase exprCases
  moduleSummary <- mkEquivalentSummaryTest "module" PE.evaluateModuleCase moduleCases
  declSummary <- mkEquivalentSummaryTest "decl" PE.evaluateDeclCase declCases
  patternSummary <- mkEquivalentSummaryTest "pattern" PE.evaluatePatternCase patternCases
  combinedSummary <- mkEquivalentCombinedSummary exprCases moduleCases declCases patternCases

  pure
    ( testGroup
        "parser-equivalent"
        [ equivalenceFixtureValidationTests,
          testGroup "expr" (exprChecks <> [exprSummary]),
          testGroup "module" (moduleChecks <> [moduleSummary]),
          testGroup "decl" (declChecks <> [declSummary]),
          testGroup "pattern" (patternChecks <> [patternSummary]),
          combinedSummary
        ]
    )

mkExprCaseTest :: PG.ParserCase -> IO TestTree
mkExprCaseTest meta = pure $ case PG.caseStatus meta of
  PG.StatusXFail -> testCaseInfo (PG.caseId meta) (xfailDetails (PG.evaluateExprCase meta) <* assertExprCase meta)
  _ -> testCase (PG.caseId meta) (assertExprCase meta)

mkModuleCaseTest :: PG.ParserCase -> IO TestTree
mkModuleCaseTest meta = pure $ case PG.caseStatus meta of
  PG.StatusXFail -> testCaseInfo (PG.caseId meta) (xfailDetails (PG.evaluateModuleCase meta) <* assertModuleCase meta)
  _ -> testCase (PG.caseId meta) (assertModuleCase meta)

mkPatternCaseTest :: PG.ParserCase -> IO TestTree
mkPatternCaseTest meta = pure $ case PG.caseStatus meta of
  PG.StatusXFail -> testCaseInfo (PG.caseId meta) (xfailDetails (PG.evaluatePatternCase meta) <* assertPatternCase meta)
  _ -> testCase (PG.caseId meta) (assertPatternCase meta)

xfailDetails :: (PG.Outcome, String) -> IO String
xfailDetails (outcome, details) = do
  case outcome of
    PG.OutcomeXFail -> pure ()
    _ -> assertFailure ("expected xfail outcome, got: " <> show outcome)
  pure details

mkSummaryTest :: String -> (PG.ParserCase -> (PG.Outcome, String)) -> [PG.ParserCase] -> IO TestTree
mkSummaryTest label evaluateCase cases = do
  let outcomes = map (evaluate evaluateCase) cases
  pure $ testCase (label <> " summary") (assertNoRegressions label outcomes)

mkCombinedSummary :: [PG.ParserCase] -> [PG.ParserCase] -> [PG.ParserCase] -> [PG.ParserCase] -> [PG.ParserCase] -> IO TestTree
mkCombinedSummary exprCases importCases moduleCases patternCases pragmaCases = do
  let exprOutcomes = map (evaluate PG.evaluateExprCase) exprCases
      importOutcomes = map (evaluate PG.evaluateModuleCase) importCases
      moduleOutcomes = map (evaluate PG.evaluateModuleCase) moduleCases
      patternOutcomes = map (evaluate PG.evaluatePatternCase) patternCases
      pragmaOutcomes = map (evaluate PG.evaluateModuleCase) pragmaCases
      outcomes = exprOutcomes <> importOutcomes <> moduleOutcomes <> patternOutcomes <> pragmaOutcomes
  pure $ testCase "summary" (assertNoRegressions "parser golden" outcomes)

assertExprCase :: PG.ParserCase -> Assertion
assertExprCase = assertCaseWith PG.evaluateExprCase

assertModuleCase :: PG.ParserCase -> Assertion
assertModuleCase = assertCaseWith PG.evaluateModuleCase

assertPatternCase :: PG.ParserCase -> Assertion
assertPatternCase = assertCaseWith PG.evaluatePatternCase

mkEquivalentExprCaseTest :: PE.EquivalentCase -> IO TestTree
mkEquivalentExprCaseTest meta = pure $ case PE.caseStatus meta of
  PE.StatusXFail -> testCaseInfo (PE.caseId meta) (equivalentXFailDetails (PE.evaluateExprCase meta) <* assertEquivalentExprCase meta)
  _ -> testCase (PE.caseId meta) (assertEquivalentExprCase meta)

mkEquivalentModuleCaseTest :: PE.EquivalentCase -> IO TestTree
mkEquivalentModuleCaseTest meta = pure $ case PE.caseStatus meta of
  PE.StatusXFail -> testCaseInfo (PE.caseId meta) (equivalentXFailDetails (PE.evaluateModuleCase meta) <* assertEquivalentModuleCase meta)
  _ -> testCase (PE.caseId meta) (assertEquivalentModuleCase meta)

mkEquivalentDeclCaseTest :: PE.EquivalentCase -> IO TestTree
mkEquivalentDeclCaseTest meta = pure $ case PE.caseStatus meta of
  PE.StatusXFail -> testCaseInfo (PE.caseId meta) (equivalentXFailDetails (PE.evaluateDeclCase meta) <* assertEquivalentDeclCase meta)
  _ -> testCase (PE.caseId meta) (assertEquivalentDeclCase meta)

mkEquivalentPatternCaseTest :: PE.EquivalentCase -> IO TestTree
mkEquivalentPatternCaseTest meta = pure $ case PE.caseStatus meta of
  PE.StatusXFail -> testCaseInfo (PE.caseId meta) (equivalentXFailDetails (PE.evaluatePatternCase meta) <* assertEquivalentPatternCase meta)
  _ -> testCase (PE.caseId meta) (assertEquivalentPatternCase meta)

equivalentXFailDetails :: (PE.Outcome, String) -> IO String
equivalentXFailDetails (outcome, details) = do
  case outcome of
    PE.OutcomeXFail -> pure ()
    _ -> assertFailure ("expected xfail outcome, got: " <> show outcome)
  pure details

mkEquivalentSummaryTest :: String -> (PE.EquivalentCase -> (PE.Outcome, String)) -> [PE.EquivalentCase] -> IO TestTree
mkEquivalentSummaryTest label evaluateCase cases = do
  let outcomes = map (evaluateEquivalent evaluateCase) cases
  pure $ testCase (label <> " summary") (assertNoEquivalentRegressions label outcomes)

mkEquivalentCombinedSummary :: [PE.EquivalentCase] -> [PE.EquivalentCase] -> [PE.EquivalentCase] -> [PE.EquivalentCase] -> IO TestTree
mkEquivalentCombinedSummary exprCases moduleCases declCases patternCases = do
  let exprOutcomes = map (evaluateEquivalent PE.evaluateExprCase) exprCases
      moduleOutcomes = map (evaluateEquivalent PE.evaluateModuleCase) moduleCases
      declOutcomes = map (evaluateEquivalent PE.evaluateDeclCase) declCases
      patternOutcomes = map (evaluateEquivalent PE.evaluatePatternCase) patternCases
      outcomes = exprOutcomes <> moduleOutcomes <> declOutcomes <> patternOutcomes
  pure $ testCase "summary" (assertNoEquivalentRegressions "parser equivalence" outcomes)

assertEquivalentExprCase :: PE.EquivalentCase -> Assertion
assertEquivalentExprCase = assertEquivalentCaseWith PE.evaluateExprCase

assertEquivalentModuleCase :: PE.EquivalentCase -> Assertion
assertEquivalentModuleCase = assertEquivalentCaseWith PE.evaluateModuleCase

assertEquivalentDeclCase :: PE.EquivalentCase -> Assertion
assertEquivalentDeclCase = assertEquivalentCaseWith PE.evaluateDeclCase

assertEquivalentPatternCase :: PE.EquivalentCase -> Assertion
assertEquivalentPatternCase = assertEquivalentCaseWith PE.evaluatePatternCase

assertCaseWith :: (PG.ParserCase -> (PG.Outcome, String)) -> PG.ParserCase -> Assertion
assertCaseWith evaluateCase meta =
  case evaluateCase meta of
    (PG.OutcomeFail, details) ->
      assertFailure
        ( "Regression in parser case "
            <> PG.caseId meta
            <> " ("
            <> PG.caseCategory meta
            <> ") expected "
            <> show (PG.caseStatus meta)
            <> " reason="
            <> PG.caseReason meta
            <> " details="
            <> details
        )
    (PG.OutcomeXPass, details) ->
      assertFailure
        ( "Unexpected pass in xfail parser case "
            <> PG.caseId meta
            <> " reason="
            <> PG.caseReason meta
            <> " details="
            <> details
        )
    _ -> pure ()

assertEquivalentCaseWith :: (PE.EquivalentCase -> (PE.Outcome, String)) -> PE.EquivalentCase -> Assertion
assertEquivalentCaseWith evaluateCase meta =
  case evaluateCase meta of
    (PE.OutcomeFail, details) ->
      assertFailure
        ( "Regression in parser equivalence case "
            <> PE.caseId meta
            <> " ("
            <> PE.caseCategory meta
            <> ") expected "
            <> show (PE.caseStatus meta)
            <> " reason="
            <> PE.caseReason meta
            <> " details="
            <> details
        )
    (PE.OutcomeXPass, details) ->
      assertFailure
        ( "Unexpected pass in xfail parser equivalence case "
            <> PE.caseId meta
            <> " reason="
            <> PE.caseReason meta
            <> " details="
            <> details
        )
    _ -> pure ()

assertNoRegressions :: String -> [(PG.ParserCase, PG.Outcome, String)] -> Assertion
assertNoRegressions label outcomes = do
  let (passN, xfailN, xpassN, failN) = PG.progressSummary outcomes
      totalN = passN + xfailN + xpassN + failN
      completion = pct passN totalN
  when (failN > 0 || xpassN > 0) $
    assertFailure
      ( label
          <> " regressions found. "
          <> "pass="
          <> show passN
          <> " xfail="
          <> show xfailN
          <> " xpass="
          <> show xpassN
          <> " fail="
          <> show failN
          <> " completion="
          <> show completion
          <> "%"
      )

evaluate :: (PG.ParserCase -> (PG.Outcome, String)) -> PG.ParserCase -> (PG.ParserCase, PG.Outcome, String)
evaluate evaluateCase meta =
  let (outcome, details) = evaluateCase meta
   in (meta, outcome, details)

assertNoEquivalentRegressions :: String -> [(PE.EquivalentCase, PE.Outcome, String)] -> Assertion
assertNoEquivalentRegressions label outcomes = do
  let (passN, xfailN, xpassN, failN) = PE.progressSummary outcomes
      totalN = passN + xfailN + xpassN + failN
      completion = pct passN totalN
  when (failN > 0 || xpassN > 0) $
    assertFailure
      ( label
          <> " regressions found. "
          <> "pass="
          <> show passN
          <> " xfail="
          <> show xfailN
          <> " xpass="
          <> show xpassN
          <> " fail="
          <> show failN
          <> " completion="
          <> show completion
          <> "%"
      )

evaluateEquivalent :: (PE.EquivalentCase -> (PE.Outcome, String)) -> PE.EquivalentCase -> (PE.EquivalentCase, PE.Outcome, String)
evaluateEquivalent evaluateCase meta =
  let (outcome, details) = evaluateCase meta
   in (meta, outcome, details)

pct :: Int -> Int -> Double
pct done totalN
  | totalN <= 0 = 0.0
  | otherwise = fromIntegral (done * 10000 `div` totalN) / 100.0

fixtureValidationTests :: TestTree
fixtureValidationTests =
  testGroup
    "fixture-parse"
    [ testCase "rejects missing required keys" $
        case PG.parseParserCaseText PG.CaseExpr "missing.yaml" "extensions: []\n" of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for missing required YAML keys",
      testCase "requires reason for xfail" $
        case PG.parseParserCaseText PG.CaseExpr "xfail.yaml" validXFailMissingReason of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure when xfail reason is missing",
      testCase "requires ast for pass" $
        case PG.parseParserCaseText PG.CaseExpr "pass.yaml" validPassMissingAst of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure when pass ast is missing",
      testCase "rejects xpass status" $
        case PG.parseParserCaseText PG.CaseExpr "xpass.yaml" validXPassFixture of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for xpass status",
      testCase "accepts xfail without ast" $
        case PG.parseParserCaseText PG.CaseExpr "xfail-no-ast.yaml" validXFailNoAst of
          Left err -> assertFailure ("expected parse success, got: " <> err)
          Right parsed ->
            if PG.caseStatus parsed == PG.StatusXFail && null (PG.caseAst parsed)
              then pure ()
              else assertFailure "expected xfail status with empty ast",
      testCase "xfail parse failures retain details" $
        case PG.parseParserCaseText PG.CaseExpr "xfail-details.yaml" validXFailWithParseFailure of
          Left err -> assertFailure ("expected parse success, got: " <> err)
          Right parsed ->
            case PG.evaluateExprCase parsed of
              (PG.OutcomeXFail, details)
                | null details -> assertFailure "expected xfail details to be non-empty"
                | otherwise -> pure ()
              other -> assertFailure ("expected xfail outcome with details, got: " <> show other),
      testCase "only YAML fixtures are loaded" $ do
        exprCases <- PG.loadExprCases
        importCases <- PG.loadImportCases
        moduleCases <- PG.loadModuleCases
        patternCases <- PG.loadPatternCases
        pragmaCases <- PG.loadPragmaCases
        let cases = exprCases <> importCases <> moduleCases <> patternCases <> pragmaCases
        mapM_
          ( \meta ->
              unless (takeExtension (PG.casePath meta) `elem` [".yaml", ".yml"]) $
                assertFailure ("unexpected non-parser fixture loaded: " <> PG.casePath meta)
          )
          cases
    ]

validXFailMissingReason :: T.Text
validXFailMissingReason =
  T.unlines
    [ "extensions: []",
      "input: bad",
      "status: xfail"
    ]

validXFailNoAst :: T.Text
validXFailNoAst =
  T.unlines
    [ "extensions: []",
      "input: bad",
      "status: xfail",
      "reason: known bug"
    ]

validXFailWithParseFailure :: T.Text
validXFailWithParseFailure =
  T.unlines
    [ "extensions: []",
      "input: \"(\"",
      "status: xfail",
      "reason: known bug"
    ]

validPassMissingAst :: T.Text
validPassMissingAst =
  T.unlines
    [ "extensions: []",
      "input: x",
      "status: pass"
    ]

validXPassFixture :: T.Text
validXPassFixture =
  T.unlines
    [ "extensions: []",
      "input: x",
      "ast: EVar \"x\"",
      "status: xpass",
      "reason: known bug"
    ]

equivalenceFixtureValidationTests :: TestTree
equivalenceFixtureValidationTests =
  testGroup
    "equivalence-fixture-parse"
    [ testCase "rejects missing required keys" $
        case PE.parseEquivalentCaseText PE.CaseExpr "missing.yaml" "extensions: []\n" of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for missing required YAML keys",
      testCase "requires two equivalent inputs" $
        case PE.parseEquivalentCaseText PE.CaseExpr "one-input.yaml" validEquivalentSingleInput of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for single equivalent input",
      testCase "requires reason for xfail" $
        case PE.parseEquivalentCaseText PE.CaseExpr "xfail.yaml" validEquivalentXFailMissingReason of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure when xfail reason is missing",
      testCase "rejects xpass status" $
        case PE.parseEquivalentCaseText PE.CaseExpr "xpass.yaml" validEquivalentXPassFixture of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for xpass status",
      testCase "accepts pass fixture" $
        case PE.parseEquivalentCaseText PE.CaseExpr "pass.yaml" validEquivalentPassFixture of
          Left err -> assertFailure ("expected parse success, got: " <> err)
          Right parsed ->
            if PE.caseStatus parsed == PE.StatusPass && length (PE.caseInputs parsed) == 2
              then pure ()
              else assertFailure "expected pass status with two inputs",
      testCase "only YAML fixtures are loaded" $ do
        exprCases <- PE.loadExprCases
        moduleCases <- PE.loadModuleCases
        declCases <- PE.loadDeclCases
        patternCases <- PE.loadPatternCases
        let cases = exprCases <> moduleCases <> declCases <> patternCases
        mapM_
          ( \meta ->
              unless (takeExtension (PE.casePath meta) `elem` [".yaml", ".yml"]) $
                assertFailure ("unexpected non-parser equivalence fixture loaded: " <> PE.casePath meta)
          )
          cases
    ]

validEquivalentSingleInput :: T.Text
validEquivalentSingleInput =
  T.unlines
    [ "extensions: []",
      "equivalent:",
      "  - x",
      "status: pass"
    ]

validEquivalentXFailMissingReason :: T.Text
validEquivalentXFailMissingReason =
  T.unlines
    [ "extensions: []",
      "equivalent:",
      "  - x",
      "  - (x)",
      "status: xfail"
    ]

validEquivalentXPassFixture :: T.Text
validEquivalentXPassFixture =
  T.unlines
    [ "extensions: []",
      "equivalent:",
      "  - x",
      "  - (x)",
      "status: xpass",
      "reason: known bug"
    ]

validEquivalentPassFixture :: T.Text
validEquivalentPassFixture =
  T.unlines
    [ "extensions: []",
      "equivalent:",
      "  - x",
      "  - (x)",
      "status: pass"
    ]