C++ Matrix
The C++ Matrix Action is designed to automate the generation of a comprehensive test matrix for your C++ libraries given their requirements. It simplifies the process of defining the contract of what platforms your library supports and testing your project across a proper selected subset of revelant compiler versions and platforms.
With this action, you can define a set of requirements to test your C++ library. It will then generate a test matrix by combining the specified requirements into a fractional factorial design to ensure all proper combinations are tested in a systematic manner. This approach significantly increases the chances of catching compatibility issues early on and helps in delivering a robust and reliable library to users.
Usage
The action should be run as extra initial “setup” job in your workflow. The job will run the action and
output matrix, which is a JSON string containing the whole matrix. This matrix should be output of your
first setup job.
In your second build job, you can attribute the entire matrix to the strategy.matrix.include parameter
and create your workflow as usual with the parameters from the matrix:
jobs:
cpp-matrix:
runs-on: ubuntu-latest
name: Generate Test Matrix
outputs:
matrix: ${{ steps.cpp-matrix.outputs.matrix }}
steps:
- name: Generate Test Matrix
uses: alandefreitas/cpp-actions/cpp-matrix@v1.9
id: cpp-matrix
with:
standards: '>=11'
build:
needs: cpp-matrix
strategy:
fail-fast: false
matrix:
include: fromJSON(needs.cpp-matrix.outputs.matrix)
# use matrix entries
name: ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
container: ${{ matrix.container }}
steps:
- name: Clone library
uses: actions/checkout@v3
- name: Setup C++ Compiler
uses: alandefreitas/cpp-actions/setup-cpp@v1.9
id: setup-cpp
with:
compiler: ${{ matrix.compiler }}
version: ${{ matrix.version }}
- name: CMake Workflow
uses: ./cmake-workflow
with:
cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }}
cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }}
cxxstd: ${{ matrix.cxxstd }}
cxxflags: ${{ matrix.cxxstd }}
# And you've safely tested your C++ library just like that...
Factors
The input parameters allow factors to be defined for each compiler. These factors can be defined as latest factors (which are duplicated and applied to the latest version of each compiler) or as regular factors (which are injected into intermediary versions of each compiler).
See the Factors Section for a better understanding of the motivation for these factor types.
Matrix entries
The values in each matrix entry include a few categories of keys: the main keys, auxiliary keys, factor keys, and suggestions.
The main keys include information such as the compiler name and version. Auxiliary keys include information that is useful for filtering and sorting the matrix, such as information about the relative position of an entry in the matrix. Factor keys include information about the factors that might have been applied to the entry.
Suggestions include opinionated recommendations for other parameters of your workflow as a
starting point for your test matrix. Some examples of keys including suggestions are
runs-on, container, generator`, b2-toolset, build-type, ccflags, cxxflags,
env, and install.
These suggestions often need to be customized and this could not be different, since there is a myriad of ways in which libraries should be tested. Common ways to customize these are:
-
Ignoring suggestions that are not applicable to your use case
-
Using the corresponding action input to customize the value of these fields (see action input parameters)
-
Use GitHub Actions Expressions to generate new values from existing values
-
Use a custom bash step to generate new values from existing values
-
Create a custom script to read the complete matrix and generate a new matrix with the desired values
For instance, if the recommendation of running a workflow in the container ubuntu:22.04 is not appropriate
because you’re going to use a container that already has your dependencies, you could set the runs-on input
parameter to:
- name: Generate Test Matrix
# ...
with:
# ...
containers: |
gcc: my-ubuntu:22.04-container
This would replace the recommendation for ubuntu:22.04 with my-ubuntu:22.04-container for all entries
with the compiler gcc.
In some cases, you could use a expression directly in the workflow to use a different value for the container:
# In your main workflow
containers: ${{ matrix.runs-on == 'ubuntu-22.04' && 'my-ubuntu:22.04-container' || matrix.container }}
This would replace the recommendation for ubuntu:22.04 with my-ubuntu:22.04-container for all entries.
If you’re familiar with GitHub Actions and bash, you can also use a custom bash step to generate new values from existing values or write a complete script to customize the matrix.
If none of these options is enough for the library requirements, the action also prints the complete matrix in YAML format, so it can be copy/pasted into the workflow as a starting point to be customized.
Extra values in entries
In many cases, we need to reuse GitHub expressions to generate new values based on
existing values in the matrix. For instance, consider an entry in the matrix that has
the x86 factor set to true.
Assume we are using b2 and should set address-model=32 for this entry. Otherwise, we should set
address-model=64. With GitHub expressions, we can generate this value based on the x86 factor:
b2 address-model={{ matrix.x86 && '32' || '64' }}
If this pattern needs to be repeated in many entries, it might become cumbersome. In this case, the action allows the user to define extra values to be injected in each entry of the matrix. These values can be used to generate new values based on existing values in the matrix with handlebars. You could set the following extra values in each entry of the matrix:
extra-values: |
address-model: \{{#if x86}}32\{{else}}64\{{/if}}
# ...
run: |
b2 address-model={{ matrix.address-model }}
or
extra-values: |
address-model-arg: address-model=\{{#if x86}}32\{{else}}64\{{/if}}
# ...
run: |
b2 {{ matrix.address-model-arg }}
The syntax for each entry in the extra-values input is a key-value pair, where the key is the name of the
value to be injected in the matrix and the value is a handlebars expression that generates the value based on
other values in the matrix. The handlebars expression can access previous values in the matrix, including
other extra values.
Besides referencing other values in the matrix, the handlebars expressions can also use the built-in helpers provided by Handlebars and the predefined helpers defined by the action:
String Helpers for Extra Values
Helper |
Description |
|
Converts the value to lowercase |
|
Converts the value to uppercase |
|
Checks if the value contains a substring (case-sensitive) |
|
Checks if the value starts with a substring |
|
Checks if the value ends with a substring |
|
Extracts a substring: |
|
Replaces all occurrences: |
|
Replaces first occurrence only |
|
Returns index of substring, or -1 if not found |
|
Returns last index of substring, or -1 if not found |
|
Splits string into array: |
|
Removes leading/trailing whitespace |
|
Removes leading whitespace |
|
Removes trailing whitespace |
|
Capitalizes first character |
|
Capitalizes first letter of each word |
|
Converts to camelCase |
|
Converts to PascalCase |
|
Converts to snake_case |
|
Converts to kebab-case |
|
Reverses a string (or array) |
Case-Insensitive String Helpers
These helpers match GitHub Actions expression behavior (case-insensitive):
Helper |
Description |
|
Case-insensitive substring check |
|
Case-insensitive prefix check |
|
Case-insensitive suffix check |
Logical Helpers
Helper |
Description |
|
Logical AND (supports multiple arguments) |
|
Logical OR (supports multiple arguments) |
|
Logical NOT |
|
Ternary operator: |
Comparison Helpers
Helper |
Description |
|
Strict equality ( |
|
Case-insensitive equality |
|
Inequality ( |
|
Case-insensitive inequality |
|
Less than ( |
|
Less than or equal ( |
|
Greater than ( |
|
Greater than or equal ( |
Math Helpers
Helper |
Description |
|
Addition: |
|
Subtraction: |
|
Multiplication: |
|
Division: |
|
Modulo: |
|
Absolute value |
|
Round down |
|
Round up |
|
Round to nearest integer |
|
Minimum of values: |
|
Maximum of values: |
|
Power: |
Array Helpers
Helper |
Description |
|
Joins array elements: |
|
Returns first element |
|
Returns last element |
|
Returns nth element (0-indexed): |
|
Returns length of array or string |
|
Extracts portion: |
|
Sorts array alphabetically |
|
Checks if array contains value |
Type Checking Helpers
Helper |
Description |
|
Returns true if value is a string |
|
Returns true if value is a number |
|
Returns true if value is an array |
|
Returns true if value is empty string, null, undefined, or empty array |
Ordering entries
Entries are ordered in the matrix according to the following criteria:
1) Latest versions of each compiler 2) Compilers with single versions 3) Oldest versions of each compiler 4) Entries with factors 5) Intermediary versions of each compiler
The input parameter generate-summary can be used to generate a summary of the matrix with the
entries ordered according to these criteria.
Semver ranges
The requirement parameters support semver ranges, including range disjunctions. For instance, consider the default
compiler range clang >=3.8, which might test your library with too many versions of clang. You could remove
intermediary versions of clang with the range clang >=3.8 <5 || >=10, which ensures old versions and
recent versions are covered, while eliminating intermediary versions.
Semver ranges can also help when a bug is found in a specific compiler version. Consider you’re testing
gcc >=4.8 and someone has reported a bug in GCC 8.1. Now you want to keep track of GCC 8.1 specifically.
The semver range gcc >=4.8 <8 || 8.1 || >=9 would ensure version GCC 8.1 specifically, and not any other
version is the range >=8.0.0 <9.0.0 is tested.
| The action still accepts open ranges to test the latest versions of a compiler. However, additional actions might be needed to test the latest compilers, which may not be implemented in this version of the project. Therefore, users are still recommended to avoid open ranges or to update the action’s version to support the latest compiler versions. |
Dynamic matrices
In some cases, it might be useful to test different matrices based on the conditions of the workflow. These conditions might be the event_type and the types of files changed in the commit triggering the workflow. The workflows might vary between disabling tests for changes that don’t affect these certain files, running a reduced subset of tests for less important changes, or enabling extra tests, such as documentation tests in case only these files have been changed. This might be useful in terms of performance, costs, and safety.
While this is hard to achieve with hard-coded matrices, this action makes variable matrices very easy to achieve.
Simply define a previous step to determine the matrix parameters and use it to generate the matrix.
For instance, the compilers parameters could be replaced from gcc >=4.8 to
gcc >=4.8 <6.0 ${{ github.event_name == 'push' && '|| >=6.0 <10' }} || >=11 to only test intermediary
GCC versions >=6.0 <10 when in push events.
A more powerful strategy to save resources is analyzing files changed in the workflow and considering these files to determine what the matrix should be according to project conventions.
Patching Node on old containers
If you need to run tests on old compilers, the matrix entries might include old containers. GitHub Actions stopped supporting many old containers since it moved to Node 20.
A workaround is to set
the volumes key of the container object so that the user can provide its own Node installation.
This action will provide these suggestions in the container key of the matrix entries.
However, the user should still include a step in the workflow to install the required Node version
in the /__e/node20 directory.
Next steps
After creating the test matrix, the next step in your workflow should usually be Setup C++.
Example
steps:
- name: Generate Test Matrix
uses: alandefreitas/cpp-actions/cpp-matrix@v1.9
id: cpp-matrix
with:
standards: '>=14'
compilers: 'gcc >=4.8 <6 || >=9
clang >=3.8 <6 || >11
msvc >=14
apple-clang *
mingw *
clang-cl *
'
subrange-policy: 'msvc: one-per-minor
'
latest-factors: 'gcc Coverage TSan UBSan Fetch-Content
clang Fetch-Content
'
factors: 'gcc ASan Shared No-Deps
msvc Shared x86
clang Time-Trace ASan+UBSan
mingw Shared
'
force-factors: 'mingw: No-Deps
'
extra-values: 'vcpkg-packages: \{{#if (not fetch-content)}}fmt\{{/if\}}
address-model: \{{#if (eq arch ''x86'')}}32\{{else}}64\{{/if\}}
'
github-token: ${{ secrets.GITHUB_TOKEN }}
Input Parameters
Parameter |
Description |
Default |
|
Trace commands executed by the workflow. |
|
|
A list of compilers to be tested. Each compiler can be complemented with its semver version requirements to be tested. When the compiler version requirements are provided, the action will break the requirements into subsets of major versions to be tested. When no version is provided, the '*' semver requirement is assumed. The action can identifies subsets of compiler versions for GCC, Clang, and MSVC. For any other compilers, the version requirements will passthrough to the output. |
|
|
The policy to be used to break the compiler version requirements into sub-ranges of versions. For instance, if the compiler requirements are The policy can be This input can be a single value for all compilers or a multi-line list of compiler-specific policies. Another policy is to break into major versions when the range contains multiple major versions and into minor versions when the range contains multiple minor versions. The name of this policy is |
|
|
A semver range describing what C standards should be tested. For instance, `>=11` indicates that the library should be tested with C11 and later standards. These requirements can include C standards as 2 or 4 digits versions, such as 11, 2011, 98, or 1998. 2 digit versions are normalized into the 4 digits form so that 11 > 98 (2011 > 1998). The action will generate entries for each compiler version that satisfies the requirements. The compiler ranges defined in `compilers` are adjusted to only include compilers that support any subrange of these requirements. Compilers that don't support any of the standards in the range will be excluded from the matrix. Each entry in the matrix will include the `cxxstd` key with a list of standards to be tested with that compiler version. This list will include the `max-standards` latest standards supported by the compiler specified in that entry. For instance, if `max-standards` is `3` and the compiler supports '11,14,17,20,23' given the the `standards` requirement `>=11`, the `cxxstd` field of the entry will include the standards `20,23` will be tested by this compiler. This allows the matrix to be more focused on the latest standards supported by each compiler (the ones that are more likely to be contain compatibility issues) while still testing all standards required by the library in the matrix. It's very common for compilers to not fully comply with the standards they claim to support, even for the old standards. The criteria used by this action for determining if a compiler supports a standard is based on the whether the compiler claims to support the standard by providing a corresponding `-std=cXX` flag to enable the standard. This criteria is easy to follow, minimizes surprises, covers the most common bugs, and ensures users the library is compliant with all standard flags supported by the compilers. |
|
|
The maximum number of standards to be tested with each compiler. For instance, if 'max-standards' is 2 and the compiler supports '11,14,17,20,23' given the in the standard requirements, the standards 20,23 will be tested by this compiler. |
|
|
The factors to be tested with the latest versions of each compiler. For each factor in this list, the entry with the latest version of a compiler will be duplicated with an entry that sets this factor to true. Other entries will also include this factor as false. The following factors are considered special: 'asan', 'ubsan', 'msan', 'tsan', and 'coverage'. When these factors are defined in an entry, its 'ccflags', 'cxxflags', and 'linkflags' value are also modified to include the suggested flags for factor. |
|
|
The factors to be tested with other versions of each compiler. Each factor in this list will be injected into a version of the compiler that is not the latest version. An entry with the latest version of the compiler will be duplicated with this factor if there are no entries left to inject the factor. Other entries will also include this factor as false. |
|
|
The factors to be tested with all combinations of other factors. When combinatorial factors are defined, for each entry in the matrix, a new entry will be created with the factors in this list set to For instance, if the library can be built both in "Standalone" mode and with dependencies, the factor 'Standalone' can be added to this list to duplicate all entries. Each copy would include a "Standalone" factor set to Typically, it is advisable to steer clear of combinatorial factors to prevent a combinatorial explosion. It’s usually better to only test the combinations that are expected to be used in practice and include an extra steps in the workflow to test any combinatorial factors. For instance, if the library can be built both in "Standalone" mode and with dependencies, its workflow can simply include an extra step to also test the library in "Standalone" mode and keep the step to test the library with dependencies. This is usually safer and cheaper than duplicating the entire matrix to test all combinations of these factors and also allows steps to be skipped when the library is not expected to be built in a given mode. For instance, testing a library on Standalone mode might not be necessary when the library is being tested with intermediary compilers. |
|
|
A multi-line list of factor flags to be injected with each range of compiler version even if the entry doesn’t have the usual requirements to have that factor. Each line has the format:
For instance, Omitting When the build type is unspecified, the action will infer the build type from the compiler name and its version. |
|
|
A multi-line list of key-value pairs to be injected in each entry of the matrix. Each line has the format:
For instance, The values also support Handlebars expressions, which can be used to generate values based on other values in the entry. For instance,
would generate a hash-key with the value
|
|
|
A multi-line list of github runner images to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the runner image is specified, a container is only be suggested for the entries if the When the runner image is unspecified, the action will infer the runner image and potentially a container from the compiler name and its version. |
|
|
A multi-line list of docker containers to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the container is specified for that compiler version and the When the container is unspecified, the action can still infer a container for the compiler version according to the rules defined in the |
|
|
A multi-line list of cmake generators to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the generator is unspecified, the action will infer the generator from the compiler name and its version. |
|
|
A multi-line list of cmake generator toolsets to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the generator toolset is unspecified, the action will infer the generator toolset from the compiler name and its version. |
|
|
A multi-line list of b2 toolsets to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the toolset is unspecified, the action will infer the toolset from the compiler name and its version. |
|
|
A multi-line list of C compiler flags to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the flag is unspecified, the action will infer the flag from the compiler name and its version. |
|
|
A multi-line list of C compiler flags to be used with each range of compiler version. Each line has the format: `<compiler-name>[ <compiler-range|compiler-factor>]: <cxxflags>` For instance, `gcc >=13.1: -O3` indicates that the C compiler flag Omitting When the flag is unspecified, the action will infer the flag from the compiler name and its version. |
|
|
A multi-line list of packages to be installed with each range of compiler version. Each line has the format:
For instance, Omitting When the package is unspecified, the action will infer the package from the compiler name and its version. |
|
|
A multi-line list of triplets to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the triplet is unspecified, the action will infer the triplet from the compiler name and its version. |
|
|
A multi-line list of build types to be used with each range of compiler version. Each line has the format:
For instance, Omitting When the build type is unspecified, the action will infer the build type from the compiler name and its version. |
|
|
The default build type to suggest for entries without a specific build type. |
|
|
Determine the default build type to suggest when testing with sanitizers. |
|
|
Determine the default build type to suggest when testing with x86. |
|
|
Determine whether to use containers whenever possible to run the tests. By using containers for all jobs, the workflow can be more stable and reproducible. For instance, without containers an existing workflow cannot start to fail because of a change in the GitHub runner environments. However, this comes at a cost of initial setup time. Some existing workflows can also break when moving to containers because existing assumptions about tools available in the runner environment are no longer valid. When the value is false, the action will still use containers when needed. This may happen because the compiler is not available in the runner image or when there’s a reported conflict between compilers in the runner image. |
|
|
The file to output the matrix as a JSON string. This is useful when the matrix is too large to be printed in the logs or when the matrix needs to be saved for later use. The file will be saved in the current working directory of the action. |
|
|
Log the generated matrix as a JSON string. The is useful for debugging purposes and when transitioning to a workflow that uses a hard-coded matrix. |
|
|
Generate summary with the complete matrix. |
|
|
Emit a GitHub warning whenever a compiler configuration results in zero matrix entries because no known versions can satisfy the requested version range and C++ standard requirements simultaneously. Leave this enabled to catch unintended gaps; set it to |
|
|
Sort matrix entries by historical failure rate. The action fetches recent workflow run history and calculates the failure rate for each matrix entry based on job name matching. Entries with higher failure rates are sorted first (stable sort preserving existing order for equal rates). This is useful when combined with When enabled, the summary table includes a "Failure Rate" column. Requires |
|
|
Number of recent workflow runs to analyze when calculating failure rates. Jobs for each run are fetched in parallel, so increasing this value has minimal impact on execution time. Only used when |
|
|
GitHub token for API access when fetching workflow run history for failure rate calculation. Required when |
Outputs
Output |
Description |
|
The test matrix is an array of dictionaries, where each entry represents a combination of compiler version and factors to be tested. Each entry in the test matrix dictionary contains key-value pairs in the following categories: Basic fields: - - - - - Auxiliary: - - - - - - - - - Factors: - Suggestions: - - - - - - - - - - - |