packages feed

screp-0.1.0.0: test/Test.hs

module Main where

import Scrappy.Grep.DSL
import Scrappy.Grep.DSL.Parser (parseExpr)
import Scrappy.Grep.DSL.Interpreter (interpret)
import Scrappy.Grep.Search (searchText, searchFile, searchFiles)
import Scrappy.Grep.Output (formatResults, OutputFormat(..))

import System.Exit (exitFailure, exitSuccess)
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive, doesDirectoryExist)
import System.FilePath ((</>))
import Control.Monad (when)

main :: IO ()
main = do
  putStrLn "Running pgrep tests...\n"

  -- Run all tests
  results <- sequence
    [ testDSLParser
    , testInterpreter
    , testSearchString
    , testSearchFile
    , testSearchDirectory
    ]

  -- Summary
  let passed = length (filter id results)
      total = length results
  putStrLn $ "\n" ++ show passed ++ "/" ++ show total ++ " tests passed"

  if and results
    then exitSuccess
    else exitFailure

-- | Test DSL parser
testDSLParser :: IO Bool
testDSLParser = do
  putStrLn "=== DSL Parser Tests ==="

  let tests =
        [ ("char 'a'", PChar 'a')
        , ("string \"hello\"", PString "hello")
        , ("digit", PDigit)
        , ("letter", PLetter)
        , ("anyChar", PAnyChar)
        , ("alphaNum", PAlphaNum)
        , ("space", PSpace)
        , ("spaces", PSpaces)
        , ("newline", PNewline)
        , ("oneOf \"abc\"", POneOf "abc")
        , ("noneOf \"xyz\"", PNoneOf "xyz")
        , ("many digit", PMany PDigit)
        , ("some letter", PSome PLetter)
        , ("optional digit", POptional PDigit)
        , ("try letter", PTry PLetter)
        , ("count 3 digit", PCount 3 PDigit)
        , ("between '(' ')' digit", PBetween '(' ')' PDigit)
        , ("digit >> letter", PSeq PDigit PLetter)
        , ("digit <+> letter", PSeqConcat PDigit PLetter)
        , ("digit <|> letter", PAlt PDigit PLetter)
        , ("ref \"email\"", PRef "email")
        -- Precedence tests
        , ("digit >> letter <|> space", PAlt (PSeq PDigit PLetter) PSpace)
        , ("digit <+> letter >> space", PSeq (PSeqConcat PDigit PLetter) PSpace)
        , ("many digit >> letter", PSeq (PMany PDigit) PLetter)
        -- Parentheses
        , ("(digit >> letter)", PSeq PDigit PLetter)
        , ("many (digit <|> letter)", PMany (PAlt PDigit PLetter))
        ]

  results <- mapM runParserTest tests
  pure $ and results

  where
    runParserTest (input, expected) = do
      case parseExpr input of
        Left err -> do
          putStrLn $ "  FAIL: '" ++ input ++ "'"
          putStrLn $ "    Error: " ++ show err
          pure False
        Right actual ->
          if actual == expected
            then do
              putStrLn $ "  PASS: '" ++ input ++ "'"
              pure True
            else do
              putStrLn $ "  FAIL: '" ++ input ++ "'"
              putStrLn $ "    Expected: " ++ show expected
              putStrLn $ "    Actual:   " ++ show actual
              pure False

-- | Test interpreter
testInterpreter :: IO Bool
testInterpreter = do
  putStrLn "\n=== Interpreter Tests ==="

  let tests =
        [ ("digit", "abc123def", ["1", "2", "3"])
        , ("some digit", "abc123def456", ["123", "456"])
        , ("string \"hello\"", "say hello world hello", ["hello", "hello"])
        , ("letter <+> digit", "a1 b2 c3", ["a1", "b2", "c3"])
        , ("char 'a' >> char 'b'", "ab ab ab", ["b", "b", "b"])
        , ("char 'a' <+> char 'b'", "ab ab ab", ["ab", "ab", "ab"])
        , ("between '(' ')' some digit", "(123) (456)", ["(123)", "(456)"])
        , ("count 3 letter", "abc def ghi", ["abc", "def", "ghi"])
        , ("digit <|> letter", "a1b2c3", ["a", "1", "b", "2", "c", "3"])
        ]

  results <- mapM runInterpTest tests
  pure $ and results

  where
    runInterpTest (pattern, input, expected) = do
      case parseExpr pattern of
        Left err -> do
          putStrLn $ "  FAIL: '" ++ pattern ++ "' - parse error: " ++ show err
          pure False
        Right ast -> case interpret ast of
          Left err -> do
            putStrLn $ "  FAIL: '" ++ pattern ++ "' - interpret error: " ++ show err
            pure False
          Right parser -> do
            let results = searchText "<test>" parser input
                matches = map mrMatchText results
            if matches == expected
              then do
                putStrLn $ "  PASS: '" ++ pattern ++ "' on \"" ++ take 20 input ++ "...\""
                pure True
              else do
                putStrLn $ "  FAIL: '" ++ pattern ++ "'"
                putStrLn $ "    Input:    \"" ++ input ++ "\""
                putStrLn $ "    Expected: " ++ show expected
                putStrLn $ "    Actual:   " ++ show matches
                pure False

