diff --git a/Actions/AL-Go-Helper.ps1 b/Actions/AL-Go-Helper.ps1
index b4f62578e..972c1393d 100644
--- a/Actions/AL-Go-Helper.ps1
+++ b/Actions/AL-Go-Helper.ps1
@@ -638,6 +638,12 @@ function ReadSettings {
"buildModes" = @()
"useCompilerFolder" = $false
"pullRequestTrigger" = "pull_request_target"
+ "bcptThresholds" = [ordered]@{
+ "DurationWarning" = 10
+ "DurationError" = 25
+ "NumberOfSqlStmtsWarning" = 5
+ "NumberOfSqlStmtsError" = 10
+ }
"fullBuildPatterns" = @()
"excludeEnvironments" = @()
"alDoc" = [ordered]@{
diff --git a/Actions/AnalyzeTests/AnalyzeTests.ps1 b/Actions/AnalyzeTests/AnalyzeTests.ps1
index ffddd95c6..1b6589cca 100644
--- a/Actions/AnalyzeTests/AnalyzeTests.ps1
+++ b/Actions/AnalyzeTests/AnalyzeTests.ps1
@@ -2,7 +2,7 @@
[Parameter(HelpMessage = "Specifies the parent telemetry scope for the telemetry signal", Mandatory = $false)]
[string] $parentTelemetryScopeJson = '7b7d',
[Parameter(HelpMessage = "Project to analyze", Mandatory = $false)]
- [string] $project
+ [string] $project = '.'
)
$telemetryScope = $null
@@ -17,25 +17,40 @@ try {
. (Join-Path -Path $PSScriptRoot 'TestResultAnalyzer.ps1')
$testResultsFile = Join-Path $ENV:GITHUB_WORKSPACE "$project\TestResults.xml"
- if (Test-Path $testResultsFile) {
- $testResults = [xml](Get-Content "$project\TestResults.xml")
- $testResultSummary = GetTestResultSummary -testResults $testResults -includeFailures 50
+ $testResultsSummaryMD, $testResultsfailuresMD, $testResultsFailuresSummaryMD = GetTestResultSummaryMD -testResultsFile $testResultsFile
- Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "TestResultMD=$testResultSummary"
- Write-Host "TestResultMD=$testResultSummary"
+ $settings = $env:Settings | ConvertFrom-Json
+ $bcptTestResultsFile = Join-Path $ENV:GITHUB_WORKSPACE "$project\bcptTestResults.json"
+ $bcptBaseLineFile = Join-Path $ENV:GITHUB_WORKSPACE "$project\bcptBaseLine.json"
+ $bcptThresholdsFile = Join-Path $ENV:GITHUB_WORKSPACE "$project\bcptThresholds.json"
+ $bcptSummaryMD = GetBcptSummaryMD `
+ -bcptTestResultsFile $bcptTestResultsFile `
+ -baseLinePath $bcptBaseLineFile `
+ -thresholdsPath $bcptThresholdsFile `
+ -bcptThresholds ($settings.bcptThresholds | ConvertTo-HashTable)
- Add-Content -path $ENV:GITHUB_STEP_SUMMARY -value "$($testResultSummary.Replace("\n","`n"))`n"
+ # If summary fits, we will display it in the GitHub summary
+ if ($testResultsSummaryMD.Length -gt 65000) {
+ # If Test results summary is too long, we will not display it in the GitHub summary, instead we will display a message to download the test results
+ $testResultsSummaryMD = "Test results summary size exceeds GitHub summary capacity. Download **TestResults** artifact to see details."
}
- else {
- Write-Host "Test results not found"
+ # If summary AND BCPT summary fits, we will display both in the GitHub summary
+ if ($testResultsSummaryMD.Length + $bcptSummaryMD.Length -gt 65000) {
+ # If Combined Test Results and BCPT summary exceeds GitHub summary capacity, we will not display the BCPT summary
+ $bcptSummaryMD = "Performance test results summary size exceeds GitHub summary capacity. Download **BcptTestResults** artifact to see details."
}
-
- $bcptTestResultsFile = Join-Path $ENV:GITHUB_WORKSPACE "$project\BCPTTestResults.json"
- if (Test-Path $bcptTestResultsFile) {
- # TODO Display BCPT Test Results
+ # If summary AND BCPT summary AND failures summary fits, we will display all in the GitHub summary
+ if ($testResultsSummaryMD.Length + $testResultsfailuresMD.Length + $bcptSummaryMD.Length -gt 65000) {
+ # If Combined Test Results, failures and BCPT summary exceeds GitHub summary capacity, we will not display the failures details, only the failures summary
+ $testResultsfailuresMD = $testResultsFailuresSummaryMD
}
- else {
- #Add-Content -path $ENV:GITHUB_STEP_SUMMARY -value "*BCPT test results not found*`n`n"
+
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "## Test results`n`n"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "$($testResultsSummaryMD.Replace("\n","`n"))`n`n"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "$($testResultsfailuresMD.Replace("\n","`n"))`n`n"
+ if ($bcptSummaryMD) {
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "## Performance test results`n`n"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "$($bcptSummaryMD.Replace("\n","`n"))`n`n"
}
TrackTrace -telemetryScope $telemetryScope
diff --git a/Actions/AnalyzeTests/TestResultAnalyzer.ps1 b/Actions/AnalyzeTests/TestResultAnalyzer.ps1
index b089cd3f4..f3efad58f 100644
--- a/Actions/AnalyzeTests/TestResultAnalyzer.ps1
+++ b/Actions/AnalyzeTests/TestResultAnalyzer.ps1
@@ -1,107 +1,324 @@
-function GetTestResultSummary {
+$statusOK = " :heavy_check_mark:"
+$statusWarning = " :warning:"
+$statusError = " :x:"
+$statusSkipped = " :question:"
+
+# Build MarkDown of TestResults file
+# This function will not fail if the file does not exist or if any test errors are found
+# TestResults is in JUnit format
+# Returns both a summary part and a failures part
+function GetTestResultSummaryMD {
Param(
- [xml] $testResults,
- [int] $includeFailures
+ [string] $testResultsFile
)
- $totalTests = 0
- $totalTime = 0.0
- $totalFailed = 0
- $totalSkipped = 0
- $failuresIncluded = 0
$summarySb = [System.Text.StringBuilder]::new()
$failuresSb = [System.Text.StringBuilder]::new()
- if ($testResults.testsuites) {
- $appNames = @($testResults.testsuites.testsuite | ForEach-Object { $_.Properties.property | Where-Object { $_.Name -eq "appName" } | ForEach-Object { $_.Value } } | Select-Object -Unique)
- if (-not $appNames) {
- $appNames = @($testResults.testsuites.testsuite | ForEach-Object { $_.Properties.property | Where-Object { $_.Name -eq "extensionId" } | ForEach-Object { $_.Value } } | Select-Object -Unique)
- }
- foreach($testsuite in $testResults.testsuites.testsuite) {
- $totalTests += $testsuite.Tests
- $totalTime += [decimal]::Parse($testsuite.time, [System.Globalization.CultureInfo]::InvariantCulture)
- $totalFailed += $testsuite.failures
- $totalSkipped += $testsuite.skipped
- }
- Write-Host "$($appNames.Count) TestApps, $totalTests tests, $totalFailed failed, $totalSkipped skipped, $totalTime seconds"
- $summarySb.Append('|Test app|Tests|Passed|Failed|Skipped|Time|\n|:---|---:|---:|---:|---:|---:|\n') | Out-Null
- foreach($appName in $appNames) {
- $appTests = 0
- $appTime = 0.0
- $appFailed = 0
- $appSkipped = 0
- $suites = $testResults.testsuites.testsuite | where-Object { $_.Properties.property | Where-Object { $_.Value -eq $appName } }
- foreach($suite in $suites) {
- $appTests += [int]$suite.tests
- $appFailed += [int]$suite.failures
- $appSkipped += [int]$suite.skipped
- $appTime += [decimal]::Parse($suite.time, [System.Globalization.CultureInfo]::InvariantCulture)
- }
- $appPassed = $appTests-$appFailed-$appSkipped
- Write-Host "- $appName, $appTests tests, $appPassed passed, $appFailed failed, $appSkipped skipped, $appTime seconds"
- $summarySb.Append("|$appName|$appTests|") | Out-Null
- if ($appPassed -gt 0) {
- $summarySb.Append("$($appPassed) :white_check_mark:") | Out-Null
- }
- $summarySb.Append("|") | Out-Null
- if ($appFailed -gt 0) {
- $summarySb.Append("$($appFailed) :x:") | Out-Null
+ if (Test-Path -Path $testResultsFile -PathType Leaf) {
+ $testResults = [xml](Get-Content -path $testResultsFile -Encoding UTF8)
+ $totalTests = 0
+ $totalTime = 0.0
+ $totalFailed = 0
+ $totalSkipped = 0
+ if ($testResults.testsuites) {
+ $appNames = @($testResults.testsuites.testsuite | ForEach-Object { $_.Properties.property | Where-Object { $_.Name -eq "appName" } | ForEach-Object { $_.Value } } | Select-Object -Unique)
+ if (-not $appNames) {
+ $appNames = @($testResults.testsuites.testsuite | ForEach-Object { $_.Properties.property | Where-Object { $_.Name -eq "extensionId" } | ForEach-Object { $_.Value } } | Select-Object -Unique)
}
- $summarySb.Append("|") | Out-Null
- if ($appSkipped -gt 0) {
- $summarySb.Append("$($appSkipped) :white_circle:") | Out-Null
+ foreach($testsuite in $testResults.testsuites.testsuite) {
+ $totalTests += $testsuite.Tests
+ $totalTime += [decimal]::Parse($testsuite.time, [System.Globalization.CultureInfo]::InvariantCulture)
+ $totalFailed += $testsuite.failures
+ $totalSkipped += $testsuite.skipped
}
- $summarySb.Append("|$($appTime)s|\n") | Out-Null
- if ($appFailed -gt 0) {
- $failuresSb.Append("$appName, $appTests tests, $appPassed passed, $appFailed failed, $appSkipped skipped, $appTime seconds
\n") | Out-Null
+ Write-Host "$($appNames.Count) TestApps, $totalTests tests, $totalFailed failed, $totalSkipped skipped, $totalTime seconds"
+ $summarySb.Append('|Test app|Tests|Passed|Failed|Skipped|Time|\n|:---|---:|---:|---:|---:|---:|\n') | Out-Null
+ foreach($appName in $appNames) {
+ $appTests = 0
+ $appTime = 0.0
+ $appFailed = 0
+ $appSkipped = 0
+ $suites = $testResults.testsuites.testsuite | where-Object { $_.Properties.property | Where-Object { ($_.Name -eq 'appName' -or $_.Name -eq 'extensionId') -and $_.Value -eq $appName } }
foreach($suite in $suites) {
- Write-Host " - $($suite.name), $($suite.tests) tests, $($suite.failures) failed, $($suite.skipped) skipped, $($suite.time) seconds"
- if ($suite.failures -gt 0 -and $failuresSb.Length -lt 32000 -and $includeFailures -gt $failuresIncluded) {
- $failuresSb.Append("$($suite.name), $($suite.tests) tests, $($suite.failures) failed, $($suite.skipped) skipped, $($suite.time) seconds
") | Out-Null
- foreach($testcase in $suite.testcase) {
- if ($testcase.ChildNodes.Count -gt 0) {
- Write-Host " - $($testcase.name), Failure, $($testcase.time) seconds"
- $failuresSb.Append("$($testcase.name), Failure
") | Out-Null
- foreach($failure in $testcase.ChildNodes) {
- Write-Host " - Error: $($failure.message)"
- Write-Host " Stacktrace:"
- Write-Host " $($failure."#text".Trim().Replace("`n","`n "))"
- $failuresSb.Append(" Error: $($failure.message)
") | Out-Null
- $failuresSb.Append(" Stack trace
") | Out-Null
- $failuresSb.Append(" $($failure."#text".Trim().Replace("`n","
"))
") | Out-Null
+ $appTests += [int]$suite.tests
+ $appFailed += [int]$suite.failures
+ $appSkipped += [int]$suite.skipped
+ $appTime += [decimal]::Parse($suite.time, [System.Globalization.CultureInfo]::InvariantCulture)
+ }
+ $appPassed = $appTests-$appFailed-$appSkipped
+ Write-Host "- $appName, $appTests tests, $appPassed passed, $appFailed failed, $appSkipped skipped, $appTime seconds"
+ $summarySb.Append("|$appName|$appTests|") | Out-Null
+ if ($appPassed -gt 0) {
+ $summarySb.Append("$($appPassed)$statusOK") | Out-Null
+ }
+ $summarySb.Append("|") | Out-Null
+ if ($appFailed -gt 0) {
+ $summarySb.Append("$($appFailed)$statusError") | Out-Null
+ }
+ $summarySb.Append("|") | Out-Null
+ if ($appSkipped -gt 0) {
+ $summarySb.Append("$($appSkipped)$statusSkipped") | Out-Null
+ }
+ $summarySb.Append("|$($appTime)s|\n") | Out-Null
+ if ($appFailed -gt 0) {
+ $failuresSb.Append("$appName, $appTests tests, $appPassed passed, $appFailed failed, $appSkipped skipped, $appTime seconds
\n") | Out-Null
+ foreach($suite in $suites) {
+ Write-Host " - $($suite.name), $($suite.tests) tests, $($suite.failures) failed, $($suite.skipped) skipped, $($suite.time) seconds"
+ if ($suite.failures -gt 0 -and $failuresSb.Length -lt 32000) {
+ $failuresSb.Append("$($suite.name), $($suite.tests) tests, $($suite.failures) failed, $($suite.skipped) skipped, $($suite.time) seconds
") | Out-Null
+ foreach($testcase in $suite.testcase) {
+ if ($testcase.ChildNodes.Count -gt 0) {
+ Write-Host " - $($testcase.name), Failure, $($testcase.time) seconds"
+ $failuresSb.Append("$($testcase.name), Failure
") | Out-Null
+ foreach($failure in $testcase.ChildNodes) {
+ Write-Host " - Error: $($failure.message)"
+ Write-Host " Stacktrace:"
+ Write-Host " $($failure."#text".Trim().Replace("`n","`n "))"
+ $failuresSb.Append(" Error: $($failure.message)
") | Out-Null
+ $failuresSb.Append(" Stack trace
") | Out-Null
+ $failuresSb.Append(" $($failure."#text".Trim().Replace("`n","
"))
") | Out-Null
+ }
+ $failuresSb.Append(" ") | Out-Null
}
- $failuresSb.Append(" ") | Out-Null
}
+ $failuresSb.Append(" ") | Out-Null
}
- $failuresSb.Append(" ") | Out-Null
- $failuresIncluded++
}
+ $failuresSb.Append(" ") | Out-Null
}
- $failuresSb.Append(" ") | Out-Null
}
}
- }
- if ($totalFailed -gt 0) {
- if ($totalFailed -gt $failuresIncluded) {
- $failuresSb.Insert(0,"$totalFailed failing tests (showing the first $failuresIncluded here, download test results to see all)
") | Out-Null
+ if ($totalFailed -gt 0) {
+ $failuresSummaryMD = "$totalFailed failing tests, download test results to see details"
+ $failuresSb.Insert(0,"$failuresSummaryMD
") | Out-Null
+ $failuresSb.Append(" ") | Out-Null
}
else {
- $failuresSb.Insert(0,"$totalFailed failing tests
") | Out-Null
+ $failuresSummaryMD = "No test failures"
+ $failuresSb.Append($failuresSummaryMD) | Out-Null
}
- $failuresSb.Append(" ") | Out-Null
- if (($summarySb.Length + $failuresSb.Length) -lt 65000) {
- $summarySb.Append("\n\n$($failuresSb.ToString())") | Out-Null
+ }
+ else {
+ $summarySb.Append("No test results found") | Out-Null
+ $failuresSummaryMD = ''
+ }
+ $summarySb.ToString()
+ $failuresSb.ToString()
+ $failuresSummaryMD
+}
+
+function ReadBcptFile {
+ Param(
+ [string] $bcptTestResultsFile
+ )
+
+ if ((-not $bcptTestResultsFile) -or (-not (Test-Path -Path $bcptTestResultsFile -PathType Leaf))) {
+ return $null
+ }
+
+ # Read BCPT file
+ $bcptResult = Get-Content -Path $bcptTestResultsFile -Encoding UTF8 | ConvertFrom-Json
+ $suites = [ordered]@{}
+ # Sort by bcptCode, codeunitID, operation
+ foreach($measure in $bcptResult) {
+ $bcptCode = $measure.bcptCode
+ $codeunitID = $measure.codeunitID
+ $codeunitName = $measure.codeunitName
+ $operation = $measure.operation
+
+ # Create Suite if it doesn't exist
+ if(-not $suites.Contains($bcptCode)) {
+ $suites."$bcptCode" = [ordered]@{}
}
- else {
- $summarySb.Append("\n\n$totalFailed failing tests. Download test results to see all") | Out-Null
+ # Create Codeunit under Suite if it doesn't exist
+ if (-not $suites."$bcptCode".Contains("$codeunitID")) {
+ $suites."$bcptCode"."$codeunitID" = @{
+ "codeunitName" = $codeunitName
+ "operations" = [ordered]@{}
+ }
}
+ # Create Operation under Codeunit if it doesn't exist
+ if (-not $suites."$bcptCode"."$codeunitID"."operations".Contains($operation)) {
+ $suites."$bcptCode"."$codeunitID"."operations"."$operation" = @{
+ "measurements" = @()
+ }
+ }
+ # Add measurement to measurements under operation
+ $suites."$bcptCode"."$codeunitID"."operations"."$operation".measurements += @(@{
+ "durationMin" = $measure.durationMin
+ "numberOfSQLStmts" = $measure.numberOfSQLStmts
+ })
+ }
+ $suites
+}
+
+function GetBcptSummaryMD {
+ Param(
+ [string] $bcptTestResultsFile,
+ [string] $baseLinePath = '',
+ [string] $thresholdsPath = '',
+ [int] $skipMeasurements = 0,
+ [hashtable] $bcptThresholds = $null
+ )
+
+ $bcpt = ReadBcptFile -bcptTestResultsFile $bcptTestResultsFile
+ if (-not $bcpt) {
+ return ''
+ }
+ $baseLine = ReadBcptFile -bcptTestResultsFile $baseLinePath
+ if ($baseLine) {
+ if ($null -eq $bcptThresholds) {
+ throw "Thresholds must be provided when comparing to a baseline"
+ }
+ # Override thresholds if thresholds file exists
+ if ($thresholdsPath -and (Test-Path -path $thresholdsPath)) {
+ Write-Host "Reading thresholds from $thresholdsPath"
+ $thresholds = Get-Content -Path $thresholdsPath -Encoding UTF8 | ConvertFrom-Json
+ foreach($threshold in 'durationWarning', 'durationError', 'numberOfSqlStmtsWarning', 'numberOfSqlStmtsError') {
+ if ($thresholds.PSObject.Properties.Name -eq $threshold) {
+ $bcptThresholds."$threshold" = $thresholds."$threshold"
+ }
+ }
+ }
+ Write-Host "Using thresholds:"
+ Write-Host "- DurationWarning: $($bcptThresholds.durationWarning)"
+ Write-Host "- DurationError: $($bcptThresholds.durationError)"
+ Write-Host "- NumberOfSqlStmtsWarning: $($bcptThresholds.numberOfSqlStmtsWarning)"
+ Write-Host "- NumberOfSqlStmtsError: $($bcptThresholds.numberOfSqlStmtsError)"
+ }
+
+ $summarySb = [System.Text.StringBuilder]::new()
+ if ($baseLine) {
+ $summarySb.Append("|BCPT Suite|Codeunit ID|Codeunit Name|Operation|Status|Duration (ms)|Duration base (ms)|Duration diff (ms)|Duration diff|SQL Stmts|SQL Stmts base|SQL Stmts diff|SQL Stmts diff|\n") | Out-Null
+ $summarySb.Append("|:---------|:----------|:------------|:--------|:----:|------------:|-----------------:|-----------------:|------------:|--------:|-------------:|-------------:|-------------:|\n") | Out-Null
}
else {
- $summarySb.Append("\n\nNo test failures") | Out-Null
+ $summarySb.Append("|BCPT Suite|Codeunit ID|Codeunit Name|Operation|Duration (ms)|SQL Stmts|\n") | Out-Null
+ $summarySb.Append("|:---------|:----------|:------------|:--------|------------:|--------:|\n") | Out-Null
+ }
+
+ $lastSuiteName = ''
+ $lastCodeunitID = ''
+ $lastCodeunitName = ''
+ $lastOperationName = ''
+
+ # calculate statistics on measurements, skipping the $skipMeasurements longest measurements
+ foreach($suiteName in $bcpt.Keys) {
+ $suite = $bcpt."$suiteName"
+ foreach($codeUnitID in $suite.Keys) {
+ $codeunit = $suite."$codeunitID"
+ $codeUnitName = $codeunit.codeunitName
+ foreach($operationName in $codeunit."operations".Keys) {
+ $operation = $codeunit."operations"."$operationName"
+ # Get measurements to use for statistics
+ $measurements = @($operation."measurements" | Sort-Object -Descending { $_.durationMin } | Select-Object -Skip $skipMeasurements)
+ # Calculate statistics and store them in the operation
+ $durationMin = ($measurements | ForEach-Object { $_.durationMin } | Measure-Object -Minimum).Minimum
+ $numberOfSQLStmts = ($measurements | ForEach-Object { $_.numberOfSQLStmts } | Measure-Object -Minimum).Minimum
+
+ $baseLineFound = $true
+ try {
+ $baseLineMeasurements = @($baseLine."$suiteName"."$codeUnitID"."operations"."$operationName"."measurements" | Sort-Object -Descending { $_.durationMin } | Select-Object -Skip $skipMeasurements)
+ if ($baseLineMeasurements.Count -eq 0) {
+ throw "No base line measurements"
+ }
+ $baseDurationMin = ($baseLineMeasurements | ForEach-Object { $_.durationMin } | Measure-Object -Minimum).Minimum
+ $diffDurationMin = $durationMin-$baseDurationMin
+ $baseNumberOfSQLStmts = ($baseLineMeasurements | ForEach-Object { $_.numberOfSQLStmts } | Measure-Object -Minimum).Minimum
+ $diffNumberOfSQLStmts = $numberOfSQLStmts-$baseNumberOfSQLStmts
+ }
+ catch {
+ $baseLineFound = $false
+ $baseDurationMin = $durationMin
+ $diffDurationMin = 0
+ $baseNumberOfSQLStmts = $numberOfSQLStmts
+ $diffNumberOfSQLStmts = 0
+ }
+
+ $pctDurationMin = ($durationMin-$baseDurationMin)*100/$baseDurationMin
+ $durationMinStr = "$($durationMin.ToString("#"))|"
+ $baseDurationMinStr = "$($baseDurationMin.ToString("#"))|"
+ $diffDurationMinStr = "$($diffDurationMin.ToString("+#;-#;0"))|$($pctDurationMin.ToString('+#;-#;0'))%|"
+
+ $pctNumberOfSQLStmts = ($numberOfSQLStmts-$baseNumberOfSQLStmts)*100/$baseNumberOfSQLStmts
+ $numberOfSQLStmtsStr = "$($numberOfSQLStmts.ToString("#"))|"
+ $baseNumberOfSQLStmtsStr = "$($baseNumberOfSQLStmts.ToString("#"))|"
+ $diffNumberOfSQLStmtsStr = "$($diffNumberOfSQLStmts.ToString("+#;-#;0"))|$($pctNumberOfSQLStmts.ToString('+#;-#;0'))%|"
+
+ $thisOperationName = ''; if ($operationName -ne $lastOperationName) { $thisOperationName = $operationName }
+ $thisCodeunitName = ''; if ($codeunitName -ne $lastCodeunitName) { $thisCodeunitName = $codeunitName; $thisOperationName = $operationName }
+ $thisCodeunitID = ''; if ($codeunitID -ne $lastCodeunitID) { $thisCodeunitID = $codeunitID; $thisOperationName = $operationName }
+ $thisSuiteName = ''; if ($suiteName -ne $lastSuiteName) { $thisSuiteName = $suiteName; $thisOperationName = $operationName }
+
+ if (!$baseLine) {
+ # No baseline provided
+ $statusStr = ''
+ $baseDurationMinStr = ''
+ $diffDurationMinStr = ''
+ $baseNumberOfSQLStmtsStr = ''
+ $diffNumberOfSQLStmtsStr = ''
+ }
+ else {
+ if (!$baseLineFound) {
+ # Baseline provided, but not found for this operation
+ $statusStr = $statusSkipped
+ $baseDurationMinStr = 'N/A|'
+ $diffDurationMinStr = '||'
+ $baseNumberOfSQLStmtsStr = 'N/A|'
+ $diffNumberOfSQLStmtsStr = '||'
+ }
+ else {
+ $statusStr = $statusOK
+ if ($pctDurationMin -ge $bcptThresholds.durationError) {
+ $statusStr = $statusError
+ if ($thisCodeunitName) {
+ # Only give errors and warnings on top level operation
+ OutputError -message "$operationName in $($suiteName):$codeUnitID degrades $($pctDurationMin.ToString('N0'))%, which exceeds the error threshold of $($bcptThresholds.durationError)% for duration"
+ }
+ }
+ if ($pctNumberOfSQLStmts -ge $bcptThresholds.numberOfSqlStmtsError) {
+ $statusStr = $statusError
+ if ($thisCodeunitName) {
+ # Only give errors and warnings on top level operation
+ OutputError -message "$operationName in $($suiteName):$codeUnitID degrades $($pctNumberOfSQLStmts.ToString('N0'))%, which exceeds the error threshold of $($bcptThresholds.numberOfSqlStmtsError)% for number of SQL statements"
+ }
+ }
+ if ($statusStr -eq $statusOK) {
+ if ($pctDurationMin -ge $bcptThresholds.durationWarning) {
+ $statusStr = $statusWarning
+ if ($thisCodeunitName) {
+ # Only give errors and warnings on top level operation
+ OutputWarning -message "$operationName in $($suiteName):$codeUnitID degrades $($pctDurationMin.ToString('N0'))%, which exceeds the warning threshold of $($bcptThresholds.durationWarning)% for duration"
+ }
+ }
+ if ($pctNumberOfSQLStmts -ge $bcptThresholds.numberOfSqlStmtsWarning) {
+ $statusStr = $statusWarning
+ if ($thisCodeunitName) {
+ # Only give errors and warnings on top level operation
+ OutputWarning -message "$operationName in $($suiteName):$codeUnitID degrades $($pctNumberOfSQLStmts.ToString('N0'))%, which exceeds the warning threshold of $($bcptThresholds.numberOfSqlStmtsWarning)% for number of SQL statements"
+ }
+ }
+ }
+ }
+ $statusStr += '|'
+ }
+
+ $summarySb.Append("|$thisSuiteName|$thisCodeunitID|$thisCodeunitName|$thisOperationName|$statusStr$durationMinStr$baseDurationMinStr$diffDurationMinStr$numberOfSQLStmtsStr$baseNumberOfSQLStmtsStr$diffNumberOfSQLStmtsStr\n") | Out-Null
+
+ $lastSuiteName = $suiteName
+ $lastCodeunitID = $codeUnitID
+ $lastCodeunitName = $codeUnitName
+ $lastOperationName = $operationName
+ }
+ }
}
- if ($summarySb.Length -lt 65500) {
- $summarySb.ToString()
+
+ if ($baseLine) {
+ $summarySb.Append("\nUsed baseline provided in $([System.IO.Path]::GetFileName($baseLinePath)).") | Out-Null
}
else {
- "$totalFailed failing tests. Download test results to see all"
+ $summarySb.Append("\nNo baseline provided. Copy a set of BCPT results to $([System.IO.Path]::GetFileName($baseLinePath)) in the project folder in order to establish a baseline.") | Out-Null
}
+
+ $summarySb.ToString()
}
diff --git a/Actions/AnalyzeTests/action.yaml b/Actions/AnalyzeTests/action.yaml
index 7e4f21123..17eb64b5f 100644
--- a/Actions/AnalyzeTests/action.yaml
+++ b/Actions/AnalyzeTests/action.yaml
@@ -11,17 +11,13 @@ inputs:
default: '7b7d'
project:
description: Project to analyze
- required: true
-outputs:
- TestResultMD:
- description: MarkDown of test result
- value: ${{ steps.AnalyzeTests.outputs.TestResultMD }}
+ required: false
+ default: '.'
runs:
using: composite
steps:
- name: run
shell: ${{ inputs.shell }}
- id: AnalyzeTests
env:
_parentTelemetryScopeJson: ${{ inputs.parentTelemetryScopeJson }}
_project: ${{ inputs.project }}
diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
index 9a312a3d7..4c1956b5c 100644
--- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
+++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
@@ -28,7 +28,7 @@ function DownloadTemplateRepository {
if ($downloadLatest) {
# Get Branches from template repository
- $response = InvokeWebRequest -Headers $headers -Uri "$apiUrl/branches" -retry
+ $response = InvokeWebRequest -Headers $headers -Uri "$apiUrl/branches?per_page=100" -retry
$branchInfo = ($response.content | ConvertFrom-Json) | Where-Object { $_.Name -eq $branch }
if (!$branchInfo) {
throw "$templateUrl doesn't exist"
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 30dcf2c79..6ff757800 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,4 +1,19 @@
+### Business Central Performance Toolkit Test Result Viewer
+
+In the summary after a Test Run, you now also have the result of performance tests.
+
+### New Settings
+
+- `bcptThresholds` is a JSON object with properties for the default thresholds for the Business Central Performance Toolkit
+ - **DurationWarning** - a warning is issued if the duration of a bcpt test degrades more than this percentage (default 10)
+ - **DurationError** - an error is issued if the duration of a bcpt test degrades more than this percentage (default 25)
+ - **NumberOfSqlStmtsWarning** - a warning is issued if the number of SQL statements from a bcpt test increases more than this percentage (default 5)
+ - **NumberOfSqlStmtsError** - an error is issued if the number of SQL statements from a bcpt test increases more than this percentage (default 10)
+
+> [!NOTE]
+> Duration thresholds are subject to varying results depending on the performance of the agent running the tests. Number of SQL statements executed by a test is often the most reliable indicator of performance degredation.
+
## v5.2
### Issues
diff --git a/Scenarios/AddAPerformanceTestApp.md b/Scenarios/AddAPerformanceTestApp.md
index d57f04ae8..8c27fc9a7 100644
--- a/Scenarios/AddAPerformanceTestApp.md
+++ b/Scenarios/AddAPerformanceTestApp.md
@@ -2,16 +2,55 @@
*Prerequisites: A completed [scenario 1](GetStarted.md)*
1. On **github.com**, open **Actions** on your solution, select **Create a new performance test app** and then choose **Run workflow**. Enter values for **name**, **publisher**, and **ID range** and choose **Run workflow**
-
-1. When the workflow is done, navigate to **Pull Requests**, **inspect the PR** and **Merge the pull request**
-
-1. Under **Actions**, you will see that a Merge pull request **CI workflow** has been kicked off
-
-1. If you wait for the workflow to complete, you will see that it completes and one of the build artifacts are the **BCPT Test Results**
-
-1. Opening the **BCPT Test Results** and inspecting the results looks like this
-
-1. Currently there isn't a visual viewer of these results. The goal is to have a PowerBI dashboard, which can gather BCPT test results from multiple builds and compare.
+
+ 
+
+ Note that if workflows are not allowed to create pull requests due to GitHub Settings, you can create the PR manually by following the link in the annotation
+
+ 
+
+2. When the workflow is done, navigate to **Pull Requests**, **inspect the PR** and **Merge the pull request**
+
+ 
+
+3. Under **Actions**, you will see that a Merge pull request **CI/CD workflow** has been kicked off
+
+ 
+
+4. If you wait for the workflow to complete, you will see that it completes and one of the build artifacts are the **BCPT Test Results**
+
+ 
+
+5. Opening the **BCPT Test Results** and inspecting the results looks like this
+
+ 
+
+6. Scrolling down further reveals the Performance Test Results in a table, which also indicates that if we want to set a baseline for comparing future BCPT Test Results, we need to add a `bcptBaseLine.json` file in the project folder.
+
+ 
+
+7. After uploading a `bcptBaseLine.json`, to the project root (which is the repo root in single project repositories), another CI/CD workflow will be kicked off, which now compares the results with the baseline:
+
+ 
+
+ Where negative numbers in the diff fields indicates faster execution or lower number of SQL statements than the baseline.
+
+> [!NOTE]
+>
+> You can specify thresholds for performance testing in project settings (see [https://aka.ms/algosettings#bcptThresholds](https://aka.ms/algosettings#bcptThresholds)) or in a file called `bcptThresholds.json`, which should be located next to the `bcptBaseLine.json` file.
+
+8. After uploading a `bcptThresholds.json` file with this content:
+
+ ```
+ {
+ "durationWarning": 0,
+ "durationError": 1
+ }
+ ```
+
+ The CI/CD workflow now uses these thresholds for the CI/CD run:
+
+ 
---
[back](../README.md)
diff --git a/Scenarios/settings.md b/Scenarios/settings.md
index 80b320157..7db7706d1 100644
--- a/Scenarios/settings.md
+++ b/Scenarios/settings.md
@@ -33,6 +33,7 @@ When running a workflow or a local script, the settings are applied by reading s
| bcptTestFolders | bcptTestFolders should be an array of folders (relative to project root), which contains performance test apps for this project. Apps in these folders are sorted based on dependencies and built, published and bcpt tests are run in that order.
If bcptTestFolders are not specified, AL-Go for GitHub will try to locate bcptTestFolders in the root of the project. | [ ] |
| appDependencyProbingPaths | Array of dependency specifications, from which apps will be downloaded when the CI/CD workflow is starting. Every dependency specification consists of the following properties:
**repo** = repository
**version** = version (default latest)
**release_status** = latestBuild/release/prerelease/draft (default release)
**projects** = projects (default * = all)
**branch** = branch (default main)
**AuthTokenSecret** = Name of secret containing auth token (default none)
| [ ] |
| cleanModePreprocessorSymbols | List of clean tags to be used in _Clean_ build mode | [ ] |
+| bcptThresholds | Structure with properties for the thresholds when running performance tests using the Business Central Performance Toolkit.
**DurationWarning** = a warning is shown if the duration of a bcpt test degrades more than this percentage (default 10)
**DurationError** - an error is shown if the duration of a bcpt test degrades more than this percentage (default 25)
**NumberOfSqlStmtsWarning** - a warning is shown if the number of SQL statements from a bcpt test increases more than this percentage (default 5)
**NumberOfSqlStmtsError** - an error is shown if the number of SQL statements from a bcpt test increases more than this percentage (default 10)
*Note that errors and warnings on the build in GitHub are only issued when a threshold is exceeded on the codeunit level, when an individual operation threshold is exceeded, it is only shown in the test results viewer.* |
## AppSource specific basic project settings
| Name | Description | Default value |
diff --git a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
index 5984a0b2e..45cd0f1c4 100644
--- a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
+++ b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
@@ -83,7 +83,7 @@ jobs:
$settings = $env:Settings | ConvertFrom-Json
if ('${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}') {
Write-Host "AdminCenterApiCredentials provided in secret $($settings.adminCenterApiCredentialsSecretName)!"
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication."
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication."
}
else {
Write-Host "AdminCenterApiCredentials not provided, initiating Device Code flow"
@@ -93,7 +93,7 @@ jobs:
. $ALGoHelperPath
DownloadAndImportBcContainerHelper
$authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0))
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)"
}
diff --git a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml
index cbc851345..06f4e3e15 100644
--- a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml
+++ b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml
@@ -101,7 +101,7 @@ jobs:
}
if ($authContext) {
Write-Host "AuthContext provided in secret $secretName!"
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication."
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication."
}
else {
Write-Host "No AuthContext provided for $envName, initiating Device Code flow"
@@ -111,7 +111,7 @@ jobs:
. $ALGoHelperPath
DownloadAndImportBcContainerHelper
$authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0))
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)"
}
diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
index 5984a0b2e..45cd0f1c4 100644
--- a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
+++ b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml
@@ -83,7 +83,7 @@ jobs:
$settings = $env:Settings | ConvertFrom-Json
if ('${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}') {
Write-Host "AdminCenterApiCredentials provided in secret $($settings.adminCenterApiCredentialsSecretName)!"
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication."
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication."
}
else {
Write-Host "AdminCenterApiCredentials not provided, initiating Device Code flow"
@@ -93,7 +93,7 @@ jobs:
. $ALGoHelperPath
DownloadAndImportBcContainerHelper
$authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0))
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)"
}
diff --git a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml
index cbc851345..06f4e3e15 100644
--- a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml
+++ b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml
@@ -101,7 +101,7 @@ jobs:
}
if ($authContext) {
Write-Host "AuthContext provided in secret $secretName!"
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication."
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication."
}
else {
Write-Host "No AuthContext provided for $envName, initiating Device Code flow"
@@ -111,7 +111,7 @@ jobs:
. $ALGoHelperPath
DownloadAndImportBcContainerHelper
$authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0))
- Set-Content -Path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)"
+ Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)"
}
diff --git a/Tests/AnalyzeTests.Test.ps1 b/Tests/AnalyzeTests.Test.ps1
new file mode 100644
index 000000000..7f0b542b8
--- /dev/null
+++ b/Tests/AnalyzeTests.Test.ps1
@@ -0,0 +1,145 @@
+Get-Module TestActionsHelper | Remove-Module -Force
+Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1')
+
+Describe "AnalyzeTests Action Tests" {
+ BeforeAll {
+ function GetBcptTestResultFile {
+ Param(
+ [int] $noOfSuites = 1,
+ [int] $noOfCodeunits = 1,
+ [int] $noOfOperations = 1,
+ [int] $noOfMeasurements = 1,
+ [int] $durationOffset = 0,
+ [int] $numberOfSQLStmtsOffset = 0
+ )
+
+ $bcpt = @()
+ for($suiteNo = 1; $suiteNo -le $noOfSuites; $suiteNo++) {
+ $suiteName = "SUITE$suiteNo"
+ for($codeUnitID = 1; $codeunitID -le $noOfCodeunits; $codeunitID++) {
+ $codeunitName = "Codeunit$codeunitID"
+ for($operationNo = 1; $operationNo -le $noOfOperations; $operationNo++) {
+ $operationName = "Operation$operationNo"
+ for($no = 1; $no -le $noOfMeasurements; $no++) {
+ $bcpt += @(@{
+ "id" = [GUID]::NewGuid().ToString()
+ "bcptCode" = $suiteName
+ "codeunitID" = $codeunitID
+ "codeunitName" = $codeunitName
+ "operation" = $operationName
+ "durationMin" = $operationNo*10+$no+$durationOffset
+ "numberOfSQLStmts" = $operationNo+$numberOfSQLStmtsOffset
+ })
+ }
+ }
+ }
+ }
+ $filename = Join-Path ([System.IO.Path]::GetTempPath()) "$([GUID]::NewGuid().ToString()).json"
+ $bcpt | ConvertTo-Json -Depth 100 | Set-Content -Path $filename -Encoding UTF8
+ return $filename
+ }
+
+ $actionName = "AnalyzeTests"
+ $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve
+ $scriptName = "$actionName.ps1"
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')]
+ $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName $scriptName
+
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'bcptFilename', Justification = 'False positive.')]
+ $bcptFilename = GetBcptTestResultFile -noOfSuites 1 -noOfCodeunits 2 -noOfOperations 5 -noOfMeasurements 4
+ # BaseLine1 has overall highter duration and more SQL statements than bcptFilename (+ one more opearion)
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'bcptBaseLine1', Justification = 'False positive.')]
+ $bcptBaseLine1 = GetBcptTestResultFile -noOfSuites 1 -noOfCodeunits 4 -noOfOperations 6 -noOfMeasurements 4 -durationOffset 5 -numberOfSQLStmtsOffset 1
+ # BaseLine2 has overall lower duration and less SQL statements than bcptFilename (+ one less opearion)
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'bcptBaseLine2', Justification = 'False positive.')]
+ $bcptBaseLine2 = GetBcptTestResultFile -noOfSuites 1 -noOfCodeunits 2 -noOfOperations 4 -noOfMeasurements 4 -durationOffset -2 -numberOfSQLStmtsOffset 0
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'thresholdsFile', Justification = 'False positive.')]
+ $thresholdsFile = Join-Path ([System.IO.Path]::GetTempPath()) "$([GUID]::NewGuid().ToString()).json"
+ @{ "NumberOfSqlStmtsThresholdWarning" = 1; "NumberOfSqlStmtsThresholdError" = 2 } | ConvertTo-Json | Set-Content -Path $thresholdsFile -Encoding UTF8
+ }
+
+ It 'Compile Action' {
+ Invoke-Expression $actionScript
+ }
+
+ It 'Test action.yaml matches script' {
+ $permissions = [ordered]@{
+ }
+ $outputs = [ordered]@{
+ }
+ YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -permissions $permissions -outputs $outputs
+ }
+
+ It 'Test ReadBcptFile' {
+ . (Join-Path $scriptRoot '../AL-Go-Helper.ps1')
+ . (Join-Path $scriptRoot 'TestResultAnalyzer.ps1')
+ $bcpt = ReadBcptFile -bcptTestResultsFile $bcptFilename
+ $bcpt.Count | should -Be 1
+ $bcpt."SUITE1".Count | should -Be 2
+ $bcpt."SUITE1"."1".operations.Count | should -Be 5
+ $bcpt."SUITE1"."1".operations."operation2".measurements.Count | should -Be 4
+ }
+
+ It 'Test GetBcptSummaryMD (no baseline)' {
+ . (Join-Path $scriptRoot '../AL-Go-Helper.ps1')
+ . (Join-Path $scriptRoot 'TestResultAnalyzer.ps1')
+ $md = GetBcptSummaryMD -bcptTestResultsFile $bcptFilename
+ Write-Host $md.Replace('\n',"`n")
+ $md | should -Match 'No baseline provided'
+ $columns = 6
+ $rows = 12
+ [regex]::Matches($md, '\|SUITE1\|').Count | should -Be 1
+ [regex]::Matches($md, '\|Codeunit.\|').Count | should -Be 2
+ [regex]::Matches($md, '\|Operation.\|').Count | should -Be 10
+ [regex]::Matches($md, '\|').Count | should -Be (($columns+1)*$rows)
+ }
+
+ It 'Test GetBcptSummaryMD (with worse baseline)' {
+ . (Join-Path $scriptRoot '../AL-Go-Helper.ps1')
+ . (Join-Path $scriptRoot 'TestResultAnalyzer.ps1')
+ $md = GetBcptSummaryMD -bcptTestResultsFile $bcptFilename -baselinePath $bcptBaseLine1 -bcptThresholds @{"durationWarning"=10;"durationError"=25;"numberOfSqlStmtsWarning"=5;"numberOfSqlStmtsError"=10}
+ Write-Host $md.Replace('\n',"`n")
+ $md | should -Not -Match 'No baseline provided'
+ $columns = 13
+ $rows = 12
+ [regex]::Matches($md, '\|SUITE1\|').Count | should -Be 1
+ [regex]::Matches($md, '\|Codeunit.\|').Count | should -Be 2
+ [regex]::Matches($md, '\|Operation.\|').Count | should -Be 10
+ [regex]::Matches($md, "\|$statusOK\|").Count | should -Be 10
+ [regex]::Matches($md, "\|$statusWarning\|").Count | should -Be 0
+ [regex]::Matches($md, "\|$statusError\|").Count | should -Be 0
+ [regex]::Matches($md, '\|').Count | should -Be (($columns+1)*$rows)
+ }
+
+ It 'Test GetBcptSummaryMD (with better baseline)' {
+ . (Join-Path $scriptRoot '../AL-Go-Helper.ps1')
+ . (Join-Path $scriptRoot 'TestResultAnalyzer.ps1')
+
+ $script:errorCount = 0
+ Mock OutputError { Param([string] $message) Write-Host "ERROR: $message"; $script:errorCount++ }
+ $script:warningCount = 0
+ Mock OutputWarning { Param([string] $message) Write-Host "WARNING: $message"; $script:warningCount++ }
+
+ $md = GetBcptSummaryMD -bcptTestResultsFile $bcptFilename -baselinePath $bcptBaseLine2 -thresholdsPath $thresholdsFile -bcptThresholds @{"durationWarning"=5;"durationError"=10;"numberOfSqlStmtsWarning"=5;"numberOfSqlStmtsError"=10}
+ Write-Host $md.Replace('\n',"`n")
+ $md | should -Not -Match 'No baseline provided'
+ $columns = 13
+ $rows = 12
+ [regex]::Matches($md, '\|SUITE1\|').Count | should -Be 1
+ [regex]::Matches($md, '\|Codeunit.\|').Count | should -Be 2
+ [regex]::Matches($md, '\|Operation.\|').Count | should -Be 10
+ [regex]::Matches($md, '\|N\/A\|').Count | should -Be 4
+ [regex]::Matches($md, "\|$statusOK\|").Count | should -Be 0
+ [regex]::Matches($md, "\|$statusWarning\|").Count | should -Be 4
+ [regex]::Matches($md, "\|$statusError\|").Count | should -Be 4
+ [regex]::Matches($md, '\|').Count | should -Be (($columns+1)*$rows)
+ $script:errorCount | Should -be 2
+ $script:warningCount | Should -be 0
+ }
+
+ AfterAll {
+ Remove-Item -Path $bcptFilename -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path $bcptBaseLine1 -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path $bcptBaseLine2 -Force -ErrorAction SilentlyContinue
+ }
+}