1#!/usr/bin/env python
2#
3# Copyright (C) 2020 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# This script generates C-Suite configuration files for a list of apps.
18
19import argparse
20import glob
21import os
22import sys
23from xml.dom import minidom
24from xml.etree import cElementTree as ET
25from xml.sax import saxutils
26
27from typing import IO, List, Text
28
29_ANDROID_BP_FILE_NAME = 'Android.bp'
30_ANDROID_XML_FILE_NAME = 'AndroidTest.xml'
31
32_TF_TEST_APP_INSTALL_SETUP =\
33  'com.android.tradefed.targetprep.TestAppInstallSetup'
34_CSUITE_APP_SETUP_PREPARER =\
35  'com.android.compatibility.targetprep.AppSetupPreparer'
36_CSUITE_LAUNCH_TEST_CLASS =\
37  'com.android.compatibility.testtype.AppLaunchTest'
38
39_CONFIG_TYPE_TARGET_PREPARER = 'target_preparer'
40_CONFIG_TYPE_TEST = 'test'
41
42
43def generate_all_modules_from_config(package_list_file_path, root_dir):
44    """Generate multiple test and build modules.
45
46    Args:
47    package_list_file_path: path of a file containing package names.
48    root_dir: root directory that modules will be generated in.
49    """
50    remove_existing_package_files(root_dir)
51
52    with open(package_list_file_path) as fp:
53        for line in parse_package_list(fp):
54            _generate_module_files(line.strip(), root_dir)
55
56
57def remove_existing_package_files(root_dir):
58    for filename in glob.iglob(root_dir + '**/AndroidTest.xml'):
59        if _is_auto_generated(filename):
60            os.remove(filename)
61
62    for filename in glob.iglob(root_dir + '**/Android.bp'):
63        if _is_auto_generated(filename):
64            os.remove(filename)
65
66    _remove_empty_dirs(root_dir)
67
68
69def _is_auto_generated(filename):
70    with open(filename, 'r') as f:
71        return 'auto-generated' in f.read()
72
73
74def _remove_empty_dirs(path):
75    for filename in os.listdir(path):
76        file_path = os.path.join(path, filename)
77        if os.path.isdir(file_path) and not os.listdir(file_path):
78            os.rmdir(file_path)
79
80
81def parse_package_list(package_list_file: IO[bytes]) -> List[bytes]:
82    return {
83        line.strip() for line in package_list_file.readlines() if line.strip()}
84
85
86def _generate_module_files(package_name, root_dir):
87    """Generate test and build modules for a single package.
88
89    Args:
90    package_name: package name of test and build modules.
91    root_dir: root directory that modules will be generated in.
92    """
93    package_dir = _create_package_dir(root_dir, package_name)
94
95    build_module_path = os.path.join(package_dir, _ANDROID_BP_FILE_NAME)
96    test_module_path = os.path.join(package_dir, _ANDROID_XML_FILE_NAME)
97
98    with open(build_module_path, 'w') as f:
99        write_build_module(package_name, f)
100
101    with open(test_module_path, 'w') as f:
102        write_test_module(package_name, f)
103
104
105def _create_package_dir(root_dir, package_name):
106    package_dir_path = os.path.join(root_dir, package_name)
107    os.mkdir(package_dir_path)
108
109    return package_dir_path
110
111
112def write_build_module(package_name: Text, out_file: IO[bytes]) -> Text:
113    build_module = _BUILD_MODULE_HEADER \
114        + _BUILD_MODULE_TEMPLATE.format(package_name=package_name)
115    out_file.write(build_module)
116
117
118def write_test_module(package_name: Text, out_file: IO[bytes]) -> Text:
119    configuration = ET.Element('configuration', {
120        'description': 'Tests the compatibility of apps'
121    })
122    ET.SubElement(
123        configuration, 'option', {
124            'name': 'config-descriptor:metadata',
125            'key': 'plan',
126            'value': 'csuite-launch'
127        }
128    )
129    ET.SubElement(
130        configuration, 'option', {
131            'name': 'package-name',
132            'value': package_name
133        }
134    )
135    test_file_name_option = {
136        'name': 'test-file-name',
137        'value': 'csuite-launch-instrumentation.apk'
138    }
139    _add_element_with_option(
140        configuration,
141        _CONFIG_TYPE_TARGET_PREPARER,
142        _TF_TEST_APP_INSTALL_SETUP,
143        options=[test_file_name_option]
144    )
145    _add_element_with_option(
146        configuration,
147        _CONFIG_TYPE_TARGET_PREPARER,
148        _CSUITE_APP_SETUP_PREPARER
149    )
150    _add_element_with_option(
151        configuration,
152        _CONFIG_TYPE_TEST,
153        _CSUITE_LAUNCH_TEST_CLASS
154    )
155
156    test_module = _TEST_MODULE_HEADER + _prettify(configuration)
157    out_file.write(test_module)
158
159
160def _add_element_with_option(elem, sub_elem, class_name, options=None):
161    if options is None:
162        options = []
163
164    new_elem = ET.SubElement(
165        elem, sub_elem, {
166            'class': class_name,
167        }
168    )
169    for option in options:
170        ET.SubElement(
171            new_elem, 'option', option
172        )
173
174
175def _prettify(elem: ET.Element) -> Text:
176    declaration = minidom.Document().toxml()
177    parsed = minidom.parseString(ET.tostring(elem, 'utf-8'))
178
179    return saxutils.unescape(
180        parsed.toprettyxml(indent='    ')[len(declaration) + 1:])
181
182_BUILD_MODULE_HEADER = """// Copyright (C) 2020 The Android Open Source Project
183//
184// Licensed under the Apache License, Version 2.0 (the "License");
185// you may not use this file except in compliance with the License.
186// You may obtain a copy of the License at
187//
188//      http://www.apache.org/licenses/LICENSE-2.0
189//
190// Unless required by applicable law or agreed to in writing, software
191// distributed under the License is distributed on an "AS IS" BASIS,
192// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
193// See the License for the specific language governing permissions and
194// limitations under the License.
195
196// This file was auto-generated by test/app_compat/csuite/tools/script/generate_module.py.
197// Do not edit manually.
198
199"""
200
201_BUILD_MODULE_TEMPLATE = """csuite_config {{
202    name: "csuite_{package_name}",
203}}
204"""
205
206_TEST_MODULE_HEADER = """<?xml version="1.0" encoding="utf-8"?>
207<!-- Copyright (C) 2020 The Android Open Source Project
208     Licensed under the Apache License, Version 2.0 (the "License");
209     you may not use this file except in compliance with the License.
210     You may obtain a copy of the License at
211
212        http://www.apache.org/licenses/LICENSE-2.0
213
214     Unless required by applicable law or agreed to in writing, software
215     distributed under the License is distributed on an "AS IS" BASIS,
216     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
217     See the License for the specific language governing permissions and
218     limitations under the License.
219-->
220<!-- This file was auto-generated by test/app_compat/csuite/tools/script/generate_module.py.
221     Do not edit manually.
222-->
223
224"""
225
226
227def _file_path(path):
228    if os.path.isfile(path):
229        return path
230    raise argparse.ArgumentTypeError('%s is not a valid path' % path)
231
232
233def _dir_path(path):
234    if os.path.isdir(path):
235        return path
236    raise argparse.ArgumentTypeError('%s is not a valid path' % path)
237
238
239def parse_args(args):
240    parser = argparse.ArgumentParser()
241    parser.add_argument('--package_list',
242                        type=_file_path,
243                        required=True,
244                        help='path of the file containing package names')
245    parser.add_argument('--root_dir',
246                        type=_dir_path,
247                        required=True,
248                        help='path of the root directory that' +
249                        'modules will be generated in')
250    return parser.parse_args(args)
251
252
253def main():
254    parser = parse_args(sys.argv[1:])
255    generate_all_modules_from_config(parser.package_list, parser.root_dir)
256
257if __name__ == '__main__':
258    main()
259