Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Comment thread
Drakonian marked this conversation as resolved.
Outdated
}

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
Comment thread
Drakonian marked this conversation as resolved.
Comment thread
Drakonian marked this conversation as resolved.

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