packages feed

skeletest-0.3.0: src/Skeletest/Internal/TestTargets.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Skeletest.Internal.TestTargets (
  TestTargets,
  TestTarget (..),
  TestAttrs (..),
  matchesTest,
  parseTestTargets,
) where

import Control.Monad.Combinators.Expr qualified as Parser
import Data.Bifunctor (first)
import Data.Char (isAlphaNum)
import Data.Foldable1 qualified as Foldable1
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Void (Void)
import Text.Megaparsec (Parsec)
import Text.Megaparsec qualified as Parser
import Text.Megaparsec.Char qualified as Parser
import Text.Megaparsec.Char.Lexer qualified as Parser.L

type TestTargets = Maybe TestTarget

data TestTarget
  = -- | Useful for selecting all tests, whether manual or not.
    TestTargetEverything
  | TestTargetFile FilePath
  | TestTargetName Text
  | TestTargetMarker Text
  | TestTargetNot TestTarget
  | TestTargetAnd TestTarget TestTarget
  | TestTargetOr TestTarget TestTarget
  deriving (Eq)

data TestAttrs = TestAttrs
  { testPath :: FilePath
  , testIdentifier :: [Text]
  , testMarkers :: [Text]
  }

matchesTest :: TestTarget -> TestAttrs -> Bool
matchesTest selection TestAttrs{..} = go selection
 where
  go = \case
    TestTargetEverything -> True
    TestTargetFile path -> testPath == path
    TestTargetName s -> s `Text.isInfixOf` Text.unwords testIdentifier
    TestTargetMarker marker -> marker `elem` testMarkers
    TestTargetNot e -> not $ go e
    TestTargetAnd l r -> go l && go r
    TestTargetOr l r -> go l || go r

{----- Parsing -----}

parseTestTargets :: [Text] -> Either Text TestTargets
parseTestTargets args =
  case NonEmpty.nonEmpty args of
    Nothing -> pure Nothing
    Just args' -> Just . Foldable1.foldr1 TestTargetOr <$> mapM parseTestTarget args'
 where
  parseTestTarget = first showTestTargetParseError . Parser.parse (testTargetParser <* Parser.eof) ""

type Parser = Parsec Void Text
type ParseErrorBundle = Parser.ParseErrorBundle Text Void

testTargetParser :: Parser TestTarget
testTargetParser =
  Parser.makeExprParser
    ( Parser.choice
        [ parens testTargetParser
        , everythingParser
        , nameParser
        , markerParser
        , do
            selectFile <- fileParser
            -- syntax sugar: FooSpec.hs[abc] == (FooSpec.hs and [abc])
            withName <- maybe id (flip TestTargetAnd) <$> Parser.optional nameParser
            pure $ withName selectFile
        ]
    )
    [ [prefix "not" TestTargetNot]
    , [binary "and" TestTargetAnd, binary "or" TestTargetOr]
    ]
 where
  prefix name f = Parser.Prefix (f <$ symbol name)
  binary name f = Parser.InfixL (f <$ symbol name)

  symbol = Parser.L.symbol Parser.space
  parens = Parser.between (symbol "(") (symbol ")")

  everythingParser = TestTargetEverything <$ symbol "*"

  nameParser =
    Parser.label "test name" $
      fmap TestTargetName . Parser.between (symbol "[") (symbol "]") $
        Parser.takeWhile1P Nothing (/= ']')

  markerParser =
    Parser.label "marker" . ignoreSpacesAfter $ do
      _ <- symbol "@"
      fmap TestTargetMarker . Parser.takeWhile1P Nothing $
        (||) <$> isAlphaNum <*> (`elem` ("-_." :: [Char]))

  fileParser =
    Parser.label "test file" . ignoreSpacesAfter $
      fmap (TestTargetFile . Text.unpack) . Parser.takeWhile1P Nothing $
        (||) <$> isAlphaNum <*> (`elem` ("-_./" :: [Char]))

  ignoreSpacesAfter m = m <* Parser.space

showTestTargetParseError :: ParseErrorBundle -> Text
showTestTargetParseError bundle =
  let
    line = Parser.pstateInput $ Parser.bundlePosState bundle
    err = NonEmpty.head $ Parser.bundleErrors bundle
    pointerLen =
      case err of
        Parser.TrivialError _ (Just (Parser.Tokens s)) _ -> length s
        _ -> 1
   in
    Text.concat
      [ "Could not parse test target: " <> Text.pack (Parser.parseErrorTextPretty err)
      , " |\n"
      , " | " <> line <> "\n"
      , " | " <> Text.replicate (Parser.errorOffset err) " " <> Text.replicate pointerLen "^"
      ]