{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} module TestDocs where import Prelude () import Prelude.Compat import Control.Arrow (first) import Control.Monad.IO.Class (liftIO) import Data.Foldable import Data.Maybe (fromMaybe) import Data.Monoid import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (getCurrentTime) import Data.Version (Version(..)) import System.Exit import qualified Language.PureScript as P import qualified Language.PureScript.Docs as Docs import Language.PureScript.Docs.AsMarkdown (codeToString) import qualified Language.PureScript.Publish as Publish import qualified Language.PureScript.Publish.ErrorsWarnings as Publish import Web.Bower.PackageMeta (parsePackageName) import TestUtils publishOpts :: Publish.PublishOptions publishOpts = Publish.defaultPublishOptions { Publish.publishGetVersion = return testVersion , Publish.publishGetTagTime = const (liftIO getCurrentTime) , Publish.publishWorkingTreeDirty = return () } where testVersion = ("v999.0.0", Version [999,0,0] []) main :: IO () main = pushd "examples/docs" $ do res <- Publish.preparePackage "bower.json" "resolutions.json" publishOpts case res of Left e -> Publish.printErrorToStdout e >> exitFailure Right pkg@Docs.Package{..} -> forM_ testCases $ \(P.moduleNameFromString -> mn, pragmas) -> let mdl = takeJust ("module not found in docs: " ++ T.unpack (P.runModuleName mn)) (find ((==) mn . Docs.modName) pkgModules) linksCtx = Docs.getLinksContext pkg in forM_ pragmas (\a -> runAssertionIO a linksCtx mdl) takeJust :: String -> Maybe a -> a takeJust msg = fromMaybe (error msg) data Assertion -- | Assert that a particular declaration is documented with the given -- children = ShouldBeDocumented P.ModuleName Text [Text] -- | Assert that a particular declaration is not documented | ShouldNotBeDocumented P.ModuleName Text -- | Assert that a particular declaration exists, but without a particular -- child. | ChildShouldNotBeDocumented P.ModuleName Text Text -- | Assert that a particular declaration has a particular type class -- constraint. | ShouldBeConstrained P.ModuleName Text Text -- | Assert that a particular typeclass declaration has a functional -- dependency list. | ShouldHaveFunDeps P.ModuleName Text [([Text],[Text])] -- | Assert that a particular value declaration exists, and its type -- satisfies the given predicate. | ValueShouldHaveTypeSignature P.ModuleName Text (ShowFn (P.Type -> Bool)) -- | Assert that a particular type alias exists, and its corresponding -- type, when rendered, matches a given string exactly -- fields: module, type synonym name, expected type | TypeSynonymShouldRenderAs P.ModuleName Text Text -- | Assert that a documented declaration includes a documentation comment -- containing a particular string | ShouldHaveDocComment P.ModuleName Text Text -- | Assert that there should be some declarations re-exported from a -- particular module in a particular package. | ShouldHaveReExport (Docs.InPackage P.ModuleName) -- | Assert that a link to some specific declaration exists within the -- rendered code for a declaration. Fields are: local module, local -- declaration title, title of linked declaration, namespace of linked -- declaration, destination of link. | ShouldHaveLink P.ModuleName Text Text Docs.Namespace Docs.LinkLocation deriving (Show) newtype ShowFn a = ShowFn a instance Show (ShowFn a) where show _ = "" data AssertionFailure -- | A declaration was not documented, but should have been = NotDocumented P.ModuleName Text -- | The expected list of child declarations did not match the actual list | ChildrenNotDocumented P.ModuleName Text [Text] -- | A declaration was documented, but should not have been | Documented P.ModuleName Text -- | A child declaration was documented, but should not have been | ChildDocumented P.ModuleName Text Text -- | A constraint was missing. | ConstraintMissing P.ModuleName Text Text -- | A functional dependency was missing. | FunDepMissing P.ModuleName Text [([Text], [Text])] -- | A declaration had the wrong "type" (ie, value, type, type class) -- Fields: declaration title, expected "type", actual "type". | WrongDeclarationType P.ModuleName Text Text Text -- | A value declaration had the wrong type (in the sense of "type -- checking"), eg, because the inferred type was used when the explicit type -- should have been. -- Fields: module name, declaration name, actual type. | ValueDeclarationWrongType P.ModuleName Text P.Type -- | A Type synonym has been rendered in an unexpected format -- Fields: module name, declaration name, expected rendering, actual rendering | TypeSynonymMismatch P.ModuleName Text Text Text -- | A doc comment was not found or did not match what was expected -- Fields: module name, expected substring, actual comments | DocCommentMissing P.ModuleName Text (Maybe Text) -- | A module was missing re-exports from a particular module. -- Fields: module name, expected re-export, actual re-exports. | ReExportMissing P.ModuleName (Docs.InPackage P.ModuleName) [Docs.InPackage P.ModuleName] -- | Expected to find some other declaration mentioned in this declaration's -- rendered code, but did not find anything. -- Fields: module name, declaration title, title of declaration which was -- expected but not found in. | LinkedDeclarationMissing P.ModuleName Text Text -- | Expected one link location for a declaration mentioned in some other -- declaration's rendered code, but found a different one. Fields: module -- name, title of the local declaration which links to some other -- declaration, title of the linked declaration, expected location, actual -- location. | BadLinkLocation P.ModuleName Text Text Docs.LinkLocation Docs.LinkLocation deriving (Show) data AssertionResult = Pass | Fail AssertionFailure deriving (Show) runAssertion :: Assertion -> Docs.LinksContext -> Docs.Module -> AssertionResult runAssertion assertion linksCtx Docs.Module{..} = case assertion of ShouldBeDocumented mn decl children -> case findChildren decl (declarationsFor mn) of Nothing -> Fail (NotDocumented mn decl) Just actualChildren -> if children == actualChildren then Pass else Fail (ChildrenNotDocumented mn decl actualChildren) ShouldNotBeDocumented mn decl -> case findChildren decl (declarationsFor mn) of Just _ -> Fail (Documented mn decl) Nothing -> Pass ChildShouldNotBeDocumented mn decl child -> case findChildren decl (declarationsFor mn) of Just children -> if child `elem` children then Fail (ChildDocumented mn decl child) else Pass Nothing -> Fail (NotDocumented mn decl) ShouldBeConstrained mn decl tyClass -> findDecl mn decl $ \Docs.Declaration{..} -> case declInfo of Docs.ValueDeclaration ty -> if checkConstrained ty tyClass then Pass else Fail (ConstraintMissing mn decl tyClass) _ -> Fail (WrongDeclarationType mn decl "value" (Docs.declInfoToString declInfo)) ShouldHaveFunDeps mn decl fds -> findDecl mn decl $ \Docs.Declaration{..} -> case declInfo of Docs.TypeClassDeclaration _ _ fundeps -> if fundeps == fds then Pass else Fail (FunDepMissing mn decl fds) _ -> Fail (WrongDeclarationType mn decl "value" (Docs.declInfoToString declInfo)) ValueShouldHaveTypeSignature mn decl (ShowFn tyPredicate) -> findDecl mn decl $ \Docs.Declaration{..} -> case declInfo of Docs.ValueDeclaration ty -> if tyPredicate ty then Pass else Fail (ValueDeclarationWrongType mn decl ty) _ -> Fail (WrongDeclarationType mn decl "value" (Docs.declInfoToString declInfo)) TypeSynonymShouldRenderAs mn decl expected -> findDecl mn decl $ \Docs.Declaration{..} -> case declInfo of Docs.TypeSynonymDeclaration [] ty -> let actual = codeToString (Docs.renderType ty) in if actual == expected then Pass else Fail (TypeSynonymMismatch mn decl expected actual) _ -> Fail (WrongDeclarationType mn decl "synonym" (Docs.declInfoToString declInfo)) ShouldHaveDocComment mn decl expected -> findDecl mn decl $ \Docs.Declaration{..} -> if maybe False (expected `T.isInfixOf`) declComments then Pass else Fail (DocCommentMissing mn decl declComments) ShouldHaveReExport reExp -> let reExps = map fst modReExports in if reExp `elem` reExps then Pass else Fail (ReExportMissing modName reExp reExps) ShouldHaveLink mn decl destTitle destNs expectedLoc -> findDecl mn decl $ \decl' -> let rendered = Docs.renderDeclaration decl' in case extract rendered destNs destTitle of Just (Docs.linkLocation -> actualLoc) -> if expectedLoc == actualLoc then Pass else Fail (BadLinkLocation mn decl destTitle expectedLoc actualLoc) Nothing -> Fail (LinkedDeclarationMissing mn decl destTitle) where declarationsFor mn = if mn == modName then modDeclarations else fromMaybe [] (lookup mn (map (first Docs.ignorePackage) modReExports)) findChildren title = fmap childrenTitles . find ((==) title . Docs.declTitle) findDecl mn title f = case find ((==) title . Docs.declTitle) (declarationsFor mn) of Nothing -> Fail (NotDocumented mn title) Just decl -> f decl childrenTitles = map Docs.cdeclTitle . Docs.declChildren extract :: Docs.RenderedCode -> Docs.Namespace -> Text -> Maybe Docs.DocLink extract rc ns title = getFirst (Docs.outputWith (First . go) rc) >>= getLink where getLink = Docs.getLink linksCtx (P.moduleNameFromString "$DocsTest") ns title go = \case Docs.Symbol ns' title' (Docs.Link containingMod) | ns' == ns && title' == title -> Just containingMod _ -> Nothing checkConstrained :: P.Type -> Text -> Bool checkConstrained ty tyClass = case ty of P.ConstrainedType c ty' | matches tyClass c -> True | otherwise -> checkConstrained ty' tyClass P.ForAll _ ty' _ -> checkConstrained ty' tyClass _ -> False where matches className = (==) className . P.runProperName . P.disqualify . P.constraintClass runAssertionIO :: Assertion -> Docs.LinksContext -> Docs.Module -> IO () runAssertionIO assertion linksCtx mdl = do putStrLn ("In " ++ T.unpack (P.runModuleName (Docs.modName mdl)) ++ ": " ++ show assertion) case runAssertion assertion linksCtx mdl of Pass -> pure () Fail reason -> do putStrLn ("Failed: " <> show reason) exitFailure testCases :: [(Text, [Assertion])] testCases = [ ("Example", [ -- From dependencies ShouldBeDocumented (n "Prelude") "Unit" [] , ShouldNotBeDocumented (n "Prelude") "unit" -- From local files , ShouldBeDocumented (n "Example2") "one" [] , ShouldNotBeDocumented (n "Example2") "two" -- Re-exports , ShouldHaveReExport (Docs.FromDep (pkg "purescript-prelude") (n "Prelude")) , ShouldHaveReExport (Docs.Local (n "Example2")) ]) , ("Example2", [ ShouldBeDocumented (n "Example2") "one" [] , ShouldBeDocumented (n "Example2") "two" [] , ShouldHaveLink (n "Example2") "one" "Int" Docs.TypeLevel (Docs.BuiltinModule (n "Prim")) ]) , ("UTF8", [ ShouldBeDocumented (n "UTF8") "thing" [] ]) , ("Transitive1", [ ShouldBeDocumented (n "Transitive2") "transitive3" [] ]) , ("NotAllCtors", [ ShouldBeDocumented (n "Prelude") "Boolean2" ["True"] , ChildShouldNotBeDocumented (n "Prelude") "Boolean2" "False" ]) , ("DuplicateNames", [ ShouldBeDocumented (n "Prelude") "Unit" [] , ShouldBeDocumented (n "DuplicateNames") "unit" [] , ShouldNotBeDocumented (n "Prelude") "unit" ]) , ("MultiVirtual", [ ShouldBeDocumented (n "MultiVirtual1") "foo" [] , ShouldBeDocumented (n "MultiVirtual2") "bar" [] , ShouldBeDocumented (n "MultiVirtual2") "baz" [] ]) , ("Clash", [ ShouldBeDocumented (n "Clash1") "value" [] , ShouldBeDocumented (n "Clash1") "Type" [] , ShouldBeDocumented (n "Clash1") "TypeClass" ["typeClassMember"] ]) , ("SolitaryTypeClassMember", [ ShouldBeDocumented (n "SomeTypeClass") "member" [] , ShouldNotBeDocumented (n "SomeTypeClass") "SomeClass" , ShouldBeConstrained (n "SomeTypeClass") "member" "SomeClass" ]) , ("ReExportedTypeClass", [ ShouldBeDocumented (n "SomeTypeClass") "SomeClass" ["member"] ]) , ("TypeClassWithoutMembers", [ ShouldBeDocumented (n "TypeClassWithoutMembersIntermediate") "SomeClass" [] , ChildShouldNotBeDocumented (n "TypeClassWithoutMembersIntermediate") "SomeClass" "member" ]) , ("TypeClassWithFunDeps", [ ShouldHaveFunDeps (n "TypeClassWithFunDeps") "TypeClassWithFunDeps" [(["a","b"], ["c"]), (["c"], ["d","e"])] ]) , ("NewOperators", [ ShouldBeDocumented (n "NewOperators2") "(>>>)" [] ]) , ("ExplicitTypeSignatures", [ ValueShouldHaveTypeSignature (n "ExplicitTypeSignatures") "explicit" (ShowFn (hasTypeVar "something")) , ValueShouldHaveTypeSignature (n "ExplicitTypeSignatures") "anInt" (ShowFn (P.tyInt ==)) , ValueShouldHaveTypeSignature (n "ExplicitTypeSignatures") "aNumber" (ShowFn (P.tyNumber ==)) ]) , ("ConstrainedArgument", [ TypeSynonymShouldRenderAs (n "ConstrainedArgument") "WithoutArgs" "forall a. (Partial => a) -> a" , TypeSynonymShouldRenderAs (n "ConstrainedArgument") "WithArgs" "forall a. (Foo a => a) -> a" , TypeSynonymShouldRenderAs (n "ConstrainedArgument") "MultiWithoutArgs" "forall a. (Partial => Partial => a) -> a" , TypeSynonymShouldRenderAs (n "ConstrainedArgument") "MultiWithArgs" "forall a b. (Foo a => Foo b => a) -> a" ]) , ("TypeOpAliases", [ ValueShouldHaveTypeSignature (n "TypeOpAliases") "test1" (renderedType "forall a b. a ~> b") , ValueShouldHaveTypeSignature (n "TypeOpAliases") "test2" (renderedType "forall a b c. a ~> b ~> c") , ValueShouldHaveTypeSignature (n "TypeOpAliases") "test3" (renderedType "forall a b c d. a ~> (b ~> c) ~> d") , ValueShouldHaveTypeSignature (n "TypeOpAliases") "test4" (renderedType "forall a b c d. ((a ~> b) ~> c) ~> d") , ValueShouldHaveTypeSignature (n "TypeOpAliases") "third" (renderedType "forall a b c. a × b × c -> c") ]) , ("DocComments", [ ShouldHaveDocComment (n "DocComments") "example" " example == 0" ]) , ("TypeLevelString", [ ShouldBeDocumented (n "TypeLevelString") "Foo" ["fooBar"] ]) , ("Desugar", [ ValueShouldHaveTypeSignature (n "Desugar") "test" (renderedType "forall a b. X (a -> b) a -> b") ]) , ("ChildDeclOrder", [ ShouldBeDocumented (n "ChildDeclOrder") "Two" ["First", "Second", "showTwo", "fooTwo"] , ShouldBeDocumented (n "ChildDeclOrder") "Foo" ["foo1", "foo2", "fooTwo", "fooInt"] ]) ] where n = P.moduleNameFromString pkg str = let Right p = parsePackageName str in p hasTypeVar varName = getAny . P.everythingOnTypes (<>) (Any . isVar varName) isVar varName (P.TypeVar name) | varName == T.unpack name = True isVar _ _ = False renderedType expected = ShowFn $ \ty -> codeToString (Docs.renderType ty) == expected