Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32bb39f
Add pass/fail count line to daml test summary
roger-bosman-da Jun 29, 2026
b854e4d
Hide passing tests from summary when there are failures
roger-bosman-da Jun 29, 2026
6df7f02
Auto-detect color support for daml test output
roger-bosman-da Jun 29, 2026
842d49e
Only show coverage stats when --show-coverage is passed
roger-bosman-da Jun 29, 2026
45ac501
Show project path and 'Running tests' header in daml test output
roger-bosman-da Jun 29, 2026
8857188
[UNDO ME] break tests
roger-bosman-da Jun 29, 2026
78e9b88
Move test summary below diagnostics and hide 'Running tests' header w…
roger-bosman-da Jun 29, 2026
5dcd752
Improve daml test output formatting
roger-bosman-da Jun 30, 2026
66684f8
Use package name as identifier and improve empty test suite messaging
roger-bosman-da Jun 30, 2026
8586324
Show relative paths in test summary, reorder test output
roger-bosman-da Jun 30, 2026
788c0de
Show absolute paths in diagnostic output
roger-bosman-da Jun 30, 2026
0af9da5
Simplify diagnostic output format for test failures
roger-bosman-da Jun 30, 2026
679e7e3
Revert "Simplify diagnostic output format for test failures"
roger-bosman-da Jun 30, 2026
7017e5b
Revert "[UNDO ME] break tests"
roger-bosman-da Jun 30, 2026
31fb9a6
Fix code review findings in test output
roger-bosman-da Jun 30, 2026
1438dcb
Include script name in diagnostic output and fix green coloring
roger-bosman-da Jun 30, 2026
291f279
Improve test output formatting: add newlines between diagnostics, sim…
roger-bosman-da Jul 1, 2026
a10bb25
Improve diagnostic output format with colored failure header
roger-bosman-da Jul 1, 2026
706f343
Fix damlc-lint test by restoring original diagnostic format
roger-bosman-da Jul 2, 2026
50815df
add release notes
roger-bosman-da Jul 20, 2026
04860b2
rm pr-description.md
roger-bosman-da Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions sdk/UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ schedule, i.e. if you add an entry effective at or after the first
header, prepend the new date header that corresponds to the
Wednesday after your change.

## Until 2026-07-21 (Exclusive)

