root/trunk/YamlReference/yaml2yeast-test/Main.hs

Revision 6, 10.6 KB (checked in by oren, 9 months ago)

Version 0.9 - compatible with the April 06, 2008 draft.

Line 
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-------------------------------------------------------------------------------
36module Main (main) where
37
38import           Control.Monad
39import qualified Data.ByteString.Lazy.Char8 as C
40import qualified Data.HashTable as Hash
41import           System.Directory
42import           System.Environment
43import           System.Exit
44import           System.IO
45import           Test.HUnit
46import           Text.Regex
47import           Text.Yaml.Reference
48
49-- | Map each tokenizer name to whether a test for it was seen.
50type 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@.
54allTokenizers :: IO Seen
55allTokenizers = 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.
61reportMissing :: Seen -> IO Int
62reportMissing 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.
74data 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.
84instance 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@\").
96isTestInputFile :: FilePath -> IO Bool
97isTestInputFile 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/@=@).
106isWith :: String -> FilePath -> IO Bool
107isWith 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/.
113testType :: FilePath -> IO TestType
114testType 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/@.@\").
128testProduction :: FilePath -> String
129testProduction 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.
135testParameter :: (Read t) => String -> FilePath -> t
136testParameter 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.
143testOutputFile :: FilePath -> FilePath
144testOutputFile file = subRegex (mkRegex "\\.input$") file ".output"
145
146-- | @testErrorFile file@ converts a test input /file/ name to test error
147-- file name.
148testErrorFile :: FilePath -> FilePath
149testErrorFile 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).
154embedVariables :: String -> String
155embedVariables 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.
160assertTest :: FilePath -> Assertion
161assertTest 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/.
184fileTest :: Seen -> FilePath -> IO Test
185fileTest 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/.
190directoryTestInputFiles :: String -> IO [FilePath]
191directoryTestInputFiles 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.
197directoryTests :: Seen -> String -> IO Test
198directoryTests 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.
205allTests :: Seen -> [String] -> IO Test
206allTests 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).
214main :: IO ()
215main = 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
Note: See TracBrowser for help on using the browser.