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 class TrustyHostTestFlags: 104 """Enable needs to be matched with provides without special casing""" 105 106 @staticmethod 107 def match_provide(_): 108 # cause host test to be filtered out if they appear in a boottests 109 # or androidportests environment which provides a set of features. 110 return False 111 112 need = TrustyHostTestFlags() 113 114 115class TrustyAndroidTest(TrustyTest): 116 """Stores a test name and command to run inside Android""" 117 118 def __init__(self, name, command, need=None, 119 port_type=PortType.TEST, enabled=True, nameprefix="", 120 runargs=(), timeout=None): 121 nameprefix = nameprefix + "android-test:" 122 cmd = ["run", "--headless", "--shell-command", command] 123 if timeout: 124 cmd += ['--timeout', str(timeout)] 125 if runargs: 126 cmd += list(runargs) 127 super().__init__(nameprefix + name, cmd, enabled, port_type) 128 self.shell_command = command 129 if need: 130 self.need = need 131 else: 132 self.need = TrustyPortTestFlags() 133 134 def needs(self, **need): 135 self.need.set(**need) 136 return self 137 138 139class TrustyPortTest(TrustyTest): 140 """Stores a trusty port name for a test to run.""" 141 142 def __init__(self, port, port_type=PortType.TEST, enabled=True, 143 timeout=None): 144 super().__init__(port, None, enabled, port_type) 145 self.port = port 146 self.need = TrustyPortTestFlags() 147 self.timeout = timeout 148 149 def needs(self, **need): 150 self.need.set(**need) 151 return self 152 153 def into_androidporttest(self, cmdargs, **kwargs): 154 cmdargs = list(cmdargs) 155 cmd = " ".join(["/vendor/bin/trusty-ut-ctrl", self.port] + cmdargs) 156 return TrustyAndroidTest(self.name, cmd, self.need, self.port_type, 157 self.enabled, timeout=self.timeout, **kwargs) 158 159 def into_bootporttest(self) -> TrustyTest: 160 cmd = ["run", "--headless", "--boot-test", self.port] 161 cmd += ['--timeout', str(self.timeout)] if self.timeout else [] 162 return TrustyTest("boot-test:" + self.port, cmd, self.enabled, 163 self.port_type) 164 165 166class TrustyCommand: 167 """Base class for all types of commands that are *not* tests""" 168 169 def __init__(self, name): 170 self.name = name 171 self.enabled = True 172 # avoids special cases in list_config 173 self.command = [] 174 # avoids special cases in porttest_match 175 self.need = TrustyPortTestFlags() 176 177 def needs(self, **_): 178 """Allows commands to be used inside a needs block.""" 179 return self 180 181 def into_androidporttest(self, **_): 182 return self 183 184 def into_bootporttest(self): 185 return self 186 187 188class TrustyRebootCommand(TrustyCommand): 189 """Marker object which causes the test environment to be rebooted before the 190 next test is run. Used to reset the test environment and to test storage. 191 192 TODO: The current qemu.py script does a factory reset as part a reboot. 193 We probably want a parameter or separate command to control that. 194 """ 195 def __init__(self): 196 super().__init__("reboot command") 197 198 199class TrustyCompositeTest(TrustyTest): 200 """Stores a sequence of tests that must execute in order""" 201 202 def __init__(self, name: str, 203 sequence: List[TrustyPortTest | TrustyCommand], 204 enabled=True): 205 super().__init__(name, [], enabled) 206 self.sequence = sequence 207 flags = set() 208 for subtest in sequence: 209 flags.update(subtest.need.flags) 210 self.need = TrustyPortTestFlags(**{flag: True for flag in flags}) 211 212 def needs(self, **need): 213 self.need.set(**need) 214 return self 215 216 def into_androidporttest(self, **kwargs): 217 # because the needs of the composite test is the union of the needs of 218 # its subtests, we do not need to filter out any subtests; all needs met 219 self.sequence = [subtest.into_androidporttest(**kwargs) 220 for subtest in self.sequence] 221 return self 222 223 def into_bootporttest(self): 224 # similarly to into_androidporttest, we do not need to filter out tests 225 self.sequence = [subtest.into_bootporttest() 226 for subtest in self.sequence] 227 return self 228 229 230class TrustyBuildConfig(object): 231 """Trusty build and test configuration file parser.""" 232 233 def __init__(self, config_file=None, debug=False, android=None): 234 """Inits TrustyBuildConfig. 235 236 Args: 237 config_file: Optional config file path. If omitted config file is 238 found relative to script directory. 239 debug: Optional boolean value. Set to True to enable debug messages. 240 """ 241 self.debug = debug 242 self.android = android 243 self.projects = {} 244 self.dist = [] 245 self.default_signing_keys = [] 246 if config_file is None: 247 config_file = os.path.join(script_dir, "build-config") 248 self.read_config_file(config_file) 249 250 def read_config_file(self, path, optional=False): 251 """Main parser function called constructor or recursively by itself.""" 252 if optional and not os.path.exists(path): 253 if self.debug: 254 print("Skipping optional config file:", path) 255 return [] 256 257 if self.debug: 258 print("Reading config file:", path) 259 260 config_dir = os.path.dirname(path) 261 262 def _flatten_list(inp, out): 263 for obj in inp: 264 if isinstance(obj, list): 265 _flatten_list(obj, out) 266 else: 267 out.append(obj) 268 269 def flatten_list(inp): 270 out = [] 271 _flatten_list(inp, out) 272 return out 273 274 def include(path, optional=False): 275 """Process include statement in config file.""" 276 if self.debug: 277 print("include", path, "optional", optional) 278 if path.startswith("."): 279 path = os.path.join(config_dir, path) 280 return self.read_config_file(path=path, optional=optional) 281 282 def build(projects, enabled=True, dist=None): 283 """Process build statement in config file.""" 284 for project_name in projects: 285 if self.debug: 286 print("build", project_name, "enabled", enabled) 287 project = self.get_project(project_name) 288 project.build = bool(enabled) 289 if dist: 290 for item in dist: 291 assert isinstance(item, TrustyArchiveBuildFile), item 292 self.dist.append(item) 293 294 def builddep(projects, needs): 295 """Process build statement in config file.""" 296 for project_name in projects: 297 project = self.get_project(project_name) 298 for project_dep_name in needs: 299 project_dep = self.get_project(project_dep_name) 300 if self.debug: 301 print("build", project_name, "needs", project_dep_name) 302 project.also_build[project_dep_name] = project_dep 303 304 def archive(src, dest=None, optional=False): 305 return TrustyArchiveBuildFile(src, dest, optional) 306 307 def testmap(projects, tests=()): 308 """Process testmap statement in config file.""" 309 for project_name in projects: 310 if self.debug: 311 print("testmap", project_name, "build", build) 312 for test in tests: 313 print(test) 314 project = self.get_project(project_name) 315 project.tests += flatten_list(tests) 316 317 def hosttest(host_cmd, enabled=True, repeat=1): 318 cmd = ["host_tests/" + host_cmd] 319 # TODO: assumes that host test is always a googletest 320 if repeat > 1: 321 cmd.append(f"--gtest_repeat={repeat}") 322 return TrustyHostTest("host-test:" + host_cmd, cmd, enabled) 323 324 def hosttests(tests): 325 return [test for test in flatten_list(tests) 326 if isinstance(test, TrustyHostTest)] 327 328 def porttest_match(test, provides): 329 return test.need.match_provide(provides) 330 331 def porttests_filter(tests, provides): 332 return [test for test in flatten_list(tests) 333 if porttest_match(test, provides)] 334 335 def boottests(port_tests, provides=None): 336 if provides is None: 337 provides = TrustyPortTestFlags(storage_boot=True, 338 smp4=True) 339 return [test.into_bootporttest() 340 for test in porttests_filter(port_tests, provides)] 341 342 def androidporttests(port_tests, provides=None, nameprefix="", 343 cmdargs=(), runargs=()): 344 nameprefix = nameprefix + "android-port-test:" 345 if provides is None: 346 provides = TrustyPortTestFlags(android=True, 347 storage_boot=True, 348 storage_full=True, 349 smp4=True, 350 abl=True, 351 tablet=True) 352 353 return [test.into_androidporttest(nameprefix=nameprefix, 354 cmdargs=cmdargs, 355 runargs=runargs) 356 for test in porttests_filter(port_tests, provides)] 357 358 def needs(tests, *args, **kwargs): 359 return [ 360 test.needs(*args, **kwargs) 361 for test in flatten_list(tests) 362 ] 363 364 def devsigningkeys( 365 default_key_paths: List[str], 366 project_overrides: Optional[Dict[str, List[str]]] = None): 367 self.default_signing_keys.extend(default_key_paths) 368 if project_overrides is None: 369 return 370 371 for project_name, overrides in project_overrides.items(): 372 project = self.get_project(project_name) 373 if project.signing_keys is None: 374 project.signing_keys = [] 375 project.signing_keys.extend(overrides) 376 377 378 file_format = { 379 "BENCHMARK": PortType.BENCHMARK, 380 "TEST": PortType.TEST, 381 "include": include, 382 "build": build, 383 "builddep": builddep, 384 "archive": archive, 385 "testmap": testmap, 386 "hosttest": hosttest, 387 "porttest": TrustyPortTest, 388 "compositetest": TrustyCompositeTest, 389 "porttestflags": TrustyPortTestFlags, 390 "hosttests": hosttests, 391 "boottests": boottests, 392 "androidtest": TrustyAndroidTest, 393 "androidporttests": androidporttests, 394 "needs": needs, 395 "reboot": TrustyRebootCommand, 396 "devsigningkeys": devsigningkeys, 397 } 398 399 with open(path, encoding="utf8") as f: 400 code = compile(f.read(), path, "eval") 401 config = eval(code, file_format) # pylint: disable=eval-used 402 return flatten_list(config) 403 404 def get_project(self, project): 405 """Return TrustyBuildConfigProject entry for a project.""" 406 if project not in self.projects: 407 self.projects[project] = TrustyBuildConfigProject() 408 return self.projects[project] 409 410 def get_projects(self, build=None, have_tests=None): 411 """Return a list of projects. 412 413 Args: 414 build: If True only return projects that should be built. If False 415 only return projects that should not be built. If None return 416 both projects that should be built and not be built. (default 417 None). 418 have_tests: If True only return projects that have tests. If False 419 only return projects that don't have tests. If None return 420 projects regardless if they have tests. (default None). 421 """ 422 423 def match(item): 424 """filter function for get_projects.""" 425 project = self.projects[item] 426 427 return ((build is None or build == project.build) and 428 (have_tests is None or 429 have_tests == bool(project.tests))) 430 431 return (project for project in sorted(self.projects.keys()) 432 if match(project)) 433 434 def signing_keys(self, project_name: str): 435 project_specific_keys = self.get_project(project_name).signing_keys 436 if project_specific_keys is None: 437 return self.default_signing_keys 438 return project_specific_keys 439 440 441def list_projects(args): 442 """Read config file and print a list of projects. 443 444 See TrustyBuildConfig.get_projects for filtering options. 445 446 Args: 447 args: Program arguments. 448 """ 449 config = TrustyBuildConfig(config_file=args.file, debug=args.debug) 450 for project in sorted(config.get_projects(**dict(args.filter))): 451 print(project) 452 453 454def list_config(args): 455 """Read config file and print all project and tests.""" 456 config = TrustyBuildConfig(config_file=args.file, debug=args.debug) 457 print("Projects:") 458 for project_name, project in sorted(config.projects.items()): 459 print(" " + project_name + ":") 460 print(" Build:", project.build) 461 print(" Tests:") 462 for test in project.tests: 463 print(" " + test.name + ":") 464 print(" " + str(test.command)) 465 466 for build in [True, False]: 467 print() 468 print("Build:" if build else "Don't build:") 469 for tested in [True, False]: 470 projects = config.get_projects(build=build, have_tests=tested) 471 for project in sorted(projects): 472 print(" " + project + ":") 473 project_config = config.get_project(project) 474 for test in project_config.tests: 475 print(" " + test.name) 476 if projects and not tested: 477 print(" No tests") 478 479 480def any_test_name(regex, tests): 481 """Checks the name of all tests in a list for a regex. 482 483 This is intended only as part of the selftest facility, do not use it 484 to decide how to consider actual tests. 485 486 Args: 487 tests: List of tests to check the names of 488 regex: Regular expression to check them for (as a string) 489 """ 490 491 return any(re.match(regex, test.name) is not None for test in tests) 492 493 494def has_host(tests): 495 """Checks for a host test in the provided tests by name. 496 497 This is intended only as part of the selftest facility, do not use it 498 to decide how to consider actual tests. 499 500 Args: 501 tests: List of tests to check for host tests 502 """ 503 return any_test_name("host-test:", tests) 504 505 506def has_unit(tests): 507 """Checks for a unit test in the provided tests by name. 508 509 This is intended only as part of the selftest facility, do not use it 510 to decide how to consider actual tests. 511 512 Args: 513 tests: List of tests to check for unit tests 514 """ 515 return any_test_name("boot-test:", tests) 516 517 518def test_config(args): 519 """Test config file parser. 520 521 Uses a test config file where all projects have names that describe if they 522 should be built and if they have tests. 523 524 Args: 525 args: Program arguments. 526 """ 527 config_file = os.path.join(script_dir, "trusty_build_config_self_test_main") 528 config = TrustyBuildConfig(config_file=config_file, debug=args.debug) 529 530 projects_build = {} 531 532 project_regex = re.compile( 533 r"self_test\.build_(yes|no)\.tests_(none|host|unit|both)\..*") 534 535 for build in [None, True, False]: 536 projects_build[build] = {} 537 for tested in [None, True, False]: 538 projects = list(config.get_projects(build=build, have_tests=tested)) 539 projects_build[build][tested] = projects 540 if args.debug: 541 print("Build", build, "tested", tested, "count", len(projects)) 542 assert projects 543 for project in projects: 544 if args.debug: 545 print("-", project) 546 m = project_regex.match(project) 547 assert m 548 if build is not None: 549 assert m.group(1) == ("yes" if build else "no") 550 if tested is not None: 551 if tested: 552 assert (m.group(2) == "host" or 553 m.group(2) == "unit" or 554 m.group(2) == "both") 555 else: 556 assert m.group(2) == "none" 557 558 assert(projects_build[build][None] == 559 sorted(projects_build[build][True] + 560 projects_build[build][False])) 561 for tested in [None, True, False]: 562 assert(projects_build[None][tested] == 563 sorted(projects_build[True][tested] + 564 projects_build[False][tested])) 565 566 print("get_projects test passed") 567 568 reboot_seen = False 569 570 def check_test(i, test): 571 match test: 572 case TrustyTest(): 573 host_m = re.match(r"host-test:self_test.*\.(\d+)", 574 test.name) 575 unit_m = re.match(r"boot-test:self_test.*\.(\d+)", 576 test.name) 577 if args.debug: 578 print(project, i, test.name) 579 m = host_m or unit_m 580 assert m 581 assert m.group(1) == str(i + 1) 582 case TrustyRebootCommand(): 583 assert False, "Reboot outside composite command" 584 case _: 585 assert False, "Unexpected test type" 586 587 def check_subtest(i, test): 588 nonlocal reboot_seen 589 match test: 590 case TrustyRebootCommand(): 591 reboot_seen = True 592 case _: 593 check_test(i, test) 594 595 for project_name in config.get_projects(): 596 project = config.get_project(project_name) 597 if args.debug: 598 print(project_name, project) 599 m = project_regex.match(project_name) 600 assert m 601 kind = m.group(2) 602 if kind == "both": 603 assert has_host(project.tests) 604 assert has_unit(project.tests) 605 elif kind == "unit": 606 assert not has_host(project.tests) 607 assert has_unit(project.tests) 608 elif kind == "host": 609 assert has_host(project.tests) 610 assert not has_unit(project.tests) 611 elif kind == "none": 612 assert not has_host(project.tests) 613 assert not has_unit(project.tests) 614 else: 615 assert False, "Unknown project kind" 616 617 for i, test in enumerate(project.tests): 618 match test: 619 case TrustyCompositeTest(): 620 # because one of its subtest needs storage_boot, 621 # the composite test should similarly need it 622 assert "storage_boot" in test.need.flags 623 for subtest in test.sequence: 624 check_subtest(i, subtest) 625 case _: 626 check_test(i, test) 627 628 assert reboot_seen 629 630 print("get_tests test passed") 631 632 633def main(): 634 top = os.path.abspath(os.path.join(script_dir, "../../../../..")) 635 os.chdir(top) 636 637 parser = argparse.ArgumentParser() 638 parser.add_argument("-d", "--debug", action="store_true") 639 parser.add_argument("--file") 640 # work around for https://bugs.python.org/issue16308 641 parser.set_defaults(func=lambda args: parser.print_help()) 642 subparsers = parser.add_subparsers() 643 644 parser_projects = subparsers.add_parser("projects", 645 help="list project names") 646 647 group = parser_projects.add_mutually_exclusive_group() 648 group.add_argument("--with-tests", action="append_const", 649 dest="filter", const=("have_tests", True), 650 help="list projects that have tests") 651 group.add_argument("--without-tests", action="append_const", 652 dest="filter", const=("have_tests", False), 653 help="list projects that don't have tests") 654 655 group = parser_projects.add_mutually_exclusive_group() 656 group.add_argument("--all", action="append_const", 657 dest="filter", const=("build", None), 658 help="include disabled projects") 659 group.add_argument("--disabled", action="append_const", 660 dest="filter", const=("build", False), 661 help="only list disabled projects") 662 parser_projects.set_defaults(func=list_projects, filter=[("build", True)]) 663 664 parser_config = subparsers.add_parser("config", help="dump config") 665 parser_config.set_defaults(func=list_config) 666 667 parser_config = subparsers.add_parser("selftest", help="test config parser") 668 parser_config.set_defaults(func=test_config) 669 670 args = parser.parse_args() 671 args.func(args) 672 673 674if __name__ == "__main__": 675 main() 676