platform_build_soong/scripts/hiddenapi/analyze_bcpf_test.py

662 lines
20 KiB
Python
Raw Normal View History

#!/usr/bin/env python
#
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for analyzing bootclasspath_fragment modules."""
import os.path
import shutil
import tempfile
import unittest
import unittest.mock
import sys
import analyze_bcpf as ab
_FRAMEWORK_HIDDENAPI = "frameworks/base/boot/hiddenapi"
_MAX_TARGET_O = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-o.txt"
_MAX_TARGET_P = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-p.txt"
_MAX_TARGET_Q = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-q.txt"
_MAX_TARGET_R = f"{_FRAMEWORK_HIDDENAPI}/hiddenapi-max-target-r-loprio.txt"
_MULTI_LINE_COMMENT = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu justo,
bibendum eu malesuada vel, fringilla in odio. Etiam gravida ultricies sem
tincidunt luctus.""".replace("\n", " ").strip()
class FakeBuildOperation(ab.BuildOperation):
def __init__(self, lines, return_code):
ab.BuildOperation.__init__(self, None)
self._lines = lines
self.returncode = return_code
def lines(self):
return iter(self._lines)
def wait(self, *args, **kwargs):
return
class TestAnalyzeBcpf(unittest.TestCase):
def setUp(self):
# Create a temporary directory
self.test_dir = tempfile.mkdtemp()
def tearDown(self):
# Remove the directory after the test
shutil.rmtree(self.test_dir)
@staticmethod
def write_abs_file(abs_path, contents):
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, "w", encoding="utf8") as f:
print(contents.removeprefix("\n"), file=f, end="")
def populate_fs(self, fs):
for path, contents in fs.items():
abs_path = os.path.join(self.test_dir, path)
self.write_abs_file(abs_path, contents)
def create_analyzer_for_test(self,
fs=None,
bcpf="bcpf",
apex="apex",
sdk="sdk",
fix=False):
if fs:
self.populate_fs(fs)
top_dir = self.test_dir
out_dir = os.path.join(self.test_dir, "out")
product_out_dir = "out/product"
bcpf_dir = f"{bcpf}-dir"
modules = {bcpf: {"path": [bcpf_dir]}}
module_info = ab.ModuleInfo(modules)
analyzer = ab.BcpfAnalyzer(
tool_path=os.path.join(out_dir, "bin"),
top_dir=top_dir,
out_dir=out_dir,
product_out_dir=product_out_dir,
bcpf=bcpf,
apex=apex,
sdk=sdk,
fix=fix,
module_info=module_info,
)
analyzer.load_all_flags()
return analyzer
def test_reformat_report_text(self):
lines = """
99. An item in a numbered list
that traverses multiple lines.
An indented example
that should not be reformatted.
"""
reformatted = ab.BcpfAnalyzer.reformat_report_test(lines)
self.assertEqual(
"""
99. An item in a numbered list that traverses multiple lines.
An indented example
that should not be reformatted.
""", reformatted)
def do_test_build_flags(self, fix):
lines = """
ERROR: Hidden API flags are inconsistent:
< out/soong/.intermediates/bcpf-dir/bcpf-dir/filtered-flags.csv
> out/soong/hiddenapi/hiddenapi-flags.csv
< Lacme/test/Class;-><init>()V,blocked
> Lacme/test/Class;-><init>()V,max-target-o
< Lacme/test/Other;->getThing()Z,blocked
> Lacme/test/Other;->getThing()Z,max-target-p
< Lacme/test/Widget;-><init()V,blocked
> Lacme/test/Widget;-><init()V,max-target-q
< Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked
> Lacme/test/Gadget;->NAME:Ljava/lang/String;,lo-prio,max-target-r
16:37:32 ninja failed with: exit status 1
""".strip().splitlines()
operation = FakeBuildOperation(lines=lines, return_code=1)
fs = {
_MAX_TARGET_O:
"""
Lacme/items/Magnet;->size:I
Lacme/test/Class;-><init>()V
""",
_MAX_TARGET_P:
"""
Lacme/items/Rocket;->size:I
Lacme/test/Other;->getThing()Z
""",
_MAX_TARGET_Q:
"""
Lacme/items/Rock;->size:I
Lacme/test/Widget;-><init()V
""",
_MAX_TARGET_R:
"""
Lacme/items/Lever;->size:I
Lacme/test/Gadget;->NAME:Ljava/lang/String;
""",
"bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt":
"""
Lacme/old/Class;->getWidget()Lacme/test/Widget;
""",
"out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv":
"""
Lacme/test/Gadget;->NAME:Ljava/lang/String;,blocked
Lacme/test/Widget;-><init()V,blocked
Lacme/test/Class;-><init>()V,blocked
Lacme/test/Other;->getThing()Z,blocked
""",
}
analyzer = self.create_analyzer_for_test(fs, fix=fix)
# Override the build_file_read_output() method to just return a fake
# build operation.
analyzer.build_file_read_output = unittest.mock.Mock(
return_value=operation)
# Override the run_command() method to do nothing.
analyzer.run_command = unittest.mock.Mock()
result = ab.Result()
analyzer.build_monolithic_flags(result)
expected_diffs = {
"Lacme/test/Gadget;->NAME:Ljava/lang/String;":
(["blocked"], ["lo-prio", "max-target-r"]),
"Lacme/test/Widget;-><init()V": (["blocked"], ["max-target-q"]),
"Lacme/test/Class;-><init>()V": (["blocked"], ["max-target-o"]),
"Lacme/test/Other;->getThing()Z": (["blocked"], ["max-target-p"])
}
self.assertEqual(expected_diffs, result.diffs, msg="flag differences")
expected_property_changes = [
ab.HiddenApiPropertyChange(
property_name="max_target_o_low_priority",
values=["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
property_comment=""),
ab.HiddenApiPropertyChange(
property_name="max_target_p",
values=["hiddenapi/hiddenapi-max-target-p.txt"],
property_comment=""),
ab.HiddenApiPropertyChange(
property_name="max_target_q",
values=["hiddenapi/hiddenapi-max-target-q.txt"],
property_comment=""),
ab.HiddenApiPropertyChange(
property_name="max_target_r_low_priority",
values=["hiddenapi/hiddenapi-max-target-r-low-priority.txt"],
property_comment=""),
]
self.assertEqual(
expected_property_changes,
result.property_changes,
msg="property changes")
return result
def test_build_flags_report(self):
result = self.do_test_build_flags(fix=False)
expected_file_changes = [
ab.FileChange(
path="bcpf-dir/hiddenapi/"
"hiddenapi-max-target-o-low-priority.txt",
description="""Add the following entries:
Lacme/test/Class;-><init>()V
""",
),
ab.FileChange(
path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt",
description="""Add the following entries:
Lacme/test/Other;->getThing()Z
""",
),
ab.FileChange(
path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt",
description="""Add the following entries:
Lacme/test/Widget;-><init()V
"""),
ab.FileChange(
path="bcpf-dir/hiddenapi/"
"hiddenapi-max-target-r-low-priority.txt",
description="""Add the following entries:
Lacme/test/Gadget;->NAME:Ljava/lang/String;
"""),
ab.FileChange(
path="frameworks/base/boot/hiddenapi/"
"hiddenapi-max-target-o.txt",
description="""Remove the following entries:
Lacme/test/Class;-><init>()V
"""),
ab.FileChange(
path="frameworks/base/boot/hiddenapi/"
"hiddenapi-max-target-p.txt",
description="""Remove the following entries:
Lacme/test/Other;->getThing()Z
"""),
ab.FileChange(
path="frameworks/base/boot/hiddenapi/"
"hiddenapi-max-target-q.txt",
description="""Remove the following entries:
Lacme/test/Widget;-><init()V
"""),
ab.FileChange(
path="frameworks/base/boot/hiddenapi/"
"hiddenapi-max-target-r-loprio.txt",
description="""Remove the following entries:
Lacme/test/Gadget;->NAME:Ljava/lang/String;
""")
]
result.file_changes.sort()
self.assertEqual(
expected_file_changes, result.file_changes, msg="file_changes")
def test_build_flags_fix(self):
result = self.do_test_build_flags(fix=True)
expected_file_changes = [
ab.FileChange(
path="bcpf-dir/hiddenapi/"
"hiddenapi-max-target-o-low-priority.txt",
description="Created with 'bcpf' specific entries"),
ab.FileChange(
path="bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt",
description="Added 'bcpf' specific entries"),
ab.FileChange(
path="bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt",
description="Created with 'bcpf' specific entries"),
ab.FileChange(
path="bcpf-dir/hiddenapi/"
"hiddenapi-max-target-r-low-priority.txt",
description="Created with 'bcpf' specific entries"),
ab.FileChange(
path=_MAX_TARGET_O,
description="Removed 'bcpf' specific entries"),
ab.FileChange(
path=_MAX_TARGET_P,
description="Removed 'bcpf' specific entries"),
ab.FileChange(
path=_MAX_TARGET_Q,
description="Removed 'bcpf' specific entries"),
ab.FileChange(
path=_MAX_TARGET_R,
description="Removed 'bcpf' specific entries")
]
result.file_changes.sort()
self.assertEqual(
expected_file_changes, result.file_changes, msg="file_changes")
expected_file_contents = {
"bcpf-dir/hiddenapi/hiddenapi-max-target-o-low-priority.txt":
"""
Lacme/test/Class;-><init>()V
""",
"bcpf-dir/hiddenapi/hiddenapi-max-target-p.txt":
"""
Lacme/old/Class;->getWidget()Lacme/test/Widget;
Lacme/test/Other;->getThing()Z
""",
"bcpf-dir/hiddenapi/hiddenapi-max-target-q.txt":
"""
Lacme/test/Widget;-><init()V
""",
"bcpf-dir/hiddenapi/hiddenapi-max-target-r-low-priority.txt":
"""
Lacme/test/Gadget;->NAME:Ljava/lang/String;
""",
_MAX_TARGET_O:
"""
Lacme/items/Magnet;->size:I
""",
_MAX_TARGET_P:
"""
Lacme/items/Rocket;->size:I
""",
_MAX_TARGET_Q:
"""
Lacme/items/Rock;->size:I
""",
_MAX_TARGET_R:
"""
Lacme/items/Lever;->size:I
""",
}
for file_change in result.file_changes:
path = file_change.path
expected_contents = expected_file_contents[path].lstrip()
abs_path = os.path.join(self.test_dir, path)
with open(abs_path, "r", encoding="utf8") as tio:
contents = tio.read()
self.assertEqual(
expected_contents, contents, msg=f"{path} contents")
def test_compute_hiddenapi_package_properties(self):
fs = {
"out/soong/.intermediates/bcpf-dir/bcpf/all-flags.csv":
"""
La/b/C;->m()V
La/b/c/D;->m()V
La/b/c/E;->m()V
Lb/c/D;->m()V
Lb/c/E;->m()V
Lb/c/d/E;->m()V
""",
"out/soong/hiddenapi/hiddenapi-flags.csv":
"""
La/b/C;->m()V
La/b/D;->m()V
La/b/E;->m()V
La/b/c/D;->m()V
La/b/c/E;->m()V
La/b/c/d/E;->m()V
La/b/c/d/e/F;->m()V
Lb/c/D;->m()V
Lb/c/E;->m()V
Lb/c/d/E;->m()V
"""
}
analyzer = self.create_analyzer_for_test(fs)
analyzer.load_all_flags()
result = ab.Result()
analyzer.compute_hiddenapi_package_properties(result)
self.assertEqual(["a.b"], list(result.split_packages.keys()))
reason = result.split_packages["a.b"]
self.assertEqual(["a.b.C"], reason.bcpf)
self.assertEqual(["a.b.D", "a.b.E"], reason.other)
self.assertEqual(["a.b.c"], list(result.single_packages.keys()))
reason = result.single_packages["a.b.c"]
self.assertEqual(["a.b.c"], reason.bcpf)
self.assertEqual(["a.b.c.d", "a.b.c.d.e"], reason.other)
self.assertEqual(["b"], result.package_prefixes)
class TestHiddenApiPropertyChange(unittest.TestCase):
def setUp(self):
# Create a temporary directory
self.test_dir = tempfile.mkdtemp()
def tearDown(self):
# Remove the directory after the test
shutil.rmtree(self.test_dir)
def check_change_fix(self, change, bpmodify_output, expected):
file = os.path.join(self.test_dir, "Android.bp")
with open(file, "w", encoding="utf8") as tio:
tio.write(bpmodify_output.strip("\n"))
bpmodify_runner = ab.BpModifyRunner(
os.path.join(os.path.dirname(sys.argv[0]), "bpmodify"))
change.fix_bp_file(file, "bcpf", bpmodify_runner)
with open(file, "r", encoding="utf8") as tio:
contents = tio.read()
self.assertEqual(expected.lstrip("\n"), contents)
def check_change_snippet(self, change, expected):
snippet = change.snippet(" ")
self.assertEqual(expected, snippet)
def test_change_property_with_value_no_comment(self):
change = ab.HiddenApiPropertyChange(
property_name="split_packages",
values=["android.provider"],
)
self.check_change_snippet(
change, """
split_packages: [
"android.provider",
],
""")
self.check_change_fix(
change, """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [
"android.provider",
],
},
}
""", """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [
"android.provider",
],
},
}
""")
def test_change_property_with_value_and_comment(self):
change = ab.HiddenApiPropertyChange(
property_name="split_packages",
values=["android.provider"],
property_comment=_MULTI_LINE_COMMENT,
)
self.check_change_snippet(
change, """
// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
// justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
// ultricies sem tincidunt luctus.
split_packages: [
"android.provider",
],
""")
self.check_change_fix(
change, """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [
"android.provider",
],
single_packages: [
"android.system",
],
},
}
""", """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
// justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
// ultricies sem tincidunt luctus.
split_packages: [
"android.provider",
],
single_packages: [
"android.system",
],
},
}
""")
def test_set_property_with_value_and_comment(self):
change = ab.HiddenApiPropertyChange(
property_name="split_packages",
values=["another.provider", "other.system"],
property_comment=_MULTI_LINE_COMMENT,
action=ab.PropertyChangeAction.REPLACE,
)
self.check_change_snippet(
change, """
// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
// justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
// ultricies sem tincidunt luctus.
split_packages: [
"another.provider",
"other.system",
],
""")
self.check_change_fix(
change, """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [
"another.provider",
"other.system",
],
},
}
""", """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut arcu
// justo, bibendum eu malesuada vel, fringilla in odio. Etiam gravida
// ultricies sem tincidunt luctus.
split_packages: [
"another.provider",
"other.system",
],
},
}
""")
def test_set_property_with_no_value_or_comment(self):
change = ab.HiddenApiPropertyChange(
property_name="split_packages",
values=[],
action=ab.PropertyChangeAction.REPLACE,
)
self.check_change_snippet(change, """
split_packages: [],
""")
self.check_change_fix(
change, """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [
"another.provider",
"other.system",
],
package_prefixes: ["android.provider"],
},
}
""", """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [],
package_prefixes: ["android.provider"],
},
}
""")
def test_set_empty_property_with_no_value_or_comment(self):
change = ab.HiddenApiPropertyChange(
property_name="split_packages",
values=[],
action=ab.PropertyChangeAction.REPLACE,
)
self.check_change_snippet(change, """
split_packages: [],
""")
self.check_change_fix(
change, """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [],
package_prefixes: ["android.provider"],
},
}
""", """
bootclasspath_fragment {
name: "bcpf",
// modified by the Soong or platform compat team.
hidden_api: {
max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
split_packages: [],
package_prefixes: ["android.provider"],
},
}
""")
if __name__ == "__main__":
unittest.main(verbosity=3)