Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Actions/.Modules/ReadSettings.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function GetDefaultSettings
"doNotBuildTests" = $false
"doNotPerformUpgrade" = $false
"doNotRunTests" = $false
"useSeparateTestAction" = $false
"doNotRunBcptTests" = $false
"doNotRunPageScriptingTests" = $false
"doNotPublishApps" = $false
Expand Down
4 changes: 4 additions & 0 deletions Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@
"doNotRunTests": {
"type": "boolean"
},
"useSeparateTestAction": {
"type": "boolean",
"description": "PREVIEW: When set to true, normal tests (testFolders) are no longer executed inside the RunPipeline action. Instead, RunPipeline keeps the build container alive and a separate RunTests action executes the tests afterwards. See https://aka.ms/ALGoSettings#useSeparateTestAction"
},
"doNotRunBcptTests": {
"type": "boolean"
},
Expand Down
47 changes: 47 additions & 0 deletions Actions/RunPipeline/RunPipeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ Param(
[string] $previousAppsPath = ''
)

function New-KeepAliveContainerCredential {
<#
.SYNOPSIS
Generates a credential used to create a build container that is kept alive for the RunTests action.
.DESCRIPTION
When useSeparateTestAction is enabled, RunPipeline keeps the build container alive so the RunTests
action can run tests against it. BcContainerHelper requires an explicit credential when a container
is kept (otherwise it is created with a random password that cannot be reused). This function returns
a PSCredential with a randomly generated complex password.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'A container password must be generated as plain text to build a reusable credential')]
param()
$password = "Pass!$([GUID]::NewGuid().ToString())"
return (New-Object pscredential 'admin', (ConvertTo-SecureString -String $password -AsPlainText -Force))
}

$containerBaseFolder = $null
$projectPath = $null

Expand Down Expand Up @@ -473,6 +489,36 @@ try {
$runAlPipelineParams["preprocessorsymbols"] = $settings.preprocessorSymbols
$runAlPipelineParams["features"] = $settings.features

# When useSeparateTestAction is enabled, the normal tests (testFolders) are run by the separate
# RunTests action instead of here, and the build container is kept alive for it. BCPT and page
# scripting tests are unaffected. This needs a build container, which only exists when apps are
# published (doNotPublishApps not set) and the build does not target an online environment;
# otherwise the tests are run here as usual.
$createsTestContainer = (-not $settings.doNotPublishApps) -and -not ($authContext -and $environmentName)
$keepContainerForSeparateTestAction = $false
if ($settings.useSeparateTestAction -and $createsTestContainer) {
Write-Host "useSeparateTestAction is enabled: skipping normal test execution in RunPipeline and keeping the container alive for the RunTests action"
$runAlPipelineParams["doNotRunTests"] = $true
$keepContainerForSeparateTestAction = $true

# A kept-alive container needs an explicit credential so the RunTests action can reconnect to it.
# Generate one, pass it to Run-AlPipeline, and surface it (masked, base64 JSON) via containerCredential.
if (-not $runAlPipelineParams.ContainsKey('credential')) {
$containerCredential = New-KeepAliveContainerCredential
$runAlPipelineParams["credential"] = $containerCredential

$containerCredentialPassword = $containerCredential.GetNetworkCredential().Password
$containerCredentialJson = @{ "username" = $containerCredential.UserName; "password" = $containerCredentialPassword } | ConvertTo-Json -Compress
$containerCredentialBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($containerCredentialJson))
Write-Host "::add-mask::$containerCredentialPassword"
Write-Host "::add-mask::$containerCredentialBase64"
Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "containerCredential=$containerCredentialBase64"
}
}
elseif ($settings.useSeparateTestAction) {
Write-Host "::Notice::useSeparateTestAction is enabled, but no build container is created for this project (doNotPublishApps is set or the build targets an online environment), so the RunTests action has no container to run tests against and will be skipped."
}

