| 1 | ------------------------------------------------------------------------------- |
|---|
| 2 | -- | |
|---|
| 3 | -- Module : yaml2yeast-test |
|---|
| 4 | -- Copyright : (c) Oren Ben-Kiki 2007 |
|---|
| 5 | -- License : LGPL |
|---|
| 6 | -- |
|---|
| 7 | -- Maintainer : oren@ben-kiki.org |
|---|
| 8 | -- Stability : alpha |
|---|
| 9 | -- Portability : portable |
|---|
| 10 | -- |
|---|
| 11 | -- Execute all test cases located in a specified list of directories. Usage |
|---|
| 12 | -- is: |
|---|
| 13 | -- @ |
|---|
| 14 | -- yaml2yeast-test [directories...] |
|---|
| 15 | -- @ |
|---|
| 16 | -- If no directories are given, @yaml2yeast-test@ looks for tests in the |
|---|
| 17 | -- current working directory (\"@.@\"). Note that @yaml2yeast-test@ does not |
|---|
| 18 | -- recurse into sub-directories. |
|---|
| 19 | -- |
|---|
| 20 | -- Each tests consists of two files, with the names |
|---|
| 21 | -- \"/production/@.@/testcase/@.input@\" and |
|---|
| 22 | -- \"/production/@.@/testcase/@.output@\", where /production/ is the syntax |
|---|
| 23 | -- production to be tested and /testcase/ is an arbitrary name. The @.input@ |
|---|
| 24 | -- file contains the YAML text fed to the parser and the @.output@ file |
|---|
| 25 | -- contains the expected output, which is either a set of YEAST tokens or the |
|---|
| 26 | -- expected parsing error message. |
|---|
| 27 | -- |
|---|
| 28 | -- If the @.output@ file is missing, the test will automatically fail. If a |
|---|
| 29 | -- test fails, a @.error@ file is created. This makes it easy to set up new |
|---|
| 30 | -- tests, simply create the input files, run @yaml2yeast-test@, and rename the |
|---|
| 31 | -- @.error@ files to @.output@ files (after reviewing them for correctness, of |
|---|
| 32 | -- course). |
|---|
| 33 | -- |
|---|
| 34 | -- Exit status is the number of failed tests (0 - success - if all tests pass). |
|---|
| 35 | ------------------------------------------------------------------------------- |
|---|
| 36 | module Main (main) where |
|---|
| 37 | |
|---|
| 38 | import Control.Monad |
|---|
| 39 | import qualified Data.ByteString.Lazy.Char8 as C |
|---|
| 40 | import qualified Data.HashTable as Hash |
|---|
| 41 | import System.Directory |
|---|
| 42 | import System.Environment |
|---|
| 43 | import System.Exit |
|---|
| 44 | import System.IO |
|---|
| 45 | import Test.HUnit |
|---|
| 46 | import Text.Regex |
|---|
| 47 | import Text.Yaml.Reference |
|---|
| 48 | |
|---|
| 49 | -- | Map each tokenizer name to whether a test for it was seen. |
|---|
| 50 | type Seen = Hash.HashTable String Bool |
|---|
| 51 | |
|---|
| 52 | -- | @allTokenizers@ returns a hash table populated with all known tokenizers |
|---|
| 53 | -- where each has the initial value of @False@. |
|---|
| 54 | allTokenizers :: IO Seen |
|---|
| 55 | allTokenizers = do hash <- Hash.new (==) Hash.hashString |
|---|
| 56 | mapM (\name -> Hash.insert hash name False) tokenizerNames |
|---|
| 57 | return hash |
|---|
| 58 | |
|---|
| 59 | -- | @reportMissing seen@ reports the productions (tokenizers) which were not |
|---|
| 60 | -- /seen/ and returns their number. |
|---|
| 61 | reportMissing :: Seen -> IO Int |
|---|
| 62 | reportMissing seen = do list <- Hash.toList seen |
|---|
| 63 | missing <- foldM reportTest 0 list |
|---|
| 64 | if missing > 0 |
|---|
| 65 | then hPutStrLn stderr $ "Missing: " ++ (show missing) |
|---|
| 66 | else return () |
|---|
| 67 | return missing |
|---|
| 68 | where reportTest count (name, wasSeen) |
|---|
| 69 | | wasSeen = return count |
|---|
| 70 | | otherwise = do hPutStrLn stderr $ "No tests for " ++ name |
|---|
| 71 | return $ count + 1 |
|---|
| 72 | |
|---|
| 73 | -- | Different types of test files. |
|---|
| 74 | data TestType = Plain -- ^ Production without arguments. |
|---|
| 75 | | WithN -- ^ Production requiring $n$ argument. |
|---|
| 76 | | WithC -- ^ Production requiring $c$ argument. |
|---|
| 77 | | WithT -- ^ Production requiring $t$ argument. |
|---|
| 78 | | WithNC -- ^ Production requiring $n$ and $c$ arguments. |
|---|
| 79 | | WithNT -- ^ Production requiring $n$ and $t$ arguments. |
|---|
| 80 | deriving Eq |
|---|
| 81 | |
|---|
| 82 | -- | @show testType@ converts a /testType/ to a human-friendly name for error |
|---|
| 83 | -- messages. |
|---|
| 84 | instance Show TestType where |
|---|
| 85 | show testType = |
|---|
| 86 | case testType of |
|---|
| 87 | Plain -> "" |
|---|
| 88 | WithN -> " n" |
|---|
| 89 | WithC -> " c" |
|---|
| 90 | WithT -> " t" |
|---|
| 91 | WithNC -> " n c" |
|---|
| 92 | WithNT -> " n t" |
|---|
| 93 | |
|---|
| 94 | -- | @isTestInputFile file@ returns whether the specified /file/ is a test |
|---|
| 95 | -- input file (ends with \"@.input@\"). |
|---|
| 96 | isTestInputFile :: FilePath -> IO Bool |
|---|
| 97 | isTestInputFile file = do isFile <- doesFileExist file |
|---|
| 98 | if not isFile |
|---|
| 99 | then return False |
|---|
| 100 | else case matchRegex (mkRegex "\\.input$") file of |
|---|
| 101 | Just _ -> return True |
|---|
| 102 | Nothing -> return False |
|---|
| 103 | |
|---|
| 104 | -- | @isWith parameter file@ returns whether the specified /file/ is for a production |
|---|
| 105 | -- that requires the specified /parameter/ (file name contains @.@/parameter/@=@). |
|---|
| 106 | isWith :: String -> FilePath -> IO Bool |
|---|
| 107 | isWith parameter file = |
|---|
| 108 | case matchRegex (mkRegex $ "\\." ++ parameter ++ "=") file of |
|---|
| 109 | Just _ -> return True |
|---|
| 110 | Nothing -> return False |
|---|
| 111 | |
|---|
| 112 | -- | @testType file@ deduces the type of test stored in the /file/. |
|---|
| 113 | testType :: FilePath -> IO TestType |
|---|
| 114 | testType file = do withN <- isWith "n" file |
|---|
| 115 | withC <- isWith "c" file |
|---|
| 116 | withT <- isWith "t" file |
|---|
| 117 | case (withN, withC, withT) of |
|---|
| 118 | (False, False, False) -> return Plain |
|---|
| 119 | (True, False, False) -> return WithN |
|---|
| 120 | (False, True, False) -> return WithC |
|---|
| 121 | (False, False, True) -> return WithT |
|---|
| 122 | (True, True, False) -> return WithNC |
|---|
| 123 | (True, False, True) -> return WithNT |
|---|
| 124 | (_, _, _) -> error $ file ++ ": unknown parameters combination" |
|---|
| 125 | |
|---|
| 126 | -- | @testProduction file@ extracts the production name from a test input |
|---|
| 127 | -- /file/ name (file name starts with \"/pattern/@.@\"). |
|---|
| 128 | testProduction :: FilePath -> String |
|---|
| 129 | testProduction file = subRegex (mkRegex "^.*/([0-9a-z+-]+)\\.[^/]*$") file "\\1" |
|---|
| 130 | |
|---|
| 131 | -- | @testParameter parameter file@ extracts the /parameter/ value from a test |
|---|
| 132 | -- input /file/ name (file name contains \"@.@/parameter/@=@/value/@.@\"). Also |
|---|
| 133 | -- patch the @-@ characters in the @c@ parameter into @_@ to make it possible |
|---|
| 134 | -- for the built-in lexer to handle them. |
|---|
| 135 | testParameter :: (Read t) => String -> FilePath -> t |
|---|
| 136 | testParameter parameter file = |
|---|
| 137 | read $ subRegex patchRegex (subRegex extractRegex file "\\1") "\\1_" |
|---|
| 138 | where extractRegex = mkRegex $ "^.*\\." ++ parameter ++ "=([^.]+)\\.[^/]*$" |
|---|
| 139 | patchRegex = mkRegex "([a-z])-" |
|---|
| 140 | |
|---|
| 141 | -- | @testOutputFile file@ converts a test input /file/ name to test output |
|---|
| 142 | -- file name. |
|---|
| 143 | testOutputFile :: FilePath -> FilePath |
|---|
| 144 | testOutputFile file = subRegex (mkRegex "\\.input$") file ".output" |
|---|
| 145 | |
|---|
| 146 | -- | @testErrorFile file@ converts a test input /file/ name to test error |
|---|
| 147 | -- file name. |
|---|
| 148 | testErrorFile :: FilePath -> FilePath |
|---|
| 149 | testErrorFile file = subRegex (mkRegex "\\.input$") file ".error" |
|---|
| 150 | |
|---|
| 151 | -- | @embedVariables text inputFile@ embeds variables in the /text/ instead of |
|---|
| 152 | -- their expanded values; currently only /InputFile/ is embedded instead of the |
|---|
| 153 | -- input file name (we cheat by replacing whatever looks like one). |
|---|
| 154 | embedVariables :: String -> String |
|---|
| 155 | embedVariables text = subRegex (mkRegex "!.*: line ") text "!$InputFile$: line " |
|---|
| 156 | |
|---|
| 157 | -- | @assertTest inputFile@ runs the parser on the input contained in the |
|---|
| 158 | -- /inputFile/ using the production extracted from the file name, asserting the |
|---|
| 159 | -- result is identical to the content of the matching output file. |
|---|
| 160 | assertTest :: FilePath -> Assertion |
|---|
| 161 | assertTest inputFile = |
|---|
| 162 | do input <- C.readFile inputFile |
|---|
| 163 | let outputFile = testOutputFile inputFile |
|---|
| 164 | existsOutputFile <- doesFileExist outputFile |
|---|
| 165 | expected <- if existsOutputFile |
|---|
| 166 | then readFile outputFile |
|---|
| 167 | else return "(missing file)" |
|---|
| 168 | runType <- testType inputFile |
|---|
| 169 | let result = case runType of |
|---|
| 170 | Plain -> tokenizer (testProduction inputFile) |
|---|
| 171 | WithN -> tokenizerWithN (testProduction inputFile) (testParameter "n" inputFile) |
|---|
| 172 | WithC -> tokenizerWithC (testProduction inputFile) (testParameter "c" inputFile) |
|---|
| 173 | WithT -> tokenizerWithT (testProduction inputFile) (testParameter "t" inputFile) |
|---|
| 174 | WithNC -> tokenizerWithNC (testProduction inputFile) (testParameter "n" inputFile) (testParameter "c" inputFile) |
|---|
| 175 | WithNT -> tokenizerWithNT (testProduction inputFile) (testParameter "n" inputFile) (testParameter "t" inputFile) |
|---|
| 176 | case result of |
|---|
| 177 | Nothing -> assertFailure $ inputFile ++ ": unknown production" ++ (show runType) ++ ": " ++ (testProduction inputFile) |
|---|
| 178 | Just resolved -> do let actual = embedVariables $ showTokens $ resolved inputFile input False |
|---|
| 179 | when (actual /= expected) $ writeFile (testErrorFile inputFile) actual |
|---|
| 180 | assertEqual inputFile expected actual |
|---|
| 181 | |
|---|
| 182 | -- | @fileTest seen file@ wraps @assertTest@ in a test case named after the |
|---|
| 183 | -- /file/ and marks it as /seen/. |
|---|
| 184 | fileTest :: Seen -> FilePath -> IO Test |
|---|
| 185 | fileTest seen file = do Hash.update seen (testProduction file) True |
|---|
| 186 | return $ TestLabel file $ TestCase $ assertTest file |
|---|
| 187 | |
|---|
| 188 | -- | @directoryTestInputFiles directory@ returns the list of test input files |
|---|
| 189 | -- contained in the /directory/. |
|---|
| 190 | directoryTestInputFiles :: String -> IO [FilePath] |
|---|
| 191 | directoryTestInputFiles directory = do entries <- getDirectoryContents directory |
|---|
| 192 | filterM isTestInputFile $ map ((directory ++) . ("/" ++)) entries |
|---|
| 193 | |
|---|
| 194 | -- | @directoryTests seen directory@ returns the list of test cases contained |
|---|
| 195 | -- in the /directory/, wrapped in a test case named after it, and updates the |
|---|
| 196 | -- /seen/ hash. |
|---|
| 197 | directoryTests :: Seen -> String -> IO Test |
|---|
| 198 | directoryTests seen directory = do files <- directoryTestInputFiles directory |
|---|
| 199 | tests <- mapM (fileTest seen) files |
|---|
| 200 | return $ TestLabel directory $ TestList tests |
|---|
| 201 | |
|---|
| 202 | -- | @allTests seen directories@ returns the list of test cases contained in |
|---|
| 203 | -- the /directories/ (or \"@.@\" if none is specified), wrapped in a test case |
|---|
| 204 | -- named @all@ if there is more than one directory, updating the /seen/ hash. |
|---|
| 205 | allTests :: Seen -> [String] -> IO Test |
|---|
| 206 | allTests seen directories = do case directories of |
|---|
| 207 | [] -> directoryTests seen "." |
|---|
| 208 | [directory] -> directoryTests seen directory |
|---|
| 209 | _ -> do tests <- mapM (directoryTests seen) directories |
|---|
| 210 | return $ TestLabel "all" $ TestList tests |
|---|
| 211 | |
|---|
| 212 | -- | @main@ executes all the tests contained in the directories specified in |
|---|
| 213 | -- the command line (or \"@.@\" if none is specified). |
|---|
| 214 | main :: IO () |
|---|
| 215 | main = do directories <- getArgs |
|---|
| 216 | seen <- allTokenizers |
|---|
| 217 | tests <- allTests seen directories |
|---|
| 218 | missing <- reportMissing seen |
|---|
| 219 | results <- runTestTT tests |
|---|
| 220 | case missing + (errors results) + (failures results) of |
|---|
| 221 | 0 -> exitWith ExitSuccess |
|---|
| 222 | n -> exitWith $ ExitFailure n |
|---|