1# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Test manifest split."""
15
16import hashlib
17import mock
18import os
19import subprocess
20import tempfile
21import unittest
22import xml.etree.ElementTree as ET
23
24import manifest_split
25
26
27class ManifestSplitTest(unittest.TestCase):
28
29  def test_read_config(self):
30    with tempfile.NamedTemporaryFile('w+t') as test_config:
31      test_config.write("""
32        <config>
33          <add_project name="add1" />
34          <add_project name="add2" />
35          <remove_project name="remove1" />
36          <remove_project name="remove2" />
37        </config>""")
38      test_config.flush()
39      remove_projects, add_projects = manifest_split.read_config(
40          test_config.name)
41      self.assertEqual(remove_projects, set(['remove1', 'remove2']))
42      self.assertEqual(add_projects, set(['add1', 'add2']))
43
44  def test_get_repo_projects(self):
45    with tempfile.NamedTemporaryFile('w+t') as repo_list_file:
46      repo_list_file.write("""
47        system/project1 : platform/project1
48        system/project2 : platform/project2""")
49      repo_list_file.flush()
50      repo_projects = manifest_split.get_repo_projects(repo_list_file.name)
51      self.assertEqual(
52          repo_projects, {
53              'system/project1': 'platform/project1',
54              'system/project2': 'platform/project2',
55          })
56
57  def test_get_module_info(self):
58    with tempfile.NamedTemporaryFile('w+t') as module_info_file:
59      module_info_file.write("""{
60        "target1a": { "path": ["system/project1"] },
61        "target1b": { "path": ["system/project1"] },
62        "target2": { "path": ["out/project2"] },
63        "target3": { "path": ["vendor/google/project3"] }
64      }""")
65      module_info_file.flush()
66      repo_projects = {
67          'system/project1': 'platform/project1',
68          'vendor/google/project3': 'vendor/project3',
69      }
70      module_info = manifest_split.get_module_info(module_info_file.name,
71                                                   repo_projects)
72      self.assertEqual(
73          module_info, {
74              'platform/project1': set(['target1a', 'target1b']),
75              'vendor/project3': set(['target3']),
76          })
77
78  def test_get_module_info_raises_on_unknown_module_path(self):
79    with tempfile.NamedTemporaryFile('w+t') as module_info_file:
80      module_info_file.write("""{
81        "target1": { "path": ["system/unknown/project1"] }
82      }""")
83      module_info_file.flush()
84      repo_projects = {}
85      with self.assertRaisesRegex(ValueError,
86                                  'Unknown module path for module target1'):
87        manifest_split.get_module_info(module_info_file.name, repo_projects)
88
89  @mock.patch.object(subprocess, 'check_output', autospec=True)
90  def test_get_kati_makefiles(self, mock_check_output):
91    with tempfile.TemporaryDirectory() as temp_dir:
92      os.chdir(temp_dir)
93
94      makefiles = [
95          'device/oem1/product1.mk',
96          'device/oem2/product2.mk',
97          'device/google/google_product.mk',
98          'overlays/oem_overlay/device/oem3/product3.mk',
99          'packages/apps/Camera/Android.mk',
100      ]
101      for makefile in makefiles:
102        os.makedirs(os.path.dirname(makefile))
103        os.mknod(makefile)
104
105      symlink_src = os.path.join(temp_dir, 'vendor/oem4/symlink_src.mk')
106      os.makedirs(os.path.dirname(symlink_src))
107      os.mknod(symlink_src)
108      symlink_dest = 'device/oem4/symlink_dest.mk'
109      os.makedirs(os.path.dirname(symlink_dest))
110      os.symlink(symlink_src, symlink_dest)
111      # Only append the symlink destination, not where the symlink points to.
112      # (The Kati stamp file does not resolve symlink sources.)
113      makefiles.append(symlink_dest)
114
115      # Mock the output of ckati_stamp_dump:
116      mock_check_output.side_effect = [
117          '\n'.join(makefiles).encode(),
118      ]
119
120      kati_makefiles = manifest_split.get_kati_makefiles(
121          'stamp-file', ['overlays/oem_overlay/'])
122      self.assertEqual(
123          kati_makefiles,
124          set([
125              # Regular product makefiles
126              'device/oem1/product1.mk',
127              'device/oem2/product2.mk',
128              # Product makefile remapped from an overlay
129              'device/oem3/product3.mk',
130              # Product makefile symlink and its source
131              'device/oem4/symlink_dest.mk',
132              'vendor/oem4/symlink_src.mk',
133          ]))
134
135  def test_scan_repo_projects(self):
136    repo_projects = {
137        'system/project1': 'platform/project1',
138        'system/project2': 'platform/project2',
139    }
140    self.assertEqual(
141        manifest_split.scan_repo_projects(repo_projects,
142                                          'system/project1/path/to/file.h'),
143        'system/project1')
144    self.assertEqual(
145        manifest_split.scan_repo_projects(
146            repo_projects, 'system/project2/path/to/another_file.cc'),
147        'system/project2')
148    self.assertIsNone(
149        manifest_split.scan_repo_projects(
150            repo_projects, 'system/project3/path/to/unknown_file.h'))
151
152  def test_get_input_projects(self):
153    repo_projects = {
154        'system/project1': 'platform/project1',
155        'system/project2': 'platform/project2',
156        'system/project4': 'platform/project4',
157    }
158    inputs = [
159        'system/project1/path/to/file.h',
160        'out/path/to/out/file.h',
161        'system/project2/path/to/another_file.cc',
162        'system/project3/path/to/unknown_file.h',
163        '/tmp/absolute/path/file.java',
164    ]
165    self.assertEqual(
166        manifest_split.get_input_projects(repo_projects, inputs),
167        set(['platform/project1', 'platform/project2']))
168
169  def test_update_manifest(self):
170    manifest_contents = """
171      <manifest>
172        <project name="platform/project1" path="system/project1" />
173        <project name="platform/project2" path="system/project2" />
174        <project name="platform/project3" path="system/project3" />
175      </manifest>"""
176    input_projects = set(['platform/project1', 'platform/project3'])
177    remove_projects = set(['platform/project3'])
178    manifest = manifest_split.update_manifest(
179        ET.ElementTree(ET.fromstring(manifest_contents)), input_projects,
180        remove_projects)
181
182    projects = manifest.getroot().findall('project')
183    self.assertEqual(len(projects), 1)
184    self.assertEqual(
185        ET.tostring(projects[0]).strip().decode(),
186        '<project name="platform/project1" path="system/project1" />')
187
188  def test_create_manifest_sha1_element(self):
189    manifest = ET.ElementTree(ET.fromstring('<manifest></manifest>'))
190    manifest_sha1 = hashlib.sha1(ET.tostring(manifest.getroot())).hexdigest()
191    self.assertEqual(
192        ET.tostring(
193            manifest_split.create_manifest_sha1_element(
194                manifest, 'test_manifest')).decode(),
195        '<hash name="test_manifest" type="sha1" value="%s" />' % manifest_sha1)
196
197  @mock.patch.object(subprocess, 'check_output', autospec=True)
198  def test_create_split_manifest(self, mock_check_output):
199    with tempfile.NamedTemporaryFile('w+t') as repo_list_file, \
200      tempfile.NamedTemporaryFile('w+t') as manifest_file, \
201      tempfile.NamedTemporaryFile('w+t') as module_info_file, \
202      tempfile.NamedTemporaryFile('w+t') as config_file, \
203      tempfile.NamedTemporaryFile('w+t') as split_manifest_file:
204
205      repo_list_file.write("""
206        system/project1 : platform/project1
207        system/project2 : platform/project2
208        system/project3 : platform/project3
209        system/project4 : platform/project4
210        system/project5 : platform/project5
211        system/project6 : platform/project6""")
212      repo_list_file.flush()
213
214      manifest_file.write("""
215        <manifest>
216          <project name="platform/project1" path="system/project1" />
217          <project name="platform/project2" path="system/project2" />
218          <project name="platform/project3" path="system/project3" />
219          <project name="platform/project4" path="system/project4" />
220          <project name="platform/project5" path="system/project5" />
221          <project name="platform/project6" path="system/project6" />
222        </manifest>""")
223      manifest_file.flush()
224
225      module_info_file.write("""{
226        "droid": { "path": ["system/project1"] },
227        "target_a": { "path": ["out/project2"] },
228        "target_b": { "path": ["system/project3"] },
229        "target_c": { "path": ["system/project4"] },
230        "target_d": { "path": ["system/project5"] },
231        "target_e": { "path": ["system/project6"] }
232      }""")
233      module_info_file.flush()
234
235      # droid needs inputs from project1 and project3
236      ninja_inputs_droid = b"""
237      system/project1/file1
238      system/project1/file2
239      system/project3/file1
240      """
241
242      # target_b (indirectly included due to being in project3) needs inputs
243      # from project3 and project4
244      ninja_inputs_target_b = b"""
245      system/project3/file2
246      system/project4/file1
247      """
248
249      # target_c (indirectly included due to being in project4) needs inputs
250      # from only project4
251      ninja_inputs_target_c = b"""
252      system/project4/file2
253      system/project4/file3
254      """
255
256      mock_check_output.side_effect = [
257          ninja_inputs_droid,
258          b'',  # Unused kati makefiles. This is tested in its own method.
259          ninja_inputs_target_b,
260          ninja_inputs_target_c,
261      ]
262
263      # The config file says to manually include project6
264      config_file.write("""
265        <config>
266          <add_project name="platform/project6" />
267        </config>""")
268      config_file.flush()
269
270      manifest_split.create_split_manifest(
271          ['droid'], manifest_file.name, split_manifest_file.name,
272          [config_file.name], repo_list_file.name, 'build-target.ninja',
273          'ninja', module_info_file.name, 'unused kati stamp',
274          ['unused overlay'])
275      split_manifest = ET.parse(split_manifest_file.name)
276      split_manifest_projects = [
277          child.attrib['name']
278          for child in split_manifest.getroot().findall('project')
279      ]
280      self.assertEqual(
281          split_manifest_projects,
282          [
283              # From droid
284              'platform/project1',
285              # From droid
286              'platform/project3',
287              # From target_b (module within project3, indirect dependency)
288              'platform/project4',
289              # Manual inclusion from config file
290              'platform/project6',
291          ])
292
293
294if __name__ == '__main__':
295  unittest.main()
296