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.8.2
        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.8.2
        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.

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 ensure old versions are recent versions are covered, while eliminating intermediary versions.

Semver ranges can also help when a bug is found in a specific compiler version. Consider someone has reported a bug in GCC 8.1, and you want to keep track of that. The semver range gcc >=4.8 <7 || 8.1 || >=10 would ensure version GCC8.1 specifically, and not any other version is the range >=8.0.0 <9.0.0 is tested.

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.

Next steps

After setting 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.8.2
  id: cpp-matrix
  with:
    standards: '>=11'
    compilers: 'gcc >=4.8 <6 || >=9

      clang >=3.8 <6 || >11

      msvc >=14

      apple-clang *

      mingw *

      clang-cl *

      '
    latest-factors: 'msvc ASan

      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

      '

Input Parameters

Parameter

Description

Default

compilers

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.

gcc >=4.8

clang >=3.8

msvc >=14

apple-clang *

mingw *

clang-cl *

standards

A semver range describing what C standards should be tested.

The compiler ranges are adjusted to only include compilers that support any subrange of these requirements.

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).

>=11

max-standards

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.

2

latest-factors

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.

gcc Coverage TSan UBSan

factors

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.

gcc Asan Shared

msvc Shared x86

clang Time-Trace

mingw Shared

combinatorial-factors

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 true.

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 true or false.

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.

runs-on

A multi-line list of github runner images to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <github-runner-image>

For instance, gcc >=13.1: ubuntu-latest indicates that the runner image ubuntu-latest should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the runner image to all versions of the compiler.

When the runner image is specified, a container is only be suggested for the entries if the container option for that compiler version is also specified.

When the runner image is unspecified, the action will infer the runner image and potentially a container from the compiler name and its version.

containers

A multi-line list of docker containers to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <docker-container>

For instance, gcc >=13.1: ubuntu:22.04 indicates that the docker container ubuntu:22.04 should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the container to all versions of the compiler.

When the container is specified for that compiler version and the runs-on option is not, an ubuntu image is suggested for the entry to run the container.

When the container is unspecified, the action can still infer a container for the compiler version according to the rules defined in the use-containers option.

generators

A multi-line list of cmake generators to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <cmake-generator>

For instance, gcc >=13.1: Ninja indicates that the cmake generator Ninja should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the generator to all versions of the compiler.

When the generator is unspecified, the action will infer the generator from the compiler name and its version.

generator-toolsets

A multi-line list of cmake generator toolsets to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <cmake-generator-toolset>

For instance, clang-cl *: ClangCL indicates that the cmake generator toolset ClangCL should be used to test clang-cl with any version.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the generator to all versions of the compiler.

When the generator toolset is unspecified, the action will infer the generator toolset from the compiler name and its version.

b2-toolsets

A multi-line list of b2 toolsets to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <b2-toolset>

For instance, gcc >=13.1: gcc indicates that the b2 toolset gcc-13 should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the toolset to all versions of the compiler.

When the toolset is unspecified, the action will infer the toolset from the compiler name and its version.

ccflags

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>]: <ccflags>

For instance, gcc >=13.1: -O3 indicates that the C compiler flag -O3 should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the flag to all versions of the compiler.

When the flag is unspecified, the action will infer the flag from the compiler name and its version.

cxxflags

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 -O3 should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the flag to all versions of the compiler.

When the flag is unspecified, the action will infer the flag from the compiler name and its version.

install

A multi-line list of packages to be installed with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <packages>

For instance, gcc >=13.1: build-essential indicates that the package build-essential should be installed to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the package to all versions of the compiler.

When the package is unspecified, the action will infer the package from the compiler name and its version.

triplets

A multi-line list of triplets to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <triplet>

For instance, gcc >=13.1: x86_64-linux-gnu indicates that the triplet x86_64-linux-gnu should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the triplet to all versions of the compiler.

When the triplet is unspecified, the action will infer the triplet from the compiler name and its version.

build_types

A multi-line list of build types to be used with each range of compiler version. Each line has the format:

<compiler-name>[ <compiler-range|compiler-factor>]: <build-type>

For instance, gcc >=13.1: Release indicates that the build type Release should be used to test gcc with any version in the semver range >=13.1.

Omitting <compiler-range|compiler-factor> is equivalent to it being set to * and will apply the build type to all versions of the compiler.

When the build type is unspecified, the action will infer the build type from the compiler name and its version.

sanitizer-build-type

Determine the default build type to suggest when testing with sanitizers.

RelWithDebInfo

x86-build-type

Determine the default build type to suggest when testing with x86.

Release

use-containers

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.

true

log-matrix

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.

true

generate-summary

Generate summary with the complete matrix.

true

trace-commands

Trace commands executed by the action.

false

Outputs

Output

Description

matrix

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:

- name: A suggested name for the job testing this entry

- compiler: Specifies the name of the compiler to be used for the test configuration. This can be used as input to the setup-cpp action.

- version: Specifies the version requirements of the compiler to be used for the test configuration. This can be used as input to the setup-cpp action.

- cxxstd: A list of standards that should be tested with this compiler version. This option considers the max-standards latest standards supported by each compiler in its subrange of standards.

- latest-cxxstd: The last standard listed in cxxstd as a convenience variable

Auxiliary:

- major, minor, patch: Specifies the version components of the compiler whenever the whole range includes a single major, minor, or patch.

- has_major, has_minor, has_patch: Determines if the corresponding version component of the compiler is available representing all versions in the range.

- is-latest: Specifies whether the entry version requirement is the latest version among the test configurations.

- is-main: Specifies whether the entry version requirement is the latest version among the test configurations without any factors applied.

- is-earliest: Specifies whether the entry version requirement is the earliest version among the test configurations.

- is-intermediary: Specifies whether the entry version requirement is neither the earliest nor the latest version among the test configurations.

- has-factors: Specifies whether the entry has any factors applied.

- is-no-factor-intermediary: Specifies whether the entry is an intermediary version without any factors applied.

Factors:

- <factors>…​: Provides additional factors or attributes associated with the test configuration as defined by the factors inputs. These usually include variant build configurations spread among the entries, such as asan, coverage, and shared libraries. For instance, if the Asan factor is applied to an entry, the entry will define the asan key with the value true and all other entries will define the asan key with the value false.

Suggestions:

- runs-on: A suggested github runner image name for the job testing this entry

- container: A suggested docker container for the job testing this entry

- cxx: The usual name of the C++ compiler executable. If using the setup-cpp action, its output should be used instead.

- cc: The usual name of the C compiler executable. If using the setup-cpp action, its output should be used instead.

- b2-toolset: The usual name of the toolset to be used in a b2 workflow.

- generator: A CMake generator recommended to run the CMake workflow.

- build-type: A build type recommended to test this entry. This is usually Release, unless some special factor that requires Debug is defined.

- ccflags: The recommended C flags to be used by this entry. It reflects the values of special factors, such as sanitizers, coverage, and time-trace.

- cxxflags: The recommended C++ flags to be used by this entry. It reflects the values of special factors, such as sanitizers, coverage, and time-trace.

- env: A dictionary of environment variables to be set for this entry.

- install: The recommended packages to be installed before running the workflow. This includes packages such as build-essential for ubuntu containers and lcov for coverage entries.