1#!/usr/bin/env python3 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17# 18# This test script to be used by the build server. 19# It is supposed to be executed from trusty root directory 20# and expects the following environment variables: 21# 22"""Parse trusty build and test configuration files.""" 23 24import argparse 25import os 26import re 27from enum import StrEnum, auto 28from typing import List, Dict, Optional 29 30script_dir = os.path.dirname(os.path.abspath(__file__)) 31 32class PortType(StrEnum): 33 TEST = auto() 34 BENCHMARK = auto() 35 36 37class TrustyBuildConfigProject(object): 38 """Stores build enabled status and test lists for a project 39 40 Attributes: 41 build: A boolean indicating if project should be built be default. 42 tests: A list of commands to run to test this project. 43 also_build: Set of project to also build if building this one. 44 """ 45 46 def __init__(self): 47 """Inits TrustyBuildConfigProject with an empty test list and no 48 build.""" 49 self.build = False 50 self.tests = [] 51 self.also_build = {} 52 self.signing_keys = None 53 54 55class TrustyPortTestFlags(object): 56 """Stores need flags for a test or provide flags for a test environment.""" 57 58 ALLOWED_FLAGS = {"android", "storage_boot", "storage_full", 59 "smp4", "abl", "tablet"} 60 61 def __init__(self, **flags): 62 self.flags = set() 63 self.set(**flags) 64 65 def set(self, **flags): 66 """Set flags.""" 67 for name, arg in flags.items(): 68 if name in self.ALLOWED_FLAGS: 69 if arg: 70 self.flags.add(name) 71 else: 72 self.flags.discard(name) 73 else: 74 raise TypeError("Unexpected flag: " + name) 75 76 def match_provide(self, provide): 77 return self.flags.issubset(provide.flags) 78 79 80class TrustyArchiveBuildFile(object): 81 """Copy a file to archive directory after a build.""" 82 def __init__(self, src, dest, optional): 83 self.src = src 84 self.dest = dest 85 self.optional = optional 86 87 88class TrustyTest(object): 89 """Stores a pair of a test name and a command to run""" 90 def __init__(self, name, command, enabled, port_type = PortType.TEST): 91 self.name = name 92 self.command = command 93 self.enabled = enabled 94 self.port_type = port_type 95 96 def type(self, port_type): 97 self.port_type = PortType(port_type) # ensure we have a valid port type 98 return self 99 100class TrustyHostTest(TrustyTest): 101 """Stores a pair of a test name and a command to run on host. 102 103 TrustyHostTest is for tests that run solely on the host. 104 This is different from TrustyHostcommandTest, which runs tests on 105 the host that issue commands to a running Android device via adb. 106 """ 107 108 class TrustyHostTestFlags: 109 """Enable needs to be matched with provides without special casing""" 110 111 @staticmethod 112 def match_provide(_): 113 # cause host test to be filtered out if they appear in a boottests 114 # or androidportests environment which provides a set of features. 115 return False 116 117 need = TrustyHostTestFlags() 118 119 120class TrustyAndroidTest(TrustyTest): 121 """Stores a test name and command to run inside Android""" 122 123 def __init__(self, name, command, need=None, 124 port_type=PortType.TEST, enabled=True, nameprefix="", 125 runargs=(), timeout=None): 126 nameprefix = nameprefix + "android-test:" 127 cmd = ["run", "--headless", "--shell-command", command] 128 if timeout: 129 cmd += ['--timeout', str(timeout)] 130 if runargs: 131 cmd += list(runargs) 132 super().__init__(nameprefix + name, cmd, enabled, port_type) 133 self.shell_command = command 134 if need: 135 self.need = need 136 else: 137 self.need = TrustyPortTestFlags() 138 139 def needs(self, **need): 140 self.need.set(**need) 141 return self 142 143class TrustyHostcommandTest(TrustyTest): 144 """Stores a test name and command to run on the host. 145 146 TrustyHostcommandTest runs tests on the host that issue commands to a 147 running Android device via adb. This is different from TrustyHostTest, 148 which is for tests that run solely on the host. 149 """ 150 151 def __init__(self, name, command, need=None, 152 port_type=PortType.TEST, enabled=True, nameprefix="", 153 timeout=None): 154 nameprefix = nameprefix + "hostcommandtest:" 155 156 # cmd stores arguments that are passed to the test runner in qemu.py. 157 # The first item in the list isn't used. 158 cmd = [None, "--headless", "--host-command", command] 159 if timeout: 160 cmd += ['--timeout', str(timeout)] 161 super().__init__(nameprefix + name, cmd, enabled, port_type) 162 163 if need: 164 self.need = need 165 else: 166 self.need = TrustyPortTestFlags() 167 168 def needs(self, **need): 169 self.need.set(**need) 170 return self 171 172 173class TrustyPortTest(TrustyTest): 174 """Stores a trusty port name for a test to run.""" 175 176 def __init__(self, port, port_type=PortType.TEST, enabled=True, 177 timeout=None): 178 super().__init__(port, None, enabled, port_type) 179 self.port = port 180 self.need = TrustyPortTestFlags() 181 self.timeout = timeout 182 183 def needs(self, **need): 184 self.need.set(**need) 185 return self 186 187 def into_androidporttest(self, cmdargs, **kwargs): 188 cmdargs = list(cmdargs) 189 cmd = " ".join(["/vendor/bin/trusty-ut-ctrl", self.port] + cmdargs) 190 return TrustyAndroidTest(self.name, cmd, self.need, self.port_type, 191 self.enabled, timeout=self.timeout, **kwargs) 192 193 def into_bootporttest(self) -> TrustyTest: 194 cmd = ["run", "--headless", "--boot-test", self.port] 195 cmd += ['--timeout', str(self.timeout)] if self.timeout else [] 196 return TrustyTest("boot-test:" + self.port, cmd, self.enabled, 197 self.port_type) 198 199 200class TrustyCommand: 201 """Base class for all types of commands that are *not* tests""" 202 203 def __init__(self, name): 204 self.name = name 205 self.enabled = True 206 # avoids special cases in list_config 207 self.command = [] 208 # avoids special cases in porttest_match 209 self.need = TrustyPortTestFlags() 210 211 def needs(self, **_): 212 """Allows commands to be used inside a needs block.""" 213 return self 214 215 def into_androidporttest(self, **_): 216 return self 217 218 def into_bootporttest(self): 219 return self 220 221 222class RebootMode(StrEnum): 223 REGULAR = "reboot" 224 FACTORY_RESET = "reboot (with factory reset)" 225 FULL_WIPE = "reboot (with full wipe)" 226 227 def factory_reset(self) -> bool: 228 """Whether this reboot includes a factory reset. 229 This function exists because we can't make the test runner module depend 230 on types defined here, so its function args have to be builtins. 231 """ 232 match self: 233 case RebootMode.REGULAR: 234 return False 235 case RebootMode.FACTORY_RESET: 236 return True 237 case RebootMode.FULL_WIPE: 238 return True 239 240 def full_wipe(self) -> bool: 241 """Whether this reboot includes a an RPMB wipe. 242 This function exists because we can't make the test runner module depend 243 on types defined here, so its function args have to be builtins. 244 """ 245 match self: 246 case RebootMode.REGULAR: 247 return False 248 case RebootMode.FACTORY_RESET: 249 return False 250 case RebootMode.FULL_WIPE: 251 return True 252 253 254class TrustyRebootCommand(TrustyCommand): 255 """Marker object which causes the test environment to be rebooted before the 256 next test is run. Used to reset the test environment and to test storage. 257 """ 258 def __init__(self, mode: RebootMode = RebootMode.FACTORY_RESET): 259 super().__init__(mode) 260 self.mode = mode 261 262class TrustyPrintCommand(TrustyCommand): 263 264 def msg(self) -> str: 265 return self.name 266 267class TrustyCompositeTest(TrustyTest): 268 """Stores a sequence of tests that must execute in order""" 269 270 def __init__(self, name: str, 271 sequence: List[TrustyPortTest | TrustyCommand], 272 enabled=True): 273 super().__init__(name, [], enabled) 274 self.sequence = sequence 275 flags = set() 276 for subtest in sequence: 277 flags.update(subtest.need.flags) 278 self.need = TrustyPortTestFlags(**{flag: True for flag in flags}) 279 280 def needs(self, **need): 281 self.need.set(**need) 282 return self 283 284 def into_androidporttest(self, **kwargs): 285 # because the needs of the composite test is the union of the needs of 286 # its subtests, we do not need to filter out any subtests; all needs met 287 self.sequence = [subtest.into_androidporttest(**kwargs) 288 for subtest in self.sequence] 289 return self 290 291 def into_bootporttest(self): 292 # similarly to into_androidporttest, we do not need to filter out tests 293 self.sequence = [subtest.into_bootporttest() 294 for subtest in self.sequence] 295 return self 296 297 298class TrustyBuildConfig(object): 299 """Trusty build and test configuration file parser.""" 300 301 def __init__(self, config_file=None, debug=False, android=None): 302 """Inits TrustyBuildConfig. 303 304 Args: 305 config_file: Optional config file path. If omitted config file is 306 found relative to script directory. 307 debug: Optional boolean value. Set to True to enable debug messages. 308 """ 309 self.debug = debug 310 self.android = android 311 self.projects = {} 312 self.dist = [] 313 self.default_signing_keys = [] 314 self.doc_files = [] 315 316 if config_file is None: 317 config_file = os.path.join(script_dir, "build-config") 318 self.read_config_file(config_file) 319 320 def read_config_file(self, path, optional=False): 321 """Main parser function called constructor or recursively by itself.""" 322 if optional and not os.path.exists(path): 323 if self.debug: 324 print("Skipping optional config file:", path) 325 return [] 326 327 if self.debug: 328 print("Reading config file:", path) 329 330 config_dir = os.path.dirname(path) 331 332 def _flatten_list(inp, out): 333 for obj in inp: 334 if isinstance(obj, list): 335 _flatten_list(obj, out) 336 else: 337 out.append(obj) 338 339 def flatten_list(inp): 340 out = [] 341 _flatten_list(inp, out) 342 return out 343 344 def include(path, optional=False): 345 """Process include statement in config file.""" 346 if self.debug: 347 print("include", path, "optional", optional) 348 if path.startswith("."): 349 path = os.path.join(config_dir, path) 350 return self.read_config_file(path=path, optional=optional) 351 352 def build(projects, enabled=True, dist=None): 353 """Process build statement in config file.""" 354 for project_name in projects: 355 if self.debug: 356 print("build", project_name, "enabled", enabled) 357 project = self.get_project(project_name) 358 project.build = bool(enabled) 359 if dist: 360 for item in dist: 361 assert isinstance(item, TrustyArchiveBuildFile), item 362 self.dist.append(item) 363 364 def builddep(projects, needs): 365 """Process build statement in config file.""" 366 for project_name in projects: 367 project = self.get_project(project_name) 368 for project_dep_name in needs: 369 project_dep = self.get_project(project_dep_name) 370 if self.debug: 371 print("build", project_name, "needs", project_dep_name) 372 project.also_build[project_dep_name] = project_dep 373 374 def archive(src, dest=None, optional=False): 375 return TrustyArchiveBuildFile(src, dest, optional) 376 377 def testmap(projects, tests=()): 378 """Process testmap statement in config file.""" 379 for project_name in projects: 380 if self.debug: 381 print("testmap", project_name, "build", build) 382 for test in tests: 383 print(test) 384 project = self.get_project(project_name) 385 project.tests += flatten_list(tests) 386 387 def hosttest(host_cmd, enabled=True, repeat=1): 388 cmd = ["host_tests/" + host_cmd] 389 # TODO: assumes that host test is always a googletest 390 if repeat > 1: 391 cmd.append(f"--gtest_repeat={repeat}") 392 return TrustyHostTest("host-test:" + host_cmd, cmd, enabled) 393 394 def hosttests(tests): 395 return [test for test in flatten_list(tests) 396 if isinstance(test, TrustyHostTest)] 397 398 def porttest_match(test, provides): 399 return test.need.match_provide(provides) 400 401 def porttests_filter(tests, provides): 402 return [test for test in flatten_list(tests) 403 if porttest_match(test, provides)] 404 405 def boottests(port_tests, provides=None): 406 if provides is None: 407 provides = TrustyPortTestFlags(storage_boot=True, 408 smp4=True) 409 return [test.into_bootporttest() 410 for test in porttests_filter(port_tests, provides)] 411 412 def androidporttests(port_tests, provides=None, nameprefix="", 413 cmdargs=(), runargs=()): 414 nameprefix = nameprefix + "android-port-test:" 415 if provides is None: 416 provides = TrustyPortTestFlags(android=True, 417 storage_boot=True, 418 storage_full=True, 419 smp4=True, 420 abl=True, 421 tablet=True) 422 423 return [test.into_androidporttest(nameprefix=nameprefix, 424 cmdargs=cmdargs, 425 runargs=runargs) 426 for test in porttests_filter(port_tests, provides)] 427 428 def needs(tests, *args, **kwargs): 429 return [ 430 test.needs(*args, **kwargs) 431 for test in flatten_list(tests) 432 ] 433 434 def devsigningkeys( 435 default_key_paths: List[str], 436 project_overrides: Optional[Dict[str, List[str]]] = None): 437 self.default_signing_keys.extend(default_key_paths) 438 if project_overrides is None: 439 return 440 441 for project_name, overrides in project_overrides.items(): 442 project = self.get_project(project_name) 443 if project.signing_keys is None: 444 project.signing_keys = [] 445 project.signing_keys.extend(overrides) 446 447 def docs(doc_files: List[str]): 448 self.doc_files.extend(doc_files) 449 450 file_format = { 451 "BENCHMARK": PortType.BENCHMARK, 452 "TEST": PortType.TEST, 453 "include": include, 454 "build": build, 455 "builddep": builddep, 456 "archive": archive, 457 "testmap": testmap, 458 "hosttest": hosttest, 459 "porttest": TrustyPortTest, 460 "compositetest": TrustyCompositeTest, 461 "porttestflags": TrustyPortTestFlags, 462 "hosttests": hosttests, 463 "boottests": boottests, 464 "androidtest": TrustyAndroidTest, 465 "hostcommandtest": TrustyHostcommandTest, 466 "androidporttests": androidporttests, 467 "needs": needs, 468 "reboot": TrustyRebootCommand, 469 "RebootMode": RebootMode, 470 "devsigningkeys": devsigningkeys, 471 "print": TrustyPrintCommand, 472 "docs": docs, 473 } 474 475 with open(path, encoding="utf8") as f: 476 code = compile(f.read(), path, "eval") 477 config = eval(code, file_format) # pylint: disable=eval-used 478 return flatten_list(config) 479 480 def get_project(self, project): 481 """Return TrustyBuildConfigProject entry for a project.""" 482 if project not in self.projects: 483 self.projects[project] = TrustyBuildConfigProject() 484 return self.projects[project] 485 486 def get_projects(self, build=None, have_tests=None): 487 """Return a list of projects. 488 489 Args: 490 build: If True only return projects that should be built. If False 491 only return projects that should not be built. If None return 492 both projects that should be built and not be built. (default 493 None). 494 have_tests: If True only return projects that have tests. If False 495 only return projects that don't have tests. If None return 496 projects regardless if they have tests. (default None). 497 """ 498 499 def match(item): 500 """filter function for get_projects.""" 501 project = self.projects[item] 502 503 return ((build is None or build == project.build) and 504 (have_tests is None or 505 have_tests == bool(project.tests))) 506 507 return (project for project in sorted(self.projects.keys()) 508 if match(project)) 509 510 def signing_keys(self, project_name: str): 511 project_specific_keys = self.get_project(project_name).signing_keys 512 if project_specific_keys is None: 513 return self.default_signing_keys 514 return project_specific_keys 515 516 517def list_projects(args): 518 """Read config file and print a list of projects. 519 520 See TrustyBuildConfig.get_projects for filtering options. 521 522 Args: 523 args: Program arguments. 524 """ 525 config = TrustyBuildConfig(config_file=args.file, debug=args.debug) 526 for project in sorted(config.get_projects(**dict(args.filter))): 527 print(project) 528 529 530def list_config(args): 531 """Read config file and print all project and tests.""" 532 config = TrustyBuildConfig(config_file=args.file, debug=args.debug) 533 print("Projects:") 534 for project_name, project in sorted(config.projects.items()): 535 print(" " + project_name + ":") 536 print(" Build:", project.build) 537 print(" Tests:") 538 for test in project.tests: 539 print(" " + test.name + ":") 540 print(" " + str(test.command)) 541 542 for build in [True, False]: 543 print() 544 print("Build:" if build else "Don't build:") 545 for tested in [True, False]: 546 projects = config.get_projects(build=build, have_tests=tested) 547 for project in sorted(projects): 548 print(" " + project + ":") 549 project_config = config.get_project(project) 550 for test in project_config.tests: 551 print(" " + test.name) 552 if projects and not tested: 553 print(" No tests") 554 555 556def any_test_name(regex, tests): 557 """Checks the name of all tests in a list for a regex. 558 559 This is intended only as part of the selftest facility, do not use it 560 to decide how to consider actual tests. 561 562 Args: 563 tests: List of tests to check the names of 564 regex: Regular expression to check them for (as a string) 565 """ 566 567 return any(re.match(regex, test.name) is not None for test in tests) 568 569 570def has_host(tests): 571 """Checks for a host test in the provided tests by name. 572 573 This is intended only as part of the selftest facility, do not use it 574 to decide how to consider actual tests. 575 576 Args: 577 tests: List of tests to check for host tests 578 """ 579 return any_test_name("host-test:", tests) 580 581 582def has_unit(tests): 583 """Checks for a unit test in the provided tests by name. 584 585 This is intended only as part of the selftest facility, do not use it 586 to decide how to consider actual tests. 587 588 Args: 589 tests: List of tests to check for unit tests 590 """ 591 return any_test_name("boot-test:", tests) 592 593 594def test_config(args): 595 """Test config file parser. 596 597 Uses a test config file where all projects have names that describe if they 598 should be built and if they have tests. 599 600 Args: 601 args: Program arguments. 602 """ 603 config_file = os.path.join(script_dir, "trusty_build_config_self_test_main") 604 config = TrustyBuildConfig(config_file=config_file, debug=args.debug) 605 606 projects_build = {} 607 608 project_regex = re.compile( 609 r"self_test\.build_(yes|no)\.tests_(none|host|unit|both)\..*") 610 611 for build in [None, True, False]: 612 projects_build[build] = {} 613 for tested in [None, True, False]: 614 projects = list(config.get_projects(build=build, have_tests=tested)) 615 projects_build[build][tested] = projects 616 if args.debug: 617 print("Build", build, "tested", tested, "count", len(projects)) 618 assert projects 619 for project in projects: 620 if args.debug: 621 print("-", project) 622 m = project_regex.match(project) 623 assert m 624 if build is not None: 625 assert m.group(1) == ("yes" if build else "no") 626 if tested is not None: 627 if tested: 628 assert (m.group(2) == "host" or 629 m.group(2) == "unit" or 630 m.group(2) == "both") 631 else: 632 assert m.group(2) == "none" 633 634 assert(projects_build[build][None] == 635 sorted(projects_build[build][True] + 636 projects_build[build][False])) 637 for tested in [None, True, False]: 638 assert(projects_build[None][tested] == 639 sorted(projects_build[True][tested] + 640 projects_build[False][tested])) 641 642 print("get_projects test passed") 643 644 reboot_seen = False 645 646 def check_test(i, test): 647 match test: 648 case TrustyTest(): 649 host_m = re.match(r"host-test:self_test.*\.(\d+)", 650 test.name) 651 unit_m = re.match(r"boot-test:self_test.*\.(\d+)", 652 test.name) 653 if args.debug: 654 print(project, i, test.name) 655 m = host_m or unit_m 656 assert m 657 assert m.group(1) == str(i + 1) 658 case TrustyRebootCommand(): 659 assert False, "Reboot outside composite command" 660 case _: 661 assert False, "Unexpected test type" 662 663 def check_subtest(i, test): 664 nonlocal reboot_seen 665 match test: 666 case TrustyRebootCommand(): 667 reboot_seen = True 668 case _: 669 check_test(i, test) 670 671 for project_name in config.get_projects(): 672 project = config.get_project(project_name) 673 if args.debug: 674 print(project_name, project) 675 m = project_regex.match(project_name) 676 assert m 677 kind = m.group(2) 678 if kind == "both": 679 assert has_host(project.tests) 680 assert has_unit(project.tests) 681 elif kind == "unit": 682 assert not has_host(project.tests) 683 assert has_unit(project.tests) 684 elif kind == "host": 685 assert has_host(project.tests) 686 assert not has_unit(project.tests) 687 elif kind == "none": 688 assert not has_host(project.tests) 689 assert not has_unit(project.tests) 690 else: 691 assert False, "Unknown project kind" 692 693 for i, test in enumerate(project.tests): 694 match test: 695 case TrustyCompositeTest(): 696 # because one of its subtest needs storage_boot, 697 # the composite test should similarly need it 698 assert "storage_boot" in test.need.flags 699 for subtest in test.sequence: 700 check_subtest(i, subtest) 701 case _: 702 check_test(i, test) 703 704 assert reboot_seen 705 706 print("get_tests test passed") 707 708 709def main(): 710 top = os.path.abspath(os.path.join(script_dir, "../../../../..")) 711 os.chdir(top) 712 713 parser = argparse.ArgumentParser() 714 parser.add_argument("-d", "--debug", action="store_true") 715 parser.add_argument("--file") 716 # work around for https://bugs.python.org/issue16308 717 parser.set_defaults(func=lambda args: parser.print_help()) 718 subparsers = parser.add_subparsers() 719 720 parser_projects = subparsers.add_parser("projects", 721 help="list project names") 722 723 group = parser_projects.add_mutually_exclusive_group() 724 group.add_argument("--with-tests", action="append_const", 725 dest="filter", const=("have_tests", True), 726 help="list projects that have tests") 727 group.add_argument("--without-tests", action="append_const", 728 dest="filter", const=("have_tests", False), 729 help="list projects that don't have tests") 730 731 group = parser_projects.add_mutually_exclusive_group() 732 group.add_argument("--all", action="append_const", 733 dest="filter", const=("build", None), 734 help="include disabled projects") 735 group.add_argument("--disabled", action="append_const", 736 dest="filter", const=("build", False), 737 help="only list disabled projects") 738 parser_projects.set_defaults(func=list_projects, filter=[("build", True)]) 739 740 parser_config = subparsers.add_parser("config", help="dump config") 741 parser_config.set_defaults(func=list_config) 742 743 parser_config = subparsers.add_parser("selftest", help="test config parser") 744 parser_config.set_defaults(func=test_config) 745 746 args = parser.parse_args() 747 args.func(args) 748 749 750if __name__ == "__main__": 751 main() 752