blob: ab59d8874563f9739bbfeae46ca2a5c2ee4665f7 [file] [log] [blame] [edit]
# Copyright (C) 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import dataclasses
import pathlib
import re
import shutil
import subprocess
import sys
import kunit_parser
@dataclasses.dataclass
class AdbDeviceHandle:
"""Handle for ADB connection to a device."""
adb_path: pathlib.Path
device: str
def __post_init__(self):
# Ensure device is connected
subprocess.check_call([self.adb_path, 'connect', self.device])
def check_call(self, *args) -> None:
subprocess.check_call(
[self.adb_path, '-s', self.device, 'shell', 'su', '0', *args]
)
def call(self, *args) -> int:
return subprocess.call(
[self.adb_path, '-s', self.device, 'shell', 'su', '0', *args]
)
def check_output(self, *args) -> str:
return subprocess.check_output(
[self.adb_path, '-s', self.device, 'shell', 'su', '0', *args],
text=True,
)
def push(self, local: str, remote: str) -> None:
subprocess.check_call(
[self.adb_path, '-s', self.device, 'push', local, remote]
)
class TestRunner:
_ADB_DEVICE_RE = re.compile(r'(?P<device>\S+)\s+device')
def __init__(
self,
name: str,
adb_path: pathlib.Path,
modules: list[pathlib.Path],
device: str | None,
) -> None:
self._name = name
self._adb_path = adb_path
self._modules = modules
self._device = device
self._module_name_by_pathstem: dict[str, str] = {}
self._debugfs_available = True
self._device_handle: AdbDeviceHandle | None = None
def __enter__(self):
# Ensure ADB server is running
subprocess.check_call([self._adb_path, 'start-server'])
# Find an ADB device if none is provided
if self._device is None:
self._device = self._find_device()
# Create a device handle
self._device_handle = AdbDeviceHandle(
adb_path=self._adb_path,
device=self._device,
)
return self
def __exit__(self, type, value, traceback):
del type, value, traceback
# Clean up modules in reverse order since modules might be inter-dependent
for module in reversed(self._modules):
if module_name := self._module_name_by_pathstem.get(module.stem):
self._device_handle.call('rmmod', module_name)
self._device_handle.call('rm', '-rf', f'/data/local/tmp/{self._name}')
# Unmount debugfs is needed if it was not mounted
if not self._debugfs_available:
self._device_handle.call('umount', '/sys/kernel/debug')
def run_test(self) -> kunit_parser.Test:
# Mount debugfs if it is not mounted
if self._device_handle.call('mountpoint', '/sys/kernel/debug') != 0:
self._device_handle.check_call(
'mount', '-t', 'debugfs', 'debugfs', '/sys/kernel/debug'
)
self._debugfs_available = False
# Create temporary directory for pushing test module
self._device_handle.check_call(
'mkdir', '-p', f'/data/local/tmp/{self._name}'
)
# Push and install modules to device
for module in self._modules:
remote_path = f'/data/local/tmp/{self._name}/{module.name}'
self._device_handle.push(module, remote_path)
self._module_name_by_pathstem[module.stem] = (
self._device_handle.check_output(
'modinfo', '-F', 'name', remote_path
).splitlines()[0]
)
self._device_handle.check_call('insmod', remote_path)
# Extract and parse test results
results = self._device_handle.check_output(
'cat', f'/sys/kernel/debug/kunit/{self._name}/results'
)
return kunit_parser.parse_run_tests(results.splitlines())
def _find_device(self) -> str:
devices_output = subprocess.check_output(
[self._adb_path, 'devices'], text=True
)
devices = []
for d in devices_output.splitlines():
if match := TestRunner._ADB_DEVICE_RE.match(d):
devices.append(match.group('device'))
if len(devices) == 0:
raise ValueError('No ADB devices provided or found')
if len(devices) > 1:
raise ValueError(
'More than 1 ADB devices found, please specify device using'
' --device option'
)
return devices[0]
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--name')
parser.add_argument(
'--adb-path', default=shutil.which('adb'), type=pathlib.Path
)
parser.add_argument('--modules', nargs='*', default=[], type=pathlib.Path)
parser.add_argument('--device', default=None)
args = parser.parse_args()
if not args.adb_path.is_file():
raise ValueError(
'ADB not found. Please provide correct ADB path using --adb-path'
' option'
)
# Filter out non-module files
# TODO(b/381406396): Remove this once we have a mechanism to filter out
# non-module files in the build system.
filtered_modules = [m for m in args.modules if m.suffix == '.ko']
with TestRunner(
name=args.name,
adb_path=args.adb_path,
modules=filtered_modules,
device=args.device,
) as tr:
test_result = tr.run_test()
if not test_result.ok_status():
sys.exit(f'Test {args.name} failed')