1#!/usr/bin/env python3
2#
3# Copyright (C) 2017 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"""A tool to generate TradeFed test config file.
18"""
19
20import argparse
21import re
22import os
23import shutil
24import sys
25from xml.dom.minidom import parse
26
27ATTRIBUTE_LABEL = 'android:label'
28ATTRIBUTE_RUNNER = 'android:name'
29ATTRIBUTE_PACKAGE = 'package'
30
31PLACEHOLDER_LABEL = '{LABEL}'
32PLACEHOLDER_EXTRA_CONFIGS = '{EXTRA_CONFIGS}'
33PLACEHOLDER_MODULE = '{MODULE}'
34PLACEHOLDER_PACKAGE = '{PACKAGE}'
35PLACEHOLDER_RUNNER = '{RUNNER}'
36PLACEHOLDER_TEST_TYPE = '{TEST_TYPE}'
37
38
39def main(argv):
40  """Entry point of auto_gen_test_config.
41
42  Args:
43    argv: A list of arguments.
44  Returns:
45    0 if no error, otherwise 1.
46  """
47
48  parser = argparse.ArgumentParser()
49  parser.add_argument(
50      "target_config",
51      help="Path to the generated output config.")
52  parser.add_argument(
53      "android_manifest",
54      help="Path to AndroidManifest.xml or output of 'aapt2 dump xmltree' with .xmltree extension.")
55  parser.add_argument(
56      "empty_config",
57      help="Path to the empty config template.")
58  parser.add_argument(
59      "instrumentation_test_config_template",
60      help="Path to the instrumentation test config template.")
61  parser.add_argument("--extra-configs", default="")
62  args = parser.parse_args(argv)
63
64  target_config = args.target_config
65  android_manifest = args.android_manifest
66  empty_config = args.empty_config
67  instrumentation_test_config_template = args.instrumentation_test_config_template
68  extra_configs = '\n'.join(args.extra_configs.split('\\n'))
69
70  module = os.path.splitext(os.path.basename(target_config))[0]
71
72  # If the AndroidManifest.xml is not available, but the APK is, this tool also
73  # accepts the output of `aapt2 dump xmltree <apk> AndroidManifest.xml` written
74  # into a file. This is a custom structured aapt2 output - not raw XML!
75  if android_manifest.endswith(".xmltree"):
76    label = module
77    with open(android_manifest, encoding="utf-8") as manifest:
78      # e.g. A: package="android.test.example.helloworld" (Raw: "android.test.example.helloworld")
79      #                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80      pattern = re.compile(r"\(Raw:\s\"(.*)\"\)$")
81      curr_element = None
82      for line in manifest:
83        curr_line = line.strip()
84        if curr_line.startswith("E:"):
85          # e.g. "E: instrumentation (line=9)"
86          #          ^^^^^^^^^^^^^^^
87          curr_element = curr_line.split(" ")[1]
88        if curr_element == "instrumentation":
89          if ATTRIBUTE_RUNNER in curr_line:
90            runner =  re.findall(pattern, curr_line)[0]
91          if ATTRIBUTE_LABEL in curr_line:
92            label = re.findall(pattern, curr_line)[0]
93        if curr_element == "manifest":
94          if ATTRIBUTE_PACKAGE in curr_line:
95            package = re.findall(pattern, curr_line)[0]
96
97    if not (runner and label and package):
98      # Failed to locate instrumentation or manifest element in AndroidManifest.
99      # file. Empty test config file will be created.
100      shutil.copyfile(empty_config, target_config)
101      return 0
102
103  else:
104    # If the AndroidManifest.xml file is directly available, read it as an XML file.
105    manifest = parse(android_manifest)
106    instrumentation_elements = manifest.getElementsByTagName('instrumentation')
107    manifest_elements = manifest.getElementsByTagName('manifest')
108    if len(instrumentation_elements) != 1 or len(manifest_elements) != 1:
109      # Failed to locate instrumentation or manifest element in AndroidManifest.
110      # file. Empty test config file will be created.
111      shutil.copyfile(empty_config, target_config)
112      return 0
113
114    instrumentation = instrumentation_elements[0]
115    manifest = manifest_elements[0]
116    if ATTRIBUTE_LABEL in instrumentation.attributes:
117      label = instrumentation.attributes[ATTRIBUTE_LABEL].value
118    else:
119      label = module
120    runner = instrumentation.attributes[ATTRIBUTE_RUNNER].value
121    package = manifest.attributes[ATTRIBUTE_PACKAGE].value
122
123  test_type = ('InstrumentationTest'
124              if runner.endswith('.InstrumentationTestRunner')
125              else 'AndroidJUnitTest')
126
127  with open(instrumentation_test_config_template) as template:
128    config = template.read()
129    config = config.replace(PLACEHOLDER_LABEL, label)
130    config = config.replace(PLACEHOLDER_MODULE, module)
131    config = config.replace(PLACEHOLDER_PACKAGE, package)
132    config = config.replace(PLACEHOLDER_TEST_TYPE, test_type)
133    config = config.replace(PLACEHOLDER_EXTRA_CONFIGS, extra_configs)
134    config = config.replace(PLACEHOLDER_RUNNER, runner)
135    with open(target_config, 'w') as config_file:
136      config_file.write(config)
137  return 0
138
139if __name__ == '__main__':
140  sys.exit(main(sys.argv[1:]))
141