tala-compiler-0.2.0: src/Progmasoft/Tala/TestRunner.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
{- | The runner of a test build.
A test build has no @Main@ of its own. The driver finds the methods with
the @[Fact]@ attribute in the parsed sources and adds a class to their
namespace whose @Main@ calls each of them in source order, closes each with
@$Test.End@ under its name, and ends with @$Test.Finish@. The class is
ordinary source to every later stage. Its name, @$Tests@, starts with @$@,
which no identifier does, so it cannot meet a class of the program.
A test is a static method without parameters that returns @void@. Facts of
instance methods need objects, and facts in several namespaces need the
multi-module link unit; both are refused as not implemented yet.
-}
module Progmasoft.Tala.TestRunner
( TestCase (..)
, runnerClassName
, findTestCases
, testRunner
) where
import Data.List (nub)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Diagnostic
-- | One @[Fact]@ method, as the runner calls and names it.
data TestCase = TestCase
{ testNamespace :: QualifiedName
, testClass :: Identifier
, testMethod :: Identifier
, testDisplayName :: String
, testSpan :: SourceSpan
}
deriving stock (Eq, Ord, Read, Show)
-- | The name of the class the driver adds to a test build.
runnerClassName :: Identifier
runnerClassName = Identifier "$Tests"
{- | The tests of parsed source units, each given by its namespace and its
declarations, in source order; and the problems of facts that cannot run.
-}
findTestCases :: [(Maybe QualifiedName, [Declaration Identifier ()])] -> Either [Diagnostic] [TestCase]
findTestCases units = case concat [problems | Left problems <- found] of
[] -> Right [testCase | Right testCase <- found]
problems -> Left problems
where
found = concatMap unitCases units
unitCases (namespace, declarations) = concatMap (declarationCases namespace) declarations
declarationCases namespace declaration = case declaration of
TypeDeclaration {declarationName = className, typeMembers = members} ->
[ classify namespace className (method, isStatic, parameters, returnSyntax) fact
| FunctionDeclaration
{ declarationName = method
, declarationIsStatic = isStatic
, declarationParameters = parameters
, declarationReturnSyntax = returnSyntax
, declarationFact = Just fact
} <-
members
]
TemplateTypeDeclaration {typeMembers = members} ->
[ Left [problem (factSpan fact) "TAE0016" "a [Fact] method of a template class is not compiled yet"]
| FunctionDeclaration {declarationFact = Just fact} <- members
]
_ -> []
classify namespace className (method, isStatic, parameters, returnSyntax) fact = case namespace of
Nothing -> Left [problem (factSpan fact) "TAE0019" "a [Fact] method must stand in a namespace, which names the test build"]
Just name
| not isStatic ->
Left [problem (factSpan fact) "TAE0016" "an instance [Fact] method is not compiled yet; declare the test static"]
| not (null parameters) -> Left [problem (factSpan fact) "TAE0017" "a [Fact] method takes no parameters"]
| not (spellsVoid returnSyntax) -> Left [problem (factSpan fact) "TAE0018" "a [Fact] method returns void"]
| otherwise ->
Right
( TestCase
name
className
method
(maybe (identifierText className ++ "." ++ identifierText method) id (factName fact))
(factSpan fact)
)
{- | The namespace of a test build, its entry and the declaration of its
runner class. The tests must stand in one namespace, and there must be at
least one.
-}
testRunner :: [TestCase] -> Either [Diagnostic] (QualifiedName, QualifiedName, Declaration Identifier ())
testRunner cases = case (cases, nub (map testNamespace cases)) of
([], _) ->
Left [Diagnostic TypeCheckerStage Error "TAE0015" Nothing "no [Fact] method was found; a test build needs at least one"]
(first : _, [namespace@(QualifiedName parts)]) ->
Right (namespace, QualifiedName (parts ++ [runnerClassName]), runner (testSpan first))
(_, namespaces) ->
Left
[ Diagnostic
TypeCheckerStage
Error
"TAE0020"
Nothing
( "[Fact] methods stand in several namespaces ("
++ unwords (map render namespaces)
++ "); a test build of several namespaces is not compiled yet"
)
]
where
render (QualifiedName parts) = foldr1 (\part rest -> part ++ "." ++ rest) (map identifierText parts)
runner spanValue =
let statement expression = ExpressionStatement spanValue expression True
call receiver method arguments =
CallExpression
spanValue
(MemberAccessExpression spanValue (NameExpression spanValue receiver ()) (Identifier method) ())
arguments
()
text value = LiteralExpression spanValue (StringLiteral value) ()
testReceiver = Identifier "$Test"
body =
concat
[ [ statement (call (testClass testCase) (identifierText (testMethod testCase)) [])
, statement (call testReceiver "End" [text (testDisplayName testCase)])
]
| testCase <- cases
]
++ [statement (call testReceiver "Finish" [])]
main =
FunctionDeclaration
spanValue
(Identifier "Main")
()
(ExplicitType (Identifier "void"))
[]
(Block body)
True
PublicAccess
Nothing
in TypeDeclaration spanValue runnerClassName () [main]
spellsVoid :: TypeSyntax -> Bool
spellsVoid syntax = case syntax of
ExplicitType (Identifier "void") -> True
QualifiedTypeSyntax (QualifiedName [Identifier "void"]) [] -> True
_ -> False
problem :: SourceSpan -> String -> String -> Diagnostic
problem spanValue code message = Diagnostic TypeCheckerStage Error code (Just spanValue) message