1#!/usr/bin/env python 2# 3# Copyright (C) 2022 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"""Unit tests for analyzing bootclasspath_fragment modules.""" 17import os.path 18import shutil 19import tempfile 20import unittest 21import unittest.mock 22 23import sys 24 25import analyze_bcpf as ab 26 27_FRAMEWORK_HIDDENAPI = "frameworks/base/boot/hiddenapi" 28_MAX_TARGET_O = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-o.txt" 29_MAX_TARGET_P = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-p.txt" 30_MAX_TARGET_Q = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-q.txt" 31_MAX_TARGET_R = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-r-loprio.txt" 32 33_MULTI_LINE_COMMENT = """ 34Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu justo, 35bibendum eu malesuada vel, fringilla in odio. Etiam gravida ultricies sem 36tincidunt luctus.""".replace("\n", " ").strip() 37 38 39class FakeBuildOperation(ab.BuildOperation): 40 41 def __init__(self, lines, return_code): 42 ab.BuildOperation.__init__(self, None) 43 self._lines = lines 44 self.returncode = return_code 45 46 def lines(self): 47 return iter(self._lines) 48 49 def wait(self, *args, **kwargs): 50 return 51 52 53class TestAnalyzeBcpf(unittest.TestCase): 54 55 def setUp(self): 56 # Create a temporary directory 57 self.test_dir = tempfile.mkdtemp() 58 59 def tearDown(self): 60 # Remove the directory after the test 61 shutil.rmtree(self.test_dir) 62 63 @staticmethod 64 def write_abs_file(abs_path, contents): 65 os.makedirs(os.path.dirname(abs_path), exist_ok=True) 66 with open(abs_path, "w", encoding="utf8") as f: 67 print(contents.removeprefix("\n"), file=f, end="") 68 69 def populate_fs(self, fs): 70 for path, contents in fs.items(): 71 abs_path = os.path.join(self.test_dir, path) 72 self.write_abs_file(abs_path, contents) 73 74 def create_analyzer_for_test(self, 75 fs=None, 76 bcpf="bcpf", 77 apex="apex", 78 sdk="sdk", 79 fix=False): 80 if fs: 81 self.populate_fs(fs) 82 83 top_dir = self.test_dir 84 out_dir = os.path.join(self.test_dir, "out") 85 product_out_dir = "out/product" 86 87 bcpf_dir = f"{bcpf}-dir" 88 modules = {bcpf: {"path": [bcpf_dir]}} 89 module_info = ab.ModuleInfo(modules) 90 91 analyzer = ab.BcpfAnalyzer( 92 tool_path=os.path.join(out_dir, "bin"), 93 top_dir=top_dir, 94 out_dir=out_dir, 95 product_out_dir=product_out_dir, 96 bcpf=bcpf, 97 apex=apex, 98 sdk=sdk, 99 fix=fix, 100 module_info=module_info, 101 ) 102 analyzer.load_all_flags() 103 return analyzer 104 105 def test_reformat_report_text(self): 106 lines = """ 10799. An item in a numbered list 108that traverses multiple lines. 109 110 An indented example 111 that should not be reformatted. 112""" 113 reformatted = ab.BcpfAnalyzer.reformat_report_test(lines) 114 self.assertEqual( 115 """ 11699. An item in a numbered list that traverses multiple lines. 117 118 An indented example 119 that should not be reformatted. 120""", reformatted) 121 122 def do_test_build_flags(self, fix): 123 lines = """ 124ERROR: Hidden API flags are inconsistent: 125< out/soong/.intermediates/bcpf-dir/bcpf-dir/filtered-flags.csv 126> out/soong/hiddenapi/hiddenapi-flags.csv 127 128< Lacme/test/Class;-><init>()V,blocked 129> Lacme/test/Class;-><init>()V,max-target-o 130 131< Lacme/test/Other;->getThing()Z,blocked 132> Lacme/test/Other;->getThing()Z,max-target-p 133 134< Lacme/test/Widget;-><init()V,blocked 135> Lacme/test/Widget;-><init()V,max-target-q 136 137< Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked 138> Lacme/test/Gadget;->NAME:Ljava/lang/String;,lo-prio,max-target-r 13916:37:32 ninja failed with: exit status 1 140""".strip().splitlines() 141 operation = FakeBuildOperation(lines=lines, return_code=1) 142 143 fs = { 144 _MAX_TARGET_O: 145 """ 146Lacme/items/Magnet;->size:I 147Lacme/test/Class;-><init>()V 148""", 149 _MAX_TARGET_P: 150 """ 151Lacme/items/Rocket;->size:I 152Lacme/test/Other;->getThing()Z 153""", 154 _MAX_TARGET_Q: 155 """ 156Lacme/items/Rock;->size:I 157Lacme/test/Widget;-><init()V 158""", 159 _MAX_TARGET_R: 160 """ 161Lacme/items/Lever;->size:I 162Lacme/test/Gadget;->NAME:Ljava/lang/String; 163""", 164 "bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt": 165 """ 166Lacme/old/Class;->getWidget()Lacme/test/Widget; 167""", 168 "out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv": 169 """ 170Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked 171Lacme/test/Widget;-><init()V,blocked 172Lacme/test/Class;-><init>()V,blocked 173Lacme/test/Other;->getThing()Z,blocked 174""", 175 } 176 177 analyzer = self.create_analyzer_for_test(fs, fix=fix) 178 179 # Override the build_file_read_output() method to just return a fake 180 # build operation. 181 analyzer.build_file_read_output = unittest.mock.Mock( 182 return_value=operation) 183 184 # Override the run_command() method to do nothing. 185 analyzer.run_command = unittest.mock.Mock() 186 187 result = ab.Result() 188 189 analyzer.build_monolithic_flags(result) 190 expected_diffs = { 191 "Lacme/test/Gadget;->NAME:Ljava/lang/String;": 192 (["blocked"], ["lo-prio", "max-target-r"]), 193 "Lacme/test/Widget;-><init()V": (["blocked"], ["max-target-q"]), 194 "Lacme/test/Class;-><init>()V": (["blocked"], ["max-target-o"]), 195 "Lacme/test/Other;->getThing()Z": (["blocked"], ["max-target-p"]) 196 } 197 self.assertEqual(expected_diffs, result.diffs, msg="flag differences") 198 199 expected_property_changes = [ 200 ab.HiddenApiPropertyChange( 201 property_name="max_target_o_low_priority", 202 values=["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 203 property_comment=""), 204 ab.HiddenApiPropertyChange( 205 property_name="max_target_p", 206 values=["hiddenapi/hiddenapi-max-target-p.txt"], 207 property_comment=""), 208 ab.HiddenApiPropertyChange( 209 property_name="max_target_q", 210 values=["hiddenapi/hiddenapi-max-target-q.txt"], 211 property_comment=""), 212 ab.HiddenApiPropertyChange( 213 property_name="max_target_r_low_priority", 214 values=["hiddenapi/hiddenapi-max-target-r-low-priority.txt"], 215 property_comment=""), 216 ] 217 self.assertEqual( 218 expected_property_changes, 219 result.property_changes, 220 msg="property changes") 221 222 return result 223 224 def test_build_flags_report(self): 225 result = self.do_test_build_flags(fix=False) 226 227 expected_file_changes = [ 228 ab.FileChange( 229 path="bcpf-dir/hiddenapi/" 230 "hiddenapi-max-target-o-low-priority.txt", 231 description="""Add the following entries: 232 Lacme/test/Class;-><init>()V 233""", 234 ), 235 ab.FileChange( 236 path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt", 237 description="""Add the following entries: 238 Lacme/test/Other;->getThing()Z 239""", 240 ), 241 ab.FileChange( 242 path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt", 243 description="""Add the following entries: 244 Lacme/test/Widget;-><init()V 245"""), 246 ab.FileChange( 247 path="bcpf-dir/hiddenapi/" 248 "hiddenapi-max-target-r-low-priority.txt", 249 description="""Add the following entries: 250 Lacme/test/Gadget;->NAME:Ljava/lang/String; 251"""), 252 ab.FileChange( 253 path="frameworks/base/boot/hiddenapi/" 254 "hiddenapi-max-target-o.txt", 255 description="""Remove the following entries: 256 Lacme/test/Class;-><init>()V 257"""), 258 ab.FileChange( 259 path="frameworks/base/boot/hiddenapi/" 260 "hiddenapi-max-target-p.txt", 261 description="""Remove the following entries: 262 Lacme/test/Other;->getThing()Z 263"""), 264 ab.FileChange( 265 path="frameworks/base/boot/hiddenapi/" 266 "hiddenapi-max-target-q.txt", 267 description="""Remove the following entries: 268 Lacme/test/Widget;-><init()V 269"""), 270 ab.FileChange( 271 path="frameworks/base/boot/hiddenapi/" 272 "hiddenapi-max-target-r-loprio.txt", 273 description="""Remove the following entries: 274 Lacme/test/Gadget;->NAME:Ljava/lang/String; 275""") 276 ] 277 result.file_changes.sort() 278 self.assertEqual( 279 expected_file_changes, result.file_changes, msg="file_changes") 280 281 def test_build_flags_fix(self): 282 result = self.do_test_build_flags(fix=True) 283 284 expected_file_changes = [ 285 ab.FileChange( 286 path="bcpf-dir/hiddenapi/" 287 "hiddenapi-max-target-o-low-priority.txt", 288 description="Created with 'bcpf' specific entries"), 289 ab.FileChange( 290 path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt", 291 description="Added 'bcpf' specific entries"), 292 ab.FileChange( 293 path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt", 294 description="Created with 'bcpf' specific entries"), 295 ab.FileChange( 296 path="bcpf-dir/hiddenapi/" 297 "hiddenapi-max-target-r-low-priority.txt", 298 description="Created with 'bcpf' specific entries"), 299 ab.FileChange( 300 path=_MAX_TARGET_O, 301 description="Removed 'bcpf' specific entries"), 302 ab.FileChange( 303 path=_MAX_TARGET_P, 304 description="Removed 'bcpf' specific entries"), 305 ab.FileChange( 306 path=_MAX_TARGET_Q, 307 description="Removed 'bcpf' specific entries"), 308 ab.FileChange( 309 path=_MAX_TARGET_R, 310 description="Removed 'bcpf' specific entries") 311 ] 312 313 result.file_changes.sort() 314 self.assertEqual( 315 expected_file_changes, result.file_changes, msg="file_changes") 316 317 expected_file_contents = { 318 "bcpf-dir/hiddenapi/hiddenapi-max-target-o-low-priority.txt": 319 """ 320Lacme/test/Class;-><init>()V 321""", 322 "bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt": 323 """ 324Lacme/old/Class;->getWidget()Lacme/test/Widget; 325Lacme/test/Other;->getThing()Z 326""", 327 "bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt": 328 """ 329Lacme/test/Widget;-><init()V 330""", 331 "bcpf-dir/hiddenapi/hiddenapi-max-target-r-low-priority.txt": 332 """ 333Lacme/test/Gadget;->NAME:Ljava/lang/String; 334""", 335 _MAX_TARGET_O: 336 """ 337Lacme/items/Magnet;->size:I 338""", 339 _MAX_TARGET_P: 340 """ 341Lacme/items/Rocket;->size:I 342""", 343 _MAX_TARGET_Q: 344 """ 345Lacme/items/Rock;->size:I 346""", 347 _MAX_TARGET_R: 348 """ 349Lacme/items/Lever;->size:I 350""", 351 } 352 for file_change in result.file_changes: 353 path = file_change.path 354 expected_contents = expected_file_contents[path].lstrip() 355 abs_path = os.path.join(self.test_dir, path) 356 with open(abs_path, "r", encoding="utf8") as tio: 357 contents = tio.read() 358 self.assertEqual( 359 expected_contents, contents, msg=f"{path} contents") 360 361 def test_compute_hiddenapi_package_properties(self): 362 fs = { 363 "out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv": 364 """ 365La/b/C;->m()V 366La/b/c/D;->m()V 367La/b/c/E;->m()V 368Lb/c/D;->m()V 369Lb/c/E;->m()V 370Lb/c/d/E;->m()V 371""", 372 "out/soong/hiddenapi/hiddenapi-flags.csv": 373 """ 374La/b/C;->m()V 375La/b/D;->m()V 376La/b/E;->m()V 377La/b/c/D;->m()V 378La/b/c/E;->m()V 379La/b/c/d/E;->m()V 380La/b/c/d/e/F;->m()V 381Lb/c/D;->m()V 382Lb/c/E;->m()V 383Lb/c/d/E;->m()V 384""" 385 } 386 analyzer = self.create_analyzer_for_test(fs) 387 analyzer.load_all_flags() 388 389 result = ab.Result() 390 analyzer.compute_hiddenapi_package_properties(result) 391 self.assertEqual(["a.b"], list(result.split_packages.keys())) 392 393 reason = result.split_packages["a.b"] 394 self.assertEqual(["a.b.C"], reason.bcpf) 395 self.assertEqual(["a.b.D", "a.b.E"], reason.other) 396 397 self.assertEqual(["a.b.c"], list(result.single_packages.keys())) 398 399 reason = result.single_packages["a.b.c"] 400 self.assertEqual(["a.b.c"], reason.bcpf) 401 self.assertEqual(["a.b.c.d", "a.b.c.d.e"], reason.other) 402 403 self.assertEqual(["b"], result.package_prefixes) 404 405 406class TestHiddenApiPropertyChange(unittest.TestCase): 407 408 def setUp(self): 409 # Create a temporary directory 410 self.test_dir = tempfile.mkdtemp() 411 412 def tearDown(self): 413 # Remove the directory after the test 414 shutil.rmtree(self.test_dir) 415 416 def check_change_fix(self, change, bpmodify_output, expected): 417 file = os.path.join(self.test_dir, "Android.bp") 418 419 with open(file, "w", encoding="utf8") as tio: 420 tio.write(bpmodify_output.strip("\n")) 421 422 bpmodify_runner = ab.BpModifyRunner( 423 os.path.join(os.path.dirname(sys.argv[0]), "bpmodify")) 424 change.fix_bp_file(file, "bcpf", bpmodify_runner) 425 426 with open(file, "r", encoding="utf8") as tio: 427 contents = tio.read() 428 self.assertEqual(expected.lstrip("\n"), contents) 429 430 def check_change_snippet(self, change, expected): 431 snippet = change.snippet(" ") 432 self.assertEqual(expected, snippet) 433 434 def test_change_property_with_value_no_comment(self): 435 change = ab.HiddenApiPropertyChange( 436 property_name="split_packages", 437 values=["android.provider"], 438 ) 439 440 self.check_change_snippet( 441 change, """ 442 split_packages: [ 443 "android.provider", 444 ], 445""") 446 447 self.check_change_fix( 448 change, """ 449bootclasspath_fragment { 450 name: "bcpf", 451 452 // modified by the Soong or platform compat team. 453 hidden_api: { 454 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 455 split_packages: [ 456 "android.provider", 457 ], 458 }, 459} 460""", """ 461bootclasspath_fragment { 462 name: "bcpf", 463 464 // modified by the Soong or platform compat team. 465 hidden_api: { 466 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 467 split_packages: [ 468 "android.provider", 469 ], 470 }, 471} 472""") 473 474 def test_change_property_with_value_and_comment(self): 475 change = ab.HiddenApiPropertyChange( 476 property_name="split_packages", 477 values=["android.provider"], 478 property_comment=_MULTI_LINE_COMMENT, 479 ) 480 481 self.check_change_snippet( 482 change, """ 483 // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu 484 // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida 485 // ultricies sem tincidunt luctus. 486 split_packages: [ 487 "android.provider", 488 ], 489""") 490 491 self.check_change_fix( 492 change, """ 493bootclasspath_fragment { 494 name: "bcpf", 495 496 // modified by the Soong or platform compat team. 497 hidden_api: { 498 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 499 split_packages: [ 500 "android.provider", 501 ], 502 503 single_packages: [ 504 "android.system", 505 ], 506 507 }, 508} 509""", """ 510bootclasspath_fragment { 511 name: "bcpf", 512 513 // modified by the Soong or platform compat team. 514 hidden_api: { 515 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 516 517 // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu 518 // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida 519 // ultricies sem tincidunt luctus. 520 split_packages: [ 521 "android.provider", 522 ], 523 524 single_packages: [ 525 "android.system", 526 ], 527 528 }, 529} 530""") 531 532 def test_set_property_with_value_and_comment(self): 533 change = ab.HiddenApiPropertyChange( 534 property_name="split_packages", 535 values=["another.provider", "other.system"], 536 property_comment=_MULTI_LINE_COMMENT, 537 action=ab.PropertyChangeAction.REPLACE, 538 ) 539 540 self.check_change_snippet( 541 change, """ 542 // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu 543 // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida 544 // ultricies sem tincidunt luctus. 545 split_packages: [ 546 "another.provider", 547 "other.system", 548 ], 549""") 550 551 self.check_change_fix( 552 change, """ 553bootclasspath_fragment { 554 name: "bcpf", 555 556 // modified by the Soong or platform compat team. 557 hidden_api: { 558 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 559 split_packages: [ 560 "another.provider", 561 "other.system", 562 ], 563 }, 564} 565""", """ 566bootclasspath_fragment { 567 name: "bcpf", 568 569 // modified by the Soong or platform compat team. 570 hidden_api: { 571 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 572 573 // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu 574 // justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida 575 // ultricies sem tincidunt luctus. 576 split_packages: [ 577 "another.provider", 578 "other.system", 579 ], 580 }, 581} 582""") 583 584 def test_set_property_with_no_value_or_comment(self): 585 change = ab.HiddenApiPropertyChange( 586 property_name="split_packages", 587 values=[], 588 action=ab.PropertyChangeAction.REPLACE, 589 ) 590 591 self.check_change_snippet(change, """ 592 split_packages: [], 593""") 594 595 self.check_change_fix( 596 change, """ 597bootclasspath_fragment { 598 name: "bcpf", 599 600 // modified by the Soong or platform compat team. 601 hidden_api: { 602 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 603 split_packages: [ 604 "another.provider", 605 "other.system", 606 ], 607 package_prefixes: ["android.provider"], 608 }, 609} 610""", """ 611bootclasspath_fragment { 612 name: "bcpf", 613 614 // modified by the Soong or platform compat team. 615 hidden_api: { 616 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 617 split_packages: [], 618 package_prefixes: ["android.provider"], 619 }, 620} 621""") 622 623 def test_set_empty_property_with_no_value_or_comment(self): 624 change = ab.HiddenApiPropertyChange( 625 property_name="split_packages", 626 values=[], 627 action=ab.PropertyChangeAction.REPLACE, 628 ) 629 630 self.check_change_snippet(change, """ 631 split_packages: [], 632""") 633 634 self.check_change_fix( 635 change, """ 636bootclasspath_fragment { 637 name: "bcpf", 638 639 // modified by the Soong or platform compat team. 640 hidden_api: { 641 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 642 split_packages: [], 643 package_prefixes: ["android.provider"], 644 }, 645} 646""", """ 647bootclasspath_fragment { 648 name: "bcpf", 649 650 // modified by the Soong or platform compat team. 651 hidden_api: { 652 max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], 653 split_packages: [], 654 package_prefixes: ["android.provider"], 655 }, 656} 657""") 658 659 660if __name__ == "__main__": 661 unittest.main(verbosity=3) 662