Write-Host "Invoke Run-AlPipeline with buildmode $buildMode"
Run-AlPipeline @runAlPipelineParams `
-accept_insiderEula `
Expand Down Expand Up @@ -518,6 +564,7 @@ try {
-pageScriptingTestResultsFolder (Join-Path $buildArtifactFolder 'PageScriptingTestResultDetails') `
-CreateRuntimePackages:$CreateRuntimePackages `
-appVersion ($versionNumber.MajorMinorVersion) -appBuild ($versionNumber.BuildNumber) -appRevision ($versionNumber.RevisionNumber) `
-keepContainer:$keepContainerForSeparateTestAction `
-uninstallRemovedApps

if ($containerBaseFolder) {
Expand Down
28 changes: 28 additions & 0 deletions Actions/RunTests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Run tests

Run the normal tests (testFolders) for an AL-Go project against the build container created and kept alive by the RunPipeline action.

This action only does anything when the `useSeparateTestAction` setting is enabled. In that case, the RunPipeline action compiles, publishes and installs the apps, skips the normal tests and keeps the build container alive. This action then runs the normal tests against that same container and writes the results to `TestResults.xml` in the project folder.

Only normal tests (testFolders) are handled here. BCPT and page scripting tests continue to be executed by the RunPipeline action.

## INPUT

### ENV variables

| Name | Description |
| :-- | :-- |
| Settings | env.Settings must be set by a prior call to the ReadSettings Action |
| containerName | env.containerName is set by the RunPipeline action and identifies the container to run tests against (the container name is otherwise derived from the project) |

### Parameters

| Name | Required | Description | Default value |
| :-- | :-: | :-- | :-- |
| shell | | The shell (powershell or pwsh) in which the PowerShell script should run | powershell |
| project | | Project folder | '.' |
| installTestAppsJson | | Path to a JSON file containing a list of test apps to run tests in | '' |

## OUTPUT

None
97 changes: 97 additions & 0 deletions Actions/RunTests/RunTests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Param(
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'token', Justification = 'Exposed as $env:_token via action.yaml so downstream test override scripts (e.g. BCApps test tolerance artifact download) can authenticate.')]
[Parameter(HelpMessage = "The GitHub token running the action", Mandatory = $false)]
[string] $token,
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
[Parameter(HelpMessage = "Project folder", Mandatory = $false)]
[string] $project = "",
[Parameter(HelpMessage = "A path to a JSON-formatted list of test apps to run tests in", Mandatory = $false)]
[string] $installTestAppsJson = ''
)

<#
.SYNOPSIS
Runs the normal tests (testFolders) for an AL-Go project against the build container
created and kept alive by the RunPipeline action.
.DESCRIPTION
Runs the normal tests (testFolders) of an AL-Go project. Tests are only run when the
useSeparateTestAction setting is enabled; otherwise this action does nothing and the tests are
run by the RunPipeline action. When enabled, RunPipeline compiles, publishes and installs the
apps and keeps the build container alive, and this action runs the normal tests against that
container and writes the results to TestResults.xml in the project folder.

Only normal tests (testFolders) are run here. BCPT and page scripting tests are run by the
RunPipeline action.
.PARAMETER token
The GitHub token running the action. It is exposed as the _token environment variable by
action.yaml so downstream test override scripts (for example, BCApps test tolerance, which
downloads the unstable-tests artifact) can authenticate against GitHub.
.PARAMETER project
Project folder.
.PARAMETER installTestAppsJson
A path to a JSON-formatted list of test apps (produced by previous jobs) to run tests in.
.EXAMPLE
RunTests.ps1 -project 'MyProject'
#>

. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve)
Import-Module (Join-Path $PSScriptRoot '..\TelemetryHelper.psm1' -Resolve)
Import-Module (Join-Path $PSScriptRoot 'RunTests.psm1' -Resolve) -DisableNameChecking -Force
DownloadAndImportBcContainerHelper

