Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions Actions/.Modules/ReadSettings.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ function GetDefaultSettings
"doNotRunTests" = $false
"doNotRunBcptTests" = $false
"doNotRunPageScriptingTests" = $false
"testIsolation" = [ordered]@{
"enabled" = $false
"defaultRunnerCodeunitId" = 0
"partitions" = @()
}
"doNotPublishApps" = $false
"doNotSignApps" = $false
"configPackages" = @()
Expand Down
100 changes: 100 additions & 0 deletions Actions/.Modules/TestIsolation.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<#
.SYNOPSIS
Test Isolation module for AL-Go for GitHub.

.DESCRIPTION
Builds a scriptblock compatible with Run-AlPipeline's -RunTestsInBcContainer
override. The scriptblock invokes Run-TestsInBcContainer once per declared
partition (each with its own test runner and codeunit-range filter), plus one
trailing call under the default runner whose filter excludes every codeunit
matched by the explicit partitions.
#>

function New-PartitionedTestRunnerScriptBlock {
<#
.SYNOPSIS
Build a scriptblock for Run-AlPipeline's -RunTestsInBcContainer hook.
.DESCRIPTION
Run-AlPipeline invokes the override once per test app with a hashtable
of parameters (extensionId, containerName, disabledTests, JUnit/XUnit
file, AppendTo*ResultFile, auth context, etc.). The returned
scriptblock loops over the configured partitions and, for each one,
invokes Run-TestsInBcContainer with the partition's runner id and
codeunit-range filter. After all partitions, it issues one trailing
call under defaultRunnerCodeunitId whose -testCodeunitRange is the
negation of the union of every explicit partition's filter — so every
test codeunit not matched by an explicit partition runs under the
default runner exactly once.

Result-file appending is preserved because we forward the JUnit/XUnit
file params Run-AlPipeline already configured (AppendTo*ResultFile = $true).
.PARAMETER Settings
The testIsolation settings object with `defaultRunnerCodeunitId` and
`partitions` (array of @{ runnerCodeunitId; codeunits }). Closed over
by the returned scriptblock.
.OUTPUTS
[scriptblock] returning $true if every invocation reported success.
#>
Param(
[Parameter(Mandatory = $true)]
$Settings
)

$capturedPartitions = @($Settings.partitions)
$capturedDefaultRunner = [int] $Settings.defaultRunnerCodeunitId

# Build the negative filter for the trailing default-runner call by
# collecting every '|'-separated piece from every partition's codeunits
# filter, prefixing each with '<>' and joining with '&'. Result is a
# BC integer-field filter expression that matches every codeunit ID NOT
# covered by an explicit partition.
$defaultRangeFilter = ''
if ($capturedPartitions.Count -gt 0) {
$negatedPieces = @()
foreach ($p in $capturedPartitions) {
foreach ($piece in ([string] $p.codeunits).Split('|')) {
$trimmed = $piece.Trim()
if ($trimmed) { $negatedPieces += "<>$trimmed" }
}
}
$defaultRangeFilter = $negatedPieces -join '&'
}

return {
Param([Hashtable] $parameters)

$appId = "$($parameters.extensionId)"
$allPassed = $true
$invocations = 0

foreach ($p in $capturedPartitions) {
$call = @{}
foreach ($k in $parameters.Keys) { $call[$k] = $parameters[$k] }
$call['testCodeunitRange'] = "$($p.codeunits)"
$call['testRunnerCodeunitId'] = "$([int] $p.runnerCodeunitId)"

Write-Host "Running partition runner=$($p.runnerCodeunitId) range='$($p.codeunits)' app=$appId"
$invocations++

$passed = Run-TestsInBcContainer @call
if (-not $passed) { $allPassed = $false }
}

$defaultCall = @{}
foreach ($k in $parameters.Keys) { $defaultCall[$k] = $parameters[$k] }
if ($defaultRangeFilter) { $defaultCall['testCodeunitRange'] = $defaultRangeFilter }
if ($capturedDefaultRunner -gt 0) { $defaultCall['testRunnerCodeunitId'] = "$capturedDefaultRunner" }
$defaultRunnerDisplay = if ($capturedDefaultRunner -gt 0) { "$capturedDefaultRunner" } else { "BC default" }

Write-Host "Running default partition runner=$defaultRunnerDisplay range='$defaultRangeFilter' app=$appId"
$invocations++

$passed = Run-TestsInBcContainer @defaultCall
if (-not $passed) { $allPassed = $false }

Write-Host "Partitioned test run for app $appId complete. Invocations: $invocations. All passed: $allPassed"
return $allPassed
}.GetNewClosure()
}

