|
#!/usr/bin/env python3 |
|
|
|
import click |
|
import subprocess |
|
import sys |
|
import os |
|
from pathlib import Path |
|
|
|
from auto_click_auto import enable_click_shell_completion |
|
from auto_click_auto.constants import ShellType |
|
|
|
@click.command() |
|
@click.option('-s', '--sketch', required=True, help='Sketch name to build/run') |
|
@click.option('-t', '--target', multiple=True, required=True, help='Target platform(s) (e.g., esp32, esp32s3, esp32c3) or "all" for all targets') |
|
@click.option('-W', '--wokwi-timeout', type=int, help='Wokwi timeout in seconds (will be multiplied by 1000)') |
|
@click.option('-Q', '--qemu', is_flag=True, help='Use QEMU platform instead of hardware') |
|
@click.option('--build-only', is_flag=True, help='Only run the build step') |
|
@click.option('--test-only', is_flag=True, help='Only run the test step (skip build)') |
|
@click.option('-c', '--chunk', is_flag=True, help='Use chunk build/run mode') |
|
@click.option('-i', '--chunk-index', type=int, help='Chunk index for parallel execution') |
|
@click.option('-m', '--chunk-max', type=int, help='Maximum number of chunks') |
|
@click.option('-o', '--options', is_flag=True, help='Enable options mode for testing') |
|
@click.option('-e', '--erase', is_flag=True, help='Erase flash before testing') |
|
@click.option('--type', 'test_type', help='Test type (validation, performance, etc.)') |
|
@click.option('--clean', is_flag=True, help='Clean build and test files') |
|
@click.option('--verbose', is_flag=True, help='Enable verbose output', default=True) |
|
def main(sketch, target, wokwi_timeout, qemu, build_only, test_only, chunk, chunk_index, |
|
chunk_max, options, erase, test_type, clean, verbose): |
|
""" |
|
Build and run ESP32 Arduino tests using the GitHub CI scripts. |
|
|
|
Examples: |
|
build-run -s gpio -t esp32s3 -W 10 |
|
build-run -s gpio -t esp32s2 esp32p4 -W 20 |
|
build-run -s hello_world -t esp32 --build-only |
|
build-run -s gpio -t all --test-only -W 10 |
|
build-run --clean |
|
""" |
|
|
|
# Define all available targets |
|
ALL_TARGETS = ['esp32', 'esp32c3', 'esp32c6', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3'] |
|
|
|
# Process target list - handle "all" special case |
|
targets = list(target) |
|
if 'all' in targets: |
|
if len(targets) > 1: |
|
click.echo("Warning: 'all' specified with other targets, using all targets", err=True) |
|
targets = ALL_TARGETS |
|
|
|
# Validate targets |
|
for t in targets: |
|
if t not in ALL_TARGETS: |
|
click.echo(f"Error: Unknown target '{t}'. Available targets: {', '.join(ALL_TARGETS)}", err=True) |
|
sys.exit(1) |
|
|
|
# Get the ESP32 Arduino core directory (assuming script is run from project root) |
|
esp32_dir = Path.cwd() |
|
build_script = esp32_dir / ".github" / "scripts" / "tests_build.sh" |
|
run_script = esp32_dir / ".github" / "scripts" / "tests_run.sh" |
|
|
|
# Verify scripts exist |
|
if not build_script.exists(): |
|
click.echo(f"Error: Build script not found at {build_script}", err=True) |
|
sys.exit(1) |
|
|
|
if not run_script.exists(): |
|
click.echo(f"Error: Run script not found at {run_script}", err=True) |
|
sys.exit(1) |
|
|
|
# Handle clean option |
|
if clean: |
|
if verbose: |
|
click.echo("Cleaning build and test files...") |
|
result = subprocess.run([str(build_script), "-clean"], capture_output=not verbose) |
|
sys.exit(result.returncode) |
|
|
|
# Validate mutually exclusive options |
|
if build_only and test_only: |
|
click.echo("Error: --build-only and --test-only are mutually exclusive", err=True) |
|
sys.exit(1) |
|
|
|
# Validate chunk options |
|
if chunk and (chunk_index is None or chunk_max is None): |
|
click.echo("Error: --chunk requires both --chunk-index and --chunk-max", err=True) |
|
sys.exit(1) |
|
|
|
# Track results for each target |
|
results = {} |
|
|
|
# Process each target |
|
for current_target in targets: |
|
if verbose and len(targets) > 1: |
|
click.echo(f"\n{'='*50}") |
|
click.echo(f"Processing target: {current_target}") |
|
click.echo(f"{'='*50}") |
|
|
|
target_success = True |
|
|
|
# Build step |
|
if not test_only: |
|
build_cmd = [str(build_script)] |
|
|
|
if chunk: |
|
build_cmd.extend(["-c", "-type", test_type or "validation"]) |
|
build_cmd.extend(["-t", current_target, "-i", str(chunk_index), "-m", str(chunk_max)]) |
|
else: |
|
build_cmd.extend(["-s", sketch, "-t", current_target]) |
|
|
|
if verbose: |
|
click.echo(f"Running build command: {' '.join(build_cmd)}") |
|
|
|
result = subprocess.run(build_cmd, capture_output=not verbose) |
|
if result.returncode != 0: |
|
click.echo(f"Build failed for target {current_target} with exit code {result.returncode}", err=True) |
|
target_success = False |
|
results[current_target] = "BUILD_FAILED" |
|
continue # Skip to next target |
|
|
|
if verbose: |
|
click.echo(f"Build completed successfully for {current_target}") |
|
|
|
# Test step |
|
if not build_only and target_success: |
|
run_cmd = [str(run_script)] |
|
|
|
if chunk: |
|
run_cmd.append("-c") |
|
|
|
run_cmd.extend(["-s", sketch, "-t", current_target]) |
|
|
|
if wokwi_timeout is not None: |
|
# Convert seconds to milliseconds as the script expects |
|
wokwi_timeout_ms = wokwi_timeout * 1000 |
|
run_cmd.extend(["-W", str(wokwi_timeout_ms)]) |
|
|
|
if qemu: |
|
run_cmd.append("-Q") |
|
|
|
if options: |
|
run_cmd.append("-o") |
|
|
|
if erase: |
|
run_cmd.append("-e") |
|
|
|
if chunk: |
|
run_cmd.extend(["-i", str(chunk_index), "-m", str(chunk_max)]) |
|
|
|
if test_type: |
|
run_cmd.extend(["-type", test_type]) |
|
|
|
if verbose: |
|
click.echo(f"Running test command: {' '.join(run_cmd)}") |
|
|
|
result = subprocess.run(run_cmd, capture_output=not verbose) |
|
if result.returncode != 0: |
|
click.echo(f"Test failed for target {current_target} with exit code {result.returncode}", err=True) |
|
target_success = False |
|
results[current_target] = "TEST_FAILED" |
|
else: |
|
if verbose: |
|
click.echo(f"Test completed successfully for {current_target}") |
|
results[current_target] = "SUCCESS" |
|
elif target_success: |
|
results[current_target] = "SUCCESS" |
|
|
|
# Print summary if multiple targets were processed |
|
if len(targets) > 1: |
|
if verbose: |
|
click.echo(f"\n{'='*50}") |
|
click.echo("SUMMARY") |
|
click.echo(f"{'='*50}") |
|
for target_name, status in results.items(): |
|
status_color = "green" if status == "SUCCESS" else "red" |
|
click.echo(f"{target_name}: ", nl=False) |
|
click.secho(status, fg=status_color) |
|
|
|
# Exit with error code if any target failed |
|
failed_targets = [t for t, s in results.items() if s != "SUCCESS"] |
|
if failed_targets: |
|
click.echo(f"\nFailed targets: {', '.join(failed_targets)}", err=True) |
|
sys.exit(1) |
|
|
|
if verbose: |
|
click.echo("All operations completed successfully") |
|
|
|
# Enable shell autocompletion for Zsh (and other shells) |
|
enable_click_shell_completion( |
|
program_name="build-run", |
|
shells={ShellType.ZSH, ShellType.BASH, ShellType.FISH}, |
|
verbose=False |
|
) |
|
|
|
if __name__ == "__main__": |
|
main() |