1#!/usr/bin/env python
2#
3# Copyright (C) 2015 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"""Runs the libc++ tests against the platform libc++."""
18from __future__ import print_function
19
20import argparse
21import logging
22import os
23import posixpath
24import sys
25
26THIS_DIR = os.path.dirname(os.path.realpath(__file__))
27ANDROID_DIR = os.path.realpath(os.path.join(THIS_DIR, '../..'))
28
29
30def logger():
31    """Returns the logger for the module."""
32    return logging.getLogger(__name__)
33
34
35def call(cmd, *args, **kwargs):
36    """subprocess.call with logging."""
37    import subprocess
38    logger().info('call %s', ' '.join(cmd))
39    return subprocess.call(cmd, *args, **kwargs)
40
41
42def check_call(cmd, *args, **kwargs):
43    """subprocess.check_call with logging."""
44    import subprocess
45    logger().info('check_call %s', ' '.join(cmd))
46    return subprocess.check_call(cmd, *args, **kwargs)
47
48
49def check_output(cmd, *args, **kwargs):
50    """subprocess.check_output with logging."""
51    import subprocess
52    logger().info('check_output %s', ' '.join(cmd))
53    return subprocess.check_output(cmd, *args, **kwargs)
54
55
56class ArgParser(argparse.ArgumentParser):
57    """Parses command line arguments."""
58
59    def __init__(self):
60        super(ArgParser, self).__init__()
61        self.add_argument('--bitness', choices=(32, 64), type=int, default=32)
62        self.add_argument('--host', action='store_true')
63
64
65def extract_build_cmds(commands, exe_name):
66    """Extracts build command information from `ninja -t commands` output.
67
68    Args:
69        commands: String containing the output of `ninja -t commands` for the
70            libcxx_test_template.
71        exe_name: The basename of the built executable.
72
73    Returns:
74        Tuple of (compiler, compiler_flags, linker_flags).
75    """
76    cc = None
77    cflags = None
78    ldflags = None
79    template_name = 'external/libcxx/libcxx_test_template.cpp'
80
81    for cmd in commands.splitlines():
82        cmd_args = cmd.split()
83        if cc is None and template_name in cmd_args:
84            for i, arg in enumerate(cmd_args):
85                if arg == '-o':
86                    cmd_args[i + 1] = '%OUT%'
87                elif arg == template_name:
88                    cmd_args[i] = '%SOURCE%'
89                # Drop dependency tracking args since they can cause file
90                # not found errors at test time.
91                if arg == '-MD':
92                    cmd_args[i] = ''
93                if arg == '-MF':
94                    cmd_args[i] = ''
95                    cmd_args[i + 1] = ''
96            if cmd_args[0] == 'PWD=/proc/self/cwd':
97                cmd_args = cmd_args[1:]
98            if cmd_args[0].endswith('gomacc'):
99                cmd_args = cmd_args[1:]
100            cc = cmd_args[0]
101            cflags = cmd_args[1:]
102        if ldflags is None:
103            is_ld = False
104            for i, arg in enumerate(cmd_args):
105                # Here we assume that the rspfile contains the path to the
106                # object file and nothing else.
107                if arg.startswith('@'):
108                    cmd_args[i] = '%SOURCE%'
109                if arg == '-o' and cmd_args[i + 1].endswith(exe_name):
110                    cmd_args[i + 1] = '%OUT%'
111                    is_ld = True
112            if is_ld:
113                ldflags = cmd_args[1:]
114
115    return cc, cflags, ldflags
116
117
118def get_build_cmds(bitness, host):
119    """Use ninja -t commands to find the build commands for an executable."""
120    out_dir = os.getenv('OUT_DIR', os.path.join(ANDROID_DIR, 'out'))
121    product_out = os.getenv('ANDROID_PRODUCT_OUT')
122
123    if host:
124        rel_out_dir = os.path.relpath(
125            os.path.join(out_dir, 'soong/host/linux-x86/bin'), ANDROID_DIR)
126        target = os.path.join(rel_out_dir, 'libcxx_test_template64')
127    else:
128        exe_name = 'libcxx_test_template' + str(bitness)
129        rel_out_dir = os.path.relpath(product_out, ANDROID_DIR)
130        target = os.path.join(rel_out_dir, 'system/bin', exe_name)
131
132    # Generate $OUT_DIR/combined-$TARGET_PRODUCT.ninja and build the
133    # template target's dependencies.
134    check_call([
135        'bash',
136        os.path.join(ANDROID_DIR, 'build/soong/soong_ui.bash'), '--make-mode',
137        target
138    ])
139
140    ninja_path = os.path.join(
141        out_dir, 'combined-' + os.getenv('TARGET_PRODUCT') + '.ninja')
142    commands = check_output([
143        os.path.join(ANDROID_DIR, 'prebuilts/build-tools/linux-x86/bin/ninja'),
144        '-C', ANDROID_DIR, '-f', ninja_path, '-t', 'commands', target
145    ])
146
147    return extract_build_cmds(commands, os.path.basename(target))
148
149
150def setup_test_directory():
151    """Prepares a device test directory for use by the shell user."""
152    stdfs_test_data = os.path.join(
153        THIS_DIR, 'test/std/input.output/filesystems/Inputs/static_test_env')
154    device_dir = '/data/local/tmp/libcxx'
155    dynamic_dir = posixpath.join(device_dir, 'dynamic_test_env')
156    check_call(['adb', 'shell', 'rm', '-rf', device_dir])
157    check_call(['adb', 'shell', 'mkdir', '-p', device_dir])
158    check_call(['adb', 'shell', 'mkdir', '-p', dynamic_dir])
159    check_call(['adb', 'push', '--sync', stdfs_test_data, device_dir])
160    check_call(['adb', 'shell', 'chown', '-R', 'shell:shell', device_dir])
161
162
163def main():
164    """Program entry point."""
165    logging.basicConfig(level=logging.INFO)
166
167    args, lit_args = ArgParser().parse_known_args()
168    lit_path = os.path.join(ANDROID_DIR, 'external/llvm/utils/lit/lit.py')
169    cc, cflags, ldflags = get_build_cmds(args.bitness, args.host)
170
171    mode_str = 'host' if args.host else 'device'
172    android_mode_arg = '--param=android_mode=' + mode_str
173    cxx_under_test_arg = '--param=cxx_under_test=' + cc
174    cxx_template_arg = '--param=cxx_template=' + ' '.join(cflags)
175    link_template_arg = '--param=link_template=' + ' '.join(ldflags)
176    site_cfg_path = os.path.join(THIS_DIR, 'test/lit.site.cfg')
177    libcxx_site_cfg_arg = '--param=libcxx_site_config=' + site_cfg_path
178    libcxxabi_site_cfg_arg = '--param=libcxxabi_site_config=' + site_cfg_path
179    default_test_paths = [
180        os.path.join(THIS_DIR, 'test'),
181        os.path.join(ANDROID_DIR, 'external/libcxxabi/test')
182    ]
183
184    have_filter_args = False
185    for arg in lit_args:
186        # If the argument is a valid path with default_test_paths, it is a test
187        # filter.
188        real_path = os.path.realpath(arg)
189        if not any(real_path.startswith(path) for path in default_test_paths):
190            continue
191        if not os.path.exists(real_path):
192            continue
193
194        have_filter_args = True
195        break  # No need to keep scanning.
196
197    if not args.host:
198        setup_test_directory()
199
200    lit_args = [
201        '-sv', android_mode_arg, cxx_under_test_arg, cxx_template_arg,
202        link_template_arg, libcxx_site_cfg_arg, libcxxabi_site_cfg_arg
203    ] + lit_args
204    cmd = ['python', lit_path] + lit_args
205    if not have_filter_args:
206        cmd += default_test_paths
207    sys.exit(call(cmd))
208
209
210if __name__ == '__main__':
211    main()
212