### Improved daml test output
- Test summary now appears at the end of output, making pass/fail results
immediately visible
- Summary header shows pass/fail counts (e.g., "Test Summary: 2 failed, 3
passed")
- When tests fail, only failed tests are shown in the summary to focus attention
on what needs fixing
- Colored output is auto-detected based on terminal capabilities
- Coverage statistics are now hidden by default; use --show-coverage to display
them
- Test file paths in the summary are now relative to the project root for
readability
- Tests are grouped by file, with cleaner formatting for multiple tests in the
same file
- Simplified diagnostic output: removed verbose LSP formatting, now shows just
the file path, script name, and error message

## Until 2026-07-15 (Exclusive)
- Daml Stdlib: add `(:|)` and `(<|)` synonyms for `DA.NonEmpty`, and define `Show`
using the same representation (PR #22460)
Expand Down
14 changes: 11 additions & 3 deletions sdk/compiler/damlc/daml-compiler/src/DA/Daml/Compiler/Output.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Development.IDE.Core.Shake (NotificationHandler(..))
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import qualified Language.LSP.Types as LSP
import System.Directory (makeAbsolute)
import System.IO
import Control.Exception (bracket)

Expand Down Expand Up @@ -49,13 +50,20 @@ writeOutputBSL :: FilePath -> BSL.ByteString -> IO ()
writeOutputBSL = writeOutputWith BSL.hPutStr

-- WARNING: Here be dragons
-- T.putStrLn is locale-dependent. This seems to cause issues with Nix patched glibc that
-- T.putStrLn is locale-dependent. This seems to cause issues with Nix' patched glibc that
-- relies on LOCALE_ARCHIVE being set correctly. This is the case in our dev-env
-- but not when we ship the SDK. If LOCALE_ARCHIVE is not set properly the colored
-- diagnostics get eaten somewhere in glibc and we dont even get a write syscall containing them.
-- diagnostics get eaten somewhere in glibc and we don't even get a write syscall containing them.
printDiagnostics :: Handle -> [FileDiagnostic] -> IO ()
printDiagnostics _ [] = return ()
printDiagnostics handle xs = BS.hPutStrLn handle $ T.encodeUtf8 $ showDiagnosticsColored xs
printDiagnostics handle xs = do
xs' <- mapM makeAbsoluteDiag xs
BS.hPutStrLn handle $ T.encodeUtf8 $ showDiagnosticsColored xs'

makeAbsoluteDiag :: FileDiagnostic -> IO FileDiagnostic
makeAbsoluteDiag (fp, showDiag, diag) = do
absPath <- makeAbsolute (fromNormalizedFilePath fp)
pure (toNormalizedFilePath' absPath, showDiag, diag)

diagnosticsLogger :: NotificationHandler
diagnosticsLogger = hDiagnosticsLogger stderr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ runSingleScriptRule =
forM scripts $ \(script, loc) -> do
(vr, res) <- runScript scriptService (Just file) ctxId (LF.moduleName m) script
let range = maybe noRange sourceLocToRange loc
pure (toDiagnostics lvl world file range res, (vr, res))
pure (toDiagnostics lvl world file range targetScriptName res, (vr, res))
let (diags, results) = unzip scriptResults
pure (concat diags, Just results)

Expand Down Expand Up @@ -1064,9 +1064,10 @@ toDiagnostics ::
-> LF.World
-> NormalizedFilePath
-> Range
-> T.Text -- ^ Script name
-> Either SS.Error SS.ScriptResult
-> [FileDiagnostic]
toDiagnostics lvl world scriptFile scriptRange = \case
toDiagnostics lvl world scriptFile scriptRange scriptName = \case
Left err -> pure $ mkDiagnostic DsError (scriptFile, scriptRange) $
formatScriptError lvl world err
Right SS.ScriptResult{..} ->
Expand All @@ -1079,7 +1080,7 @@ toDiagnostics lvl world scriptFile scriptRange = \case
mkDiagnostic severity (file, range) pretty = (file, ShowDiag, ) $ Diagnostic
{ _range = range
, _severity = Just severity
, _source = Just "Script"
, _source = Just $ "Script: " <> scriptName
, _message = Pretty.renderPlain pretty
, _code = Nothing
, _tags = Nothing
Expand Down
4 changes: 2 additions & 2 deletions sdk/compiler/damlc/lib/DA/Cli/Damlc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ runTestsInPackageOrFiles packageLocationOpts Nothing allTests _ coverage color m
-- Therefore we keep the behavior of only passing the root file
-- if source points to a specific file.
files <- getDamlRootFiles pSrc
execTest files allTests coverage color mbJUnitOutput (Just pkgConfig) cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters
execTest files allTests coverage color mbJUnitOutput (Just pkgConfig) cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters (Just pPath)
runTestsInPackageOrFiles packageLocationOpts (Just inFiles) allTests _ coverage color mbJUnitOutput cliOptions initPkgDb tableOutputPath transactionsOutputPath coveragePaths coverageFilters = Command Test (Just packageLocationOpts) effect
where effect = withPackageRoot (packageRoot packageLocationOpts) (packageLocationCheck packageLocationOpts) $ \mPackageRoot relativize -> do
cliOptions <- addResolutionData cliOptions
Expand All @@ -518,7 +518,7 @@ runTestsInPackageOrFiles packageLocationOpts (Just inFiles) allTests _ coverage
Just packagePath -> withMaybeConfig (withPackageConfig (PackagePath packagePath)) pure
Nothing -> pure Nothing
inFiles' <- mapM (fmap toNormalizedFilePath' . relativize) inFiles
execTest inFiles' allTests coverage color mbJUnitOutput mPkgConfig cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters
execTest inFiles' allTests coverage color mbJUnitOutput mPkgConfig cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters mPackageRoot

cmdInspect :: Mod CommandFields Command
cmdInspect =
Expand Down
150 changes: 113 additions & 37 deletions sdk/compiler/damlc/lib/DA/Cli/Damlc/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Data.Text.Lazy as TL
import Data.Tuple.Extra
import qualified Data.Vector as V
import Development.IDE.Core.API
import Development.IDE.Core.IdeState.Daml
import Development.IDE.Core.RuleTypes.Daml
Expand All @@ -52,11 +51,11 @@ import qualified Development.Shake as Shake
import Safe
import qualified ScriptService as SS
import qualified DA.Cli.Damlc.Test.TestResults as TR
import System.Console.ANSI (SGR(..), setSGRCode, Underlining(..), ConsoleIntensity(..))
import System.Console.ANSI (SGR(..), setSGRCode, Underlining(..), ConsoleIntensity(..), Color(..), ColorIntensity(..), ConsoleLayer(..))
import System.Directory (createDirectoryIfMissing)
import System.Exit (exitFailure)
import System.FilePath
import System.IO (hPutStrLn, stderr)
import System.IO (hIsTerminalDevice, hPutStrLn, stderr, stdout)
import System.IO.Error (isPermissionError, isAlreadyExistsError, isDoesNotExistError)
import qualified Text.XML.Light as XML
import qualified Text.Blaze.Html.Renderer.Text as Blaze
Expand All @@ -67,6 +66,8 @@ import ComponentVersion.Class (ComponentVersioned)

newtype UseColor = UseColor {getUseColor :: Bool}
newtype ShowCoverage = ShowCoverage {getShowCoverage :: Bool}

type ScriptTestResult = (TR.LocalOrExternal, ScriptName, Either SSC.Error SSC.ScriptResult)
newtype CoverageFilter = CoverageFilter {getCoverageFilter :: Regex}
newtype RunAllOption = RunAllOption {getRunAllTests :: Bool}
newtype TableOutputPath = TableOutputPath {getTableOutputPath :: Maybe String}
Expand All @@ -91,16 +92,27 @@ execTest
-> TransactionsOutputPath
-> CoveragePaths
-> [CoverageFilter]
-> Maybe FilePath
-> IO ()
execTest inFiles runAllOption coverage color mbJUnitOutput mPkgConfig opts tableOutputPath transactionsOutputPath resultsIO coverageFilters = do
execTest inFiles runAllOption coverage color mbJUnitOutput mPkgConfig opts tableOutputPath transactionsOutputPath resultsIO coverageFilters mbProjectPath = do
loggerH <- getLogger opts "test"
color <- if getUseColor color then pure color else do
isTTY <- hIsTerminalDevice stdout
pure $ UseColor isTTY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pure $ UseColor isTTY
color <- if getUseColor color then pure color else fmap UseColor (hIsTerminalDevice stdout)

Dropping unnecessary use of do

let optsWithPkg = case mPkgConfig of
Just PackageConfigFields{..} -> opts { optMbPackageName = Just pName, optMbPackageVersion = pVersion }
Nothing -> opts
-- Prefer package name over path for identification in headers
packageIdentifier = case mPkgConfig of
Just PackageConfigFields{..} -> Just $ T.unpack $ LF.unPackageName pName
Nothing -> mbProjectPath
printTestSuiteBegin color packageIdentifier
withDamlIdeState optsWithPkg loggerH noopLogger $ \h -> do
flip finally (getDiagnostics h >>= printDiagnostics stderr) $ do
runAndReport h inFiles (optDetailLevel opts) (optDamlLfVersion opts) runAllOption coverage color mbJUnitOutput tableOutputPath transactionsOutputPath resultsIO coverageFilters
summaryResults <- runAndReport h inFiles (optDetailLevel opts) (optDamlLfVersion opts) runAllOption coverage mbJUnitOutput tableOutputPath transactionsOutputPath resultsIO coverageFilters
diags <- getDiagnostics h
-- Pass both identifier (for header) and path (for relativizing test file paths)
printSummary color packageIdentifier mbProjectPath summaryResults
when (any (\(_, _, diag) -> Just DsError == _severity diag) diags) exitFailure

loadAggregatePrintResults :: CoveragePaths -> [CoverageFilter] -> ShowCoverage -> Maybe TR.TestResults -> IO ()
Expand Down Expand Up @@ -131,19 +143,17 @@ runAndReport ::
-> LF.Version
-> RunAllOption
-> ShowCoverage
-> UseColor
-> Maybe FilePath
-> TableOutputPath
-> TransactionsOutputPath
-> CoveragePaths
-> [CoverageFilter]
-> IO ()
runAndReport ideState inFiles lvl lfVersion runAllOption coverage color mbJUnitOutput tableOutputPath transactionsOutputPath resultsIO coverageFilters = do
-> IO [ScriptTestResult]
runAndReport ideState inFiles lvl lfVersion runAllOption coverage mbJUnitOutput tableOutputPath transactionsOutputPath resultsIO coverageFilters = do
(localResults, extResults) <- runAllScripts ideState inFiles runAllOption
let allResults = localResults ++ extResults
let allPackages = [loe | TR.ScriptResults loe _ _ <- allResults]
-- print test summary after all tests have run
printSummary color [(loe, scriptName, res) | TR.ScriptResults loe _ (Just results) <- allResults, (scriptName, res) <- results]
let summaryResults = [(loe, scriptName, res) | TR.ScriptResults loe _ (Just results) <- allResults, (scriptName, res) <- results]

let newTestResults = TR.scriptResultsToTestResults allPackages allResults
loadAggregatePrintResults resultsIO coverageFilters coverage (Just newTestResults)
Expand Down Expand Up @@ -179,6 +189,8 @@ runAndReport ideState inFiles lvl lfVersion runAllOption coverage color mbJUnitO
]
writeFile junitOutput $ XML.showTopElement $ toJUnit res

pure summaryResults

runAllScripts :: IdeState -> [NormalizedFilePath] -> RunAllOption -> IO ([TR.ScriptResults], [TR.ScriptResults])
runAllScripts h inFiles (RunAllOption runAllOption) = do
-- make sure none of the files disappear
Expand Down Expand Up @@ -295,25 +307,98 @@ failedTestOutput h file = do
pure $ map (, Just errMsg) scriptNames


printSummary :: UseColor -> [(TR.LocalOrExternal, ScriptName, Either SSC.Error SSC.ScriptResult)] -> IO ()
printSummary color res =
printTestSuiteBegin :: UseColor -> Maybe String -> IO ()
printTestSuiteBegin color mbIdentifier =
whenJust mbIdentifier $ \identifier -> do
let colored = getUseColor color
putStrLn $
(if colored then setSGRCode [SetConsoleIntensity BoldIntensity] else "")
<> "Running tests (" <> identifier <> ") ..."
<> (if colored then setSGRCode [] else "")
Comment on lines +315 to +317

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, let's try a function like "renderColor"

Suggested change
(if colored then setSGRCode [SetConsoleIntensity BoldIntensity] else "")
<> "Running tests (" <> identifier <> ") ..."
<> (if colored then setSGRCode [] else "")
renderColor color [Important] ("Running tests (" <> identifier <> ") ...")

Which handles unsetting the color, and lets us add another backend in the future (Html, for IDE output)


printSummary :: UseColor -> Maybe String -> Maybe FilePath -> [ScriptTestResult] -> IO ()
printSummary color mbIdentifier mbProjectPath res =
liftIO $ do
putStrLn $
unlines
[ setSGRCode [SetUnderlining SingleUnderline, SetConsoleIntensity BoldIntensity]
, "Test Summary" <> setSGRCode []
]
printScriptResults color res

printScriptResults :: UseColor -> [(TR.LocalOrExternal, ScriptName, Either SSC.Error SS.ScriptResult)] -> IO ()
printScriptResults color results = do
liftIO $ forM_ results $ \(loe, ScriptName scriptName, resultOrErr) -> do
let name = DA.Pretty.pretty (TR.localOrExternalName loe) <> ":" <> DA.Pretty.pretty scriptName
let stringStyleToRender = if getUseColor color then DA.Pretty.renderColored else DA.Pretty.renderPlain
putStrLn $ stringStyleToRender $
case resultOrErr of
Left _err -> name <> ": " <> DA.Pretty.error_ "failed"
Right result -> name <> ": " <> prettyResult result
let failedTests = [r | r@(_, _, Left _) <- res]
nFailed = length failedTests
nPassed = length res - nFailed
nTotal = length res
colored = getUseColor color
identifierSuffix = maybe "" (\ident -> " (" <> ident <> ")") mbIdentifier

-- Handle the "no tests found" case
if nTotal == 0
then do
putStrLn $
(if colored then setSGRCode [SetUnderlining SingleUnderline, SetConsoleIntensity BoldIntensity] else "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

<> "Test Summary" <> identifierSuffix
<> (if colored then setSGRCode [] else "")
<> ": No tests found"
else do
let countLine
| nFailed > 0 =
(if colored then setSGRCode [SetColor Foreground Vivid Red] else "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

<> show nFailed <> " failed"
<> (if colored then setSGRCode [] else "")
<> ", " <> show nPassed <> " passed"
| otherwise =
(if colored then setSGRCode [SetColor Foreground Vivid Green] else "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, and I let you find the rest

<> show nPassed <> " passed"
<> (if colored then setSGRCode [] else "")

-- Print combined header line
putStrLn $
(if colored then setSGRCode [SetUnderlining SingleUnderline, SetConsoleIntensity BoldIntensity] else "")
<> "Test Summary" <> identifierSuffix
<> (if colored then setSGRCode [] else "")
<> ": " <> countLine

-- Only show failed tests in summary (passed tests are hidden when there are failures)
-- This keeps the output focused on what needs attention
when (nFailed == 0) $
printScriptResults color mbProjectPath res

-- Show failed tests last (most visible)
when (nFailed > 0) $

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to report that these are only the failed tests, so that it's clear that the successes aren't included.

printScriptResults color mbProjectPath failedTests

printScriptResults :: UseColor -> Maybe FilePath -> [ScriptTestResult] -> IO ()
printScriptResults color mbProjectPath results = do
-- Group results by file/package
let grouped = groupBy (\(loe1, _, _) (loe2, _, _) -> TR.localOrExternalName loe1 == TR.localOrExternalName loe2) results
liftIO $ forM_ grouped $ \groupResults -> do
-- Use short relative paths in test summary for readability.
-- Note: makeRelative returns the absolute path unchanged if paths have different
-- roots (e.g., different drives on Windows). This is acceptable as the absolute
-- path is still a valid, clickable path in the output.
let (loe, _, _) = head groupResults
loeName <- case loe of
TR.Local file _ -> do
let absPath = fromNormalizedFilePath file
let relativePath = maybe absPath (\projectPath -> makeRelative projectPath absPath) mbProjectPath
pure $ T.pack relativePath
TR.External _ -> pure $ TR.localOrExternalName loe
let colored = getUseColor color
failedResults = [(name, err) | (_, ScriptName name, Left err) <- groupResults]
passedResults = [(name, res) | (_, ScriptName name, Right res) <- groupResults]
nFailed = length failedResults
nPassed = length passedResults
-- Print summary line for this file
if nFailed > 0
then do
let failedNames = map fst failedResults
testWord = if nFailed == 1 then "test" else "tests"
failedText = if colored then setSGRCode [SetColor Foreground Vivid Red] <> "failed" <> setSGRCode [] else "failed"
if nFailed == 1
then putStrLn $ T.unpack loeName <> ": 1 " <> testWord <> " " <> failedText <> ": " <> T.unpack (head failedNames)
else do
putStrLn $ T.unpack loeName <> ": " <> show nFailed <> " " <> testWord <> " " <> failedText
forM_ failedNames $ \name ->
putStrLn $ " - " <> T.unpack name
else do
let testWord = if nPassed == 1 then "test" else "tests"
passedText = if colored then setSGRCode [SetColor Foreground Vivid Green] <> "passed" <> setSGRCode [] else "passed"
putStrLn $ T.unpack loeName <> ": " <> show nPassed <> " " <> testWord <> " " <> passedText


prettyErr :: PrettyLevel -> LF.Version -> SSC.Error -> DA.Pretty.Doc Pretty.SyntaxClass
Expand All @@ -328,15 +413,6 @@ prettyErr lvl lfVersion err = case err of
SSC.ExceptionError e -> DA.Pretty.string $ show e


prettyResult :: SS.ScriptResult -> DA.Pretty.Doc Pretty.SyntaxClass
prettyResult result =
let nTx = length (SS.scriptResultScriptSteps result)
nActive = length $ filter (SS.isActive (SS.activeContractsFromScriptResult result)) (V.toList (SS.scriptResultNodes result))
in DA.Pretty.typeDoc_ "ok, "
<> DA.Pretty.int nActive <> DA.Pretty.typeDoc_ " active contracts, "
<> DA.Pretty.int nTx <> DA.Pretty.typeDoc_ " transactions."


toJUnit :: [(NormalizedFilePath, [(ScriptName, Maybe T.Text)])] -> XML.Element
toJUnit results =
XML.node
Expand Down
2 changes: 2 additions & 0 deletions sdk/compiler/damlc/lib/DA/Cli/Damlc/Test/TestResults.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Set as S
import qualified Data.Map.Strict as M
import Control.Monad (when)
import Data.Maybe (mapMaybe)
import Data.Foldable (fold)
import Text.Printf
Expand Down Expand Up @@ -537,6 +538,7 @@ printTestCoverage showCoverage testResults@TestResults { templates, interfaceIns
[ printf " %s: %d" variety (M.size names)
] ++ [ " " ++ printer id | (id, _value) <- M.toList names ]
in
when showCoverage $
putStrLn $
unlines $
concat
Expand Down
Loading
Loading