Testing Guide

Like the IREE project in general, IREE tests are divided into a few different components and use different tooling depending on the needs of that component.

Runtime Tests

Tests for the runtime C++ code use the Google Test testing framework. They should generally follow the style and best practices of that framework.

Running a Test

For the test iree/base/arena_test.cc

With CMake, run this from the build directory:

$ ctest -R iree/base/arena_test

With Bazel, run this from the repo root:

$ bazel test iree/base:arena_test

Setting test environments

Parallel testing for ctest can be enabled via the CTEST_PARALLEL_LEVEL environment variable. For example:

$ export CTEST_PARALLEL_LEVEL=$(nproc)

To use the Vulkan backend as test driver, you may need to select between a Vulkan implementation from SwiftShader and multiple Vulkan-capable hardware devices. This can be done via environment variables. See the generic Vulkan setup page for details regarding these variables.

For Bazel, you can persist the configuration in user.bazelrc to save typing. For example:

test:vkswiftshader --test_env="LD_LIBRARY_PATH=..."
test:vkswiftshader --test_env="VK_LAYER_PATH=..."
test:vknative --test_env="LD_LIBRARY_PATH=..."
test:vknative --test_env="VK_LAYER_PATH=..."

Then you can use bazel test --config=vkswiftshader to select SwiftShader as the Vulkan implementation. Similarly for other implementations.

Writing a Test

For advice on writing tests in the Googletest framework, see the Googletest primer. Test files for source file foo.cc with build target foo should live in the same directory with source file foo_test.cc and build target foo_test. You should #include iree/testing/gtest.h instead of any of the gtest or gmock headers.

As with all parts of the IREE runtime, these should not have a dependency on the compiler.

Configuring the Build System

In the Bazel BUILD file, create a cc_test target with your test file as the source and any necessary dependencies. Usually, you can link in a standard gtest main function. Use iree/testing:gtest_main instead of the gtest_main that comes with gtest.

cc_test(
    name = "arena_test",
    srcs = ["arena_test.cc"],
    deps = [
        ":arena",
        "//iree/testing:gtest_main",
    ],
)

We have created a corresponding CMake function iree_cc_test that mirrors the Bazel rule's behavior. Our Bazel to CMake converter should generally derive the CMakeLists.txt file from the BUILD file:

iree_cc_test(
  NAME
    arena_test
  SRCS
    "arena_test.cc"
  DEPS
    ::arena
    iree::testing::gtest_main
)

Compiler Tests

Tests for the IREE compilation pipeline are written as lit tests in the same style as MLIR.