-- | Test search on a string
testSearchString :: IO Bool
testSearchString = do
  putStrLn "\n=== String Search Tests ==="

  -- Test multi-line matching
  let multiLineContent = "Hello World\nThis is line 2\nAnd line 3"

  case parseExpr "string \"line\"" of
    Left err -> do
      putStrLn $ "  FAIL: parse error: " ++ show err
      pure False
    Right ast -> case interpret ast of
      Left err -> do
        putStrLn $ "  FAIL: interpret error: " ++ show err
        pure False
      Right parser -> do
        let results = searchText "<test>" parser multiLineContent
        if length results == 2
          then do
            putStrLn $ "  PASS: Found " ++ show (length results) ++ " matches for 'line'"
            -- Check positions
            let r1 = head results
                r2 = results !! 1
            if mrLine r1 == 2 && mrLine r2 == 3
              then do
                putStrLn $ "  PASS: Line numbers correct (line 2 and 3)"
                pure True
              else do
                putStrLn $ "  FAIL: Line numbers wrong"
                putStrLn $ "    Result 1 line: " ++ show (mrLine r1)
                putStrLn $ "    Result 2 line: " ++ show (mrLine r2)
                pure False
          else do
            putStrLn $ "  FAIL: Expected 2 matches, got " ++ show (length results)
            pure False

-- | Test search on a file
testSearchFile :: IO Bool
testSearchFile = do
  putStrLn "\n=== File Search Tests ==="

  -- Create a temp test file
  let testDir = "/tmp/parsec-grep-test"
      testFile = testDir </> "test.txt"
      testContent = unlines
        [ "Hello World"
        , "Email: test@example.com"
        , "Phone: 123-456-7890"
        , "Another email: foo@bar.org"
        ]

  createDirectoryIfMissing True testDir
  writeFile testFile testContent

  -- Test finding email-like patterns
  case parseExpr "some alphaNum <+> char '@' <+> some alphaNum <+> char '.' <+> some letter" of
    Left err -> do
      putStrLn $ "  FAIL: parse error: " ++ show err
      cleanup testDir
      pure False
    Right ast -> case interpret ast of
      Left err -> do
        putStrLn $ "  FAIL: interpret error: " ++ show err
        cleanup testDir
        pure False
      Right parser -> do
        results <- searchFile parser testFile
        let matches = map mrMatchText results
        if length matches == 2 && "test@example.com" `elem` matches && "foo@bar.org" `elem` matches
          then do
            putStrLn $ "  PASS: Found 2 email addresses in file"
            putStrLn $ "    Matches: " ++ show matches
            cleanup testDir
            pure True
          else do
            putStrLn $ "  FAIL: Expected 2 emails"
            putStrLn $ "    Got: " ++ show matches
            cleanup testDir
            pure False

  where
    cleanup dir = do
      exists <- doesDirectoryExist dir
      when exists $ removeDirectoryRecursive dir

-- | Test search on a directory
testSearchDirectory :: IO Bool
testSearchDirectory = do
  putStrLn "\n=== Directory Search Tests ==="

  -- Create a temp test directory with multiple files
  let testDir = "/tmp/parsec-grep-test-dir"
      file1 = testDir </> "file1.txt"
      file2 = testDir </> "file2.txt"
      subDir = testDir </> "subdir"
      file3 = subDir </> "file3.txt"

  createDirectoryIfMissing True testDir
  createDirectoryIfMissing True subDir

  writeFile file1 "TODO: fix this bug\nTODO: add tests"
  writeFile file2 "Regular content\nNothing special"
  writeFile file3 "TODO: implement feature"

  -- Test finding TODO comments
  case parseExpr "string \"TODO\"" of
    Left err -> do
      putStrLn $ "  FAIL: parse error: " ++ show err
      cleanup testDir
      pure False
    Right ast -> case interpret ast of
      Left err -> do
        putStrLn $ "  FAIL: interpret error: " ++ show err
        cleanup testDir
        pure False
      Right parser -> do
        results <- searchFiles parser [file1, file2, file3]
        if length results == 3
          then do
            putStrLn $ "  PASS: Found 3 TODO comments across 3 files"
            -- Check that results come from different files
            let files = map mrFilePath results
            if file1 `elem` files && file3 `elem` files
              then do
                putStrLn $ "  PASS: Results from correct files"
                cleanup testDir
                pure True
              else do
                putStrLn $ "  FAIL: Results from wrong files"
                putStrLn $ "    Files: " ++ show files
                cleanup testDir
                pure False
          else do
            putStrLn $ "  FAIL: Expected 3 matches"
            putStrLn $ "    Got: " ++ show (length results)
            cleanup testDir
            pure False

  where
    cleanup dir = do
      exists <- doesDirectoryExist dir
      when exists $ removeDirectoryRecursive dir