This action runs tests for Particular Software repos according to our conventions:
- Finds test projects by locating *.csproj files that have a
PackageReferenceforMicrosoft.NET.Test.Sdk - Finds all the target frameworks for the test projects
- Runs
dotnet testfor each target framework, skippingnet4*on Linux
Basic:
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0With a reset script between each test run:
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
reset-script: |
echo "Do whatever is necessary to reset the test infrastructure between runs of each framework"
echo "The script is invoked by PowerShell Invoke-Expression."In cases where the test matrix subdivides by target framework, you can also short-circuit most of what this action does by specifying the framework to use for testing. While sounding counter-intuitive, it helps to keep the arguments given to dotnet test consistent with other repositories. (Added in v1.1.0)
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
framework: net6.0By default, only failed tests are reported. To report warnings for tests that have neither failed nor succeeded (i.e. skipped or inconclusive):
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
report-warnings: trueBy default, dotnet test uses x64 as the target platform. This can be overridden:
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
target-platform: x86By default the action discovers every *.csproj under src/ that references Microsoft.NET.Test.Sdk and runs all of them. Pass projects to run an explicit list of project paths instead, skipping discovery entirely. The list may be newline- or semicolon-delimited. (Added in v1.8.0)
This is the intended integration point for repositories that subdivide their test suite by category and select a subset of assemblies per matrix job. For example, ServiceControl's tools/select-test-projects.ps1 writes each category's project list to $GITHUB_OUTPUT as a multiline test-projects value, which can be passed straight through:
steps:
- id: select
shell: pwsh
run: ./tools/select-test-projects.ps1
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
projects: ${{ steps.select.outputs.test-projects }}When projects is combined with framework, each listed project is run only against that framework (projects that do not target it are skipped), mirroring the behavior of the discovery path.
By default the action runs dotnet test sequentially. Pass max-parallel (1–16) to run several test assemblies concurrently. (Added in v1.8.0)
steps:
- name: Run tests
uses: Particular/run-tests-action@v1.8.0
with:
projects: ${{ steps.select.outputs.test-projects }}
max-parallel: 4When max-parallel > 1, each run's stdout and stderr are buffered to files under $RUNNER_TEMP/run-tests-action and replayed inside a ::group:: block once that run completes, because interleaved live dotnet test output is unreadable. The step fails if any run exits non-zero. A run that never completes, because the job was cancelled or timed out, leaves its files behind, and the action's final step shows them.
Every spawned dotnet test process has the environment variable PARTICULAR_RUN_TESTS_ACTION_PARALLEL_INDEX set to its 0-based position in the flattened run list, immediately before it is spawned (so the child inherits it). The value is unique across all runs in the invocation, so concurrent runs always see distinct indices. In sequential mode (max-parallel == 1) the index is always 0.
Consumers that need per-run distinct resources — ports, temp directories, or anything else — can read this env var and derive what they need from the index. The action itself does no port arithmetic, keeping it repository-agnostic. For example, a suite using RavenDB.Embedded (which binds a fixed port and would otherwise collide across concurrent runs) can compute its port from the index:
var index = int.Parse(Environment.GetEnvironmentVariable("PARTICULAR_RUN_TESTS_ACTION_PARALLEL_INDEX") ?? "0");
var port = 33334 + (index * 10);Consumers that do not need per-run distinction simply ignore the variable.
reset-script runs between consecutive target frameworks on the sequential path (max-parallel == 1), as it always has. When max-parallel > 1, runs are flattened across frameworks, so "between frameworks" no longer has a meaningful boundary and running the script concurrently with in-flight test processes is unsafe. In that case the reset script is ignored and the action emits a ::warning:: to make the skip visible. If you need a reset between batches, run sequential (max-parallel: 1) or invoke the reset script from a separate workflow step.
This action does not support the dotnet test filter syntax. This is because it's impossible to distinguish between the following cases:
- No tests were found in an assembly becuase they did not match the filter, as was intended.
- No tests were found in an assembly because there was an error in the test adapter package, and valid tests are not being properly executed.
Due to the danger of the second case, filtering is not supported. Instead, attributes that implement NUnit's IApplyToContext interface can be applied at the method, class, or assembly level and call Assert.Ignore(reason) to ignore groups of tests in certain conditions.
An example of this can be found in SQL Persistence in the EngineSpecificTestAttribute class, which is inherited by other attribute classes for each supported database engine. This makes it possible to use a single [assembly: SqlServerTest] to only run tests in that project when a SQL Server connection string is available.
Using this method also results in visual tests summaries that clearly show which tests were run and which were ignored, making it easy to see if any tests are missing.
The scripts and documentation in this project are released under the MIT License.