| # Testing the NDK |
| |
| The latest version of this document is available at |
| https://android.googlesource.com/platform/ndk/+/master/docs/Testing.md. |
| |
| The NDK tests are built as part of a normal build (with `checkbuild.py`) and run |
| with `run_tests.py`. See [Building.md] for more instructions on building the |
| NDK. |
| |
| [Building.md]: Building.md |
| |
| ## Prerequisites |
| |
| 1. `adb` must be in your `PATH`. |
| 2. You must have compatible devices connected. See the "Devices and Emulators" |
| section. |
| |
| ## tl;dr |
| |
| If you don't care how this works (if you want to know how this works, sorry, but |
| you're going to have to read the whole thing) and just want to copy paste |
| something that will build and run all the tests: |
| |
| ```bash |
| # In the //ndk directory of an NDK `repo init` tree. |
| $ poetry shell |
| $ ./checkbuild.py # Build the NDK and tests. |
| $ ./run_tests.py # Pushes the tests to test devices and runs them. |
| ``` |
| |
| **Pay attention to the warnings.** Running tests requires that the correct set |
| of devices are available to adb. If the right devices are not available, **your |
| tests will not run**. |
| |
| ### Typical test cycle for fixing a bug |
| |
| This section describes the typical way to test and fix a bug in the NDK. |
| |
| ```bash |
| # All done from //ndk, starting from a clean tree. |
| # 1. Update your tree. |
| $ repo sync |
| # 2. Create a branch for development. |
| $ repo start $BRANCH_NAME_FOR_BUG_FIX . |
| # 3. Make sure your python dependencies are up to date. |
| $ poetry install |
| # 4. Enter the poetry environment. You can alternatively prefix all python |
| # commands below with `poetry run`. |
| $ poetry shell |
| # 5. Build the NDK and tests. |
| $ ./checkbuild.py |
| # 6. Run the tests to make sure everything is passing before you start changing |
| # things. |
| $ ./run_tests.py |
| # 7. Write the regression test for the bug. The new rest of the instructions |
| # will assume your new test is called "new_test". |
| # 8. Build and run the new test to make sure it catches the bug. The new test |
| # should fail. If it doesn't, either your test is wrong or the bug doesn't |
| # exist. |
| # |
| # We use --rebuild here because run_tests.py does not build tests by default, |
| # since that's usually a waste of time (see below). We use --filter to ignore |
| # everything except our new test. |
| $ ./run_tests.py --rebuild --filter new_test |
| # 9. Attempt to fix the bug. |
| # 10. Rebuild the affected NDK component. If you don't know which component you |
| # altered, it's best to just build the whole NDK again |
| # (`./checkbuild.py --no-build-tests)`. One case where you can avoid a full |
| # rebuild is if the fix is contained to just ndk-build or CMake. We'll assume |
| # that's the case here. |
| $ ./checkbuild.py --no-build-tests ndk-build |
| # 11. Re-build and run the test with the supposedly fixed NDK. |
| $ ./run_tests.py --rebuild --filter new_test |
| # If the test fails, return to step 9. Otherwise, continue. |
| # 12. Rebuild and run *all* the tests to check that your fix didn't break |
| # something else. If you only rebuilt a portion of the NDK in step 10, it's best |
| # to do a full `./checkbuild.py` here as well (either use `--no-build-tests` or |
| # omit `--rebuild` for `run_tests.py` to avoid rebuilding all the tests |
| # *twice*). |
| $ ./run_tests.py --rebuild |
| # If other tests fail, return to step 9. Otherwise, continue. |
| # 13. Commit and upload changes. Don't forget to `git add` the new test! |
| ``` |
| |
| ## Types of tests |
| |
| The NDK has a few different types of tests. Each type of test belongs to its own |
| "suite", and these suites are defined by the directories in `//ndk/tests`. |
| |
| ### Build tests |
| |
| Build tests are the tests in [//ndk/tests/build]. These exercise the build |
| systems and compilers in ways where it is not important to run the output of the |
| build; all that is required for the test to pass is for the build to succeed. |
| |
| For example, [//ndk/tests/build/cmake-find_library] verifies that CMake's |
| `find_library` is able to find libraries in the Android sysroot. If the test |
| builds, the feature under test works. We could also run the executable it builds |
| on the connected devices, but it wouldn't tell us anything interesting about |
| that feature, so we skip that step to save time. |
| |
| #### Test subtypes |
| |
| Because the test outputs of build tests do not need to be run, build tests have |
| a few subtypes that can test more flexibly than other test types. These are |
| `test.py` and `build.sh` tests. |
| |
| One test directory can be used as more than one type of test. This is quite |
| common when a behavior should be tested in both CMake and ndk-build. |
| |
| The test types in a directory are determined as follows (in order of |
| precedence): |
| |
| 1. If there is a `build.sh` file in the directory, it is a `build.sh` test. No |
| other test types will be considered. |
| 2. If there is a `test.py` file in the directory, it is a `test.py` test. No |
| other test types will be considered. |
| 3. If there are files matching `jni/*.mk` in the directory, it is an ndk-build |
| test. These tests may co-exist with CMake tests. |
| 4. If there is a `CMakeLists.txt file in the directory, it is a CMake test. |
| These tests may co-exist with ndk-build tests. |
| |
| [//ndk/tests/build]: ../tests/build |
| [//ndk/tests/build/cmake-find_library]: ../tests/build/cmake-find_library |
| |
| ##### ndk-build |
| |
| An ndk-build test will treat the directory as an ndk-build project. |
| `ndk-build` will build the project for each configuration. |
| |
| ##### CMake |
| |
| A CMake test will treat the directory as a CMake project. CMake will configure |
| and build the project for each configuration. |
| |
| ##### test.py |
| |
| A `test.py` build test allows the test to customize its execution and results. |
| It does this by delegating those details to the `test.py` script in the test |
| directory. Any (direct) subdirectory of `//ndk/tests/build` that contains a |
| `test.py` file will be executed as this type of test. |
| |
| **These types of tests are rarely needed.** Unless you need to inspect the |
| output of the build, need to build in a very non-standard way, or need to test |
| a behavior outside CMake or ndk-build, you probably do not want this type of |
| test. |
| |
| For example, [//ndk/tests/build/NDK_ANALYZE] builds an ndk-build project that |
| emits clang static analyzer warnings that the test then checks for. |
| |
| For some commonly reused `test.py` patterns, there are helpers in [ndk.testing] |
| that will simplify writing these forms of tests. Verifying that the build system |
| passes a specific flag to the compiler when building is a common pattern, such |
| as in [//ndk/tests/build/branch-protection]. |
| |
| [//ndk/tests/build/NDK_ANALYZE]: ../tests/build/NDK_ANALYZE |
| [ndk.testing]: ../ndk/testing |
| [//ndk/tests/build/branch-protection]: ../tests/build/branch-protection |
| |
| ##### build.sh |
| |
| A `build.sh` test is similar to a `test.py` test, but with a worse feature set |
| in a worse language, and also can't be tested on Windows. Do not write new |
| `build.sh` tests. If you need to modify an existing `build.sh` test, consider |
| migrating it to `test.py` first. |
| |
| #### Negative build tests |
| |
| Most build tests cannot easily check negative test cases, since they typically |
| are only verified by the exit status of the build process (`build.sh` and |
| `test.py` tests can of course do better). To make a negative test for an |
| ndk-build or CMake build test, use the `is_negative_test` `test_config.py` |
| option: |
| |
| ```python |
| def is_negative_test() -> bool: |
| return True |
| ``` |
| |
| #### Passing additional command line arguments to build systems |
| |
| For tests that need to pass specific command line arguments to the build system, |
| use the `extra_cmake_flags` and `extra_ndk_build_flags` `test_config.py` |
| options: |
| |
| ```python |
| def extra_cmake_flags() -> list[str]: |
| return ["-DANDROID_STL=system"] |
| |
| |
| def extra_ndk_build_flags() -> list[str]: |
| return ["NDK_GRADLE_INJECTED_IMPORT_PATH=foo"] |
| ``` |
| |
| ### Device tests |
| |
| Device tests are the tests in [//ndk/tests/device]. Device tests inherit most of |
| their behavior from build tests. It differs from build tests in that the |
| executables that are in the build output will be run on compatible attached |
| devices (see "Devices and Emulators" further down the page). |
| |
| These test will be built in the same way as build tests are, although `build.sh` |
| and `test.py` tests are not valid for device tests. Each executable in the |
| output directory of the build will be treated as a single test case. The |
| executables and shared libraries in the output directory will all be pushed to |
| compatible devices and run. |
| |
| [//ndk/tests/build]: ../tests/device |
| |
| ### libc++ tests |
| |
| libc++ tests are the tests in [//ndk/tests/libc++]. These are a special case of |
| device test that are built by LIT (LLVM's test runner) rather than ndk-build or |
| CMake, and the test sources are in the libc++ source tree. |
| |
| As with device tests, executables and shared libraries in the output directory |
| will be pushed to the device to be run. The directory structure differs from our |
| device tests though because some libc++ tests are sensitive to that. Some tests |
| also contain test data that will be pushed alongside the binaries. |
| |
| You will never write one of these tests in the NDK. If you need to add a test to |
| libc++, do it in the upstream LLVM repository. You probably do not need to |
| continue reading this section unless you are debugging libc++ test failures or |
| test runner behavior. |
| |
| There is only one "test" in the libc++ test directory. This is not a real test, |
| it is just a convenience for the test scanner. The test builder will invoke LIT |
| on the libc++ test directory, which will build all the libc++ tests to the test |
| output directory. This will emit an xunit report that the test builder parses |
| and converts into new "tests" that do nothing but report the result from xunit. |
| This is a hack that makes the test results more readable. |
| |
| [//ndk/tests/build]: ../tests/libc++ |
| |
| ### ndk-stack |
| |
| These are typic Python tests that use the `unittest` library to exercise |
| ndk-stack. Unlike all the other tests in the NDK, these are not checked by |
| `checkbuild.py` or `run_tests.py`. To run these tests, run: |
| |
| ```bash |
| poetry run pytest tests/ndk-stack/*.py |
| ``` |
| |
| ## Controlling test build and execution |
| |
| ### Re-building tests |
| |
| **The tests will not be rebuilt unless you use `--rebuild`.** `run_tests.py` |
| will not _build_ tests unless it is specifically requested because doing so is |
| expensive. If you've changed something and need to rebuild the test, use |
| `--rebuild` as well as `--filter`. |
| |
| ### Running a subset of tests |
| |
| To re-check a single test during development, use the `--filter` option of |
| `run_tests.py`. For example, `poetry run ./run_tests.py --filter math` will re- |
| run the math tests. |
| |
| To run more than one test, the `--filter` argument does support shell-like |
| globbing. `--filter "emutls-*"` will re-run the tests that match the pattern |
| `emultls-*`, for example. |
| |
| Keep in mind that `run_tests.py` will not rebuild tests by default. If you're |
| iterating on a single test, you probably need the `--rebuild` flag described |
| above to rebuild the test after any changes. |
| |
| ### Restricting test configurations |
| |
| By default, every variant of the test will be run (and, if using `--rebuild`, |
| built). Some test matrix dimensions can be limited to speed up debug iteration. |
| If you only need to debug 64-bit Arm, for example, pass `--abi arm64-v8a` to |
| `run_tests.py`. |
| |
| The easiest way to prevent tests from running on API levels you don't want to |
| re-check is to just unplug those devices. Alternatively, you can modify |
| `qa_config.json` to remove those API levels. |
| |
| Other test matrix dimensions (such as build system or CMake toolchain file |
| variant) cannot currently be filtered. |
| |
| ### Showing all test results |
| |
| By default `run_tests.py` will only show failing tests. Failing means either |
| tests that are expected to pass but failed, or were expected to fail but passed. |
| Tests that pass, were skipped due to an invalid configuration, or failed but |
| have been marked as a known failure will not be shown unless the `--show-all` |
| flag is used. This is helpful for checking that your test really did run rather |
| than being skipped, or to verify that your `test_config.py` is correctly |
| identifying a known failure. |
| |
| ## Testing Releases |
| |
| When testing a release candidate, your first choice should be to run the test |
| artifacts built on the build server for the given build. This is the |
| ndk-tests.tar.bz2 artifact in the same directory as the NDK zip. Extract the |
| tests somewhere, and then run: |
| |
| ```bash |
| $ ./run_tests.py --clean-device path/to/extracted/tests |
| ``` |
| |
| `--clean-device` is necessary to ensure that the new tests do get pushed to the |
| device even if the timestamps on the tests are older than what's currently |
| there. If you need to re-run those tests (say, to debug a failing test), you |
| will want to omit `--clean-device` for each subsequent run of the same test |
| package or each test run will take a very long time. |
| |
| The ndk-tests.tar.bz2 artifact will exist for each of the "linux", "darwin_mac", |
| and "win64_tests" targets. All of them must be downloaded and run. Running only |
| the tests from the linux build will not verify that the windows or darwin NDKs |
| produces usable binaries. |
| |
| ## Broken and Unsupported Tests |
| |
| To mark tests as currently broken or as unsupported for a given configuration, |
| add a `test_config.py` to the test's root directory (in the same directory as |
| `jni/`). |
| |
| Unsupported tests will not be built or run. They will show as "SKIPPED" if you |
| use `--show-all`. Tests should be marked unsupported for configurations that do |
| not work **when failure is not a bug**. For example, yasm is an x86 only |
| assembler, so the yasm tests are unsupported for non-x86 ABIs. |
| |
| Broken tests will be built and run, and the result of the test will be inverted. |
| A test that fails will become an "EXPECTED FAILURE" and not be counted as a |
| failure, whereas a passing test will become an "UNEXPECTED SUCCESS" and count as |
| a failure. Tests should be marked broken **when they are known to fail and that |
| failure is a bug to be fixed**. For example, at the time of writing, ASan |
| doesn't work on API 21. It's supposed to, so this is a known bug. |
| |
| By default, `run_tests.py` will hide expected failures from the output since the |
| caller is most likely only interested in seeing what effect their change had. To |
| see the list of expected failures, pass `--show-all`. |
| |
| "Broken" and "unsupported" come in both "build" and "run" variants. This allows |
| better fidelity for describing a test that is known to fail at runtime, but |
| should build correctly. Such a test would use `run_broken` rather than |
| `build_broken`. |
| |
| Here's an example `test_config.py` that marks the tests in the same directory as |
| broken when building for arm64 and unsupported when running on a pre-Lollipop |
| device: |
| |
| ```python |
| from typing import Optional |
| |
| from ndk.test.devices import Device |
| from ndk.test.types import Test |
| |
| |
| def build_broken(test: Test) -> tuple[Optional[str], Optional[str]]: |
| if test.abi == 'arm64-v8a': |
| return test.abi, 'https://github.com/android-ndk/ndk/issues/foo' |
| return None, None |
| |
| |
| def run_unsupported(test: Test, device: Device) -> Optional[str]: |
| if device.version < 21: |
| return f'{device.version}' |
| return None |
| ``` |
| |
| The `*_broken` checks return a tuple of `(broken_configuration, bug_url)` if the |
| given configuration is known to be broken, else `(None, None)`. All known |
| failures must have a (public!) bug filed. If there is no bug tracking the |
| failure yet, file one on GitHub. |
| |
| The `*_unsupported` checks return `broken_configuration` if the given |
| configuration is unsupported, else `None`. |
| |
| The configuration is available in the `Test` and `Device` objects which are |
| arguments to each function. Check the definition of each class to find which |
| properties can be used, but the most commonly used are: |
| |
| * `test.abi`: The ABI being built for. |
| * `test.api`: The platform version being *built* for. Not necessarily the |
| platform version that the test will be run on. |
| * `device.version`: The API level of the device the test will be run on. |
| * `test.name`: The full name of the test, as would be reported by the test |
| runner. For example, the `fuzz_test` executable built by `tests/device/fuzzer` |
| is named `fuzzer.fuzz_test`. Build tests should never need to use this |
| property, as there is only one test per directory. libc++ tests will most |
| likely prefer `test.case_name` (see below). |
| * `test.case_name`: The shortened name of the test case. This property only |
| exists for device tests (for `run_unsupported` and `run_broken`). This |
| property will not exactly match the name of the executable. If the executable |
| is named `foo.pass.cpp.exe`, but `test.case_name` will be `foo.pass`. |
| |
| ## Devices and Emulators |
| |
| For testing a release, make sure you're testing against the released user builds |
| of Android. |
| |
| For Nexus/Pixel devices, use https://source.android.com/docs/setup/build/flash |
| (Googlers, use http://go/flash). Factory images are also available here: |
| https://developers.google.com/android/nexus/images. |
| |
| For emulators, use emulator images from the SDK rather than from a platform |
| build, as these are what our users will be using. Note that some NDK tests |
| (namely test-googletest-full and asan-smoke) are known to break between emulator |
| updates. It is not known whether these are NDK bugs, emulator bugs, or x86_64 |
| system image bugs. Just be aware of them, and update the test config if needed. |
| |
| After installing the emulator images from the SDK manager, they can be |
| configured and launched for testing with (assuming the SDK tools directory is in |
| your path): |
| |
| ```bash |
| $ android create avd --name $NAME --target android-$LEVEL --abi $ABI |
| $ emulator -avd $NAME |
| ``` |
| |
| This will create and launch a new virtual device. |
| |
| Whether physical devices or emulators will be more useful depends on your host |
| OS. |
| |
| For an x86_64 host, physical devices for the Arm ABIs will be much faster than |
| emulation. x86/x86_64 emulators will be virtualized on x86_64 hosts, which are |
| very fast. |
| |
| For M1 Macs, it is very difficult to test x86/x86_64, as devices with those ABIs |
| are very rare, and the emulators for M1 Macs are also Arm. For this reason, it's |
| easiest to use an x86_64 host for testing x86/x86_64 device behavior. |
| |
| ### Device selection |
| |
| `run_tests.py` will only consider devices that match the configurations |
| specified by `qa_config.json` when running tests. We do not test against every |
| supported version of the OS (as much as I'd like to, my desk isn't big enough |
| for that many phones), but only the subset specified in that file. |
| |
| Any connected devices that do not match the configurations specified by |
| `qa_config.json` will be ignored. Devices that match the tested configs will be |
| pooled to allow sharding. |
| |
| Each test will be run on every device that it is compatible with. For example, |
| a test that was built for armeabi-v7a with a minSdkVersion of 21 will run on all |
| device pools that support that ABI with an OS API level of 21 or newer (unless |
| otherwise disabled by `run_unsupported`). |
| |
| **Read the warnings printed at the top of `run_tests.py` output to figure out |
| what device configurations your test pools are missing.** If any warnings are |
| printed, the configuration named in the warning **will not be tested**. This is |
| a warning rather than an error because it is very common to not have all |
| configurations available (as mentioned above, it's not viable for M1 Macs to |
| check x86 or x86_64). If you cannot test every configuration, be aware of what |
| configurations your changes are likely to break and make sure those are at least |
| tested. When testing a release, make sure that all configurations have been |
| tested before shipping. |
| |
| `qa_config.json` has the following format: |
| |
| ```json |
| { |
| "devices": { |
| "21": [ |
| "armeabi-v7a", |
| "arm64-v8a" |
| ], |
| "32": [ |
| "armeabi-v7a", |
| "arm64-v8a", |
| "x86_64" |
| ] |
| } |
| } |
| ``` |
| |
| The `devices` section specifies which types of devices should be used for |
| running tests. Each key defines the OS API level that should be tested, and the |
| value is a list of ABIs that should be checked for that OS version. In the |
| example above, tests will be run on each of the following device configurations: |
| |
| * API 21 armeabi-v7a |
| * API 21 arm64-v8a |
| * API 32 armeabi-v7a |
| * API 32 arm64-v8a |
| * API 32 x86_64 |
| |
| The format also supports the infrequently used `abis` and `suites` keys. **You |
| probably do not need to read this paragraph.** Each has a list of strings as the |
| value. Both can be used to restrict the build configurations of the tests. |
| `abis` selects which ABIs to build. This property will be overridden by `--abis` |
| if that argument is used, and will default to all ABIs if neither are present, |
| which is the normal case. `suites` selects which test suites to build. Valid |
| entries in this list are the directory names within `tests`, with the exception |
| of `ndk-stack`. In other words (at the time of writing), `build`, `device`, and |
| `libc++` are valid items. |
| |
| ## Windows VMs |
| |
| Warning: the process below hasn't been tested in a very long time. Googlers |
| should refer to http://go/ndk-windows-vm for slightly more up-to-date Google- |
| specific setup instructions, but http://go/windows-cloudtop may be easier. |
| |
| Windows testing can be done on Windows VMs in Google Compute Engine. To create |
| one: |
| |
| * Install the [Google Cloud SDK](https://cloud.google.com/sdk/). |
| * Run `scripts/create_windows_instance.py $PROJECT_NAME $INSTANCE_NAME` |
| * The project name is the name of the project you configured for the VMs. |
| * The instance name is whatever name you want to use for the VM. |
| |
| This process will create a `secrets.py` file in the NDK project directory that |
| contains the connection information. |
| |
| The VM will have Chrome and Git installed and WinRM will be configured for |
| remote command line access. |
| |
| TODO: Implement `run_tests.py --remote-build`. |