| # Testing | |
| > [!IMPORTANT] | |
| > The debug info test suite is undergoing a substantial rewrite. This section will be filled out as | |
| > the rewrite makes progress. | |
| > | |
| > Please see [this tracking issue][148483] for more information. | |
| [148483]: https://github.com/rust-lang/rust/issues/148483 | |
| Debug info tests check a few important things: | |
| * Are we outputting the information in the way we expect? | |
| * Is what we output readable by the debugger? | |
| * Do our visualizers work the way we expect? | |
| The first question is typically answered by `tests/codegen-llvm`, but debug info generation is often | |
| tested incidentally, rather than deliberately. | |
| As of <!-- date-check --> Jul 2026, there is a much larger focus on the latter two questions, | |
| and those will be covered in detail here. | |
| The tests that answer those questions live in `tests/debuginfo`, which is executed by `compiletest`. | |
| For much of the test suite's lifespan, debuggers were discovered automatically, and tests were | |
| tests were comprised of `$DEBUGGER-command` and `$DEBUGGER-check` directives (i.e. raw string | |
| comparisons) that checked variable printing, breakpoint locations, etc. Put bluntly, this system was | |
| a nightmare and lead to a [litany of issues](https://github.com/rust-lang/rust/issues/134682). | |
| To help remedy this: | |
| 1. [`tests/debuginfo` is now opt-in](https://github.com/rust-lang/rust/pull/159455) for GDB and LLDB. | |
| 2. a new directive was added: `$DEBUGGER-repr`. | |
| This directive dispatches to custom logic that polls | |
| the debugger for additional information that isn't visible in the printed output. | |
| It also automatically separates output by target, | |
| allowing the tests to be run on different platforms without conflicts. | |
| # The `repr` directive | |
| > [!IMPORTANT] | |
| > As of July 2026, this command is only supported by LLDB. GDB support is planned, but | |
| > has not been implemented. It is unclear whether this directive will ever be suited for use with | |
| > CDB. | |
| In short, `$DEBUGGER-repr` commands are desugared to: | |
| ``` | |
| //@ $DEBUGGER-command:repr $VAR_NAME | |
| //@ $DEBUGGER-check:$VAR_NAME ok | |
| ``` | |
| When the commands are passed to the debugger, our test framework intercepts `repr` pseudo-commands | |
| and runs special logic on them, testing against data stored in | |
| `tests/debuginfo/<test_name>/input/<debugger>_input/<target_group>.json`. | |
| "Target groups" cover the set of targets where we cannot guarantee identical output. | |
| Those targets are defined by the | |
| [`Target` enum in `common.py`](https://github.com/rust-lang/rust/blob/bf9944f0b8006b152ef4d5f408ae75a0dde3d044/src/etc/lldb_batchmode/common.py#L54). | |
| As of <!-- date-check --> Jul 2026, this list includes `non_windows`, `windows_gnu`, and `windows_msvc`. | |
| It is intentionally kept as short as possible, | |
| since each target is a new set of test data that must be updated when changes are made. | |
| There is still not a perfect solution for how tests can be | |
| `--bless`-ed by contributors who do not have access to all of the targets. | |
| The input data can be automatically updated for expected changes by adding `--bless` to the test | |
| invocation (e.g. `./x test tests/debuginfo/basic-types/main.rs --bless`). | |
| `--bless` updates the in-memory representation, tests against it, | |
| and if no errors occur, saves the data back to the target file (or creates a new file if necessary). | |
| The schema of the input data is defined by the classes in | |
| [`common.py`](https://github.com/rust-lang/rust/blob/be3d26db984c6f96335faca1f254dc04873cb1c1/src/etc/lldb_batchmode/common.py). | |
| The top-level container is `TargetData`. | |
| This schema is identical for all debuggers. | |
| ## Converting existing tests | |
| Nearly any time a variable is tested, the `repr` directive should be preferred over `command` + | |
| `check`. | |
| As of <!-- date-check --> Jul 2026, only a single test has been converted over, but more will follow as | |
| part of the test rewrite mentioned above. | |
| Thankfully, the conversion process is fairly easy. | |
| For a given check: | |
| ``` | |
| //@ lldb-command:v foo | |
| //@ lldb-check:<foo output> | |
| ``` | |
| The equivalent `repr` test is: | |
| ``` | |
| //@ lldb-repr:foo | |
| ``` | |
| Once all `command` + `checks` are converted to `repr`, run the tests with the `--bless` option. | |
| If you have access to additional targets, `--bless` the data for the remainder of the target groups | |
| as well (e.g. if you are on a Windows machine, bless once for `x86_64-pc-windows-msvc`, once for | |
| `x86_64-pc-windows-gnu`, and use WSL to bless for `x86_64-unknown-linux-gnu`). | |
| ## Implementation | |
| ### Ser/De | |
| `TargetData` is converted to a dictionary with `dataclasses.asdict`, and is serialized with Python's | |
| built-in JSON library. | |
| When testing, the data is read into a `dict`, converted to a `TargetData`, | |
| and stored in the top level `INPUT_DATA` variable. | |
| The current deserialization logic should be resilient to changes in the schema, | |
| but requires that all fields contain ONLY types that can be | |
| directly serialized/deserialized by `json.dumps`. | |
| The acceptable types are those that make up | |
| [`common.JsonType`](https://github.com/rust-lang/rust/blob/bf9944f0b8006b152ef4d5f408ae75a0dde3d044/src/etc/lldb_batchmode/common.py#L17) | |
| Since the serialization/deserialization is decoupled from the debugger logic, we can easily switch | |
| to an alternative format if we find a better alternative to json. | |
| The conversion logic from the debugger's internal representation to our schema classes lives in | |
| `from_$DEBUGGER.py`. | |
| Once imported, `common` automatically deserializes any existing input data and [stores it in the | |
| global variable `INPUT_DATA`](https://github.com/rust-lang/rust/blob/bf9944f0b8006b152ef4d5f408ae75a0dde3d044/src/etc/lldb_batchmode/common.py#L523). | |
| This data is what we test against. | |
| > [!NOTE] | |
| > Special care was taken to prevent `lldb_batchmode` from importing `common` unless a `repr` command | |
| > was actually processed. This saves us from reading/writing input data for tests that don't need | |
| > it. | |
| #### Format minutae | |
| Since type information is unique and unchanging once the debug session has begun, types are only | |
| stored once at the top level, and are referred to by name everywhere else. | |
| Pointer values change from run to run. | |
| To prevent mismatches, pointer variables do not store their value. | |
| This is equivalent to the wildcard `[...]` used in `-check` directives. | |
| `BlessMetadata` is included in `TargetData`, but is not tested against. | |
| It exists solely as a record of how the test data was generated, | |
| to help in diagnosing issues that may occur due to Python or the debugger changing versions. | |
| ### Entry point and `--bless` | |
| Upon encountering a `repr` pseudo-command, `lldb_batchmode.main` dispatches to | |
| `check_$DEBUGGER.check()`. | |
| If the `--bless` option was specified, the variable is converted from | |
| the in-memory representation to our equivalent schema class. | |
| This includes the variable's type, | |
| visualizers, children, the children's types, etc. | |
| Once inserted into `TargetData`, | |
| the variable is tested against the data that was just saved to `TargetData`. | |
| If no exception or errors occurred and the `--bless` option was specified, `INPUT_DATA` is written | |
| to the appropriate file just before `lldb_batchmode` exits. | |
| If errors occur, `INPUT_DATA` is simply discarded. | |
| Currently, the `repr` pseudo-command is checked for directly. | |
| GDB and LLDB both support creating custom CLI commands via Python code. | |
| In the future, `repr` may be implemented as a CLI command for one or both debuggers. | |
| ### Check logic | |
| `check_$DEBUGGER.check` converts the debugger's variable object into a `Variable` object and | |
| compares the two. | |
| If any mismatches are found, further processing is done to report errors in a more helpful manner. | |
| This means that errors are encountered and reported immediately, which has a number of advantages. | |
| Most importantly, since the debugger state has not changed since the failure, and we | |
| still have access to the debugger's variable object, we can poll the debugger for more information | |
| to provide more useful error messages. | |
| For example, LLDB can be a bit coy when it comes to reporting errors that occur within | |
| synthetic/summary provider calls. | |
| This is especially true when running the command within another | |
| command, as the tests do by calling `script import lldb_batchmode; lldb_batchmode.main()` and | |
| executing commands in that context. | |
| When we encounter an error, we can import the appropriate summary provider, pass the variable object | |
| to it, and print the exception ourselves. | |
| We can also inspect the synthetic provider class to make | |
| sure it implements all the mandatory functions. | |
| Errors *do not* immediately end the test. | |
| This is especially important now that a `--bless` option has been added. | |
| `--bless` updates all of the input data, so we need to print all of the errors so the reader can | |
| make an informed decision about whether or not there are further changes that need to be made. | |
| We absolutely do not want people accidentally blessing bad data | |
| purely because the first error happened to be an expected change. | |
| Errors are printed directly to `stdout` to appear as visible output from the `repr` pseudo command. | |
| There are [several error helper functions](https://github.com/rust-lang/rust/blob/e7b595554e664e6bd281c8cf881093d6c71bc0e1/src/etc/lldb_batchmode/common.py#L35-L51) | |
| to keep formatting consistent. | |
| > [!NOTE] | |
| > When LLDB is running a `script` command, it does not print the Python interpreter's `stderr`. | |
| > If the interpreter exits with an exception, it will print that, but none of the rest of `stderr`. | |
| > Instead, if we decide we want to print to `stderr`, we can use the debugger's by calling | |
| > `lldb.debugger.GetErrorFileHandle` which returns a Python `io.TextIoWrapper`. | |
| If no errors occurred for a given variable, `$VAR_NAME ok` is printed to `stdout` for `compiletest` | |
| to match against. | |
| Before `lldb_batchmode` exits, one last check is done to ensure that all the types and variables | |
| that were present in `INPUT_DATA` have been checked against. | |
| If this check fails, the script reports the untested types/variables and exits with an error code. | |
| # LLDB versioning | |
| Apple distributes a fork of LLDB with Xcode that contains Swift support. | |
| This fork of LLDB does not use the same versioning scheme as LLVM's LLDB: | |
| ``` | |
| # Apple: | |
| lldb-1703.0.236.21 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2) | |
| # LLVM: | |
| lldb version 22.1.2 (https://github.com/llvm/llvm-project revision 1ab49a973e210e97d61e5db6557180dcb92c3e98) | |
| clang revision 1ab49a973e210e97d61e5db6557180dcb92c3e98 | |
| llvm revision 1ab49a973e210e97d61e5db6557180dcb92c3e98 | |
| ``` | |
| It does not appear that the Apple LLDB's version is derived from LLVM's version, so we cannot easily | |
| or automatically convert between the two. | |
| Luckily, we can still check the base LLVM version manually | |
| by checking the appropriate release branch in the Swift LLVM repo. | |
| For our example above, no branch exists for `Swift 6.2.3`, but there is one for `6.2.2`. | |
| The LLVM version is located in | |
| [`llvm/utils/gn/secondary/llvm/version.gni`](https://github.com/swiftlang/llvm-project/blob/swift/release/6.2.2/llvm/utils/gn/secondary/llvm/version.gni). | |
| As we can see from that example, the Apple LLDB version above corresponds to (roughly) LLVM LLDB | |
| `19.1.5`. | |
| This can be useful when diagnosing or writing new tests, as it allows us to get a better idea of | |
| what features are available in the Apple LLDB used in CI. | |
| For example, LLDB 19 was the first version to support Type Recognizer functions, | |
| so we can assume our example Apple LLDB supports them. |