{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -------------------------------------------------------------------------------- -- See end of this file for licence information. -------------------------------------------------------------------------------- -- | -- Module : RunW3CTests -- Copyright : (c) 2013 Douglas Burke -- License : GPL V2 -- -- Maintainer : Douglas Burke -- Stability : experimental -- Portability : OverloadedStrings, RecordWildCards -- -- Run the W3C Turtle tests using the supplied manifest file (Turtle format). -- It requires that the tests are installed locally (i.e. it will /not/ -- download from the Turtle test suite at ). -- -- Possible improvements: -- -- - add an @--enable-roundtrip@ flag which would do a round-trip test -- on all the valid files, to check the formatter as well as the parser -- (or just for those tests with a NTriples version, since that can be -- used to create the output graph, but may miss a few edge cases). -- -- - create an EARL report (), for -- . -- See also . -- -- - option to download the tests from the W3C site. -- -------------------------------------------------------------------------------- module Main where import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.IO as L import qualified Swish.RDF.Parser.Turtle as TTL import qualified Swish.RDF.Parser.NTriples as NT import Control.Monad (forM_) import Data.Maybe (catMaybes) import Data.Version (showVersion) import Network.URI (URI, parseURI, parseURIReference, relativeTo, uriPath, uriScheme) import Swish.RDF.Graph import Swish.Namespace (ScopedName, getScopeURI) import Swish.RDF.Query import Swish.RDF.Vocabulary.RDF (rdfType) import Swish.RDF.Vocabulary.XSD (xsdString) import System.Directory (canonicalizePath) import System.Environment import System.Exit (exitFailure, exitSuccess) import System.FilePath (splitFileName) import System.IO (hFlush, hPutStr, hPutStrLn, stderr, stdout) import Paths_swish (version) -- | The base URI for the tests. base :: Maybe URI base = parseURI "http://www.w3.org/2013/TurtleTests/" -- Could include the language type for the Parse version. -- | I have decided to treat @rdf:type rdft:TestTurtleNegativeEval@ -- tests the same as @rdf:TestTurtleNegativeSyntax@. data TestType = NTriplesParse Bool -- ^ Should the NTriples file parse successfully? | TurtleParse Bool -- ^ Should the Turtle file parse successfully? | TurtleCompare -- ^ The Turtle and NTriples files should match. _showBool :: Bool -> String _showBool True = "pass" _showBool _ = "fail" instance Show TestType where show (NTriplesParse a) = "" show (TurtleParse a) = "" show TurtleCompare = "" data Test = Test { _tName :: String , _tAction :: IO (Maybe String) -- ^ If the test fails a string reporting the error is returned. } -- | Returns the name of the test if it failed. runTest :: Test -> IO (Maybe String) runTest Test {..} = _tAction >>= \r -> hFlush stdout >> return r runTests :: [Test] -> IO () runTests ts = do putStrLn $ "Running " ++ show (length ts) ++ " tests" hFlush stdout fails <- catMaybes `fmap` mapM runTest ts putStrLn "" case fails of [] -> putStrLn "All tests passed." >> exitSuccess [f] -> hPutStrLn stderr ("One test failed: " ++ f) >> exitFailure _ -> do let nf = show $ length fails hPutStrLn stderr $ "There were " ++ nf ++ " failures:" forM_ (zip [(1::Int)..] fails) $ \(n,m) -> do hPutStr stderr $ "# [" ++ show n ++ "/" ++ nf ++ "] " hPutStrLn stderr m exitFailure mfEntries, mfName :: ScopedName mfEntries = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#entries" mfName = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#name" mfAction, mfResult :: ScopedName mfAction = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#action" mfResult = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#result" rdftTestTurtleEval, rdftTestTurtleNegativeEval, rdftTestTurtlePositiveSyntax, rdftTestTurtleNegativeSyntax :: ScopedName rdftTestTurtleEval = "http://www.w3.org/ns/rdftest#TestTurtleEval" rdftTestTurtleNegativeEval = "http://www.w3.org/ns/rdftest#TestTurtleNegativeEval" rdftTestTurtlePositiveSyntax ="http://www.w3.org/ns/rdftest#TestTurtlePositiveSyntax" rdftTestTurtleNegativeSyntax = "http://www.w3.org/ns/rdftest#TestTurtleNegativeSyntax" rdftTestNTriplesPositiveSyntax, rdftTestNTriplesNegativeSyntax :: ScopedName rdftTestNTriplesPositiveSyntax = "http://www.w3.org/ns/rdftest#rdftTestNTriplesPositiveSyntax" rdftTestNTriplesNegativeSyntax = "http://www.w3.org/ns/rdftest#rdftTestNTriplesNegativeSyntax" {- rdftApproval :: RDFLabel rdftApproval = u2L "http://www.w3.org/ns/rdftest#approval" -} -- | Extract out the object from the list of triples, -- erroring out if there is not a single match. getVal :: ScopedName -- ^ predicate to search for -> [RDFTriple] -> Either String RDFLabel -- ^ object, if found getVal p ts = let ns = filter ((== Res p) . arcPred) ts in case ns of [n] -> Right $ arcObj n [] -> Left $ "No " ++ show p ++ " predicate found" _ -> Left $ "Found multiple " ++ show p ++ " attributes" -- | Note: assuming that the literals are untyped at the moment. toString :: RDFLabel -> Either String String toString (Lit s) = Right $ T.unpack s toString (LangLit s _) = Right $ T.unpack s toString (TypedLit s dt) | dt == xsdString = Right $ T.unpack s | otherwise = Left $ "Not a string, but " ++ show dt toString v = Left $ "Not a string literal, but " ++ show v toTestType :: RDFLabel -> Either String TestType toTestType (Res r) | r == rdftTestTurtleEval = Right TurtleCompare | r == rdftTestTurtlePositiveSyntax = Right $ TurtleParse True | r == rdftTestTurtleNegativeSyntax = Right $ TurtleParse False | r == rdftTestTurtleNegativeEval = Right $ TurtleParse False | r == rdftTestNTriplesPositiveSyntax = Right $ NTriplesParse True | r == rdftTestNTriplesNegativeSyntax = Right $ NTriplesParse False | otherwise = Left $ "Unrecognized test type: " ++ show r toTestType x = Left $ "Not a resource, but " ++ show x getScheme, getPath :: ScopedName -> String getScheme = uriScheme . getScopeURI getPath = uriPath . getScopeURI toFilePath :: RDFLabel -> Either String FilePath toFilePath (Res r) | getScheme r == "file:" = Right $ getPath r | otherwise = Left $ "Not a file URL: " ++ show r toFilePath x = Left $ "Not a resource, but " ++ show x -- | Indicates that the details of the test in the manifest graph -- do not contain the required details. failedAction :: String -> IO (Maybe String) failedAction = return . Just pass :: IO (Maybe String) pass = putStrLn "[PASS]" >> return Nothing nopass :: String -> IO (Maybe String) nopass e = putStrLn "[FAIL]" >> failedAction e -- Ensure that the string ends in a space; it may exceed -- 60 characters (ASCII) wide. ljust :: String -> IO () ljust m = putStr $ m ++ replicate (59 - length m) ' ' ++ " " -- | Compare the two files. evalAction :: String -- ^ test name -> FilePath -- ^ turtle file (to test) -> FilePath -- ^ NTriples file (to compare against) -> IO (Maybe String) -- ^ If the test fpasses return @Nothing@, otherwise -- a string descibing the error evalAction name tFile nFile = do ljust $ "*** " ++ name cts1 <- L.readFile tFile cts2 <- L.readFile nFile let filename = snd $ splitFileName tFile Just frag = parseURIReference filename nbase = (frag `relativeTo`) `fmap` base let res = do tgr <- TTL.parseTurtle cts1 nbase ngr <- NT.parseNT cts2 return $ if tgr == ngr then Nothing else -- should look at Swish.Commands.swishOutputDiffs -- but that is quite involved, so just dump the -- two graphs, which should be small let f = concatMap show . S.toList . getArcs in Just $ name ++ "\nDoes not compare equal.\nExpected:\n" ++ f ngr ++ "\nTurtle:\n" ++ f tgr case res of Left e -> nopass (name ++ "\nParse failure:\n" ++ e) Right Nothing -> pass Right (Just e) -> nopass e -- | Does the file parse? -- -- TODO: should we ensure the graph is evaluated to make sure -- that laziness does not catch us out here? evalSyntaxPass :: (L.Text -> Either String a) -- ^ parser to test -> String -- ^ test name -> FilePath -- ^ turtle file (to test) -> IO (Maybe String) evalSyntaxPass parser name tFile = do ljust $ "*** " ++ name cts <- L.readFile tFile case parser cts of Left e -> nopass (name ++ "\n" ++ e) Right _ -> pass -- | Does the file fail to parse? -- -- TODO: should we ensure the graph is evaluated to make sure -- that laziness does not catch us out here? evalSyntaxFail :: (L.Text -> Either String a) -- ^ parser to test -> String -- ^ test name -> FilePath -- ^ turtle file (to test) -> IO (Maybe String) evalSyntaxFail parser name tFile = do ljust $ "*** " ++ name cts <- L.readFile tFile case parser cts of Left _ -> pass Right _ -> nopass (name ++ "\nShould not have parsed, but it did!") -- | Create a test for the given label. For now ignore the -- approved field. makeTest :: RDFGraph -> RDFLabel -> Test makeTest gr lbl = let arcs = rdfFindArcs (rdfSubjEq lbl) gr getMetaData = do testName <- getVal mfName arcs >>= toString testType <- getVal rdfType arcs >>= toTestType return (testName, testType) getAction name (NTriplesParse b) = do inFile <- getVal mfAction arcs >>= toFilePath return $ if b then evalSyntaxPass NT.parseNT name inFile else evalSyntaxFail NT.parseNT name inFile getAction name (TurtleParse b) = do inFile <- getVal mfAction arcs >>= toFilePath return $ if b then evalSyntaxPass TTL.parseTurtlefromText name inFile else evalSyntaxFail TTL.parseTurtlefromText name inFile getAction name TurtleCompare = do inFile <- getVal mfAction arcs >>= toFilePath outFile <- getVal mfResult arcs >>= toFilePath return $ evalAction name inFile outFile in case getMetaData of Left e -> Test "Failed to build test" $ failedAction $ "No test data found: " ++ e Right (n,t) -> case getAction n t of Left e -> Test n $ failedAction $ "Failed to build test " ++ n ++ ": " ++ e Right a -> Test n a makeTests :: RDFGraph -> [Test] makeTests gr = let [Arc _ _ ehead] = rdfFindArcs (rdfPredEq (Res mfEntries)) gr in map (makeTest gr) $ rdfFindList gr ehead readManifest :: FilePath -> IO [Test] readManifest fname = do putStrLn $ "Reading manifest: " ++ fname cts <- L.readFile fname path <- canonicalizePath fname let (dName, _) = splitFileName path baseName = parseURI $ "file://" ++ dName case baseName of Just bn -> putStrLn $ "Using as base: " ++ show bn _ -> hPutStrLn stderr ("Unable to convert " ++ dName ++ " to a base URI!") >> exitFailure case TTL.parseTurtle cts baseName of Left e -> hPutStrLn stderr ("Error parsing " ++ fname) >> hPutStrLn stderr ("--> " ++ e) >> exitFailure Right gr -> return $ makeTests gr main :: IO () main = do -- As there's no command-line options, always display the version putStrLn $ "Swish library: " ++ showVersion version args <- getArgs case args of [fname] -> readManifest fname >>= runTests _ -> do pName <- getProgName hPutStrLn stderr $ "Usage: " ++ pName ++ " " exitFailure