packages feed

aihc-parser-6.0.0.0: test/Test/Oracle/Suite.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Oracle.Suite
  ( oracleTests,
  )
where

import Aihc.Parser (ParserConfig (..), TraceEntry (..), defaultConfig, parseModuleWithTrace, tokenKindWithoutSpacing)
import Aihc.Parser.Lex (LexToken (..))
import Aihc.Parser.Syntax qualified as Syntax
import Control.Monad (when)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as TIO
import ExtensionSupport
  ( CaseMeta (..),
    Expected (..),
    Outcome (..),
    caseSourcePath,
    evaluateCaseFromFile,
    evaluateCaseText,
    loadOracleCases,
    preprocessCaseSource,
  )
import Prettyprinter (Pretty (..), defaultLayoutOptions, layoutPretty)
import Prettyprinter.Render.Text (renderStrict)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, testCaseInfo)

oracleTests :: IO TestTree
oracleTests = do
  cases <- loadOracleCases
  checks <- mapM mkCaseTest cases
  framework <- frameworkTests
  summary <- mkSummaryTest cases
  traces <- mapM mkTraceTest (filter ((== ExpectPass) . caseExpected) cases)
  pure (testGroup "oracle" (checks <> [framework, summary, testGroup "token trace" traces]))

-- | The parser gives the same token trace for a source file and for its
-- pretty-printed form, when the discarded tokens are removed. A formatter
-- pairs the tokens of the two files by their position in this trace.
mkTraceTest :: CaseMeta -> IO TestTree
mkTraceTest meta = do
  source <- TIO.readFile (caseSourcePath meta)
  pure (testCase (caseId meta) (assertTraceMatches meta (preprocessCaseSource meta source)))

assertTraceMatches :: CaseMeta -> Text -> Assertion
assertTraceMatches meta source =
  case parseModuleWithTrace config source of
    (_ : _, _, _) -> assertFailure "the source does not parse"
    ([], modu, sourceTrace) ->
      let rendered = renderStrict (layoutPretty defaultLayoutOptions (pretty modu))
       in case parseModuleWithTrace config rendered of
            (_ : _, _, _) -> assertFailure "the pretty-printed source does not parse"
            ([], _, renderedTrace) ->
              let kept = filter (not . traceDiscarded)
                  sourceKept = kept sourceTrace
                  renderedKept = kept renderedTrace
                  kindOf = tokenKindWithoutSpacing . lexTokenKind . traceToken
                  sameKind a b = kindOf a == kindOf b
                  common = length (takeWhile id (zipWith sameKind sourceKept renderedKept))
               in when (common < length sourceKept || common < length renderedKept) $
                    assertFailure
                      ( "token traces differ at entry "
                          <> show common
                          <> "\nsource:   "
                          <> showEntries (drop (common - 3) sourceKept)
                          <> "\nrendered: "
                          <> showEntries (drop (common - 3) renderedKept)
                      )
  where
    config =
      defaultConfig
        { parserSourceName = caseSourcePath meta,
          parserExtensions = Syntax.effectiveExtensions Syntax.Haskell2010Edition (caseExtensions meta)
        }
    showEntries = unwords . map showEntry . take 7
    showEntry entry =
      let tok = traceToken entry
          text = lexTokenText tok
       in (if T.null text then show (lexTokenKind tok) else T.unpack text)
            <> "@"
            <> show (Syntax.sourceSpanStartLine (lexTokenSpan tok))
            <> ":"
            <> show (Syntax.sourceSpanStartCol (lexTokenSpan tok))

mkCaseTest :: CaseMeta -> IO TestTree
mkCaseTest meta = do
  source <- TIO.readFile (caseSourcePath meta)
  pure $ case caseExpected meta of
    ExpectXFail -> testCaseInfo (caseId meta) (xfailDetails (evaluateCaseText meta source) <* assertCase meta source)
    _ -> testCase (caseId meta) (assertCase meta source)

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

