Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
951 changes: 951 additions & 0 deletions .ci/scripts/check_arm_buck_declarations.py

Large diffs are not rendered by default.

1,138 changes: 1,138 additions & 0 deletions .ci/scripts/tests/test_arm_buck_declarations.py

Large diffs are not rendered by default.

212 changes: 212 additions & 0 deletions .ci/scripts/tests/test_arm_buck_entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Integration checks for the toolchain-free Arm Buck declaration entrypoint."""

import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


ENTRYPOINT = (
Path(__file__).resolve().parents[3] / "backends/arm/test/test_arm_backend.sh"
)


class ArmBuckEntrypointTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.repo = self.root / "repo with spaces"
self.repo.mkdir()
self.git("init", "-q")
self.git("config", "user.name", "Test")
self.git("config", "user.email", "test@example.com")
self.git("config", "commit.gpgsign", "false")
self.git("config", "core.hooksPath", str(self.root / "no-hooks"))
self.script = self.repo / "backends/arm/test/test_arm_backend.sh"
self.script.parent.mkdir(parents=True)
shutil.copyfile(ENTRYPOINT, self.script)
self.checker_dir = self.repo / ".ci/scripts"
tests = self.checker_dir / "tests"
tests.mkdir(parents=True)
(self.checker_dir / "pytest-parallelism.sh").write_text("")
(tests / "test_arm_buck_declarations.py").write_text(
"import os, unittest\n"
"class CheckerTest(unittest.TestCase):\n"
" def test_checker(self):\n"
" self.assertNotIn('FAIL_CHECKER_SELF_TEST', os.environ)\n"
)
(self.checker_dir / "check_arm_buck_declarations.py").write_text(
"import json, os, pathlib, sys\n"
"pathlib.Path(os.environ['ENTRYPOINT_RESULT']).write_text(json.dumps({\n"
" 'base': sys.argv[sys.argv.index('--base') + 1],\n"
" 'cwd': os.getcwd(),\n"
"}))\n"
"sys.exit(int(os.environ.get('CHECKER_EXIT_CODE', '0')))\n"
)
self.initial = self.commit("initial")
self.base = self.commit("base")
self.head = self.commit("head")
self.result_file = self.root / "result.json"

def git(self, *args):
return subprocess.check_output(
["git", *args], cwd=self.repo, text=True, stderr=subprocess.PIPE
).strip()

def commit(self, message):
(self.repo / "revision").write_text(message)
self.git("add", ".")
self.git("commit", "-qm", message)
return self.git("rev-parse", "HEAD")

def run_suite(self, *args, **overrides):
env = os.environ.copy()
for key in ("BASE_REVISION", "GITHUB_EVENT_NAME", "PYTHONPATH"):
env.pop(key, None)
env.update(
PYTHON_EXECUTABLE=sys.executable,
ENTRYPOINT_RESULT=str(self.result_file),
)
env.update(overrides)
if self.result_file.exists():
self.result_file.unlink()
# A caller may invoke the entrypoint from outside the repository.
result = subprocess.run(
["bash", str(self.script), *args],
cwd=self.root,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=30,
)
return result

def assert_base(self, expected, **env):
result = self.run_suite("test_buck_declarations", **env)
self.assertEqual(result.returncode, 0, result.stdout)
actual = json.loads(self.result_file.read_text())
self.assertEqual(actual["base"], expected)
self.assertEqual(Path(actual["cwd"]).resolve(), self.repo.resolve())

def test_local_manual_and_new_branch_compare_latest_commit(self):
for env in (
{},
{"GITHUB_EVENT_NAME": "workflow_dispatch"},
{"GITHUB_EVENT_NAME": "push", "BASE_REVISION": "0" * 40},
):
with self.subTest(env=env):
self.assert_base(self.base, **env)

def test_local_base_override(self):
self.assert_base(self.initial, BASE_REVISION=self.initial)

def test_push_keeps_event_base(self):
self.assert_base(
self.initial, GITHUB_EVENT_NAME="push", BASE_REVISION=self.initial
)

def test_pull_request_uses_merge_base(self):
self.git("checkout", "-qb", "target", self.base)
target = self.commit("target-only")
self.git("checkout", "-q", self.head)
self.assert_base(
self.base, GITHUB_EVENT_NAME="pull_request", BASE_REVISION=target
)

def test_fetches_missing_base_commit(self):
remote = self.root / "remote"
subprocess.run(["git", "clone", "-q", str(self.repo), str(remote)], check=True)
for key, value in (
("user.name", "Test"),
("user.email", "test@example.com"),
("commit.gpgsign", "false"),
("core.hooksPath", str(self.root / "no-hooks")),
):
subprocess.run(["git", "-C", str(remote), "config", key, value], check=True)
subprocess.run(
["git", "-C", str(remote), "checkout", "-qb", "target", self.base],
check=True,
)
subprocess.run(
["git", "-C", str(remote), "commit", "-qm", "remote", "--allow-empty"],
check=True,
)
target = subprocess.check_output(
["git", "-C", str(remote), "rev-parse", "HEAD"], text=True
).strip()
self.git("remote", "add", "origin", str(remote))
self.assert_base(
self.base, GITHUB_EVENT_NAME="pull_request", BASE_REVISION=target
)
self.git("cat-file", "-e", target)

def test_declaration_suite_skips_arm_and_pytest_setup(self):
setup = self.repo / "examples/arm/arm-scratch/setup_path.sh"
setup.parent.mkdir(parents=True)
setup.write_text("exit 91\n")
(self.checker_dir / "pytest-parallelism.sh").write_text("exit 92\n")
self.assert_base(self.base)

def test_other_suite_still_requires_arm_setup(self):
result = self.run_suite("test_pytest_ops_no_target")
self.assertNotEqual(result.returncode, 0)
self.assertIn("Missing", result.stdout)
self.assertIn("setup_path.sh", result.stdout)
self.assertFalse(self.result_file.exists())

def test_all_does_not_run_change_specific_declarations(self):
setup = self.repo / "examples/arm/arm-scratch/setup_path.sh"
setup.parent.mkdir(parents=True)
setup.write_text("")
tools = self.root / "bin"
tools.mkdir()
shell = tools / "sh"
shell.write_text('#!/bin/sh\ncat > "$ENTRYPOINT_COMMANDS"\n')
shell.chmod(0o755)
commands = self.root / "all-commands"
result = self.run_suite(
"all",
PATH=str(tools) + os.pathsep + os.environ["PATH"],
ENTRYPOINT_COMMANDS=str(commands),
)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertNotIn("test_buck_declarations", commands.read_text())
self.assertIn("test_pytest_ops_no_target", commands.read_text())
self.assertFalse(self.result_file.exists())

def test_help_does_not_require_arm_setup(self):
for args in ((), ("help",)):
with self.subTest(args=args):
result = self.run_suite(*args)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertIn("test_buck_declarations", result.stdout)

def test_checker_failure_reaches_caller(self):
result = self.run_suite("test_buck_declarations", CHECKER_EXIT_CODE="7")
self.assertEqual(result.returncode, 7, result.stdout)

def test_self_test_failure_stops_checker(self):
result = self.run_suite("test_buck_declarations", FAIL_CHECKER_SELF_TEST="1")
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertFalse(self.result_file.exists())

def test_invalid_base_stops_checker(self):
result = self.run_suite("test_buck_declarations", BASE_REVISION="missing-base")
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertFalse(self.result_file.exists())


if __name__ == "__main__":
unittest.main()
25 changes: 25 additions & 0 deletions .ci/scripts/unittest-arm-buck2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
set -euo pipefail

# This C++ package does not load the Arm Python tests' internal Buck helpers.
target=//backends/arm/test/vela:vela_external_blocks_test
buck2 test "${target}"
binary=$(buck2 build "${target}" --show-full-simple-output)

# Run each case in a fresh process, as internal CI does, to expose dependencies
# on initialization performed by an earlier case in the same test binary.
cases=$("${binary}" --gtest_list_tests | awk '
/^[^ ]/ { suite = $1 }
/^ [^ ]/ { print suite $1 }
')
if [[ -z "${cases}" ]]; then
echo "No Vela parser tests were discovered" >&2
exit 1
fi
while IFS= read -r test_case; do
"${binary}" --gtest_filter="${test_case}"
done <<< "${cases}"
3 changes: 3 additions & 0 deletions .ci/scripts/unittest-buck2.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -43,6 +44,8 @@ for op in "build" "test"; do
//runtime/executor: //runtime/kernel/... //runtime/platform/...
done

bash .ci/scripts/unittest-arm-buck2.sh

# Build only without testing
buck2 build //codegen/tools/... \
//exir/_serialize/test/... \
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/pull.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

name: pull

on:
Expand Down Expand Up @@ -994,6 +999,26 @@ jobs:
exit 1
fi

test-arm-backend-buck-declarations:
name: test-arm-backend-buck-declarations
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Test declaration entrypoint
run: python -m unittest discover -s .ci/scripts/tests -p test_arm_buck_entrypoint.py
- name: Check changed Arm Python declarations
env:
BASE_REVISION: ${{ github.event.pull_request.base.sha || github.event.before }}
run: backends/arm/test/test_arm_backend.sh test_buck_declarations

test-arm-backend-no-driver:
needs: docker-image
name: test-arm-backend-no-driver
Expand Down
56 changes: 56 additions & 0 deletions backends/arm/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<!--
Copyright 2026 Arm Limited and/or its affiliates.

This source code is licensed under the BSD-style license found in the
LICENSE file in the root directory of this source tree.
-->

# ExecuTorch Arm&reg; Backend

This subtree contains the Arm&reg; Backend implementation for ExecuTorch.
Expand Down Expand Up @@ -402,6 +409,7 @@ Below is an overview of some of the testing options this script provides:

| Command | Description |
| ---------------------------------------------------- | ------------------------------------------------------------ |
| `test_arm_backend.sh test_buck_declarations` | Checks changed Arm Python Buck declarations without Arm tool setup. |
| `test_arm_backend.sh test_pytest_ops_no_target` | Runs operator unit tests for non-target specific use-cases. |
| `test_arm_backend.sh test_pytest_models_no_target` | Runs model unit tests for non-target specific use-cases. |
| `test_arm_backend.sh test_pytest_ops_tosa` | Runs operator unit tests for TOSA specific use-cases. |
Expand Down Expand Up @@ -471,6 +479,54 @@ Installing model test dependencies is a standalone process. The script installs
only the dependencies needed for model tests, skipping all other setup
procedures.

### Buck2 CI coverage

The GitHub Buck2 job runs the Vela parser C++ tests together and then runs each
case in a separate process, so a test cannot depend on runtime initialization by
an earlier case. With the revision-pinned Buck2 on your `PATH`, run this check from
the repository root:

```bash
bash .ci/scripts/unittest-arm-buck2.sh
```

The separate **test-arm-backend-buck-declarations** job checks for new dependency
and test-registration gaps in the Arm test package. It uses the same entrypoint as
local runs and requires only Git and Python 3, without Arm tool setup or third-party
Python packages:

```bash
backends/arm/test/test_arm_backend.sh test_buck_declarations
```

By default, local runs compare the working tree against `HEAD^`. To check a change
spanning several commits, set `BASE_REVISION` to the commit before your change:

```bash
BASE_REVISION=<base-commit> backends/arm/test/test_arm_backend.sh test_buck_declarations
```

GitHub pull requests compare against the merge base with the PR's target branch;
pushes use the previous commit from the event. Manual runs and new branches use
`HEAD^`. Declaration checks are excluded from the `all` suite because they need a
change-specific baseline.

This static check follows supported in-repository Python library declarations,
including transitive dependencies, and compares findings against the base revision.
Existing findings do not fail the job. New confirmed missing dependencies,
unregistered Arm tests, and unsupported declarations under `backends/arm/` fail
the check. Unsupported declarations outside Arm are reported as warnings with
their declaration file and unsupported construct; dependency gaps that cannot be
proven because of those declarations are also warnings. A warning becoming an
error is treated as a new regression.

Arm backend contributors maintain this checker alongside the Arm Buck declarations
and its regression tests in `.ci/scripts/tests/test_arm_buck_declarations.py`.
Moving the Arm Python tests onto the public runtime wrapper would allow Buck2
queries to replace this limited static analysis. The checker does not run Meta's
internal Python Buck tests or validate third-party dependencies. Those internal
tests remain necessary for full packaged-test coverage.

## Using git hooks

The repo-wide pre-commit hook (lintrunner + torch_pin sync) is installed automatically
Expand Down
2 changes: 1 addition & 1 deletion backends/arm/runtime/tests/ethos-u/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ message(
add_executable(
vela_external_blocks_test
"${EXECUTORCH_ROOT}/backends/arm/runtime/VelaBinStream.cpp"
"${EXECUTORCH_ROOT}/backends/arm/test/vela_external_blocks_test.cpp"
"${EXECUTORCH_ROOT}/backends/arm/test/vela/vela_external_blocks_test.cpp"
)
target_link_libraries(
vela_external_blocks_test PRIVATE executorch_core GTest::gtest_main
Expand Down
9 changes: 8 additions & 1 deletion backends/arm/test/targets.bzl
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# load("//caffe2/test/fb:defs.bzl", "define_tests")
load("@fbcode_macros//build_defs:python_pytest.bzl", "python_pytest")
load("@bazel_skylib//lib:paths.bzl", "paths")
Expand Down Expand Up @@ -199,9 +204,11 @@ def define_arm_tests():
] if test_file == "misc/test_diff_pte.py" else []),
)

# Preserve the label used by internal CI to select individual Vela cases.
# OSS CI uses the separate vela package to avoid internal-only Python loads.
runtime.cxx_test(
name = "vela_external_blocks_test",
srcs = ["vela_external_blocks_test.cpp"],
srcs = ["//executorch/backends/arm/test/vela:vela_external_blocks_test.cpp"],
deps = [
"//executorch/backends/arm/runtime:vela_bin_stream",
"//executorch/runtime/core:core",
Expand Down
Loading
Loading