diff --git a/CssOsvvmStyle.css b/CssOsvvmStyle.css index 581910b..999a975 100644 --- a/CssOsvvmStyle.css +++ b/CssOsvvmStyle.css @@ -46,9 +46,9 @@ body { header { } -main { +/* main { max-width: 1200px; -} +} */ footer { } @@ -138,6 +138,18 @@ table, th, td { } table.testsuite-summary-table { + table-layout: auto; +} + +/* Keep displayed suite title/name on one line in the Test Suite Summary table */ +table.testsuite-summary-table th:first-child, +table.testsuite-summary-table td:first-child { + /* width: 1%; */ + white-space: nowrap; +} + +table.testsuite-summary-table td:first-child a { + white-space: nowrap; } table.testsuite-details-table { @@ -203,10 +215,97 @@ tfoot { text-align: right; } +/* Datatype badges (used for Type columns: string/integer/boolean) */ +.datatype { + display: inline-block; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + font-size: 0.95em; + padding: 0px 0.35em; + border: 1px solid #CCCCCC; + background-color: #f8f8f8; + border-radius: 3px; +} + summary > b { font-weight: bold; } +/* Per-suite Test Case Summary heading: emphasize suite name, de-emphasize suffix */ +summary.suite-testcase-summary-heading { + font-weight: bold; +} + +summary.suite-testcase-summary-heading .suite-name { + font-weight: bold; +} + +summary.suite-testcase-summary-heading .suite-sep { + font-weight: bold; +} + +summary.suite-testcase-summary-heading .suite-suffix { + font-weight: bold; + font-style: italic; + font-size: 0.95em; +} + +/* Per-testcase report sections: " — Summary/Description/Tags/Generics" */ +summary.testcase-section-heading { + font-weight: bold; +} + +summary.testcase-section-heading .tc-name { + font-weight: bold; +} + +summary.testcase-section-heading .tc-sep { + font-weight: bold; +} + +summary.testcase-section-heading .tc-suffix { + font-weight: bold; + font-style: italic; + font-size: 0.95em; +} + +/* Section titles that are not (e.g. h2 Alert Report) */ +h2.testcase-section-title { + font-weight: bold; +} + +h2.testcase-section-title .tc-name { + font-weight: bold; +} + +h2.testcase-section-title .tc-sep { + font-weight: bold; +} + +h2.testcase-section-title .tc-suffix { + font-weight: bold; + font-style: italic; + font-size: 0.95em; +} + +/* Keep displayed test title/name on one line in the Test Case Summary table */ +table.testcase-summary-table { + table-layout: auto; + display: inline-table; + width: auto; + max-width: 100%; +} + +table.testcase-summary-table th:first-child, +table.testcase-summary-table td:first-child { + /* width: 1%; */ + white-space: nowrap; +} + +table.testcase-summary-table td:first-child a { + white-space: nowrap; +} + /* #logo { width: 100%; }*/ diff --git a/OsvvmScriptsCore.tcl b/OsvvmScriptsCore.tcl index 6121117..f31d83b 100644 --- a/OsvvmScriptsCore.tcl +++ b/OsvvmScriptsCore.tcl @@ -1266,9 +1266,11 @@ proc AfterSimulateReports {} { WriteTestCaseSettingsYaml $TestCaseSettingsFile + # FinishSimulateBuildYaml computes ElapsedTime and writes it to build YAML. + # It also appends ElapsedTime into the per-test *_run.yml so per-test HTML can display it. + FinishSimulateBuildYaml + Simulate2Html $TestCaseSettingsFile - - FinishSimulateBuildYaml } @@ -1418,6 +1420,128 @@ proc TestSuite {SuiteName} { # CreateDirectory [file join ${::osvvm::CurrentSimulationDirectory} ${::osvvm::ResultsDirectory} ${TestSuiteName}] } +# ------------------------------------------------- +# SetTestSuiteDescription +# Sets a suite-level description which is written into the build YAML +# and displayed in the HTML "Test Suite Summary" Description column. +# +# Call this after TestSuite and before the suite finishes. +proc SetTestSuiteDescription {Description} { + set ::osvvm::TestSuiteDescription $Description +} + +# ------------------------------------------------- +# ClearTestSuiteDescription +# Clears any previously set suite-level description. +proc ClearTestSuiteDescription {} { + set ::osvvm::TestSuiteDescription "" +} + +# ------------------------------------------------- +# GetTestSuiteDescription +# Returns the currently configured suite-level description. +proc GetTestSuiteDescription {} { + if {[info exists ::osvvm::TestSuiteDescription]} { + return $::osvvm::TestSuiteDescription + } + return "" +} + +# ------------------------------------------------- +# SetTestSuiteBrief +# Sets a suite-level brief (plain text) for summary tables. +# +# Call this after TestSuite and before the suite finishes. +proc SetTestSuiteBrief {Brief} { + if {![info exists ::osvvm::TestSuiteBriefMaxLength]} { + set ::osvvm::TestSuiteBriefMaxLength 120 + } + if {$::osvvm::TestSuiteBriefMaxLength > 0 && [string length $Brief] > $::osvvm::TestSuiteBriefMaxLength} { + puts "Warning: SetTestSuiteBrief length ([string length $Brief]) exceeds TestSuiteBriefMaxLength ($::osvvm::TestSuiteBriefMaxLength)" + } + set ::osvvm::TestSuiteBrief $Brief +} + +# ------------------------------------------------- +# SetTestSuiteTitle +# Sets a suite-level title (human-friendly) for reports. +# The suite name remains the identifier. +# +# Call this after TestSuite and before the suite finishes. +proc SetTestSuiteTitle {Title} { + if {![info exists ::osvvm::TestSuiteTitleMaxLength]} { + set ::osvvm::TestSuiteTitleMaxLength 80 + } + if {$::osvvm::TestSuiteTitleMaxLength > 0 && [string length $Title] > $::osvvm::TestSuiteTitleMaxLength} { + puts "Warning: SetTestSuiteTitle length ([string length $Title]) exceeds TestSuiteTitleMaxLength ($::osvvm::TestSuiteTitleMaxLength)" + } + set ::osvvm::TestSuiteTitle $Title +} + +# ------------------------------------------------- +# ClearTestSuiteTitle +# Clears any previously set suite-level title. +proc ClearTestSuiteTitle {} { + set ::osvvm::TestSuiteTitle "" +} + +# ------------------------------------------------- +# GetTestSuiteTitle +# Returns the currently configured suite-level title. +proc GetTestSuiteTitle {} { + if {[info exists ::osvvm::TestSuiteTitle]} { + return $::osvvm::TestSuiteTitle + } + return "" +} + +# ------------------------------------------------- +# ClearTestSuiteBrief +# Clears any previously set suite-level brief. +proc ClearTestSuiteBrief {} { + set ::osvvm::TestSuiteBrief "" +} + +# ------------------------------------------------- +# GetTestSuiteBrief +# Returns the currently configured suite-level brief. +proc GetTestSuiteBrief {} { + if {[info exists ::osvvm::TestSuiteBrief]} { + return $::osvvm::TestSuiteBrief + } + return "" +} + +# ------------------------------------------------- +# Test Case Summary (HTML) Column Control +# +# These APIs control which columns are shown in the build HTML " Test Case Summary" table. +# Defaults: +# - Generics: visible (all generics found in the suite) +# - Tags: visible (all visible tags found in the suite) +# +# Notes: +# - Calling SetTestCaseSummaryGenerics with no args clears the whitelist (show all). +# - Calling SetTestCaseSummaryTags with no args clears the whitelist (show all tags found). + +proc SetTestCaseSummaryGenerics {args} { + set ::osvvm::TestCaseSummaryShowGenerics 1 + set ::osvvm::TestCaseSummaryGenericNames $args +} + +proc HideTestCaseSummaryGenerics {} { + set ::osvvm::TestCaseSummaryShowGenerics 0 +} + +proc SetTestCaseSummaryTags {args} { + set ::osvvm::TestCaseSummaryShowTags 1 + set ::osvvm::TestCaseSummaryTagNames $args +} + +proc HideTestCaseSummaryTags {} { + set ::osvvm::TestCaseSummaryShowTags 0 +} + # ------------------------------------------------- proc TestName {Name} { variable TestCaseName diff --git a/OsvvmScriptsCreateYamlReports.tcl b/OsvvmScriptsCreateYamlReports.tcl index b13cae5..ed5027b 100644 --- a/OsvvmScriptsCreateYamlReports.tcl +++ b/OsvvmScriptsCreateYamlReports.tcl @@ -48,6 +48,80 @@ namespace eval ::osvvm { package require fileutil +# ------------------------------------------------- +# SanitizeTextForReport +# Removes ASCII control characters (except \t, \n, \r) that can break YAML/HTML. +# This does not attempt full UTF-8 validation; Tcl strings are Unicode. +proc SanitizeTextForReport {Text} { + if {$Text eq ""} { + return "" + } + # Strip C0 controls excluding tab/newline/carriage return, plus DEL. + # - \x00-\x08, \x0B-\x0C, \x0E-\x1F, \x7F + regsub -all {[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]} $Text { } Clean + return $Clean +} + + +# ------------------------------------------------- +# FormatYamlScalar +# Return a YAML scalar string that uses native types when safe: +# - empty -> null (empty scalar) +# - true/false (case-insensitive, also accepts TRUE/FALSE) +# - integer / float +# Otherwise returns a double-quoted string with minimal escaping. +# +proc FormatYamlScalar {Value} { + # Treat unset/missing as empty + if {$Value eq ""} { + return "null" + } + + set Value [SanitizeTextForReport $Value] + set Trimmed [string trim $Value] + if {$Trimmed eq ""} { + # Keep whitespace-only values as strings + set Escaped [string map [list "\\" "\\\\" "\"" "\\\"" "\n" "\\n" "\r" "\\r" "\t" "\\t"] $Value] + return "\"$Escaped\"" + } + + # null + if {[string equal -nocase $Trimmed "null"]} { + return "null" + } + + # boolean + if {[string equal -nocase $Trimmed "true"] || [string equal -nocase $Trimmed "false"]} { + return [string tolower $Trimmed] + } + + # integer + if {[regexp {^[-+]?\d+$} $Trimmed]} { + return $Trimmed + } + + # float (simple forms, incl exponent) + if {[regexp {^[-+]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][-+]?\d+)?$} $Trimmed] || [regexp {^[-+]?\d+(?:[eE][-+]?\d+)$} $Trimmed]} { + return $Trimmed + } + + # default: quoted string + # Use [list] to avoid Tcl list parsing edge cases + set Escaped [string map [list "\\" "\\\\" "\"" "\\\"" "\n" "\\n" "\r" "\\r" "\t" "\\t"] $Value] + return "\"$Escaped\"" +} + + +# ------------------------------------------------- +# FormatYamlDoubleQuotedScalar +# Always returns a double-quoted YAML string (never native/null). +proc FormatYamlDoubleQuotedScalar {Value} { + set Value [SanitizeTextForReport $Value] + set Escaped [string map [list "\\" "\\\\" "\"" "\\\"" "\n" "\\n" "\r" "\\r" "\t" "\\t"] $Value] + return "\"$Escaped\"" +} + + variable TclZone [clock format [clock seconds] -format %z] variable IsoZone [format "%s:%s" [string range $TclZone 0 2] [string range $TclZone 3 4]] # variable TimeZoneName [clock format [clock seconds] -format %Z] @@ -197,8 +271,9 @@ proc WriteDictOfDict2Yaml {YamlFile DictName {DictValues ""} {Prefix ""} } { # puts $YamlFile "${Prefix}${DictName}: \"\"" } else { puts $YamlFile "${Prefix}${DictName}:" - foreach {Name Value} $DictValues { - puts $YamlFile "${Prefix} ${Name}: \"$Value\"" + foreach Name [lsort -dictionary [dict keys $DictValues]] { + set Value [dict get $DictValues $Name] + puts $YamlFile "${Prefix} ${Name}: [FormatYamlScalar $Value]" } } } @@ -217,7 +292,7 @@ proc WriteDictOfList2Yaml {YamlFile DictName {ListValues ""} {Prefix ""} } { # ------------------------------------------------- proc WriteDictOfString2Yaml {YamlFile DictName {StringValue ""} {Prefix ""} } { - puts $YamlFile "${Prefix}${DictName}: \"$StringValue\"" + puts $YamlFile "${Prefix}${DictName}: [FormatYamlDoubleQuotedScalar $StringValue]" } # ------------------------------------------------- @@ -292,6 +367,11 @@ proc WriteTestCaseSettingsYaml {FileName} { # ------------------------------------------------- proc StartTestSuiteBuildYaml {SuiteName FirstRun} { variable TestSuiteStartTimeMs + # Suite-level description is set by user scripts (ex: .pro files) + # using ::osvvm::TestSuiteDescription. + set ::osvvm::TestSuiteDescription "" + set ::osvvm::TestSuiteTitle "" + set ::osvvm::TestSuiteBrief "" set RunFile [open ${::osvvm::OsvvmTempYamlFile} a] @@ -313,6 +393,15 @@ proc FinishTestSuiteBuildYaml {} { variable TestSuiteStartTimeMs set RunFile [open ${::osvvm::OsvvmTempYamlFile} a] + if {[info exists ::osvvm::TestSuiteTitle] && $::osvvm::TestSuiteTitle ne ""} { + WriteDictOfString2Yaml $RunFile Title $::osvvm::TestSuiteTitle " " + } + if {[info exists ::osvvm::TestSuiteBrief] && $::osvvm::TestSuiteBrief ne ""} { + WriteDictOfString2Yaml $RunFile Brief $::osvvm::TestSuiteBrief " " + } + if {[info exists ::osvvm::TestSuiteDescription] && $::osvvm::TestSuiteDescription ne ""} { + WriteDictOfString2Yaml $RunFile Description $::osvvm::TestSuiteDescription " " + } puts $RunFile " ElapsedTime: [ElapsedTimeMs $TestSuiteStartTimeMs]" close $RunFile } @@ -347,13 +436,53 @@ proc FinishSimulateBuildYaml {} { set SimulateFinishTimeMs [clock milliseconds] set SimulateElapsedTimeMs [expr ($SimulateFinishTimeMs - $SimulateStartTimeMs)] + + set TestCaseElapsedTimeSeconds [format %.3f [expr ${SimulateElapsedTimeMs}/1000.0]] set RunFile [open ${::osvvm::OsvvmTempYamlFile} a] puts $RunFile " TestCaseFileName: \"$TestCaseFileName\"" WriteDictOfDict2Yaml $RunFile Generics $::osvvm::GenericDict " " # puts $RunFile " TestCaseGenerics: \"$::osvvm::GenericDict\"" - puts $RunFile " ElapsedTime: [format %.3f [expr ${SimulateElapsedTimeMs}/1000.0]]" + puts $RunFile " ElapsedTime: $TestCaseElapsedTimeSeconds" close $RunFile + + # Make per-test reports self-contained: add ElapsedTime to the per-test *_run.yml + # so per-test HTML does not require joining the suite/build YAML. + AppendElapsedTimeToTestCaseSettingsYaml $TestCaseElapsedTimeSeconds +} + +# ------------------------------------------------- +# AppendElapsedTimeToTestCaseSettingsYaml +# +# Adds a top-level ElapsedTime key to the per-test *_run.yml (if not already present). +# This is safe to call after WriteTestCaseSettingsYaml has created the file. +proc AppendElapsedTimeToTestCaseSettingsYaml {ElapsedTimeSeconds} { + variable TestCaseFileName + + if {![info exists ::osvvm::ReportsTestSuiteDirectory] || $::osvvm::ReportsTestSuiteDirectory eq ""} { + return + } + if {![info exists TestCaseFileName] || $TestCaseFileName eq ""} { + return + } + + set SettingsFileName [file join $::osvvm::ReportsTestSuiteDirectory ${TestCaseFileName}_run.yml] + if {![file exists $SettingsFileName]} { + return + } + + # Avoid duplicate keys if rerun. + set InFile [open $SettingsFileName r] + set Contents [read $InFile] + close $InFile + if {[regexp {(?m)^ElapsedTime\s*:} $Contents]} { + return + } + + set OutFile [open $SettingsFileName a] + puts $OutFile "" + puts $OutFile "ElapsedTime: $ElapsedTimeSeconds" + close $OutFile } # ------------------------------------------------- diff --git a/OsvvmSettingsRequired.tcl b/OsvvmSettingsRequired.tcl index 6ad4260..78c710a 100644 --- a/OsvvmSettingsRequired.tcl +++ b/OsvvmSettingsRequired.tcl @@ -66,6 +66,18 @@ namespace eval ::osvvm { variable OsvvmRequirementsYamlVersion InVhdlCodeVersionTbd ;# file is an array of requirements - version not possible w/o file change # Do not update if user set these already + # ------------------------------------------------- + # Brief length guard (soft warning) + # 0 disables warning. + variable TestSuiteBriefMaxLength 120 + + # ------------------------------------------------- + # Test Case Summary (HTML) column limits + # These caps prevent the summary table from becoming too wide when many generics/tags exist. + # 0 (or negative) disables the cap. + variable TestCaseSummaryMaxGenericsColumns 12 + variable TestCaseSummaryMaxTagsColumns 12 + if {![info exists OsvvmVersionCompatibility]} { variable OsvvmVersionCompatibility $OsvvmVersion } diff --git a/ReportAlert2Html.tcl b/ReportAlert2Html.tcl index 308b751..9a12485 100644 --- a/ReportAlert2Html.tcl +++ b/ReportAlert2Html.tcl @@ -63,20 +63,47 @@ proc LocalAlert2Html {TestCaseName TestSuiteName AlertYamlFile} { variable ResultsFile set Alert2HtmlDict [::yaml::yaml2dict -file ${AlertYamlFile}] + + # Prefer Title (if set) for visible headings; fall back to YAML Name. + set DisplayName "" + if {[dict exists $Alert2HtmlDict Name]} { + set DisplayName [dict get $Alert2HtmlDict Name] + } + if {[dict exists $Alert2HtmlDict Title]} { + set CandidateTitle [string trim [dict get $Alert2HtmlDict Title]] + if {$CandidateTitle ne ""} { + set DisplayName $CandidateTitle + } + } + + AlertSettings $Alert2HtmlDict $DisplayName - AlertSettings $Alert2HtmlDict + # Extract and store Description and Tags for use in other reports + if {[dict exists $Alert2HtmlDict Description]} { + set ::osvvm::Report2TestDescription [dict get $Alert2HtmlDict Description] + } else { + set ::osvvm::Report2TestDescription "" + } + if {[dict exists $Alert2HtmlDict Tags]} { + set ::osvvm::Report2TestTags [dict get $Alert2HtmlDict Tags] + } else { + set ::osvvm::Report2TestTags "" + } - CreateAlertResultsHeader $TestCaseName + CreateAlertResultsHeader $DisplayName AlertWrite $Alert2HtmlDict CreateAlertResultsFooter } -proc AlertSettings {AlertDict} { +proc AlertSettings {AlertDict {DisplayName ""}} { variable ResultsFile - + set Name [dict get $AlertDict Name] + if {$DisplayName eq ""} { + set DisplayName $Name + } set Settings [dict get $AlertDict Settings] set External [dict get $Settings ExternalErrors] set Failure [dict get $External Failure] @@ -91,9 +118,9 @@ proc AlertSettings {AlertDict} { puts $ResultsFile "
" puts $ResultsFile "
" - puts $ResultsFile "

$Name Alert Report

" + puts $ResultsFile "

[EscapeHtml $DisplayName]Alert Report

" puts $ResultsFile "
" - puts $ResultsFile "
$Name Alert Settings" + puts $ResultsFile "
[EscapeHtml $DisplayName]Alert Settings" puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -156,7 +183,7 @@ proc CreateAlertResultsHeader {TestCaseName} { variable ResultsFile puts $ResultsFile "
" - puts $ResultsFile "
$TestCaseName Alert Results" + puts $ResultsFile "
[EscapeHtml $TestCaseName]Alert Results" puts $ResultsFile "
" puts $ResultsFile " " puts $ResultsFile " " @@ -194,6 +221,13 @@ proc AlertWrite {AlertDict {Prefix ""}} { set DisabledAlertCount [dict get $Results DisabledAlertCount] set Name [dict get $AlertDict Name] + set DisplayName $Name + if {$Prefix eq "" && [dict exists $AlertDict Title]} { + set CandidateTitle [string trim [dict get $AlertDict Title]] + if {$CandidateTitle ne ""} { + set DisplayName $CandidateTitle + } + } set Status [dict get $AlertDict Status] set PassedCount [dict get $Results PassedCount] set AffirmCount [dict get $Results AffirmCount] @@ -272,7 +306,7 @@ proc AlertWrite {AlertDict {Prefix ""}} { } puts $ResultsFile " " - puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " diff --git a/ReportBuildDict2Html.tcl b/ReportBuildDict2Html.tcl index 11ede1f..790f0ac 100644 --- a/ReportBuildDict2Html.tcl +++ b/ReportBuildDict2Html.tcl @@ -223,6 +223,15 @@ proc CreateTestSuiteSummary {} { variable ReportBuildName if { $HaveTestSuites } { + # Show Brief column only when at least one suite explicitly sets Brief. + set ShowSuiteBriefCol 0 + foreach TsForBrief $TestSuiteSummaryArrayOfDictionaries { + if { [dict exists $TsForBrief Brief] && [string trim [dict get $TsForBrief Brief]] ne "" } { + set ShowSuiteBriefCol 1 + break + } + } + puts $ResultsFile "
" puts $ResultsFile "
Test Suite Summary" puts $ResultsFile "
${Prefix}${Name}${Prefix}${DisplayName}$Status$AffirmCount$PassedCount
" @@ -233,6 +242,9 @@ proc CreateTestSuiteSummary {} { puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + if {$ShowSuiteBriefCol} { + puts $ResultsFile " " + } puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -244,7 +256,19 @@ proc CreateTestSuiteSummary {} { foreach TestSuite $TestSuiteSummaryArrayOfDictionaries { set SuiteName [dict get $TestSuite Name] + set SuiteDisplayName $SuiteName + if {[dict exists $TestSuite Title]} { + set CandidateTitle [string trim [dict get $TestSuite Title]] + if {$CandidateTitle ne ""} { + set SuiteDisplayName $CandidateTitle + } + } set SuiteStatus [dict get $TestSuite Status] + if {$ShowSuiteBriefCol && [dict exists $TestSuite Brief]} { + set SuiteBrief [dict get $TestSuite Brief] + } else { + set SuiteBrief "" + } set PassedClass "" set FailedClass "" @@ -259,7 +283,7 @@ proc CreateTestSuiteSummary {} { } puts $ResultsFile " " - puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -279,6 +303,13 @@ proc CreateTestSuiteSummary {} { } puts $ResultsFile " " puts $ResultsFile " " + if {$ShowSuiteBriefCol} { + puts $ResultsFile " " + } puts $ResultsFile " " } puts $ResultsFile " " @@ -297,19 +328,199 @@ proc CreateTestCaseSummaries {TestDict} { if { [dict exists $TestDict TestSuites] } { foreach TestSuite [dict get $TestDict TestSuites] { set SuiteName [dict get $TestSuite Name] + set SuiteDisplayName $SuiteName + if {[dict exists $TestSuite Title]} { + set CandidateTitle [string trim [dict get $TestSuite Title]] + if {$CandidateTitle ne ""} { + set SuiteDisplayName $CandidateTitle + } + } + + # Show Brief column only when at least one testcase explicitly sets Brief. + set ShowTestBriefCol 0 + foreach TcForBrief [dict get $TestSuite TestCases] { + if { [dict exists $TcForBrief Brief] && [string trim [dict get $TcForBrief Brief]] ne "" } { + set ShowTestBriefCol 1 + break + } + } + + # Configuration hooks (set via OSVVM-Scripts/OsvvmScriptsCore.tcl APIs) + set ConfigShowGenerics 1 + if {[info exists ::osvvm::TestCaseSummaryShowGenerics]} { + set ConfigShowGenerics $::osvvm::TestCaseSummaryShowGenerics + } + set ConfigGenericWhitelist {} + if {[info exists ::osvvm::TestCaseSummaryGenericNames]} { + set ConfigGenericWhitelist $::osvvm::TestCaseSummaryGenericNames + } + + # Default: cap the number of generic columns to keep tables readable. + set ConfigMaxGenericsColumns 0 + if {[info exists ::osvvm::TestCaseSummaryMaxGenericsColumns]} { + set ConfigMaxGenericsColumns $::osvvm::TestCaseSummaryMaxGenericsColumns + } + + # Default: show tags in the Test Case Summary table + set ConfigShowTags 1 + if {[info exists ::osvvm::TestCaseSummaryShowTags]} { + set ConfigShowTags $::osvvm::TestCaseSummaryShowTags + } + set ConfigTagWhitelist {} + if {[info exists ::osvvm::TestCaseSummaryTagNames]} { + set ConfigTagWhitelist $::osvvm::TestCaseSummaryTagNames + } + + # Default: cap the number of tag columns to keep tables readable. + set ConfigMaxTagsColumns 0 + if {[info exists ::osvvm::TestCaseSummaryMaxTagsColumns]} { + set ConfigMaxTagsColumns $::osvvm::TestCaseSummaryMaxTagsColumns + } + + # Collect a stable list of generic names used by any test case in this suite. + # These become the subcolumns under the "Generics" column group. + set SuiteGenericNamesAll {} + foreach TcForGenerics [dict get $TestSuite TestCases] { + if { [dict exists $TcForGenerics Generics] } { + set GenDict [dict get $TcForGenerics Generics] + if {![catch {dict size $GenDict}]} { + foreach GenName [dict keys $GenDict] { + if {[lsearch -exact $SuiteGenericNamesAll $GenName] < 0} { + lappend SuiteGenericNamesAll $GenName + } + } + } + } + } + + # Apply generics configuration + if {!$ConfigShowGenerics} { + set SuiteGenericNames {} + } elseif {[llength $ConfigGenericWhitelist] == 0} { + set SuiteGenericNames $SuiteGenericNamesAll + } else { + set SuiteGenericNames {} + foreach GenName $ConfigGenericWhitelist { + if {[lsearch -exact $SuiteGenericNamesAll $GenName] >= 0} { + lappend SuiteGenericNames $GenName + } + } + } + set SuiteGenericCount [llength $SuiteGenericNames] + set SuiteGenericCountAll [llength $SuiteGenericNamesAll] + + # Enforce max generics columns (0/negative => unlimited) + if {$SuiteGenericCount > 0 && $ConfigMaxGenericsColumns > 0 && $SuiteGenericCount > $ConfigMaxGenericsColumns} { + puts "Warning: Test Case Summary generics columns truncated from $SuiteGenericCount to $ConfigMaxGenericsColumns for suite $SuiteName" + set SuiteGenericNames [lrange $SuiteGenericNames 0 [expr {$ConfigMaxGenericsColumns - 1}]] + set SuiteGenericCount [llength $SuiteGenericNames] + } + + # Collect tag names (stable order) and determine visibility across all testcases. + # A tag column is included if the tag is visible in ANY testcase. + set SuiteTagNamesAll {} + set SuiteTagVisibleAny [dict create] + if {$ConfigShowTags} { + foreach TcForTags [dict get $TestSuite TestCases] { + if { [dict exists $TcForTags Tags] } { + set TagsDict [dict get $TcForTags Tags] + if {![catch {dict size $TagsDict}]} { + foreach TagName [dict keys $TagsDict] { + # Maintain stable ordering based on first occurrence in the suite. + if {[lsearch -exact $SuiteTagNamesAll $TagName] < 0} { + lappend SuiteTagNamesAll $TagName + } + + # Per-tag visibility for this testcase (default visible). + # New schema: Tags(tag).Visibility.Summary + set IsVisible 1 + set TagRec [dict get $TagsDict $TagName] + if {![catch {dict size $TagRec}] && [dict exists $TagRec Visibility]} { + set VisRec [dict get $TagRec Visibility] + if {![catch {dict size $VisRec}] && [dict exists $VisRec Summary]} { + set VisVal [dict get $VisRec Summary] + if {$VisVal eq 0 || $VisVal eq "0" || [string equal -nocase $VisVal "false"]} { + set IsVisible 0 + } + } + } + + # Track if visible in any testcase. + if {$IsVisible} { + dict set SuiteTagVisibleAny $TagName 1 + } elseif {![dict exists $SuiteTagVisibleAny $TagName]} { + dict set SuiteTagVisibleAny $TagName 0 + } + } + } + } + } + } + + # Filter final set of tag columns: include only tags visible in ANY testcase. + if {!$ConfigShowTags} { + set SuiteTagNames {} + } else { + # Candidate tags by order (either auto-discovered or whitelist) + if {[llength $ConfigTagWhitelist] == 0} { + set CandidateTagNames $SuiteTagNamesAll + } else { + set CandidateTagNames $ConfigTagWhitelist + } + + set SuiteTagNames {} + foreach TagName $CandidateTagNames { + # If a whitelist asks for a tag not present in the suite, skip it. + if {[lsearch -exact $SuiteTagNamesAll $TagName] < 0} { + continue + } + # Only include if visible in any testcase. + if {[dict exists $SuiteTagVisibleAny $TagName] && [dict get $SuiteTagVisibleAny $TagName]} { + lappend SuiteTagNames $TagName + } + } + } + set SuiteTagCount [llength $SuiteTagNames] + + # Enforce max tags columns (0/negative => unlimited) + if {$SuiteTagCount > 0 && $ConfigMaxTagsColumns > 0 && $SuiteTagCount > $ConfigMaxTagsColumns} { + puts "Warning: Test Case Summary tag columns truncated from $SuiteTagCount to $ConfigMaxTagsColumns for suite $SuiteName" + set SuiteTagNames [lrange $SuiteTagNames 0 [expr {$ConfigMaxTagsColumns - 1}]] + set SuiteTagCount [llength $SuiteTagNames] + } + puts $ResultsFile "
" - puts $ResultsFile "
$SuiteName Test Case Summary" + puts $ResultsFile "
[EscapeHtml $SuiteDisplayName]Test Case Summary" puts $ResultsFile "
Requirements
passed / goal
Disabled
Alerts
Elapsed
Time
Brief
PASSED
${SuiteName}[EscapeHtml ${SuiteDisplayName}]$SuiteStatus[dict get $TestSuite PASSED] [dict get $TestSuite FAILED] [dict get $TestSuite DisabledAlerts][dict get $TestSuite ElapsedTime]" + if {${SuiteBrief} ne ""} { + puts $ResultsFile " [EscapeHtml $SuiteBrief]" + } + puts $ResultsFile "
" puts $ResultsFile " " puts $ResultsFile " " + if { $SuiteGenericCount > 0 } { + puts $ResultsFile " " + } + if { $SuiteTagCount > 0 } { + puts $ResultsFile " " + } puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + if {$ShowTestBriefCol} { + puts $ResultsFile " " + } puts $ResultsFile " " puts $ResultsFile " " + if { $SuiteGenericCount > 0 } { + foreach GenName $SuiteGenericNames { + puts $ResultsFile " " + } + } + if { $SuiteTagCount > 0 } { + foreach TagName $SuiteTagNames { + puts $ResultsFile " " + } + } puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -323,6 +534,22 @@ proc CreateTestCaseSummaries {TestDict} { foreach TestCase [dict get $TestSuite TestCases] { set TestName [dict get $TestCase TestCaseName] + + # Display title in the Name column when explicitly set. + set DisplayName $TestName + if {[dict exists $TestCase Title]} { + set CandidateTitle [string trim [dict get $TestCase Title]] + if {$CandidateTitle ne ""} { + set DisplayName $CandidateTitle + } + } + + # Get test brief only if explicitly set + if {$ShowTestBriefCol && [dict exists $TestCase Brief]} { + set TestBrief [dict get $TestCase Brief] + } else { + set TestBrief "" + } if { [dict exists $TestCase Status] } { set TestStatus [dict get $TestCase Status] set TestResults [dict get $TestCase Results] @@ -375,25 +602,82 @@ proc CreateTestCaseSummaries {TestDict} { } set TestCaseHtmlFile [file join ${TestSuiteReportsDirectory} ${TestFileName}.html] set TestCaseName $TestName - if { [dict exists $TestCase Generics] } { + set DisplayNameWithGenerics $DisplayName + # Backward compatibility: if there are no generic columns, append generic values to the Test Case name. + # Do this only when the suite truly has no generics (not when generics are hidden via config). + if { ($SuiteGenericCountAll == 0) && [dict exists $TestCase Generics] } { set TestCaseGenerics [dict get $TestCase Generics] - if {${TestCaseGenerics} ne ""} { - set GenericValueList [dict values $TestCaseGenerics] + if {![catch {dict size $TestCaseGenerics}] && ([dict size $TestCaseGenerics] > 0)} { + set GenericValueList [dict values $TestCaseGenerics] set i 0 set ListLen [llength ${GenericValueList}] - append TestCaseName " (" + append TestCaseName " (" + append DisplayNameWithGenerics " (" foreach GenericValue $GenericValueList { incr i if {$i != $ListLen} { append TestCaseName $GenericValue " ," + append DisplayNameWithGenerics $GenericValue " ," } else { append TestCaseName $GenericValue ")" + append DisplayNameWithGenerics $GenericValue ")" } } } } puts $ResultsFile " " - puts $ResultsFile " " + puts $ResultsFile " " + + # Generics are the second column group (after Test Case name). + if { $SuiteGenericCount > 0 } { + if { [dict exists $TestCase Generics] } { + set TestCaseGenerics [dict get $TestCase Generics] + } else { + set TestCaseGenerics "" + } + set HasGenerics 0 + if {![catch {dict size $TestCaseGenerics}] && ([dict size $TestCaseGenerics] > 0)} { + set HasGenerics 1 + } + foreach GenName $SuiteGenericNames { + if { $HasGenerics && [dict exists $TestCaseGenerics $GenName] } { + set GenValue [dict get $TestCaseGenerics $GenName] + } else { + set GenValue "⸻" + } + set GenDisplayValue [FormatGenericValueForHtml $GenName $GenValue $TestFileName] + puts $ResultsFile " " + } + } + + # Optional Tags column group (after Generics, before Status). + if { $SuiteTagCount > 0 } { + if { [dict exists $TestCase Tags] } { + set TestCaseTags [dict get $TestCase Tags] + } else { + set TestCaseTags "" + } + set HasTags 0 + if {![catch {dict size $TestCaseTags}] && ([dict size $TestCaseTags] > 0)} { + set HasTags 1 + } + foreach TagName $SuiteTagNames { + # Column exists only for tags visible in any testcase. + # For each testcase that has the tag, display Tags(tag).Value. + if { $HasTags && [dict exists $TestCaseTags $TagName] } { + set TagRec [dict get $TestCaseTags $TagName] + set TagValue "" + if {![catch {dict size $TagRec}] && [dict exists $TagRec Value]} { + set TagValue [dict get $TagRec Value] + } + set TagDisplay [EscapeHtml [FormatScalarForHtml $TagValue]] + } else { + set TagDisplay "⸻" + } + puts $ResultsFile " " + } + } + puts $ResultsFile " " if { $TestReport eq "REPORT" } { puts $ResultsFile " " @@ -425,8 +709,17 @@ proc CreateTestCaseSummaries {TestDict} { set TestCaseElapsedTime missing } puts $ResultsFile " " + if {$ShowTestBriefCol} { + puts $ResultsFile " " + } } else { - puts $ResultsFile " " + # Remaining columns after Test Case + (optional Generics) + Status + set RemainingColumns [expr {8 + ($ShowTestBriefCol ? 1 : 0)}] + puts $ResultsFile " " } puts $ResultsFile " " } diff --git a/ReportBuildDict2Junit.tcl b/ReportBuildDict2Junit.tcl index 6df1860..8f92014 100644 --- a/ReportBuildDict2Junit.tcl +++ b/ReportBuildDict2Junit.tcl @@ -47,6 +47,16 @@ package require yaml +# ------------------------------------------------- +# EscapeXmlAttr +# +# Escape content for safe use inside XML attribute values. +proc EscapeXmlAttr {Text} { + set Escaped $Text + set Escaped [string map [list "&" "&" "<" "<" ">" ">" "\"" """ "'" "'"] $Escaped] + return $Escaped +} + # ------------------------------------------------- # ReportBuildDict2Junit # @@ -180,31 +190,87 @@ proc CreateJunitTestSuiteSummaries {TestDict TestSuiteSummary } { set ResolvedTestName $TestName } + # Collect testcase properties (generics + brief + tags) + set PropertyLines {} + if { [dict exists $TestCase Generics] } { + set TestCaseGenerics [dict get $TestCase Generics] + if {${TestCaseGenerics} ne ""} { + foreach {GenericName GenericValue} $TestCaseGenerics { + set GName [EscapeXmlAttr $GenericName] + set GValue [EscapeXmlAttr "${GenericName}=${GenericValue}"] + lappend PropertyLines " " + } + } + } + + if { [dict exists $TestCase Brief] } { + set BriefValue [dict get $TestCase Brief] + if {$BriefValue ne ""} { + set BriefEsc [EscapeXmlAttr $BriefValue] + lappend PropertyLines " " + } + } + + if { [dict exists $TestCase Title] } { + set TitleValue [dict get $TestCase Title] + if {$TitleValue ne ""} { + set TitleEsc [EscapeXmlAttr $TitleValue] + lappend PropertyLines " " + } + } + + if { [dict exists $TestCase Tags] } { + set TagsDict [dict get $TestCase Tags] + if {![catch {dict size $TagsDict}] && ([dict size $TagsDict] > 0)} { + foreach TagName [dict keys $TagsDict] { + set TagRec [dict get $TagsDict $TagName] + + set TagValue "" + if {[dict exists $TagRec Value]} { + set TagValue [dict get $TagRec Value] + } + + # Type is required when Tags are present (no fallback/inference). + if {![dict exists $TagRec Type]} { + error "JUnit: Missing Type for tag '$TagName'." + } + set TagTypeToken [dict get $TagRec Type] + + set TagNameEsc [EscapeXmlAttr $TagName] + set TagValueEsc [EscapeXmlAttr $TagValue] + lappend PropertyLines " " + + set TagTypeNameEsc [EscapeXmlAttr "tagtype:${TagName}"] + set TagTypeValueEsc [EscapeXmlAttr $TagTypeToken] + lappend PropertyLines " " + } + } + } + puts $ResultsFile "" - - if { [dict exists $TestCase Generics] } { - set TestCaseGenerics [dict get $TestCase Generics] - if {${TestCaseGenerics} ne ""} { - puts $ResultsFile " " - foreach {GenericName GenericValue} $TestCaseGenerics { - puts $ResultsFile " " - } - puts $ResultsFile " " + + if {[llength $PropertyLines] > 0} { + puts $ResultsFile " " + foreach Line $PropertyLines { + puts $ResultsFile $Line } + puts $ResultsFile " " } if { $TestStatus eq "FAILED" } { - puts $ResultsFile "$Reason" + set ReasonEsc [EscapeXmlAttr $Reason] + puts $ResultsFile "$ReasonEsc" } elseif { $TestStatus eq "SKIPPED" } { set Reason [dict get $TestResults Reason] - puts $ResultsFile "$Reason" + set ReasonEsc [EscapeXmlAttr $Reason] + puts $ResultsFile "$ReasonEsc" } puts $ResultsFile "" } diff --git a/ReportBuildYaml2Dict.tcl b/ReportBuildYaml2Dict.tcl index fb220a3..60e5903 100644 --- a/ReportBuildYaml2Dict.tcl +++ b/ReportBuildYaml2Dict.tcl @@ -211,6 +211,21 @@ proc ElaborateTestSuites {TestDict} { set SuiteStatus "FAILED" set BuildStatus "FAILED" } + if {[dict exists $TestSuite Description]} { + set SuiteDescription [dict get $TestSuite Description] + } else { + set SuiteDescription "" + } + if {[dict exists $TestSuite Title]} { + set SuiteTitle [dict get $TestSuite Title] + } else { + set SuiteTitle "" + } + if {[dict exists $TestSuite Brief]} { + set SuiteBrief [dict get $TestSuite Brief] + } else { + set SuiteBrief "" + } set SuiteDict [dict create Name $SuiteName] dict append SuiteDict Status $SuiteStatus dict append SuiteDict PASSED $SuitePassed @@ -220,6 +235,9 @@ proc ElaborateTestSuites {TestDict} { dict append SuiteDict ReqGoal $SuiteReqGoal dict append SuiteDict DisabledAlerts $SuiteDisabledAlerts dict append SuiteDict ElapsedTime $SuiteElapsedTime + dict set SuiteDict Title $SuiteTitle + dict set SuiteDict Brief $SuiteBrief + dict set SuiteDict Description $SuiteDescription lappend TestSuiteSummaryArrayOfDictionaries $SuiteDict } } diff --git a/ReportCov2Html.tcl b/ReportCov2Html.tcl index 013a90b..bfe5f3b 100644 --- a/ReportCov2Html.tcl +++ b/ReportCov2Html.tcl @@ -64,9 +64,18 @@ proc Cov2Html {TestCaseName TestSuiteName CovYamlFile} { proc LocalCov2Html {TestCaseName TestSuiteName CovYamlFile} { variable ResultsFile + # Prefer Title (if set) for visible headings; fall back to testcase name. + set DisplayName $TestCaseName + if {[info exists ::osvvm::Report2TestTitle]} { + set CandidateTitle [string trim $::osvvm::Report2TestTitle] + if {$CandidateTitle ne ""} { + set DisplayName $CandidateTitle + } + } + puts $ResultsFile "
" puts $ResultsFile "
" - puts $ResultsFile "

$TestCaseName Coverage Report

" + puts $ResultsFile "

$DisplayName Coverage Report

" set TestDict [::yaml::yaml2dict -file ${CovYamlFile}] set VersionNum [dict get $TestDict Version] diff --git a/ReportScoreboard2Html.tcl b/ReportScoreboard2Html.tcl index 04966cb..3f12276 100644 --- a/ReportScoreboard2Html.tcl +++ b/ReportScoreboard2Html.tcl @@ -59,10 +59,19 @@ proc Scoreboard2Html {TestCaseName TestSuiteName SbYamlFile SbName} { proc LocalScoreboard2Html {TestCaseName TestSuiteName SbYamlFile SbName} { variable ResultsFile + + # Prefer Title (if set) for visible headings; fall back to testcase name. + set DisplayName $TestCaseName + if {[info exists ::osvvm::Report2TestTitle]} { + set CandidateTitle [string trim $::osvvm::Report2TestTitle] + if {$CandidateTitle ne ""} { + set DisplayName $CandidateTitle + } + } puts $ResultsFile "
" puts $ResultsFile "
" - puts $ResultsFile "

$TestCaseName Scoreboard Report for ${SbName}

" + puts $ResultsFile "

$DisplayName Scoreboard Report for ${SbName}

" puts $ResultsFile "
" puts $ResultsFile "
Test CaseGenericsTagsStatusChecksRequirementsFunctional
Coverage
Disabled
Alerts
Elapsed
Time
Brief
$GenName$TagNameTotalPassedFailed
${TestCaseName}${DisplayNameWithGenerics}$GenDisplayValue$TagDisplay$TestStatus[dict get $TestResults AffirmCount]$TestCaseElapsedTime" + if {${TestBrief} ne ""} { + puts $ResultsFile " [EscapeHtml $TestBrief]" + } + puts $ResultsFile "   $Reason   $Reason
" diff --git a/ReportSimulate2Html.tcl b/ReportSimulate2Html.tcl index 375192c..0468031 100644 --- a/ReportSimulate2Html.tcl +++ b/ReportSimulate2Html.tcl @@ -62,6 +62,11 @@ proc Simulate2Html {SettingsFileWithPath} { GetTestCaseSettings $SettingsFileWithPath + + # Align local script variables with ::osvvm settings parsed from YAML + # (avoids stale values between testcases) + set Report2AlertYamlFile $::osvvm::Report2AlertYamlFile + set Report2CovYamlFile $::osvvm::Report2CovYamlFile set TestCaseFileName $::osvvm::Report2TestCaseFileName set TestCaseName $::osvvm::Report2TestCaseName @@ -69,9 +74,124 @@ proc Simulate2Html {SettingsFileWithPath} { set BuildName $::osvvm::Report2BuildName set GenericDict $::osvvm::Report2GenericDict - + # Some older/alternate run.yml formats may not include TestCaseFileName. + # When possible, derive it from TestCaseName + GenericNames (the file naming convention). + if {$TestCaseFileName eq "" && [info exists ::osvvm::Report2GenericNames] && $::osvvm::Report2GenericNames ne ""} { + set TestCaseFileName "${TestCaseName}${::osvvm::Report2GenericNames}" + } + + # Initialize fields sourced from alerts/build YAML to empty + set ::osvvm::Report2TestTitle "" + set ::osvvm::Report2TestDescription "" + set ::osvvm::Report2TestBrief "" + set ::osvvm::Report2TestTags "" + set ::osvvm::Report2TestStatus "" + if {![info exists ::osvvm::Report2TestCaseSimulationTime]} { + set ::osvvm::Report2TestCaseSimulationTime "" + } + if {![info exists ::osvvm::Report2TestCaseElapsedTime]} { + set ::osvvm::Report2TestCaseElapsedTime "" + } + + # Try to get SimulationTime/ElapsedTime from the build YAML. + # Different flows place this file in slightly different locations, so search a few common candidates. + set BuildYamlCandidates {} + lappend BuildYamlCandidates [file join $::osvvm::Report2BaseDirectory ${BuildName}.yml] + lappend BuildYamlCandidates [file join $::osvvm::Report2BaseDirectory ${BuildName}.yaml] + if {[info exists ::osvvm::Report2ReportsSubdirectory] && $::osvvm::Report2ReportsSubdirectory ne ""} { + lappend BuildYamlCandidates [file join $::osvvm::Report2BaseDirectory $::osvvm::Report2ReportsSubdirectory ${BuildName}.yml] + lappend BuildYamlCandidates [file join $::osvvm::Report2BaseDirectory $::osvvm::Report2ReportsSubdirectory ${BuildName}.yaml] + } + if {[info exists ::osvvm::Report2ReportsDirectory] && $::osvvm::Report2ReportsDirectory ne ""} { + lappend BuildYamlCandidates [file join $::osvvm::Report2ReportsDirectory ${BuildName}.yml] + lappend BuildYamlCandidates [file join $::osvvm::Report2ReportsDirectory ${BuildName}.yaml] + } + if {[info exists ::osvvm::Report2ReportsTestSuiteDirectory] && $::osvvm::Report2ReportsTestSuiteDirectory ne ""} { + set SuiteReportsBase [file dirname $::osvvm::Report2ReportsTestSuiteDirectory] + lappend BuildYamlCandidates [file join $SuiteReportsBase ${BuildName}.yml] + lappend BuildYamlCandidates [file join $SuiteReportsBase ${BuildName}.yaml] + } + + set BuildYamlFile "" + foreach Candidate $BuildYamlCandidates { + if {[file exists $Candidate]} { + set BuildYamlFile $Candidate + break + } + } + + if {$BuildYamlFile ne ""} { + set BuildDict [::yaml::yaml2dict -file $BuildYamlFile] + if {[dict exists $BuildDict TestSuites]} { + foreach Suite [dict get $BuildDict TestSuites] { + if {![dict exists $Suite Name] || ![dict exists $Suite TestCases]} { + continue + } + if {[dict get $Suite Name] ne $TestSuiteName} { + continue + } + set FoundTc 0 + foreach Tc [dict get $Suite TestCases] { + if {[dict exists $Tc TestCaseFileName] && [dict get $Tc TestCaseFileName] eq $TestCaseFileName} { + if {[dict exists $Tc SimulationTime] && $::osvvm::Report2TestCaseSimulationTime eq ""} { + set ::osvvm::Report2TestCaseSimulationTime [dict get $Tc SimulationTime] + } + if {[dict exists $Tc ElapsedTime] && $::osvvm::Report2TestCaseElapsedTime eq ""} { + set ::osvvm::Report2TestCaseElapsedTime [dict get $Tc ElapsedTime] + } + set FoundTc 1 + break + } + } + # Fallback: if no exact file-name match, match by TestCaseName (useful when TestCaseFileName is absent). + if {!$FoundTc} { + foreach Tc [dict get $Suite TestCases] { + if {[dict exists $Tc TestCaseName] && [dict get $Tc TestCaseName] eq $TestCaseName} { + if {[dict exists $Tc SimulationTime] && $::osvvm::Report2TestCaseSimulationTime eq ""} { + set ::osvvm::Report2TestCaseSimulationTime [dict get $Tc SimulationTime] + } + if {[dict exists $Tc ElapsedTime] && $::osvvm::Report2TestCaseElapsedTime eq ""} { + set ::osvvm::Report2TestCaseElapsedTime [dict get $Tc ElapsedTime] + } + break + } + } + } + break + } + } + } + + # Read Description and Tags from Alert YAML file before creating summary table + if {[file exists ${Report2AlertYamlFile}]} { + set AlertDict [::yaml::yaml2dict -file ${Report2AlertYamlFile}] + if {[dict exists $AlertDict Status]} { + set ::osvvm::Report2TestStatus [dict get $AlertDict Status] + } + if {[dict exists $AlertDict Title]} { + set ::osvvm::Report2TestTitle [dict get $AlertDict Title] + } + if {[dict exists $AlertDict Brief]} { + set ::osvvm::Report2TestBrief [dict get $AlertDict Brief] + } + if {[dict exists $AlertDict Description]} { + set ::osvvm::Report2TestDescription [dict get $AlertDict Description] + } + if {[dict exists $AlertDict Tags]} { + set ::osvvm::Report2TestTags [dict get $AlertDict Tags] + } + } + + # Compute display name for the HTML page: prefer Title when set. + set TestDisplayName $TestCaseName + if {[info exists ::osvvm::Report2TestTitle]} { + set CandidateTitle [string trim $::osvvm::Report2TestTitle] + if {$CandidateTitle ne ""} { + set TestDisplayName $CandidateTitle + } + } - CreateTestCaseSummaryTable ${TestCaseName} ${TestSuiteName} ${BuildName} ${GenericDict} + CreateTestCaseSummaryTable ${TestCaseName} ${TestDisplayName} ${TestSuiteName} ${BuildName} ${GenericDict} if {[file exists ${Report2AlertYamlFile}]} { Alert2Html ${TestCaseName} ${TestSuiteName} ${Report2AlertYamlFile} @@ -107,12 +227,12 @@ proc OpenSimulationReportFile {FileName {initialize 0}} { } #-------------------------------------------------------------- -proc CreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName GenericDict} { +proc CreateTestCaseSummaryTable {TestCaseName TestDisplayName TestSuiteName BuildName GenericDict} { variable ResultsFile OpenSimulationReportFile [file join $::osvvm::Report2TestCaseHtml] 1 - set ErrorCode [catch {LocalCreateTestCaseSummaryTable $TestCaseName $TestSuiteName $BuildName $GenericDict} errmsg] + set ErrorCode [catch {LocalCreateTestCaseSummaryTable $TestCaseName $TestDisplayName $TestSuiteName $BuildName $GenericDict} errmsg] close $ResultsFile @@ -122,7 +242,7 @@ proc CreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName GenericDic } #-------------------------------------------------------------- -proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName GenericDict} { +proc LocalCreateTestCaseSummaryTable {TestCaseName TestDisplayName TestSuiteName BuildName GenericDict} { variable ResultsFile @@ -132,7 +252,7 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener set ReportsPrefix "../.." } - CreateOsvvmReportHeader $ResultsFile "$TestCaseName Test Case Report" $ReportsPrefix + CreateOsvvmReportHeader $ResultsFile "$TestDisplayName Test Case Report" $ReportsPrefix puts $ResultsFile "
" @@ -143,13 +263,6 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener puts $ResultsFile " " puts $ResultsFile "
" - # Print the Generics - if {${GenericDict} ne ""} { - foreach {GenericName GenericValue} $GenericDict { - puts $ResultsFile " " - } - } - if {[file exists ${::osvvm::Report2AlertYamlFile}]} { puts $ResultsFile " " } @@ -197,6 +310,188 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener LinkLogoFile $ResultsFile $ReportsPrefix puts $ResultsFile " " + + # Test Result near top (quick essentials) + puts $ResultsFile "
" + puts $ResultsFile "
[EscapeHtml $TestDisplayName]Summary" + puts $ResultsFile "
Generic: $GenericName = $GenericValue
Alert Report
" + puts $ResultsFile " " + puts $ResultsFile " " + set StatusValue "⸻" + if {[info exists ::osvvm::Report2TestStatus] && $::osvvm::Report2TestStatus ne ""} { + set StatusValue $::osvvm::Report2TestStatus + } + set SimTimeValue "⸻" + if {[info exists ::osvvm::Report2TestCaseSimulationTime] && $::osvvm::Report2TestCaseSimulationTime ne ""} { + set SimTimeValue $::osvvm::Report2TestCaseSimulationTime + } + set ElapsedValue "⸻" + if {[info exists ::osvvm::Report2TestCaseElapsedTime] && $::osvvm::Report2TestCaseElapsedTime ne ""} { + set ElapsedValue $::osvvm::Report2TestCaseElapsedTime + } + set StatusClass "" + set StatusUpper [string toupper $StatusValue] + if {[string first "PASS" $StatusUpper] >= 0} { + set StatusClass "passed" + } elseif {[string first "FAIL" $StatusUpper] >= 0 || [string first "ERROR" $StatusUpper] >= 0} { + set StatusClass "failed" + } elseif {[string first "SKIP" $StatusUpper] >= 0} { + set StatusClass "skipped" + } + if {$StatusClass ne ""} { + puts $ResultsFile " " + } else { + puts $ResultsFile " " + } + puts $ResultsFile " " + puts $ResultsFile " " + + # Include the VHDL test case source file name when available + set TestCaseSourceFileName "⸻" + if {[info exists ::osvvm::Report2TestCaseFile] && $::osvvm::Report2TestCaseFile ne ""} { + set TestCaseSourceFileName [file tail $::osvvm::Report2TestCaseFile] + } + puts $ResultsFile " " + + # Include Title (only when explicitly set; no fallback) + set TitleValue "⸻" + if {[info exists ::osvvm::Report2TestTitle] && [string trim $::osvvm::Report2TestTitle] ne ""} { + set TitleValue $::osvvm::Report2TestTitle + } + puts $ResultsFile " " + + # Include Brief (only when explicitly set; no fallback) + set BriefValue "⸻" + if {[info exists ::osvvm::Report2TestBrief] && [string trim $::osvvm::Report2TestBrief] ne ""} { + set BriefValue $::osvvm::Report2TestBrief + } + puts $ResultsFile " " + + puts $ResultsFile " " + puts $ResultsFile "
FieldValue
Status$StatusValue
Status$StatusValue
Elapsed Time$ElapsedValue
Suite Name$TestSuiteName
File[EscapeHtml $TestCaseSourceFileName]
Title[EscapeHtml $TitleValue]
Brief[EscapeHtml $BriefValue]
" + puts $ResultsFile "
" + puts $ResultsFile "
" + + # Render Description / Tags / Generics as independent sections + # (user-requested: Description not in a table; Tags + Generics in tables) + if {[info exists ::osvvm::Report2TestDescription] && $::osvvm::Report2TestDescription ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
[EscapeHtml $TestDisplayName]Description" + WriteMarkdownSubsetAsHtml $ResultsFile $::osvvm::Report2TestDescription " " + puts $ResultsFile "
" + puts $ResultsFile "
" + } + + if {[info exists ::osvvm::Report2TestTags] && $::osvvm::Report2TestTags ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
[EscapeHtml $TestDisplayName]Tags" + puts $ResultsFile " " + puts $ResultsFile " " + puts $ResultsFile " " + foreach TagName [dict keys $::osvvm::Report2TestTags] { + set TagRec [dict get $::osvvm::Report2TestTags $TagName] + + set TagValue "" + if {[dict exists $TagRec Value]} { + set TagValue [dict get $TagRec Value] + } + set TagDisplayValue [FormatScalarForHtml $TagValue] + + # Explicit tag type from YAML (no inference/fallback) + set TagType "" + if {[dict exists $TagRec Type]} { + set TagTypeToken [dict get $TagRec Type] + switch -nocase -- $TagTypeToken { + TAG_STRING { set TagType "string" } + TAG_BOOL { set TagType "boolean" } + TAG_INT { set TagType "integer" } + TAG_REAL { set TagType "real" } + TAG_TIME { set TagType "time" } + TAG_STD_LOGIC { set TagType "std_logic" } + default { set TagType "" } + } + } + + if {$TagType eq ""} { + set TagType "⸻" + } + + set TagTypeClass [string tolower $TagType] + regsub -all {[^a-z0-9_-]} $TagTypeClass "_" TagTypeClass + set TagTypeHtml "$TagType" + set ShowText "⸻" + if {[dict exists $TagRec Visibility]} { + set VisRec [dict get $TagRec Visibility] + if {![catch {dict size $VisRec}] && [dict exists $VisRec Summary]} { + set ShowValue [dict get $VisRec Summary] + if {$ShowValue eq 1 || $ShowValue eq "1" || [string equal -nocase $ShowValue "true"]} { + set ShowText "true" + } elseif {$ShowValue eq 0 || $ShowValue eq "0" || [string equal -nocase $ShowValue "false"]} { + set ShowText "false" + } + } + } + puts $ResultsFile " " + } + puts $ResultsFile " " + puts $ResultsFile "
NameValueTypeShowInSummary
[EscapeHtml $TagName]$TagDisplayValue$TagTypeHtml$ShowText
" + puts $ResultsFile "
" + puts $ResultsFile "
" + } + + if {${GenericDict} ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
[EscapeHtml $TestDisplayName]Generics" + puts $ResultsFile " " + puts $ResultsFile " " + puts $ResultsFile " " + + # Determine whether generics are configured to show in the suite summary. + # Note: These controls are not stored in YAML today; they reflect the current + # script settings (set by SetTestCaseSummaryGenerics/HideTestCaseSummaryGenerics). + set ShowGenericsInSummary 1 + if {[info exists ::osvvm::TestCaseSummaryShowGenerics]} { + set ShowGenericsInSummary $::osvvm::TestCaseSummaryShowGenerics + } + set GenericWhitelist {} + if {[info exists ::osvvm::TestCaseSummaryGenericNames]} { + set GenericWhitelist $::osvvm::TestCaseSummaryGenericNames + } + + foreach {GenericName GenericValue} $GenericDict { + set GenericDisplayValue [FormatGenericValueForHtml $GenericName $GenericValue $::osvvm::Report2GenericNames] + + # Infer scalar type (consistent with tag typing). + if {$GenericDisplayValue eq "True" || $GenericDisplayValue eq "False"} { + set GenericType "boolean" + } else { + set GenericType [InferScalarTypeForHtml $GenericValue] + } + + set GenericTypeClass [string tolower $GenericType] + regsub -all {[^a-z0-9_-]} $GenericTypeClass "_" GenericTypeClass + set GenericTypeHtml "$GenericType" + + # Compute whether this generic would be shown in the Test Case Summary. + if {!$ShowGenericsInSummary} { + set GenericShowText "false" + } elseif {[llength $GenericWhitelist] == 0} { + set GenericShowText "true" + } else { + if {[lsearch -exact $GenericWhitelist $GenericName] >= 0} { + set GenericShowText "true" + } else { + set GenericShowText "false" + } + } + + puts $ResultsFile " " + } + puts $ResultsFile " " + puts $ResultsFile "
NameValueTypeShowInSummary
$GenericName$GenericDisplayValue$GenericTypeHtml$GenericShowText
" + puts $ResultsFile "
" + puts $ResultsFile "
" + } } proc FinalizeSimulationReportFile {} { diff --git a/ReportSupport.tcl b/ReportSupport.tcl index ee90704..a286885 100644 --- a/ReportSupport.tcl +++ b/ReportSupport.tcl @@ -41,6 +41,342 @@ package require yaml +# ------------------------------------------------- +# FormatGenericValueForHtml +# +# YAML boolean scalars may load into Tcl as 0/1. For generics, prefer showing +# True/False in HTML (matching VHDL/OSVVM conventions) when we can infer the +# original boolean value from the encoded GenericNames/TestCaseFileName string. +# +proc FormatGenericValueForHtml {GenericName GenericValue {GenericNames ""}} { + set EncodedGenericName $GenericName + if {[string match "G_*" $EncodedGenericName]} { + set EncodedGenericName [string range $EncodedGenericName 2 end] + } + + if {$GenericNames ne ""} { + if {[string first "_G_${EncodedGenericName}_TRUE" $GenericNames] >= 0} { return "True" } + if {[string first "_G_${EncodedGenericName}_FALSE" $GenericNames] >= 0} { return "False" } + } + + if {[string equal -nocase $GenericValue "true"]} { return "True" } + if {[string equal -nocase $GenericValue "false"]} { return "False" } + + return $GenericValue +} + +# ------------------------------------------------- +# FormatScalarForHtml +# +# YAML booleans commonly load into Tcl as 0/1. For HTML reports, render +# booleans as True/False. +# +proc FormatScalarForHtml {Value} { + if {[string equal -nocase $Value "true"]} { return "True" } + if {[string equal -nocase $Value "false"]} { return "False" } + + # Heuristic: yaml::yaml2dict represents YAML booleans as Tcl 0/1 + if {$Value eq 1 || $Value eq "1"} { return "True" } + if {$Value eq 0 || $Value eq "0"} { return "False" } + + return $Value +} + +# ------------------------------------------------- +# InferScalarTypeForHtml +# +# Best-effort type inference for scalar values loaded from YAML. +# Used by the per-test HTML page to display a "Type" column for tags. +# +proc InferScalarTypeForHtml {Value} { + # Normalize to string for regex checks. + set S "${Value}" + + # Boolean + if {[string equal -nocase $S "true"] || [string equal -nocase $S "false"]} { + return "boolean" + } + + # Heuristic: yaml::yaml2dict commonly maps YAML booleans to Tcl 0/1 + if {$S eq "0" || $S eq "1"} { + return "boolean" + } + + # Time: number + unit (common VHDL time units) + # Examples: 100 ns, 5ps, 1.25 us + if {[regexp -nocase {^\s*[-+]?\d+(?:\.\d+)?\s*(fs|ps|ns|us|ms|s|sec|secs|second|seconds|min|mins|minute|minutes|hr|hrs|hour|hours)\s*$} $S]} { + return "time" + } + + # Integer + if {[string is integer -strict $S]} { + return "integer" + } + + # Real + if {[string is double -strict $S]} { + return "real" + } + + return "string" +} + +# ------------------------------------------------- +# EscapeHtml +# +proc EscapeHtml {Text} { + # Strip ASCII control chars that can break HTML rendering. + regsub -all {[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]} $Text { } Escaped + set Escaped [string map [list "&" "&" "<" "<" ">" ">" "\"" """ "'" "'"] $Escaped] + return $Escaped +} + +# ------------------------------------------------- +# FormatInlineMarkdownSubset +# +# Minimal inline Markdown subset: +# - Escapes for literals using backslash: \*, \#, \[, \], \(, \), \`, \-, \\ +# - Inline code using backticks: `code` +# - Links: [text](url) +# - Allowed URL schemes/targets: https://, http://, #anchors, /absolute, ./relative, ../relative +# - Other URLs are rendered as plain text (not a link) +# - Emphasis: **bold** and *italic* +# +# Input must already be HTML-escaped. + +proc _IsSafeMarkdownUrl {Url} { + set U [string trim $Url] + if {$U eq ""} { + return 0 + } + if {[regexp {\s} $U]} { + return 0 + } + if {[regexp -nocase {^https?://} $U]} { + return 1 + } + if {[string match "#*" $U]} { + return 1 + } + if {[string match "/*" $U]} { + return 1 + } + if {[string match "./*" $U] || [string match "../*" $U]} { + return 1 + } + return 0 +} + +proc _ProtectInlineCodeSpans {S CodeMapVar} { + upvar 1 $CodeMapVar CodeMap + set Out "" + set Remainder $S + set I 0 + while {[regexp -indices {`([^`]+)`} $Remainder MatchIdx CodeIdx]} { + lassign $MatchIdx M0 M1 + lassign $CodeIdx C0 C1 + + if {$M0 > 0} { + append Out [string range $Remainder 0 [expr {$M0 - 1}]] + } + + set CodeText [string range $Remainder $C0 $C1] + set Token "\uE000${I}\uE001" + set CodeMap($Token) $CodeText + append Out $Token + incr I + + set Remainder [string range $Remainder [expr {$M1 + 1}] end] + } + append Out $Remainder + return $Out +} + +proc _RestoreInlineCodeSpans {S CodeMapVar} { + upvar 1 $CodeMapVar CodeMap + set Out $S + foreach Token [lsort -dictionary [array names CodeMap]] { + set Out [string map [list $Token "$CodeMap($Token)"] $Out] + } + return $Out +} + +proc FormatInlineMarkdownSubset {EscapedText} { + set S $EscapedText + + # Handle backslash escapes first so escaped markup is treated as literal. + # Use control characters as placeholders to avoid interacting with later regex. + set TOK_BSLASH "\u0001" + set TOK_STAR "\u0002" + set TOK_HASH "\u0003" + set TOK_LB "\u0004" + set TOK_RB "\u0005" + set TOK_LP "\u0006" + set TOK_RP "\u0007" + set TOK_BT "\u0008" + set TOK_DASH "\u0009" + set S [string map [list {\\} $TOK_BSLASH {\*} $TOK_STAR {\#} $TOK_HASH {\[} $TOK_LB {\]} $TOK_RB {\(} $TOK_LP {\)} $TOK_RP {\`} $TOK_BT {\-} $TOK_DASH] $S] + + # Protect inline code spans so other formatting does not apply inside them. + array set CodeMap {} + set S [_ProtectInlineCodeSpans $S CodeMap] + + # Links: [text](url) + # Note: Both text and url are already HTML-escaped here. + set Out "" + set Remainder $S + while {[regexp -indices {\[([^\]]+)\]\(([^\)]+)\)} $Remainder MatchIdx TextIdx UrlIdx]} { + lassign $MatchIdx M0 M1 + lassign $TextIdx T0 T1 + lassign $UrlIdx U0 U1 + + if {$M0 > 0} { + append Out [string range $Remainder 0 [expr {$M0 - 1}]] + } + + set LinkText [string range $Remainder $T0 $T1] + set LinkUrl [string range $Remainder $U0 $U1] + if {[_IsSafeMarkdownUrl $LinkUrl]} { + append Out "$LinkText" + } else { + append Out "$LinkText ($LinkUrl)" + } + + set Remainder [string range $Remainder [expr {$M1 + 1}] end] + } + append Out $Remainder + set S $Out + + # Emphasis + regsub -all {\*\*([^*]+)\*\*} $S {\1} S + regsub -all {\*([^*]+)\*} $S {\1} S + + # Restore inline code spans + set S [_RestoreInlineCodeSpans $S CodeMap] + + # Restore escaped literals + set S [string map [list \ + $TOK_BSLASH "\\" \ + $TOK_STAR {*} \ + $TOK_HASH {#} \ + $TOK_LB {[} \ + $TOK_RB {]} \ + $TOK_LP {(} \ + $TOK_RP {)} \ + $TOK_BT {`} \ + $TOK_DASH {-} \ + ] $S] + return $S +} + +# ------------------------------------------------- +# WriteMarkdownSubsetAsHtml +# +# Minimal Markdown subset: +# - Paragraphs separated by blank lines +# - Headings: ##, ###, ####, ##### +# - Bullet list items: - +# - Enumerated list items: 1. +# - Inline: **bold**, *italic*, `code`, [text](url), and backslash escapes +# +proc WriteMarkdownSubsetAsHtml {ResultsFile Text {Indent ""}} { + # Normalize newlines + set Normalized [string map {"\r\n" "\n" "\r" "\n"} $Text] + set Lines [split $Normalized "\n"] + + # "" | "ul" | "ol" + set ListKind "" + set ParaLines {} + + proc _FlushParagraph {ResultsFile Indent ParaLinesVar} { + upvar 1 $ParaLinesVar ParaLines + if {[llength $ParaLines] == 0} { + return + } + set Raw [join $ParaLines " "] + set Escaped [EscapeHtml $Raw] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}

${Html}

" + set ParaLines {} + } + + proc _CloseListIfOpen {ResultsFile Indent ListKindVar} { + upvar 1 $ListKindVar ListKind + if {$ListKind ne ""} { + puts $ResultsFile "${Indent}" + set ListKind "" + } + } + + foreach Line $Lines { + set Line [string trimright $Line] + set Trimmed [string trim $Line] + + if {$Trimmed eq ""} { + _FlushParagraph $ResultsFile $Indent ParaLines + _CloseListIfOpen $ResultsFile $Indent ListKind + continue + } + + # Support headings written as \##, \###, ... (workaround for YAML parsers + # that incorrectly treat lines starting with '#' as comments inside | blocks). + if {[regexp {^\\?(#{2,5})\s+(.+)$} $Trimmed -> Hashes Title]} { + _FlushParagraph $ResultsFile $Indent ParaLines + _CloseListIfOpen $ResultsFile $Indent ListKind + + set Level [string length $Hashes] + if {$Level == 2} { + set Tag "h3" + } elseif {$Level == 3} { + set Tag "h4" + } elseif {$Level == 4} { + set Tag "h5" + } else { + set Tag "h6" + } + set Escaped [EscapeHtml $Title] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}<${Tag} class=\"subtitle\">${Html}" + continue + } + + if {[string match "- *" $Trimmed]} { + _FlushParagraph $ResultsFile $Indent ParaLines + if {$ListKind ne "ul"} { + _CloseListIfOpen $ResultsFile $Indent ListKind + puts $ResultsFile "${Indent}
    " + set ListKind "ul" + } + set Item [string range $Trimmed 2 end] + set Escaped [EscapeHtml $Item] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}
  • ${Html}
  • " + continue + } + + if {[regexp {^\d+\.\s+(.+)$} $Trimmed -> Item]} { + _FlushParagraph $ResultsFile $Indent ParaLines + if {$ListKind ne "ol"} { + _CloseListIfOpen $ResultsFile $Indent ListKind + puts $ResultsFile "${Indent}
      " + set ListKind "ol" + } + set Escaped [EscapeHtml $Item] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}
    1. ${Html}
    2. " + continue + } + + if {$ListKind ne ""} { + _CloseListIfOpen $ResultsFile $Indent ListKind + } + lappend ParaLines $Line + } + + _FlushParagraph $ResultsFile $Indent ParaLines + _CloseListIfOpen $ResultsFile $Indent ListKind +} + # ------------------------------------------------- # CreateOsvvmReportHeader # @@ -159,6 +495,16 @@ proc GetOsvvmPathSettings {TestDict} { variable ::osvvm::Report2PngFile [dict get $SettingsInfoDict Report2PngFile] } +# ------------------------------------------------- +# SumAlertCount +# +# Used by multiple report generators. +if {![llength [info commands SumAlertCount]]} { + proc SumAlertCount {AlertCountDict} { + return [expr [dict get $AlertCountDict Failure] + [dict get $AlertCountDict Error] + [dict get $AlertCountDict Warning]] + } +} + # ------------------------------------------------- # GetTestCaseSettings # @@ -181,6 +527,11 @@ proc GetTestCaseSettings {SettingsFileName} { variable ::osvvm::Report2TranscriptFiles [dict get $TestDict TranscriptFiles ] variable ::osvvm::Report2TestCaseHtml [file join $Report2TestSuiteDirectory ${Report2TestCaseFileName}.html] + + # Optional fields (older run.yml files may not have these) + if {[dict exists $TestDict ElapsedTime]} { + variable ::osvvm::Report2TestCaseElapsedTime [dict get $TestDict ElapsedTime] + } GetOsvvmPathSettings $TestDict