mkSummaryTest :: [CaseMeta] -> IO TestTree
mkSummaryTest cases = do
  outcomes <- mapM evaluateCaseFromFile cases
  pure $
    testCase "summary" $ do
      let (passN, xfailN, xpassN, failN) = foldr (countOutcome . snd3) (0, 0, 0, 0) outcomes
          totalN = passN + xfailN + xpassN + failN
          completion = pct (passN + xpassN) totalN
      when (failN > 0 || xpassN > 0) $
        assertFailure
          ( "Oracle regressions found. "
              <> "pass="
              <> show passN
              <> " xfail="
              <> show xfailN
              <> " xpass="
              <> show xpassN
              <> " fail="
              <> show failN
              <> " completion="
              <> show completion
              <> "%"
          )
  where
    snd3 (_, b, _) = b

countOutcome :: Outcome -> (Int, Int, Int, Int) -> (Int, Int, Int, Int)
countOutcome outcome (passN, xfailN, xpassN, failN) =
  case outcome of
    OutcomePass -> (passN + 1, xfailN, xpassN, failN)
    OutcomeXFail -> (passN, xfailN + 1, xpassN, failN)
    OutcomeXPass -> (passN, xfailN, xpassN + 1, failN)
    OutcomeFail -> (passN, xfailN, xpassN, failN + 1)

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

assertCase :: CaseMeta -> Text -> Assertion
assertCase meta source = do
  let (_, outcome, details) = evaluateCaseText meta source
  case outcome of
    OutcomeFail ->
      assertFailure
        ( "Regression in case "
            <> caseId meta
            <> " ("
            <> caseCategory meta
            <> ") expected "
            <> show (caseExpected meta)
            <> " reason="
            <> caseReason meta
            <> " details="
            <> details
        )
    OutcomeXPass ->
      assertFailure
        ( "Unexpected pass in xfail case "
            <> caseId meta
            <> " ("
            <> caseCategory meta
            <> ") reason="
            <> caseReason meta
        )
    _ -> pure ()

frameworkTests :: IO TestTree
frameworkTests =
  pure $
    testGroup
      "framework"
      [ testCase "oracle parse failure fails xfail case" $
          let meta =
                CaseMeta
                  { caseId = "framework-invalid-xfail",
                    caseCategory = "framework",
                    casePath = "framework-invalid-xfail.hs",
                    caseExpected = ExpectXFail,
                    caseReason = "regression coverage",
                    caseExtensions = []
                  }
           in do
                let (_, outcome, _) = evaluateCaseText meta "module M where\nx = { y = 1, }\n"
                if outcome == OutcomeFail
                  then pure ()
                  else assertFailure ("expected OutcomeFail when oracle rejects fixture, got " <> show outcome),
        testCase "oracle rejects top-level block-argument lambda" $
          let meta =
                CaseMeta
                  { caseId = "framework-block-argument-lambda",
                    caseCategory = "framework",
                    casePath = "framework-block-argument-lambda.hs",
                    caseExpected = ExpectPass,
                    caseReason = "",
                    caseExtensions = []
                  }
           in do
                let (_, outcome, _) = evaluateCaseText meta "module M where\nf \\x -> x\n"
                if outcome == OutcomeFail
                  then pure ()
                  else assertFailure ("expected OutcomeFail when oracle rejects fixture, got " <> show outcome),
        testCase "oracle parse failure fails pass case" $
          let meta =
                CaseMeta
                  { caseId = "framework-invalid-pass",
                    caseCategory = "framework",
                    casePath = "framework-invalid-pass.hs",
                    caseExpected = ExpectPass,
                    caseReason = "",
                    caseExtensions = []
                  }
           in do
                let (_, outcome, _) = evaluateCaseText meta "module M where\nx = { y = 1, }\n"
                if outcome == OutcomeFail
                  then pure ()
                  else assertFailure ("expected OutcomeFail when oracle rejects fixture, got " <> show outcome)
      ]