Test Plans
The Test Plan is the top-level entry point for RTF. It defines variables, matrix dimensions, and references to Scenario and Environment configurations.
An example of a valid
test-plan.yamlis provided in the Full example section below.
Top level keys
name: The name for this Test Plan.- Uniqueness is not enforced by the
rtfCLI but test plans should have unique names that can be used to distinguish them.
- Uniqueness is not enforced by the
description: A brief, human readable description of the behaviour of the Test Plan.- If there are any pre-requisites to running this Test Plan it is best to call them out here rather than in comments or other files (such as a README).
variables: Key value pairs for templating the Test Plan where the variables are all scalar.- Scalar here is defined to be a number, string or boolean.
matrix: Dimensions specified as key value pairs for templating the Test Plan where the variables are arrays of scalars.- Each matrix entry must have a consistent type for the variables array. Mixing different scalar variables will result in an error when the Test Plan is run.
- An optional
variant_nameskey can be provided to customise the names of the output directories used by each variant.
custom_providers: Declarations for loading Custom Provider Definitions.- For full details on the structure of Custom Provider Declarations and Definitions see the Custom Providers page of the Framework documentation.
scenario: A Config Spec for the scenario to be run.- For full details on the structure of a Scenario see the Scenario page of the Framework documentation.
environment: A Config Spec for the test environment to provision.- For full details on the structure of an Environment see the Environment page of the Framework documentation.
Config Specs
Both the scenario and environment keys map to a structure known as a Config Spec, which tells
RTF how to find the appropriate configuration for that aspect of the Test Plan. Config Specs support
three source types:
inline: Embed configuration directly in the Test Planlocal: Reference a local file by relative pathgithub: Fetch from a GitHub repository
The local and github types support optional overrides that merge on top of the base
configuration before the Test Plan is templated and checked.
Inline configuration
To provide configuration inline, add an inline key under the relevant top level scenario or
environment key and provide the config file contents underneath.
scenario:
inline:
# your scenario.yaml configuration goes here
environment:
inline:
# your environment.yaml configuration goes here
From a local file
To use a local file as a base, add a from key under the relevant top level scenario or
environment key, specifying the kind as local and the relative path from the test-plan.yaml
file under the relative_path key.
To define overrides, add the overrides key at the same indentation level as from and then add
your override configuration under that key. The structure here is not required to parse as a full
config file so you are free to only specify the keys that you need.
For details on how overrides are applied, see Applying Overrides below.
scenario:
from:
kind: local
relative_path: ../my-scenario.yaml
overrides:
# overrides go here
environment:
from:
kind: local
relative_path: ../my-environment.yaml
overrides:
# overrides go here
From a remote file in GitHub
To fetch from a GitHub repository, add a from key under the relevant top level scenario or
environment key, specifying the kind as github along with details for the org, repo and
path to the file relative to the root of the repository. It is also possible to optionally provide
a git_ref to pull the file from. If this is not specified then RTF will default to pulling from
the mainline branch.
You must have a valid GitHub access token with permissions to interact with your chosen repository exported as
GITHUB_TOKENin your shell environment for this method to work. See here for GitHub’s documentation on how to create and manage access tokens.
As with using a local file, overrides can be defined by adding the
overrides key at the same indentation level as from and then adding your override configuration
under that key. The structure here is not required to parse as a full config file so you are free
to only specify the keys that you need.
For details on how overrides are applied, see Applying Overrides below.
scenario:
from:
kind: github
org: my-org
repo: my-repo
# git_ref: testing-branch
path: test-plans/example/my-scenario.yaml
overrides:
# overrides go here
environment:
from:
kind: github
org: my-org
repo: my-repo
# git_ref: testing-branch
path: test-plans/example/my-environment.yaml
overrides:
# overrides go here
Applying overrides
The overrides section of a Config Spec is merged with the configuration file provided under the
from key as raw YAML before the configuration is parsed by RTF.
The merging strategy used is as follows:
- For maps, keys from overrides merge on top of matching keys in the base. If a key exists in both, RTF recursively merges the values; otherwise, RTF inserts the override key directly.
- For arrays, override values are appended to base values.
- For differing types or scalars, RTF replaces the base value with the override.
Once the overrides have been applied and the resulting config file is successfully parsed, all arrays are then sorted and deduplicated based on an appropriate key in order to support replacing array elements:
- For File providers the key used is
env_var. - For variable declarations the key used is
name.
A note on relative paths
There are several places within RTF config files that you be required to specify relative paths to other files on disk. In order to help with reasoning about how to provide these paths, RTF has strict semantics about how such relative paths are resolved.
Namely, they are always resolved relative to the file the path is written in. At first glance this
may sound obvious but the important aspect to remember is around writing overrides in your
test-plan.yaml.
When a base config file is loaded by RTF, all pre-existing relative paths will be resolved relative
to the path that the config file was loaded from (this is also true when pulling remote files from
GitHub). When overrides are then applied from the Test Plan, any new relative paths found there
are resolved relative to the location of the test-plan.yaml file, not the location of the base
config file.
Working with matrices
The matrix key expands to multiple test plan variants via the cartesian product of its
dimensions.
For example, the following matrix:
matrix:
dimensions:
a: ["foo", "bar"]
b: [1, 2, 3]
Expands to six test plans (known as “variants”) covering each of the possible combinations of values
for a and b.
Each variant is run individually and writes its output to its own subdirectory, named
matrix_variant_$n by default. The order in which variants are run is deterministic: dimensions are
ordered alphanumerically and the cartesian product is formed from the user provided ordering of
values for each dimension (as shown below).
- a=foo b=1 (matrix_variant_1)
- a=foo b=2 (matrix_variant_2)
- a=foo b=3 (matrix_variant_3)
- a=bar b=1 (matrix_variant_4)
- a=bar b=2 (matrix_variant_5)
- a=bar b=3 (matrix_variant_6)
Naming variants
To customize output subdirectory names, specify the matrix.variant_names key in your test plan
with a template string for generating the variant names:
matrix:
variant_names: "${a}_${b}"
dimensions:
a: ["foo", "bar"]
b: [1, 2, 3]
When doing so, the ordering for variants remains the same but the output directory names are generated using the template provided:
- a=foo b=1 (foo_1)
- a=foo b=2 (foo_2)
- a=foo b=3 (foo_3)
- a=bar b=1 (bar_1)
- a=bar b=2 (bar_2)
- a=bar b=3 (bar_3)
The syntax used for template strings involves placing matrix dimension names inside of ${} along
with static string content in order to generate a unique name for each variant. The resulting string
is then slugified to remove whitespace and slashes.
Combining related variables
The following initial matrix expands out to four variants covering different crate revisions for inclusion in a Rust build as shown below:
matrix:
dimensions:
federation_rev: [ "v2.6.2", "v2.7.0" ]
compiler_rev: [ "apollo-compiler@1.28.0", "apollo-compiler@1.30.0" ]
# Produces the following variants:
# - federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.30.0
#
# - federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.28.0
#
# - federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
If each value of federation_rev only has a single matching compiler_rev, two of the resulting
four variants are invalid. Adding further matrix dimensions makes the problem worse, by adding more
undesirable variants.
To fix this, use matrix.compound to define a named compound dimension that groups several
variables together. Each entry in the compound dimension must contain the same set of variables, and
only those explicit groups of values will be used to construct the resulting matrix variants:
matrix:
# The dimensions key must always be present, even if it is an empty map
dimensions: {}
compound:
crate_revisions:
- federation_rev: v2.6.2
compiler_rev: apollo-compiler@1.28.0
- federation_rev: v2.7.0
compiler_rev: apollo-compiler@1.30.0
# Produces the following variants:
# - federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
Now only the valid revision pairs are produced. The group name (crate_revisions here) exists only
to identify the group within the compound map and allow for runtime overriding of the dimension;
it is not templated into the test plan itself. You can add further dimensions to the matrix as
normal while preserving the correct compound combinations:
matrix:
dimensions:
graph_ref: [ "graph_1@prod", "graph_2@dev" ]
compound:
crate_revisions:
- federation_rev: v2.6.2
compiler_rev: apollo-compiler@1.28.0
- federation_rev: v2.7.0
compiler_rev: apollo-compiler@1.30.0
# Produces the following variants:
# - graph_ref: "graph_1@prod"
# federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - graph_ref: "graph_1@prod"
# federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
#
# - graph_ref: "graph_2@dev"
# federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - graph_ref: "graph_2@dev"
# federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
When working with older test plans you may encounter the
matrix.includekey, which is a deprecated alias for a single compound group namedinclude.matrix.include: [...]behaves exactly likematrix.compound: { include: [...] }and is still supported for backwards compatibility purposes, but if you see it in a test plan you’re working with, you should migrate it to usematrix.compoundinstead.
Combining multiple compound dimensions
Compound dimensions interact with one another in the way you would expect: with each compound dimension contributing blocks of values to the expanded set of variants rather than individual ones (as with a normal matrix dimension). If in the above example we found that we needed to work with the graph name and variant as individual variables, we could express that using a second compound dimension like so:
matrix:
dimensions: {}
compound:
graph_ref:
- graph_name: graph_1
variant: prod
- graph_name: graph_2
variant: dev
crate_revisions:
- federation_rev: v2.6.2
compiler_rev: apollo-compiler@1.28.0
- federation_rev: v2.7.0
compiler_rev: apollo-compiler@1.30.0
# Produces the following variants:
# - graph_name: "graph_1"
# variant: "prod"
# federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - graph_name: "graph_1"
# variant: "prod"
# federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
#
# - graph_name: "graph_2"
# variant: "dev"
# federation_rev: v2.6.2
# compiler_rev: apollo-compiler@1.28.0
#
# - graph_name: "graph_2"
# variant: "dev"
# federation_rev: v2.7.0
# compiler_rev: apollo-compiler@1.30.0
Producing the same number of variants as before, but now with the ability to reference graph name and variant directly.
Full example
The following is a minimal “kitchen sink” example of the structure of a valid test-plan.yaml.
name: example
description: An example description
variables:
foo: "A value for foo"
matrix:
variant_names: "${bar}_${baz}_${a}_${b}"
dimensions:
bar: [1, 2, 3]
baz: [true, false]
compound:
extra:
- a: 4
b: 5
- a: 6
b: 7
custom_providers:
- kind: local
relative_path: ./providers
using:
my_provider: my_provider.yaml
scenario:
inline:
name: An inline scenario
description: A description for the inline scenario
variable_definitions:
- name: foo
description: "A description for foo"
command:
name: my-test.sh
kind: relative_path
path: scripts/my-test.sh
env_vars:
FOO: "{{ foo }}"
environment:
from:
kind: local
relative_path: environment.yaml
overrides:
file_providers:
- name: my-additional-file.txt
env_var: ADDITIONAL_FILE
kind: inline
content: |
An additional file that wasn't present in the original environment.yaml