Export-ModuleMember -Function New-PartitionedTestRunnerScriptBlock
38 changes: 38 additions & 0 deletions Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,44 @@
"treatTestFailuresAsWarnings": {
"type": "boolean"
},
"testIsolation": {
"type": "object",
"additionalProperties": false,
"required": [ "enabled", "defaultRunnerCodeunitId", "partitions" ],
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable partitioned test runs. When true, AL-Go invokes Run-TestsInBcContainer once per entry in 'partitions' (each with its own test runner) plus one fallback call covering everything else under 'defaultRunnerCodeunitId'."
},
"defaultRunnerCodeunitId": {
"type": "integer",
"minimum": 0,
"description": "Test runner codeunit ID used for codeunits not matched by any 'partitions' entry. 0 = the BcContainerHelper default runner."
},
"partitions": {
"type": "array",
"description": "Each entry runs the codeunits matching its 'codeunits' BC filter under the configured 'runnerCodeunitId'. Codeunits not matched by any entry fall through to 'defaultRunnerCodeunitId'.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [ "runnerCodeunitId", "codeunits" ],
"properties": {
"runnerCodeunitId": {
"type": "integer",
"minimum": 1,
"description": "ID of a Subtype = TestRunner codeunit whose TestIsolation property satisfies the codeunits matched below."
},
"codeunits": {
"type": "string",
"minLength": 1,
"description": "BC filter expression against the codeunit ID (e.g. '60100|60200..60299'). Same syntax as the BC Test Tool's TestCodeunitRangeFilter."
}
}
}
}
},
"description": "See https://aka.ms/ALGoSettings#testIsolation"
},
"rulesetFile": {
"type": "string"
},
Expand Down
12 changes: 12 additions & 0 deletions Actions/RunPipeline/RunPipeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,18 @@ try {
$runAlPipelineParams["preprocessorsymbols"] = $settings.preprocessorSymbols
$runAlPipelineParams["features"] = $settings.features

if ($settings.testIsolation.enabled -and -not $settings.doNotRunTests) {
Import-Module (Join-Path $PSScriptRoot '../.Modules/TestIsolation.psm1' -Resolve)
Write-Host "Test isolation enabled - $($settings.testIsolation.partitions.Count) explicit partition(s) + default runner ($($settings.testIsolation.defaultRunnerCodeunitId))"

$testIsolationTelemetry = [System.Collections.Generic.Dictionary[[System.String], [System.String]]]::new()
Add-TelemetryProperty -Hashtable $testIsolationTelemetry -Key 'PartitionCount' -Value "$($settings.testIsolation.partitions.Count)"
Add-TelemetryProperty -Hashtable $testIsolationTelemetry -Key 'DefaultRunnerCodeunitId' -Value "$($settings.testIsolation.defaultRunnerCodeunitId)"
Trace-Information -Message "Test Isolation enabled" -AdditionalData $testIsolationTelemetry

$runAlPipelineParams["RunTestsInBcContainer"] = New-PartitionedTestRunnerScriptBlock -Settings $settings.testIsolation
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What if there already is RunTestsInBcContainer override for other purposes?
Is there a way to add test isolation w/o using RunTestsInBcContainer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@mazhelez

Great questions!

  1. I can combine the user scriptblock with the new hashtable testCodeunitRange + testRunnerCodeunitId. In that case, nothing will be overwritten.

  2. In the current design of bccontainerhelper, I see an alternative, in recent versions, RequiredTestIsolation was added, and it looks like this can be used as an alternative.

https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-requiredtestisolation-property

https://github.com/microsoft/navcontainerhelper/blob/c5fd2aa4e7a36405e50d552a3947102a05931003/AppHandling/Run-TestsInNavContainer.ps1#L532

I think I should look into point 2, I will try testing it and let you know if it works or not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@mazhelez the main problem with RequiredTestIsolation that it will work only for Runtime 16.0 +

}

Write-Host "Invoke Run-AlPipeline with buildmode $buildMode"
Run-AlPipeline @runAlPipelineParams `
-accept_insiderEula `
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Try out the [AL-Go workshop](https://aka.ms/algoworkshop) for an in-depth worksh
1. [DeliveryTargets and NuGet/GitHub Packages](Scenarios/DeliveryTargets.md)
1. [Enabling Telemetry for AL-Go workflows and actions](Scenarios/EnablingTelemetry.md)
1. [Add a performance test app to an existing project](Scenarios/AddAPerformanceTestApp.md)
1. [Partition test runs by required isolation](Scenarios/TestIsolation.md)
1. [Publish your app to AppSource](Scenarios/PublishToAppSource.md)
1. [Connect your GitHub repository to Power Platform](Scenarios/SetupPowerPlatform.md)
1. [How to set up Service Principal for Power Platform](Scenarios/SetupServicePrincipalForPowerPlatform.md)
Expand Down
23 changes: 23 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
### Test Isolation - run selected test codeunits under a custom test runner

Business Central test codeunits can declare a `RequiredTestIsolation` value (BC runtime 16+) that tells the runtime which transactional isolation they need. The standard test runner shipped by BC has a single fixed `TestIsolation` value and cannot satisfy multiple requirements at once. AL-Go for GitHub now supports running tests under multiple runners in a single pipeline pass.

When `testIsolation.enabled` is set in your settings, AL-Go partitions the test stage: each entry in `partitions` runs the matched codeunits under the configured `runnerCodeunitId`, and everything else runs under `defaultRunnerCodeunitId`. Results are merged back into the same JUnit file downstream reporting already consumes; container lifecycle and `disabledTests.json` handling are unchanged.

Enable it by adding:

```json
{
"testIsolation": {
"enabled": true,
"defaultRunnerCodeunitId": 0,
"partitions": [
{ "runnerCodeunitId": 130451, "codeunits": "60200..60299" },
{ "runnerCodeunitId": 130452, "codeunits": "60300|60301" }
]
}
}
```

Requires BC 15+ (the test page must expose the `TestCodeunitRangeFilter` control used by BcContainerHelper for codeunit-range filtering). See the full settings reference and compatibility notes in [Test Isolation](Scenarios/TestIsolation.md). The feature is opt-in - existing projects are unaffected unless they set `testIsolation.enabled = true`.

### Issues

- Issue 2204 - Workspace compilation ignores vsixFile setting
Expand Down
Loading