By convention, IREE includes tests for printing and parsing of MLIR ops in .../IR/test/{OP_CATEGORY}_ops.mlir files, tests for folding and canonicalization in .../IR/test/{OP_CATEGORY}_folding.mlir files, and tests for compiler passes and pipelines in other .../test/*.mlir files.

Running a Test

For the test https://github.com/google/iree/tree/main/iree/compiler/Dialect/VMLA/Conversion/HLOToVMLA/test/math_ops.mlir

With CMake, run this from the build directory:

$ ctest -R iree/compiler/Dialect/VMLA/Conversion/HLOToVMLA/test/math_ops.mlir.test

With Bazel, run this from the repo root:

$ bazel test iree/compiler/Dialect/VMLA/Conversion/HLOToVMLA/test:math_ops.mlir.test

Writing a Test

For advice on writing MLIR compiler tests, see the MLIR testing guide. Tests should be .mlir files in test directory adjacent to the functionality they are testing. Instead of mlir-opt, use iree-opt, which registers IREE dialects and passes and doesn't register some unnecessary core ones. Instead of FileCheck, use IreeFileCheck, a shell-script wrapper around FileCheck that passes it a few --do-the-right-thing flags.

As with most parts of the IREE compiler, these should not have a dependency on the runtime.

Configuring the Build System

In the Bazel BUILD file, create a iree_lit_test_suite rule. We usually create a single suite that globs all .mlir files in the directory and is called “lit”.

load("//iree:lit_test.bzl", "iree_lit_test_suite")

iree_lit_test_suite(
    name = "lit",
    srcs = glob(["*.mlir"]),
    data = [
        "//iree/tools:IreeFileCheck",
        "//iree/tools:iree-opt",
    ],
)

There is a corresponding CMake function, calls to which will be generated by our Bazel to CMake Converter.

file(GLOB _GLOB_X_MLIR LIST_DIRECTORIES false RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} CONFIGURE_DEPENDS *.mlir)
iree_lit_test_suite(
  NAME
    lit
  SRCS
    "${_GLOB_X_MLIR}"
  DATA
    iree::tools::IreeFileCheck
    iree::tools::iree-opt
)

You can also create a test for a single file with iree_lit_test.

IREE Core End-to-End Tests

Here “End-to-End” means from the input accepted by the IREE core compiler (the XLA HLO MLIR dialect) to execution using the IREE runtime components. It does not include tests of the integrations with ML frameworks (e.g. TF) or bindings to other languages (e.g. Python).

To test these flows we use a custom framework called check.

Note:
    IREE end-to-end tests historically used iree-run-mlir. We are in the process of transitioning them to use the check framework, but that migration is incomplete, so some tests still use iree-run-mlir.

Running a Test

For the test https://github.com/google/iree/tree/main/iree/test/e2e/xla_ops/floor.mlir compiled for the VMLA target backend and running on the VMLA driver (here they match exactly, but in principle there's a many-to-many mapping from backends to drivers).

With CMake, run this from the build directory:

$ ctest -R iree/test/e2e/xla_ops/check_vmvx_vmvx_floor.mlir

With Bazel, run this from the repo root:

$ bazel test iree/test/e2e/xla_ops:check_vmvx_vmvx_floor.mlir

Setting test environments

Similarly, you can use environment variables to select Vulkan implementations for running tests as explained in the Runtime Tests section.

Writing a Test

These tests live in iree/test/e2e. A single test consists of a .mlir source file specifying an IREE module where each exported function takes no inputs and returns no results and corresponds to a single test case.

As an example, here are some tests for the XLA HLO floor operation:

func @tensor() {
  %input = util.unfoldable_constant dense<[0.0, 1.1, 2.5, 4.9]> : tensor<4xf32>
  %result = "mhlo.floor"(%input) : (tensor<4xf32>) -> tensor<4xf32>
  check.expect_almost_eq_const(%result, dense<[0.0, 1.0, 2.0, 4.0]> : tensor<4xf32>): tensor<4xf32>
  return
}

func @scalar() {
  %input = util.unfoldable_constant dense<101.3> : tensor<f32>
  %result = "mhlo.floor"(%input) : (tensor<f32>) -> tensor<f32>
  check.expect_almost_eq_const(%result, dense<101.0> : tensor<f32>): tensor<f32>
  return
}

func @double() {
  %input = util.unfoldable_constant dense<11.2> : tensor<f64>
  %result = "mhlo.floor"(%input) : (tensor<f64>) -> tensor<f64>
  check.expect_almost_eq_const(%result, dense<11.0> : tensor<f64>): tensor<f64>
  return
}

func @negative() {
  %input = util.unfoldable_constant dense<-1.1> : tensor<f32>
  %result = "mhlo.floor"(%input) : (tensor<f32>) -> tensor<f32>
  check.expect_almost_eq_const(%result, dense<-2.0> : tensor<f32>): tensor<f32>
  return
}

Test cases are created in gtest for each public function exported by the module.

Note the use of util.unfoldable_constant to specify test constants. If we were to use a regular constant, the compiler would “helpfully” fold away everything at compile time and our test would not actually test the runtime. unfoldable_constant hides the value of the constant from the compiler so it cannot use it at compile time. To hide an arbitrary SSA-value, you can use util.do_not_optimize. This wraps any value in an unoptimizable identity function.

Next we use this input constant to exercise the runtime feature under test (in this case, just a single floor operation). Finally, we use a check dialect operation to make an assertion about the output. There are a few different assertion operations. Here we use the expect_almost_eq_const op: almost because we are comparing floats and want to allow for floating-point imprecision, and const because we want to compare it to a constant value. This last part is just syntactic sugar around

%expected = arith.constant dense<101.0> : tensor<f32>
check.expect_almost_eq(%result, %expected) : tensor<f32>

The output of running this test looks like:

[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from module
[ RUN      ] module.tensor
[       OK ] module.tensor (76 ms)
[ RUN      ] module.scalar
[       OK ] module.scalar (79 ms)
[ RUN      ] module.double
[       OK ] module.double (55 ms)
[ RUN      ] module.negative
[       OK ] module.negative (54 ms)
[----------] 4 tests from module (264 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 1 test suite ran. (264 ms total)
[  PASSED  ] 4 tests.

The “module” name for the test suite comes from the default name for an implicit MLIR module. To give the test suite a more descriptive name, use an explicit named top-level module in this file.

Dynamic Shapes

Constants with dynamic shape are not yet supported. See https://github.com/google/iree/issues/1601. For now, these tests have to use iree-run-mlir lit tests and input arguments.

Configuring the Build System

A single .mlir source file can be turned into a test target with the iree_check_test Bazel macro (and corresponding CMake function).

load("//build_tools/bazel:iree_check_test.bzl", "iree_check_test")

iree_check_test(
    name = "check_vmvx_vmvx_floor.mlir",
    src = "floor.mlir",
    driver = "vmvx",
    target_backend = "vmvx",
)

The target naming convention is “check_backend_driver_src”. The generated test will automatically be tagged with a “driver=vmvx” tag, which can help filter tests by backend (especially when many tests are generated, as below).

Usually we want to create a suite of tests across many backends and drivers. This can be accomplished with additional macros. For a single backend/driver pair:

load("//build_tools/bazel:iree_check_test.bzl", "iree_check_single_backend_test_suite")

iree_check_single_backend_test_suite(
    name = "check_vmvx_vmvx",
    srcs = glob(["*.mlir"]),
    driver = "vmvx",
    target_backend = "vmvx",
)

This will generate a separate test target for each file in srcs with a name following the convention above as well as a Bazel test_suite called “check_vmvx_vmvx” that will run all the generated tests.

You can also generate suites across multiple pairs:

load("//build_tools/bazel:iree_check_test.bzl", "iree_check_test_suite")

iree_check_test_suite(
    name = "check",
    srcs = ["success.mlir"],
    # Leave this argument off to run on all supported backend/driver pairs.
    target_backends_and_drivers = [
        ("vmvx", "vmvx"),
        ("vulkan-spirv", "vulkan"),
    ],
)

This will create a test per source file and backend/driver pair, a test suite per backend/driver pair, and a test suite, “check”, that will run all the tests.

The CMake functions follow a similar pattern. The calls to them are generated in our CMakeLists.txt file by bazel_to_cmake.

Binding Tests

TODO(laurenzo): Explain binding test setup.

Integration Tests

TODO(silvasean): Explain integration test setup.