[tools] Add build_test_fast.py for a faster edit-compile-test loop
This change introduces `tools/build_test_fast.py`, a developer-focused wrapper script that automatically infers and builds the *minimal* required GN targets needed to run a specific test, avoiding the expensive overhead of building the full SDK (e.g. `create_sdk`) during local iteration. Features: - Infers the correct `test.py` compiler (`-c`) from the provided test paths (e.g., `tests/web/wasm` -> `dart2wasm`). - Infers the correct minimal GN targets for the selected compiler. For example, DDC tests now only build `ddc_stable_test_local` and `create_common_sdk`, taking ~4 seconds instead of 5 minutes. - Automatically defaults web compilers (`dart2js`, `dart2wasm`, `ddc`) to use the `d8` runtime to prevent noisy browser popups during testing, unless explicitly overridden. - Synchronizes the default `mode` between `build.py` (debug) and `test.py` (release) to prevent silent snapshot resolution failures. - Prints clear, copy-pasteable execution commands for both the build and test phases. - Displays a prominent warning if a test fails, reminding the developer to verify the minimal target mapping against `tools/bots/test_matrix.json`. - Adds documentation for the script to `docs/Testing.md`. Change-Id: I75473cdacca13f78dd271d9a31d5dcb15df57f6f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499043 Commit-Queue: Kevin Moore <kevmoo@google.com> Reviewed-by: Jake Macdonald <jakemac@google.com>
This commit is contained in:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
2e413148cb
commit
a5d8c88952
@@ -346,6 +346,21 @@ is out of date even the tool is doing what it should.
|
||||
Eventually, this way of running tests should be removed, along with the status
|
||||
files.
|
||||
|
||||
### Using build_test_fast.py (Developer Fast Path)
|
||||
|
||||
For local development, building the entire SDK for a single test can be time-consuming. You can use the `build_test_fast.py` wrapper, which automatically infers the correct compiler and builds only the *minimal* required targets needed to run your tests:
|
||||
|
||||
```sh
|
||||
# Infers dart2wasm and builds only its minimal targets (takes seconds instead of minutes)
|
||||
./tools/build_test_fast.py tests/web/wasm/simd/simd_test.dart
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Infer the compiler from your test path (e.g., `tests/web/wasm` -> `dart2wasm`).
|
||||
2. Build only the minimal GN targets required for that compiler (e.g., `dart2wasm_platform.dill`, `dartaotruntime`).
|
||||
3. Automatically default to the headless `d8` runtime for web compilers to prevent noisy browser popups.
|
||||
4. Execute `tools/test.py` with the correct arguments.
|
||||
|
||||
### Using test.dart
|
||||
|
||||
The new entrypoint is "test.dart":
|
||||
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
|
||||
# for details. All rights reserved. Use of this source code is governed by a
|
||||
# BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
# build_test_fast.py
|
||||
#
|
||||
# A smart wrapper around tools/build.py and tools/test.py that uses a
|
||||
# minimal, hardcoded mapping to determine exactly what needs to be built.
|
||||
# This avoids building the entire SDK (like CI does) just to run a single test.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
|
||||
def get_host_os():
|
||||
if sys.platform == 'win32':
|
||||
return 'win'
|
||||
elif sys.platform == 'darwin':
|
||||
return 'mac'
|
||||
return 'linux'
|
||||
|
||||
|
||||
def get_host_arch():
|
||||
machine = platform.machine().lower()
|
||||
if machine in ['arm64', 'aarch64']:
|
||||
return 'arm64'
|
||||
return 'x64'
|
||||
|
||||
|
||||
def get_minimal_build_targets(compiler, test_paths):
|
||||
targets = set()
|
||||
|
||||
# 1. Base compiler targets (Minimal!)
|
||||
if compiler == 'dart2wasm':
|
||||
targets.update([
|
||||
'dartaotruntime', 'dart2wasm_platform.dill', 'dart2wasm',
|
||||
'create_common_sdk'
|
||||
])
|
||||
elif compiler == 'dart2js':
|
||||
# Often dart2js_bot or dart2js_platform.dill is enough, rather than create_sdk
|
||||
targets.update(
|
||||
['dartaotruntime', 'dart2js_platform.dill', 'create_common_sdk'])
|
||||
elif compiler == 'ddc':
|
||||
targets.update(['ddc_stable_test_local', 'create_common_sdk'])
|
||||
elif compiler in ['analyzer', 'dart2analyzer']:
|
||||
pass # Often no build needed, or just dartanalyzer
|
||||
elif compiler == 'dartkp':
|
||||
targets.update(['runtime', 'runtime_precompiled'])
|
||||
elif compiler in ['vm', 'dartk', 'app_jitk', 'none']:
|
||||
targets.update(['runtime'])
|
||||
else:
|
||||
targets.update(['runtime'])
|
||||
|
||||
# 2. Path-specific add-ons
|
||||
for path in test_paths:
|
||||
if 'tests/ffi' in path:
|
||||
targets.update(['ffi_test_functions', 'ffi_test_dynamic_library'])
|
||||
|
||||
return list(targets)
|
||||
|
||||
|
||||
def guess_compiler_from_paths(test_paths):
|
||||
# Try to guess the best compiler based on the test paths
|
||||
for path in test_paths:
|
||||
# Wasm tests
|
||||
if 'tests/web/wasm' in path or 'pkg/dart2wasm' in path:
|
||||
return 'dart2wasm'
|
||||
# DDC specific tests
|
||||
elif 'tests/dartdevc' in path or 'pkg/dev_compiler' in path:
|
||||
return 'ddc'
|
||||
# General web / dart2js tests
|
||||
elif 'tests/web' in path or 'pkg/compiler' in path:
|
||||
return 'dart2js'
|
||||
# Analyzer / language server tests
|
||||
elif 'pkg/analyzer' in path or 'pkg/analysis_server' in path:
|
||||
return 'dart2analyzer'
|
||||
|
||||
# Default to the VM's JIT compiler
|
||||
return 'dartk'
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
compiler = None
|
||||
runtime = None
|
||||
test_paths = []
|
||||
|
||||
# test.py defaults to release mode, while build.py defaults to debug.
|
||||
# We MUST pass the mode and arch explicitly to build.py to match.
|
||||
mode = 'release'
|
||||
arch = get_host_arch()
|
||||
system = get_host_os()
|
||||
|
||||
# Parse out compiler, mode, arch and test paths
|
||||
skip_next = False
|
||||
for i, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
|
||||
if arg == '-c' or arg == '--compiler':
|
||||
compiler = args[i + 1]
|
||||
skip_next = True
|
||||
elif arg.startswith('--compiler='):
|
||||
compiler = arg.split('=', 1)[1]
|
||||
elif arg == '-m' or arg == '--mode':
|
||||
mode = args[i + 1]
|
||||
skip_next = True
|
||||
elif arg.startswith('--mode='):
|
||||
mode = arg.split('=', 1)[1]
|
||||
elif arg == '-a' or arg == '--arch':
|
||||
arch = args[i + 1]
|
||||
skip_next = True
|
||||
elif arg.startswith('--arch='):
|
||||
arch = arg.split('=', 1)[1]
|
||||
elif arg == '-r' or arg == '--runtime':
|
||||
runtime = args[i + 1]
|
||||
skip_next = True
|
||||
elif arg.startswith('--runtime='):
|
||||
runtime = arg.split('=', 1)[1]
|
||||
elif not arg.startswith('-'):
|
||||
test_paths.append(arg)
|
||||
|
||||
# Infer the compiler if not explicitly provided
|
||||
inferred = False
|
||||
if compiler is None:
|
||||
compiler = guess_compiler_from_paths(test_paths)
|
||||
inferred = True
|
||||
|
||||
if inferred:
|
||||
print(
|
||||
f"🔮 Inferred compiler '\033[1m{compiler}\033[0m' from test paths.")
|
||||
|
||||
# Default to d8 for web tests to reduce noise, if no runtime specified
|
||||
inferred_runtime = False
|
||||
if runtime is None and compiler in ['dart2js', 'dart2wasm', 'ddc']:
|
||||
runtime = 'd8'
|
||||
inferred_runtime = True
|
||||
print(
|
||||
f"🔮 Inferred runtime '\033[1m{runtime}\033[0m' for web compiler to reduce noise."
|
||||
)
|
||||
|
||||
build_args = get_minimal_build_targets(compiler, test_paths)
|
||||
|
||||
targets_str = ' '.join(build_args) if build_args else '(none)'
|
||||
print(
|
||||
f"🎯 Determined minimal build targets for compiler '\033[1m{compiler}\033[0m': \033[1m{targets_str}\033[0m"
|
||||
)
|
||||
|
||||
stars_line = "⭐ " * 35
|
||||
|
||||
if build_args:
|
||||
# 3. Build Dart using the minimal targets, matching test.py's mode and arch defaults
|
||||
build_script = os.path.join(os.path.dirname(__file__), 'build.py')
|
||||
build_cmd = [sys.executable, build_script, '-m', mode, '-a', arch
|
||||
] + build_args
|
||||
print(f"🚀 Running: python3 tools/build.py {' '.join(build_cmd[2:])}")
|
||||
print(stars_line)
|
||||
|
||||
build_result = subprocess.run(build_cmd)
|
||||
print(stars_line)
|
||||
|
||||
if build_result.returncode != 0:
|
||||
print("❌ Build failed! Aborting test run.")
|
||||
sys.exit(build_result.returncode)
|
||||
|
||||
print("✅ Build succeeded!\n")
|
||||
else:
|
||||
print("✅ No build targets required!\n")
|
||||
|
||||
# 4. Run the tests
|
||||
test_script = os.path.join(os.path.dirname(__file__), 'test.py')
|
||||
|
||||
if inferred:
|
||||
args = ['-c', compiler] + args
|
||||
|
||||
if inferred_runtime:
|
||||
args = ['-r', runtime] + args
|
||||
|
||||
test_cmd = [sys.executable, test_script] + args
|
||||
print(f"🧪 Running tests: python3 tools/test.py {' '.join(args)}")
|
||||
print(stars_line)
|
||||
test_result = subprocess.run(test_cmd)
|
||||
print(stars_line)
|
||||
|
||||
if test_result.returncode != 0:
|
||||
print("\n" + "⚠️ " * 40)
|
||||
print(" TEST FAILED!")
|
||||
print(
|
||||
"If this failure looks like a missing file, missing snapshot, or compilation error,"
|
||||
)
|
||||
print(
|
||||
"it's possible that `build_test_fast.py` didn't build all the required dependencies."
|
||||
)
|
||||
print(
|
||||
"💡 You may need to update the `get_minimal_build_targets()` mapping in this script."
|
||||
)
|
||||
print(
|
||||
" (Check tools/bots/test_matrix.json to see what CI builds for this test suite!)"
|
||||
)
|
||||
print("⚠️ " * 40 + "\n")
|
||||
|
||||
sys.exit(test_result.returncode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user