function Get-TestRunnerCredential {
<#
.SYNOPSIS
Returns the credential used by the test runner to connect to the build container.
.DESCRIPTION
RunPipeline creates the container and keeps it alive when useSeparateTestAction is set.
When RunPipeline surfaces the container credential (masked, as base64-encoded JSON in
the containerCredential environment variable), it is used here so the test runner can
connect to the same container. Otherwise a default credential is used.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The container credential is surfaced by RunPipeline as plain text')]
param()
if ($ENV:containerCredential) {
$credentialJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($ENV:containerCredential)) | ConvertFrom-Json
$securePassword = ConvertTo-SecureString -String $credentialJson.password -AsPlainText -Force
return New-Object System.Management.Automation.PSCredential($credentialJson.username, $securePassword)
}
$securePassword = ConvertTo-SecureString -String ([GUID]::NewGuid().ToString()) -AsPlainText -Force
return New-Object System.Management.Automation.PSCredential("admin", $securePassword)
}

if ($project -eq ".") { $project = "" }

$baseFolder = $ENV:GITHUB_WORKSPACE
$projectPath = Join-Path $baseFolder $project

Write-Host "Use settings"
$settings = $env:Settings | ConvertFrom-Json | ConvertTo-HashTable

# Tests only run here when useSeparateTestAction is enabled; otherwise RunPipeline runs them.
if (-not $settings.useSeparateTestAction) {
Write-Host "useSeparateTestAction is not enabled. Tests are executed by the RunPipeline action. Skipping."
return
}

# Analyze the repository to determine the test folders (and other test related settings)
$settings = AnalyzeRepo -settings $settings -baseFolder $baseFolder -project $project -doNotCheckArtifactSetting

# Resolve the container kept alive by RunPipeline (name is deterministic per project, also exported to the environment).
$containerName = $ENV:containerName
if (-not $containerName) {
$containerName = GetContainerName($project)
}

# Credentials used to connect to the build container.
$credential = Get-TestRunnerCredential

# A RunTestsInBcContainer override script, if present, replaces the built-in BcContainerHelper test runner.
$overrideParams = Get-ScriptOverrides -ALGoFolderName (Join-Path $projectPath ".AL-Go") -OverrideScriptNames @("RunTestsInBcContainer")

Invoke-AlGoTestRun `
-settings $settings `
-projectPath $projectPath `
-containerName $containerName `
-credential $credential `
-installTestAppsJson $installTestAppsJson `
-runTestsOverride $overrideParams['RunTestsInBcContainer']
155 changes: 155 additions & 0 deletions Actions/RunTests/RunTests.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<#
.SYNOPSIS
Helper module for the RunTests action.
.DESCRIPTION
Contains the logic for running the normal tests (testFolders) of an AL-Go project against a
build container that was created and kept alive by the RunPipeline action. Kept in a module
so the logic can be unit tested independently of the action entry script.
#>

function Get-TestAppsToRun {
<#
.SYNOPSIS
Determines the set of test app files to run tests in.
.DESCRIPTION
Collects the test apps compiled for the project (found in the build artifacts TestApps
folder) and, when runTestsInAllInstalledTestApps is enabled, the test apps installed from
previous jobs (listed in installTestAppsJson). Test apps wrapped in parentheses are
unwrapped (matching Run-AlPipeline semantics where such apps are otherwise not tested).
.PARAMETER settings
The (analyzed) AL-Go settings hashtable.
.PARAMETER projectPath
The full path to the project folder.
.PARAMETER installTestAppsJson
Path to a JSON file with the list of installed test apps.
#>
Param(
[hashtable] $settings,
[string] $projectPath,
[string] $installTestAppsJson = ''
)

$testAppOutputFolder = Join-Path $projectPath ".buildartifacts\TestApps"

$testApps = @()
if (Test-Path $testAppOutputFolder) {
$testApps += @(Get-ChildItem -Path $testAppOutputFolder -Filter "*.app" -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })
}

