• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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