packages feed

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

{-# LANGUAGE OverloadedStrings #-}

module Test.Lexer.Suite
  ( lexerTests,
  )
where

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

lexerTests :: IO TestTree
lexerTests = do
  cases <- LG.loadLexerCases
  checks <- mapM mkCaseTest cases
  summary <- mkSummaryTest cases
  pure (testGroup "lexer-golden" ([fixtureValidationTests] <> checks <> [summary]))

mkCaseTest :: LG.LexerCase -> IO TestTree
mkCaseTest meta = pure $ case LG.caseStatus meta of
  LG.StatusXFail -> testCaseInfo (LG.caseId meta) (xfailDetails (LG.evaluateLexerCase meta) <* assertCase meta)
  _ -> testCase (LG.caseId meta) (assertCase meta)

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

mkSummaryTest :: [LG.LexerCase] -> IO TestTree
mkSummaryTest cases = do
  let outcomes = map evaluate cases
  pure $
    testCase "summary" $ do
      let (passN, xfailN, xpassN, failN) = LG.progressSummary outcomes
          totalN = passN + xfailN + xpassN + failN
          completion = pct passN totalN
      when (failN > 0 || xpassN > 0) $
        assertFailure
          ( "lexer golden regressions found. "
              <> "pass="
              <> show passN
              <> " xfail="
              <> show xfailN
              <> " xpass="
              <> show xpassN
              <> " fail="
              <> show failN
              <> " completion="
              <> show completion
              <> "%"
          )

assertCase :: LG.LexerCase -> Assertion
assertCase meta =
  case LG.evaluateLexerCase meta of
    (LG.OutcomeFail, details) ->
      assertFailure
        ( "Regression in lexer case "
            <> LG.caseId meta
            <> " ("
            <> LG.caseCategory meta
            <> ") expected "
            <> show (LG.caseStatus meta)
            <> " reason="
            <> LG.caseReason meta
            <> " details="
            <> details
        )
    (LG.OutcomeXPass, details) ->
      assertFailure
        ( "Unexpected pass in xpass lexer case "
            <> LG.caseId meta
            <> " reason="
            <> LG.caseReason meta
            <> " details="
            <> details
        )
    _ -> pure ()

evaluate :: LG.LexerCase -> (LG.LexerCase, LG.Outcome, String)
evaluate meta =
  let (outcome, details) = LG.evaluateLexerCase 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 LG.parseLexerCaseText "missing.yaml" "extensions: []\n" of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure for missing required YAML keys",
      testCase "requires reason for xfail" $
        case LG.parseLexerCaseText "xfail.yaml" validXFailMissingReason of
          Left _ -> pure ()
          Right _ -> assertFailure "expected parse failure when xfail reason is missing",
      testCase "accepts xpass with reason" $
        case LG.parseLexerCaseText "xpass.yaml" validXPassFixture of
          Left err -> assertFailure ("expected parse success, got: " <> err)
          Right parsed ->
            if LG.caseStatus parsed == LG.StatusXPass
              then pure ()
              else assertFailure "expected xpass status",
      testCase "xfail lexer mismatches retain details" $
        case LG.parseLexerCaseText "xfail-details.yaml" validXFailFixture of
          Left err -> assertFailure ("expected parse success, got: " <> err)
          Right parsed ->
            case LG.evaluateLexerCase parsed of
              (LG.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
        cases <- LG.loadLexerCases
        mapM_
          ( \meta ->
              unless (takeExtension (LG.casePath meta) `elem` [".yaml", ".yml"]) $
                assertFailure ("unexpected non-lexer fixture loaded: " <> LG.casePath meta)
          )
          cases
    ]

validXFailMissingReason :: T.Text
validXFailMissingReason =
  T.unlines
    [ "extensions: []",
      "input: bad",
      "tokens:",
      "  - 'TkVarId \"bad\"'",
      "status: xfail"
    ]

validXFailFixture :: T.Text
validXFailFixture =
  T.unlines
    [ "extensions: []",
      "input: \"-10\"",
      "tokens:",
      "  - 'TkVarSym \"-\"'",
      "  - 'TkInteger 10 TInteger'",
      "status: xfail",
      "reason: known bug"
    ]

validXPassFixture :: T.Text
validXPassFixture =
  T.unlines
    [ "extensions: []",
      "input: \"-10\"",
      "tokens:",
      "  - 'TkVarSym \"-\"'",
      "  - 'TkInteger 10 TInteger'",
      "status: xpass",
      "reason: known bug"
    ]