if ($settings.runTestsInAllInstalledTestApps -and $installTestAppsJson -and (Test-Path $installTestAppsJson)) {
try {
$installedTestApps = @(Get-Content -Path $installTestAppsJson -Raw | ConvertFrom-Json)
}
catch {
throw "Failed to parse JSON file at path '$installTestAppsJson'. Error: $($_.Exception.Message)"
}
$testApps += @($installedTestApps | ForEach-Object { $_.TrimStart("(").TrimEnd(")") } | Where-Object { $_ -and (Test-Path $_) })
}

return @($testApps | Select-Object -Unique)
}

function Invoke-AlGoTestRun {
<#
.SYNOPSIS
Runs the normal tests for an AL-Go project against a kept-alive build container.
.DESCRIPTION
Runs tests in each test app against the given container and writes the results to
testResultsFile in JUnit format. Honors the doNotRunTests, doNotPublishApps and
treatTestFailuresAsWarnings settings. When a RunTestsInBcContainer override script is
provided, it is used instead of the built-in BcContainerHelper test runner.
.PARAMETER settings
The (analyzed) AL-Go settings hashtable.
.PARAMETER projectPath
The full path to the project folder.
.PARAMETER containerName
The name of the build container to run the tests against.
.PARAMETER credential
The credential used to connect to the build container.
.PARAMETER installTestAppsJson
Path to a JSON file with the list of installed test apps.
.PARAMETER runTestsOverride
Optional scriptblock overriding the BcContainerHelper test runner (RunTestsInBcContainer).
#>
Param(
[hashtable] $settings,
[string] $projectPath,
[string] $containerName,
[System.Management.Automation.PSCredential] $credential,
[string] $installTestAppsJson = '',
[scriptblock] $runTestsOverride = $null
)

if ($settings.doNotRunTests) {
Write-Host "doNotRunTests is set. Skipping test execution."
return
}

if ($settings.doNotPublishApps) {
Write-Host "doNotPublishApps is set, so RunPipeline did not keep a build container alive. Skipping test execution."
return
}

$testApps = Get-TestAppsToRun -settings $settings -projectPath $projectPath -installTestAppsJson $installTestAppsJson
if ($testApps.Count -eq 0) {
Write-Host "No test apps found to run tests in. Skipping test execution."
return
}

Write-Host "Running tests against container '$containerName'"

$testResultsFile = Join-Path $projectPath "TestResults.xml"
if (Test-Path $testResultsFile) {
Remove-Item $testResultsFile -Force
}

# Test failures surface as warnings when treatTestFailuresAsWarnings is set, otherwise as errors.
$gitHubActionsSeverity = if ($settings.treatTestFailuresAsWarnings) { 'warning' } else { 'error' }

$allTestsPassed = $true
Push-Location $projectPath
try {
foreach ($testApp in $testApps) {
$appJson = Get-AppJsonFromAppFile -appFile $testApp
Write-Host "Running tests in $($appJson.name) ($($appJson.id))"

$runTestsParams = @{
"containerName" = $containerName
"credential" = $credential
"companyName" = $settings.companyName
"extensionId" = $appJson.id
"appName" = $appJson.name
"JUnitResultFileName" = $testResultsFile
"AppendToJUnitResultFile" = $true
"detailed" = $true
"GitHubActions" = $gitHubActionsSeverity
"returnTrueIfAllPassed" = $true
}

if ($runTestsOverride) {
$passed = & $runTestsOverride -parameters $runTestsParams
}
else {
$passed = Run-TestsInBcContainer @runTestsParams
}

if (-not $passed) {
$allTestsPassed = $false
}
}
}
finally {
Pop-Location
}

if (-not $allTestsPassed) {
if ($settings.treatTestFailuresAsWarnings) {
OutputWarning -message "There are test failures, but they are treated as warnings (treatTestFailuresAsWarnings is set)."
}
else {
throw "There are test failures."
}
}
}

Export-ModuleMember -Function Invoke-AlGoTestRun, Get-TestAppsToRun
Loading
Loading