diff --git a/tools/clang_tidy/README.md b/tools/clang_tidy/README.md index 460ecbbfb7959..5c7fa9414fcde 100644 --- a/tools/clang_tidy/README.md +++ b/tools/clang_tidy/README.md @@ -1,15 +1,94 @@ # clang_tidy -This is a Dart program/library that runs clang_tidy over modified files in the Flutter engine repo. +A wrapper library and program that runs `clang_tidy` on the Flutter engine repo. -By default the linter runs on the repo files changed contained in `src/out/host_debug/compile_commands.json` command. -To check files other than in `host_debug` use `--target-variant android_debug_unopt`, -`--target-variant ios_debug_sim_unopt`, etc. +```shell +# Assuming you are in the `flutter` root of the engine repo. +dart ./tools/clang_tidy/bin/main.dart +``` + +By default, the linter runs over _modified_[^1] files in the _latest_[^2] build +of the engine. -Alternatively, use `--compile-commands` to specify a path to a `compile_commands.json` file. +A subset of checks can also be fixed automatically by passing `--fix`: +```shell +dart ./tools/clang_tidy/bin/main.dart --fix ``` -$ bin/main.dart --target-variant -$ bin/main.dart --compile-commands -$ bin/main.dart --help + +To configure what lints are enabled, see [`.clang-tidy`](../../.clang-tidy). + +> **💡 TIP**: If you're looking for the git pre-commit hook configuration, see +> [`githooks`](../githooks). + +## Advanced Usage + +Some common use cases are described below, or use `--help` to see all options. + +### Run with checks added or removed + +To run adding a check _not_ specified in `.clang-tidy`: + +```shell +dart ./tools/clang_tidy/bin/main.dart --checks="" ``` + +It's possible also to use wildcards to add multiple checks: + +```shell +dart ./tools/clang_tidy/bin/main.dart --checks="readability-*" +``` + +To remove a specific check: + +```shell +dart ./tools/clang_tidy/bin/main.dart --checks="-" +``` + +To remove multiple checks: + +```shell +dart ./tools/clang_tidy/bin/main.dart --checks="-readability-*" +``` + +To remove _all_ checks (usually to add a specific check): + +```shell +dart ./tools/clang_tidy/bin/main.dart --checks="-*," +``` + +### Specify a specific build + +There are some rules that are only applicable to certain builds, or to check +a difference in behavior between two builds. + +Use `--target-variant` to specify a build: + +```shell +dart ./tools/clang_tidy/bin/main.dart --target-variant +``` + +For example, to check the `android_debug_unopt` build: + +```shell +dart ./tools/clang_tidy/bin/main.dart --target-variant android_debug_unopt +``` + +In rarer cases, for example comparing two different checkouts of the engine, +use `--src-dir=`. + +### Lint entire repository + +When adding a new lint rule, or when checking lint rules that impact files that +have not changed. + +Use `--lint-all` to lint all files in the repo: + +```shell +dart ./tools/clang_tidy/bin/main.dart --lint-all +``` + +> **⚠️ WARNING**: This will take a long time to run. + +[^1]: Modified files are determined by a `git diff` command compared to `HEAD`. +[^2]: Latest build is the last updated directory in `src/out/`. diff --git a/tools/clang_tidy/lib/clang_tidy.dart b/tools/clang_tidy/lib/clang_tidy.dart index f54d3c78a15e2..e8cb4db6e9f9e 100644 --- a/tools/clang_tidy/lib/clang_tidy.dart +++ b/tools/clang_tidy/lib/clang_tidy.dart @@ -5,6 +5,7 @@ import 'dart:convert' show LineSplitter, jsonDecode; import 'dart:io' as io show File, stderr, stdout; +import 'package:engine_repo_tools/engine_repo_tools.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:process/process.dart'; @@ -92,25 +93,29 @@ class ClangTidy { ), _outSink = outSink ?? io.stdout, _errSink = errSink ?? io.stderr, - _processManager = processManager; + _processManager = processManager, + _engine = null; /// Builds an instance of [ClangTidy] from a command line. ClangTidy.fromCommandLine( List args, { + Engine? engine, StringSink? outSink, StringSink? errSink, ProcessManager processManager = const LocalProcessManager(), }) : - options = Options.fromCommandLine(args, errSink: errSink), + options = Options.fromCommandLine(args, errSink: errSink, engine: engine), _outSink = outSink ?? io.stdout, _errSink = errSink ?? io.stderr, - _processManager = processManager; + _processManager = processManager, + _engine = engine; /// The [Options] that specify how this [ClangTidy] operates. final Options options; final StringSink _outSink; final StringSink _errSink; final ProcessManager _processManager; + final Engine? _engine; late final DateTime _startTime; @@ -119,12 +124,12 @@ class ClangTidy { _startTime = DateTime.now(); if (options.help) { - options.printUsage(); + options.printUsage(engine: _engine); return 0; } if (options.errorMessage != null) { - options.printUsage(message: options.errorMessage); + options.printUsage(message: options.errorMessage, engine: _engine); return 1; } diff --git a/tools/clang_tidy/lib/src/options.dart b/tools/clang_tidy/lib/src/options.dart index 401d34b8e81bc..dca4c3a3874e7 100644 --- a/tools/clang_tidy/lib/src/options.dart +++ b/tools/clang_tidy/lib/src/options.dart @@ -5,11 +5,10 @@ import 'dart:io' as io show Directory, File, Platform, stderr; import 'package:args/args.dart'; +import 'package:engine_repo_tools/engine_repo_tools.dart'; import 'package:path/path.dart' as path; -// Path to root of the flutter/engine repository containing this script. -final String _engineRoot = path.dirname(path.dirname(path.dirname(path.dirname(path.fromUri(io.Platform.script))))); - +final Engine _engineRoot = Engine.findWithin(path.dirname(path.fromUri(io.Platform.script))); /// Adds warnings as errors for only specific runs. This is helpful if migrating one platform at a time. String? _platformSpecificWarningsAsErrors(ArgResults options) { @@ -91,8 +90,13 @@ class Options { factory Options.fromCommandLine( List arguments, { StringSink? errSink, + Engine? engine, }) { - final ArgResults argResults = _argParser.parse(arguments); + // TODO(matanlurey): Refactor this further, ideally moving all of the engine + // resolution logic (i.e. --src-dir, --target-variant, --compile-commands) + // into a separate method, and perhaps also adding `engine.output(name)` + // to engine_repo_tools instead of path manipulation inlined below. + final ArgResults argResults = _argParser(defaultEngine: engine).parse(arguments); String? buildCommandsPath = argResults['compile-commands'] as String?; @@ -134,74 +138,85 @@ class Options { ); } - static final ArgParser _argParser = ArgParser() - ..addFlag( - 'help', - abbr: 'h', - help: 'Print help.', - negatable: false, - ) - ..addFlag( - 'lint-all', - help: 'Lint all of the sources, regardless of FLUTTER_NOLINT.', - ) - ..addFlag( - 'lint-head', - help: 'Lint files changed in the tip-of-tree commit.', - ) - ..addFlag( - 'fix', - help: 'Apply suggested fixes.', - ) - ..addFlag( - 'verbose', - help: 'Print verbose output.', - ) - ..addOption( - 'shard-id', - help: 'When used with the shard-commands option this identifies which shard will execute.', - valueHelp: 'A number less than 1 + the number of shard-commands arguments.', - ) - ..addOption( - 'shard-variants', - help: 'Comma separated list of other targets, this invocation ' - 'will only execute a subset of the intersection and the difference of the ' - 'compile commands. Use with `shard-id`.' - ) - ..addOption( - 'compile-commands', - help: 'Use the given path as the source of compile_commands.json. This ' - 'file is created by running "tools/gn". Cannot be used with --target-variant ' - 'or --src-dir.', - ) - ..addOption( - 'target-variant', - aliases: ['variant'], - help: 'The engine variant directory containing compile_commands.json ' - 'created by running "tools/gn". Cannot be used with --compile-commands.', - valueHelp: 'host_debug|android_debug_unopt|ios_debug|ios_debug_sim_unopt', - defaultsTo: 'host_debug', - ) - ..addOption('mac-host-warnings-as-errors', + static ArgParser _argParser({required Engine? defaultEngine}) { + defaultEngine ??= _engineRoot; + final io.Directory? latestBuild = defaultEngine.latestOutput()?.path; + return ArgParser() + ..addFlag( + 'help', + abbr: 'h', + help: 'Print help.', + negatable: false, + ) + ..addFlag( + 'lint-all', + help: 'Lint all of the sources, regardless of FLUTTER_NOLINT.', + ) + ..addFlag( + 'lint-head', + help: 'Lint files changed in the tip-of-tree commit.', + ) + ..addFlag( + 'fix', + help: 'Apply suggested fixes.', + ) + ..addFlag( + 'verbose', + help: 'Print verbose output.', + ) + ..addOption( + 'shard-id', + help: 'When used with the shard-commands option this identifies which shard will execute.', + valueHelp: 'A number less than 1 + the number of shard-commands arguments.', + ) + ..addOption( + 'shard-variants', + help: 'Comma separated list of other targets, this invocation ' + 'will only execute a subset of the intersection and the difference of the ' + 'compile commands. Use with `shard-id`.' + ) + ..addOption( + 'compile-commands', + help: 'Use the given path as the source of compile_commands.json. This ' + 'file is created by running "tools/gn". Cannot be used with --target-variant ' + 'or --src-dir.', + ) + ..addOption( + 'target-variant', + aliases: ['variant'], + help: 'The engine variant directory name containing compile_commands.json ' + 'created by running "tools/gn".\n\nIf not provided, the default is ' + 'the latest build in the engine defined by --src-dir (or the ' + 'default path, see --src-dir for details).\n\n' + 'Cannot be used with --compile-commands.', + valueHelp: 'host_debug|android_debug_unopt|ios_debug|ios_debug_sim_unopt', + defaultsTo: latestBuild == null ? 'host_debug' : path.basename(latestBuild.path), + ) + ..addOption('mac-host-warnings-as-errors', + help: + 'checks that will be treated as errors when running debug_host on mac.') + ..addOption( + 'src-dir', help: - 'checks that will be treated as errors when running debug_host on mac.') - ..addOption( - 'src-dir', - help: 'Path to the engine src directory. Cannot be used with --compile-commands.', - valueHelp: 'path/to/engine/src', - defaultsTo: path.dirname(_engineRoot), - ) - ..addOption( - 'checks', - help: 'Perform the given checks on the code. Defaults to the empty ' - 'string, indicating all checks should be performed.', - defaultsTo: '', - ) - ..addFlag( - 'enable-check-profile', - help: 'Enable per-check timing profiles and print a report to stderr.', - negatable: false, - ); + 'Path to the engine src directory.\n\n' + 'If not provided, the default is the engine root directory that ' + 'contains the `clang_tidy` tool.\n\n' + 'Cannot be used with --compile-commands.', + valueHelp: 'path/to/engine/src', + defaultsTo: _engineRoot.srcDir.path, + ) + ..addOption( + 'checks', + help: 'Perform the given checks on the code. Defaults to the empty ' + 'string, indicating all checks should be performed.', + defaultsTo: '', + ) + ..addFlag( + 'enable-check-profile', + help: 'Enable per-check timing profiles and print a report to stderr.', + negatable: false, + ); + } /// Whether to print a help message and exit. final bool help; @@ -219,7 +234,7 @@ class Options { final int? shardId; /// The root of the flutter/engine repository. - final io.Directory repoPath = io.Directory(_engineRoot); + final io.Directory repoPath = _engineRoot.flutterDir; /// Argument sent as `warnings-as-errors` to clang-tidy. final String? warningsAsErrors; @@ -249,7 +264,7 @@ class Options { final StringSink _errSink; /// Print command usage with an additional message. - void printUsage({String? message}) { + void printUsage({String? message, required Engine? engine}) { if (message != null) { _errSink.writeln(message); } @@ -257,7 +272,7 @@ class Options { 'Usage: bin/main.dart [--help] [--lint-all] [--lint-head] [--fix] [--verbose] ' '[--diff-branch] [--target-variant variant] [--src-dir path/to/engine/src]', ); - _errSink.writeln(_argParser.usage); + _errSink.writeln(_argParser(defaultEngine: engine).usage); } /// Command line argument validation. diff --git a/tools/clang_tidy/pubspec.yaml b/tools/clang_tidy/pubspec.yaml index e869ef5f41684..b06cd30247821 100644 --- a/tools/clang_tidy/pubspec.yaml +++ b/tools/clang_tidy/pubspec.yaml @@ -18,6 +18,7 @@ environment: dependencies: args: any + engine_repo_tools: any meta: any path: any process: any @@ -38,6 +39,8 @@ dependency_overrides: path: ../../../third_party/dart/pkg/async_helper collection: path: ../../../third_party/dart/third_party/pkg/collection + engine_repo_tools: + path: ../pkg/engine_repo_tools expect: path: ../../../third_party/dart/pkg/expect file: diff --git a/tools/clang_tidy/test/clang_tidy_test.dart b/tools/clang_tidy/test/clang_tidy_test.dart index 5fcdd984a73e0..4dbb39e2b125e 100644 --- a/tools/clang_tidy/test/clang_tidy_test.dart +++ b/tools/clang_tidy/test/clang_tidy_test.dart @@ -7,6 +7,7 @@ import 'dart:io' as io show Directory, File, Platform, stderr; import 'package:clang_tidy/clang_tidy.dart'; import 'package:clang_tidy/src/command.dart'; import 'package:clang_tidy/src/options.dart'; +import 'package:engine_repo_tools/engine_repo_tools.dart'; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; import 'package:process/process.dart'; @@ -19,6 +20,7 @@ final class Fixture { /// Simulates running the tool with the given [args]. factory Fixture.fromCommandLine(List args, { ProcessManager? processManager, + Engine? engine, }) { processManager ??= FakeProcessManager(); final StringBuffer outBuffer = StringBuffer(); @@ -28,6 +30,7 @@ final class Fixture { outSink: outBuffer, errSink: errBuffer, processManager: processManager, + engine: engine, ), errBuffer, outBuffer); } @@ -106,21 +109,58 @@ void _withTempFile(String prefix, void Function(String path) func) { } Future main(List args) async { - if (args.isEmpty) { + final String? buildCommands = + args.firstOrNull ?? + Engine.findWithin().latestOutput()?.compileCommandsJson.path; + + if (buildCommands == null || args.length > 1) { io.stderr.writeln( 'Usage: clang_tidy_test.dart [path/to/compile_commands.json]', ); return 1; } - final String buildCommands = args[0]; - test('--help gives help', () async { - final Fixture fixture = Fixture.fromCommandLine(['--help']); - final int result = await fixture.tool.run(); + test('--help gives help, and uses host_debug by default outside of an engine root', () async { + final io.Directory rootDir = io.Directory.systemTemp.createTempSync('clang_tidy_test'); + try { + final Fixture fixture = Fixture.fromCommandLine( + ['--help'], + engine: TestEngine.createTemp(rootDir: rootDir) + ); + final int result = await fixture.tool.run(); - expect(fixture.tool.options.help, isTrue); - expect(result, equals(0)); - expect(fixture.errBuffer.toString(), contains('Usage: ')); + expect(fixture.tool.options.help, isTrue); + expect(result, equals(0)); + + final String errors = fixture.errBuffer.toString(); + expect(errors, contains('Usage: ')); + expect(errors, contains('defaults to "host_debug"')); + } finally { + rootDir.deleteSync(recursive: true); + } + }); + + test('--help gives help, and uses the latest build by default outside in an engine root', () async { + final io.Directory rootDir = io.Directory.systemTemp.createTempSync('clang_tidy_test'); + final io.Directory buildDir = io.Directory(path.join(rootDir.path, 'out', 'host_debug_unopt_arm64'))..createSync(recursive: true); + try { + final Fixture fixture = Fixture.fromCommandLine( + ['--help'], + engine: TestEngine.createTemp(rootDir: rootDir, outputs: [ + TestOutput(buildDir), + ]) + ); + final int result = await fixture.tool.run(); + + expect(fixture.tool.options.help, isTrue); + expect(result, equals(0)); + + final String errors = fixture.errBuffer.toString(); + expect(errors, contains('Usage: ')); + expect(errors, contains('defaults to "host_debug_unopt_arm64"')); + } finally { + rootDir.deleteSync(recursive: true); + } }); test('trimmed clang-tidy output', () { diff --git a/tools/githooks/pubspec.yaml b/tools/githooks/pubspec.yaml index a0a6a9789b8cb..07d1820110b37 100644 --- a/tools/githooks/pubspec.yaml +++ b/tools/githooks/pubspec.yaml @@ -18,9 +18,9 @@ environment: dependencies: args: any + clang_tidy: any meta: any path: any - clang_tidy: any dev_dependencies: async_helper: any @@ -39,6 +39,8 @@ dependency_overrides: path: ../clang_tidy collection: path: ../../../third_party/dart/third_party/pkg/collection + engine_repo_tools: + path: ../pkg/engine_repo_tools expect: path: ../../../third_party/dart/pkg/expect file: diff --git a/tools/pkg/engine_repo_tools/lib/engine_repo_tools.dart b/tools/pkg/engine_repo_tools/lib/engine_repo_tools.dart index 820f1bf29045b..f52cf8a6c4329 100644 --- a/tools/pkg/engine_repo_tools/lib/engine_repo_tools.dart +++ b/tools/pkg/engine_repo_tools/lib/engine_repo_tools.dart @@ -35,6 +35,12 @@ import 'package:path/path.dart' as p; /// If you have a path to a directory within the `$ENGINE/src` directory, or /// want to use the current working directory, use [Engine.findWithin]. final class Engine { + const Engine._({ + required this.srcDir, + required this.flutterDir, + required this.outDir, + }); + /// Creates an [Engine] from a path such as `/Users/.../flutter/engine/src`. /// /// ```dart @@ -66,25 +72,44 @@ final class Engine { // don't want to fail if it doesn't exist. final io.Directory outDir = io.Directory(p.join(srcPath, 'out')); - return Engine._(srcDir, flutterDir, outDir); + return Engine._( + srcDir: srcDir, + flutterDir: flutterDir, + outDir: outDir, + ); } /// Creates an [Engine] by looking for a `src/` directory in the given path. /// + /// Similar to [tryFindWithin], but throws a [StateError] if the path is not + /// within a valid engine. This is useful for tools that require an engine + /// and do not have a reasonable fallback or recovery path. + factory Engine.findWithin([String? path]) { + final Engine? engine = tryFindWithin(path); + if (engine == null) { + throw StateError('The path "$path" is not within a valid engine.'); + } + return engine; + } + + /// Creates an [Engine] by looking for a `src/` directory in the given [path]. + /// /// ```dart /// // Use the current working directory. /// final Engine engine = Engine.findWithin(); /// print(engine.srcDir.path); // /Users/.../engine/src /// /// // Use a specific directory. - /// final Engine engine = Engine.findWithin('/Users/.../engine/src/foo/bar/baz'); + /// final Engine engine = Engine.findWithin('/Users/.../engine/src/foo/bar'); /// print(engine.srcDir.path); // /Users/.../engine/src /// ``` /// - /// If a path is not provided, the current working directory is used. + /// If a [path] is not provided, the current working directory is used. /// - /// Throws a [StateError] if the path is not within a valid engine. - factory Engine.findWithin([String? path]) { + /// If path does not exist, or is not a directory, an error is thrown. + /// + /// Returns `null` if the path is not within a valid engine. + static Engine? tryFindWithin([String? path]) { path ??= p.current; // Search parent directories for a `src` directory. @@ -105,17 +130,9 @@ final class Engine { maybeSrcDir = maybeSrcDir.parent; } while (maybeSrcDir.parent.path != maybeSrcDir.path /* at root */); - throw StateError( - 'The path "$path" is not within a Flutter engine source directory.' - ); + return null; } - const Engine._( - this.srcDir, - this.flutterDir, - this.outDir, - ); - /// The path to the `$ENGINE/src` directory. final io.Directory srcDir; @@ -145,12 +162,88 @@ final class Engine { return null; } outputs.sort((Output a, Output b) { - return b.dir.statSync().modified.compareTo(a.dir.statSync().modified); + return b.path.statSync().modified.compareTo(a.path.statSync().modified); }); return outputs.first; } } +/// An implementation of [Engine] that has pre-defined outputs for testing. +final class TestEngine extends Engine { + /// Creates a [TestEngine] with pre-defined paths. + /// + /// The [srcDir] and [flutterDir] must exist, but the [outDir] is optional. + /// + /// Optionally, provide a list of [outputs] to use, otherwise it is empty. + TestEngine.withPaths({ + required super.srcDir, + required super.flutterDir, + required super.outDir, + List outputs = const [], + }) : _outputs = outputs, super._() { + if (!srcDir.existsSync()) { + throw ArgumentError.value(srcDir, 'srcDir', 'does not exist'); + } + if (!flutterDir.existsSync()) { + throw ArgumentError.value(flutterDir, 'flutterDir', 'does not exist'); + } + } + + /// Creates a [TestEngine] within a temporary directory. + /// + /// The [rootDir] is the temporary directory that will contain the engine. + /// + /// Optionally, provide a list of [outputs] to use, otherwise it is empty. + factory TestEngine.createTemp({ + required io.Directory rootDir, + List outputs = const [], + }) { + final io.Directory srcDir = io.Directory(p.join(rootDir.path, 'src')); + final io.Directory flutterDir = io.Directory(p.join(srcDir.path, 'flutter')); + final io.Directory outDir = io.Directory(p.join(srcDir.path, 'out')); + srcDir.createSync(recursive: true); + flutterDir.createSync(recursive: true); + outDir.createSync(recursive: true); + return TestEngine.withPaths( + srcDir: srcDir, + flutterDir: flutterDir, + outDir: outDir, + outputs: outputs, + ); + } + + final List _outputs; + + @override + List outputs() => List.unmodifiable(_outputs); + + @override + Output? latestOutput() { + if (_outputs.isEmpty) { + return null; + } + _outputs.sort((TestOutput a, TestOutput b) { + return b.lastModified.compareTo(a.lastModified); + }); + return _outputs.first; + } +} + +/// An implementation of [Output] that has a pre-defined path for testing. +final class TestOutput extends Output { + /// Creates a [TestOutput] with a pre-defined path. + /// + /// Optionally, provide a [lastModified] date. + TestOutput(super.path, {DateTime? lastModified}) + : lastModified = lastModified ?? _defaultLastModified, + super._(); + + static final DateTime _defaultLastModified = DateTime.now(); + + /// The last modified date of the output target. + final DateTime lastModified; +} + /// Thrown when an [Engine] could not be created from a path. sealed class InvalidEngineException implements Exception { /// Thrown when an [Engine] was created from a path not ending in `src`. @@ -210,19 +303,15 @@ final class InvalidEngineMissingFlutterDirectoryException implements InvalidEngi /// Represents a single output target in the `$ENGINE/src/out` directory. final class Output { - const Output._(this.dir); + const Output._(this.path); /// The directory containing the output target. - final io.Directory dir; + final io.Directory path; - /// The `compile_commands.json` file for this output target. + /// The `compile_commands.json` file that should exist for this output target. /// - /// Returns `null` if the file does not exist. - io.File? get compileCommandsJson { - final io.File file = io.File(p.join(dir.path, 'compile_commands.json')); - if (!file.existsSync()) { - return null; - } - return file; + /// The file may not exist. + io.File get compileCommandsJson { + return io.File(p.join(path.path, 'compile_commands.json')); } } diff --git a/tools/pkg/engine_repo_tools/test/engine_repo_tools_test.dart b/tools/pkg/engine_repo_tools/test/engine_repo_tools_test.dart index 48c18210b304e..8e34590ba8118 100644 --- a/tools/pkg/engine_repo_tools/test/engine_repo_tools_test.dart +++ b/tools/pkg/engine_repo_tools/test/engine_repo_tools_test.dart @@ -187,7 +187,7 @@ void main() { io.Directory(p.join(emptyDir.path, 'src', 'out', 'host_debug_unopt_arm64')).createSync(recursive: true); final Engine engine = Engine.fromSrcPath(p.join(emptyDir.path, 'src')); - final List outputs = engine.outputs().map((Output o) => p.basename(o.dir.path)).toList()..sort(); + final List outputs = engine.outputs().map((Output o) => p.basename(o.path.path)).toList()..sort(); expect(outputs, [ 'host_debug', 'host_debug_unopt_arm64', @@ -218,7 +218,7 @@ void main() { final Engine engine = Engine.fromSrcPath(p.join(emptyDir.path, 'src')); final Output? latestOutput = engine.latestOutput(); expect(latestOutput, isNotNull); - expect(p.basename(latestOutput!.dir.path), 'host_debug_unopt_arm64'); + expect(p.basename(latestOutput!.path.path), 'host_debug_unopt_arm64'); expect(latestOutput.compileCommandsJson, isNotNull); } finally { tearDown();