Alex Light | eb7c144 | 2015-08-31 13:17:42 -0700 | [diff] [blame] | 1 | #!/usr/bin/python3 |
| 2 | # |
| 3 | # Copyright (C) 2015 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 | """ |
Alex Light | 8a0e033 | 2015-10-26 10:11:58 -0700 | [diff] [blame] | 18 | Generate Smali Main file from a classes.xml file. |
Alex Light | eb7c144 | 2015-08-31 13:17:42 -0700 | [diff] [blame] | 19 | """ |
| 20 | |
| 21 | import os |
| 22 | import sys |
| 23 | from pathlib import Path |
| 24 | |
| 25 | BUILD_TOP = os.getenv("ANDROID_BUILD_TOP") |
| 26 | if BUILD_TOP is None: |
| 27 | print("ANDROID_BUILD_TOP not set. Please run build/envsetup.sh", file=sys.stderr) |
| 28 | sys.exit(1) |
| 29 | |
| 30 | # Allow us to import utils and mixins. |
| 31 | sys.path.append(str(Path(BUILD_TOP)/"art"/"test"/"utils"/"python")) |
| 32 | |
| 33 | from testgen.utils import get_copyright |
| 34 | import testgen.mixins as mixins |
| 35 | |
| 36 | from collections import namedtuple |
| 37 | import itertools |
| 38 | import functools |
| 39 | import xml.etree.ElementTree as ET |
| 40 | |
| 41 | class MainClass(mixins.DumpMixin, mixins.Named, mixins.SmaliFileMixin): |
| 42 | """ |
| 43 | A mainclass and main method for this test. |
| 44 | """ |
| 45 | |
| 46 | MAIN_CLASS_TEMPLATE = """{copyright} |
| 47 | .class public LMain; |
| 48 | .super Ljava/lang/Object; |
| 49 | |
| 50 | # class Main {{ |
| 51 | |
| 52 | .method public constructor <init>()V |
| 53 | .registers 1 |
| 54 | invoke-direct {{p0}}, Ljava/lang/Object;-><init>()V |
| 55 | return-void |
| 56 | .end method |
| 57 | |
| 58 | {test_groups} |
| 59 | |
| 60 | {test_funcs} |
| 61 | |
| 62 | {main_func} |
| 63 | |
| 64 | # }} |
| 65 | """ |
| 66 | |
| 67 | MAIN_FUNCTION_TEMPLATE = """ |
| 68 | # public static void main(String[] args) {{ |
| 69 | .method public static main([Ljava/lang/String;)V |
| 70 | .locals 2 |
| 71 | sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream; |
| 72 | |
| 73 | {test_group_invoke} |
| 74 | |
| 75 | return-void |
| 76 | .end method |
| 77 | # }} |
| 78 | """ |
| 79 | |
| 80 | TEST_GROUP_INVOKE_TEMPLATE = """ |
| 81 | # {test_name}(); |
| 82 | invoke-static {{}}, {test_name}()V |
| 83 | """ |
| 84 | |
| 85 | def __init__(self): |
| 86 | """ |
| 87 | Initialize this MainClass |
| 88 | """ |
| 89 | self.tests = set() |
| 90 | self.global_funcs = set() |
| 91 | |
| 92 | def add_instance(self, it): |
| 93 | """ |
| 94 | Add an instance test for the given class |
| 95 | """ |
| 96 | self.tests.add(it) |
| 97 | |
| 98 | def add_func(self, f): |
| 99 | """ |
| 100 | Add a function to the class |
| 101 | """ |
| 102 | self.global_funcs.add(f) |
| 103 | |
| 104 | def get_name(self): |
| 105 | """ |
| 106 | Get the name of this class |
| 107 | """ |
| 108 | return "Main" |
| 109 | |
| 110 | def __str__(self): |
| 111 | """ |
| 112 | Print this class |
| 113 | """ |
| 114 | all_tests = sorted(self.tests) |
| 115 | test_invoke = "" |
| 116 | test_groups = "" |
| 117 | for t in all_tests: |
| 118 | test_groups += str(t) |
| 119 | for t in sorted(all_tests): |
| 120 | test_invoke += self.TEST_GROUP_INVOKE_TEMPLATE.format(test_name=t.get_name()) |
| 121 | main_func = self.MAIN_FUNCTION_TEMPLATE.format(test_group_invoke=test_invoke) |
| 122 | |
| 123 | funcs = "" |
| 124 | for f in self.global_funcs: |
| 125 | funcs += str(f) |
| 126 | return self.MAIN_CLASS_TEMPLATE.format(copyright = get_copyright('smali'), |
| 127 | test_groups=test_groups, |
| 128 | main_func=main_func, test_funcs=funcs) |
| 129 | |
| 130 | |
| 131 | class InstanceTest(mixins.Named, mixins.NameComparableMixin): |
| 132 | """ |
| 133 | A method that runs tests for a particular concrete type, It calls the test |
| 134 | cases for running it in all possible ways. |
| 135 | """ |
| 136 | |
| 137 | INSTANCE_TEST_TEMPLATE = """ |
| 138 | # public static void {test_name}() {{ |
| 139 | # System.out.println("Testing for type {ty}"); |
| 140 | # String s = "{ty}"; |
| 141 | # {ty} v = new {ty}(); |
| 142 | .method public static {test_name}()V |
| 143 | .locals 3 |
| 144 | sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream; |
| 145 | const-string v0, "Testing for type {ty}" |
| 146 | invoke-virtual {{v2,v0}}, Ljava/io/PrintStream;->println(Ljava/lang/Object;)V |
| 147 | |
| 148 | const-string v0, "{ty}" |
| 149 | new-instance v1, L{ty}; |
| 150 | invoke-direct {{v1}}, L{ty};-><init>()V |
| 151 | |
| 152 | {invokes} |
| 153 | |
| 154 | const-string v0, "End testing for type {ty}" |
| 155 | invoke-virtual {{v2,v0}}, Ljava/io/PrintStream;->println(Ljava/lang/Object;)V |
| 156 | return-void |
| 157 | .end method |
| 158 | # System.out.println("End testing for type {ty}"); |
| 159 | # }} |
| 160 | """ |
| 161 | |
| 162 | TEST_INVOKE_TEMPLATE = """ |
| 163 | # {fname}(s, v); |
| 164 | invoke-static {{v0, v1}}, {fname}(Ljava/lang/String;L{farg};)V |
| 165 | """ |
| 166 | |
| 167 | def __init__(self, main, ty): |
| 168 | """ |
| 169 | Initialize this test group for the given type |
| 170 | """ |
| 171 | self.ty = ty |
| 172 | self.main = main |
| 173 | self.funcs = set() |
| 174 | self.main.add_instance(self) |
| 175 | |
| 176 | def get_name(self): |
| 177 | """ |
| 178 | Get the name of this test group |
| 179 | """ |
| 180 | return "TEST_NAME_"+self.ty |
| 181 | |
| 182 | def add_func(self, f): |
| 183 | """ |
| 184 | Add a test function to this test group |
| 185 | """ |
| 186 | self.main.add_func(f) |
| 187 | self.funcs.add(f) |
| 188 | |
| 189 | def __str__(self): |
| 190 | """ |
| 191 | Returns the smali code for this function |
| 192 | """ |
| 193 | func_invokes = "" |
| 194 | for f in sorted(self.funcs, key=lambda a: (a.func, a.farg)): |
| 195 | func_invokes += self.TEST_INVOKE_TEMPLATE.format(fname=f.get_name(), |
| 196 | farg=f.farg) |
| 197 | |
| 198 | return self.INSTANCE_TEST_TEMPLATE.format(test_name=self.get_name(), ty=self.ty, |
| 199 | invokes=func_invokes) |
| 200 | |
| 201 | class Func(mixins.Named, mixins.NameComparableMixin): |
| 202 | """ |
| 203 | A single test case that attempts to invoke a function on receiver of a given type. |
| 204 | """ |
| 205 | |
| 206 | TEST_FUNCTION_TEMPLATE = """ |
| 207 | # public static void {fname}(String s, {farg} v) {{ |
| 208 | # try {{ |
| 209 | # System.out.printf("%s-{invoke_type:<9} {farg:>9}.{callfunc}()='%s'\\n", s, v.{callfunc}()); |
| 210 | # return; |
| 211 | # }} catch (Error e) {{ |
| 212 | # System.out.printf("%s-{invoke_type} on {farg}: {callfunc}() threw exception!\\n", s); |
| 213 | # e.printStackTrace(System.out); |
| 214 | # }} |
| 215 | # }} |
| 216 | .method public static {fname}(Ljava/lang/String;L{farg};)V |
| 217 | .locals 7 |
| 218 | :call_{fname}_try_start |
| 219 | const/4 v0, 2 |
| 220 | new-array v1,v0, [Ljava/lang/Object; |
| 221 | const/4 v0, 0 |
| 222 | aput-object p0,v1,v0 |
| 223 | |
| 224 | sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream; |
| 225 | const-string v3, "%s-{invoke_type:<9} {farg:>9}.{callfunc}()='%s'\\n" |
| 226 | |
| 227 | invoke-{invoke_type} {{p1}}, L{farg};->{callfunc}()Ljava/lang/String; |
| 228 | move-result-object v4 |
| 229 | const/4 v0, 1 |
| 230 | aput-object v4, v1, v0 |
| 231 | |
| 232 | invoke-virtual {{v2,v3,v1}}, Ljava/io/PrintStream;->printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; |
| 233 | return-void |
| 234 | :call_{fname}_try_end |
| 235 | .catch Ljava/lang/Error; {{:call_{fname}_try_start .. :call_{fname}_try_end}} :error_{fname}_start |
| 236 | :error_{fname}_start |
| 237 | move-exception v3 |
| 238 | const/4 v0, 1 |
| 239 | new-array v1,v0, [Ljava/lang/Object; |
| 240 | const/4 v0, 0 |
| 241 | aput-object p0, v1, v0 |
| 242 | sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream; |
| 243 | const-string v4, "%s-{invoke_type} on {farg}: {callfunc}() threw exception!\\n" |
| 244 | invoke-virtual {{v2,v4,v1}}, Ljava/io/PrintStream;->printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; |
| 245 | invoke-virtual {{v3,v2}}, Ljava/lang/Error;->printStackTrace(Ljava/io/PrintStream;)V |
| 246 | return-void |
| 247 | .end method |
| 248 | """ |
| 249 | |
| 250 | def __init__(self, func, farg, invoke): |
| 251 | """ |
| 252 | Initialize this test function for the given invoke type and argument |
| 253 | """ |
| 254 | self.func = func |
| 255 | self.farg = farg |
| 256 | self.invoke = invoke |
| 257 | |
| 258 | def get_name(self): |
| 259 | """ |
| 260 | Get the name of this test |
| 261 | """ |
| 262 | return "Test_Func_{}_{}_{}".format(self.func, self.farg, self.invoke) |
| 263 | |
| 264 | def __str__(self): |
| 265 | """ |
| 266 | Get the smali code for this test function |
| 267 | """ |
| 268 | return self.TEST_FUNCTION_TEMPLATE.format(fname=self.get_name(), |
| 269 | farg=self.farg, |
| 270 | invoke_type=self.invoke, |
| 271 | callfunc=self.func) |
| 272 | |
| 273 | def flatten_classes(classes, c): |
| 274 | """ |
| 275 | Iterate over all the classes 'c' can be used as |
| 276 | """ |
| 277 | while c: |
| 278 | yield c |
| 279 | c = classes.get(c.super_class) |
| 280 | |
| 281 | def flatten_class_methods(classes, c): |
| 282 | """ |
| 283 | Iterate over all the methods 'c' can call |
| 284 | """ |
| 285 | for c1 in flatten_classes(classes, c): |
| 286 | yield from c1.methods |
| 287 | |
| 288 | def flatten_interfaces(dat, c): |
| 289 | """ |
| 290 | Iterate over all the interfaces 'c' transitively implements |
| 291 | """ |
| 292 | def get_ifaces(cl): |
| 293 | for i2 in cl.implements: |
| 294 | yield dat.interfaces[i2] |
| 295 | yield from get_ifaces(dat.interfaces[i2]) |
| 296 | |
| 297 | for cl in flatten_classes(dat.classes, c): |
| 298 | yield from get_ifaces(cl) |
| 299 | |
| 300 | def flatten_interface_methods(dat, i): |
| 301 | """ |
| 302 | Iterate over all the interface methods 'c' can call |
| 303 | """ |
| 304 | yield from i.methods |
| 305 | for i2 in flatten_interfaces(dat, i): |
| 306 | yield from i2.methods |
| 307 | |
| 308 | def make_main_class(dat): |
| 309 | """ |
| 310 | Creates a Main.smali file that runs all the tests |
| 311 | """ |
| 312 | m = MainClass() |
| 313 | for c in dat.classes.values(): |
| 314 | i = InstanceTest(m, c.name) |
| 315 | for clazz in flatten_classes(dat.classes, c): |
| 316 | for meth in flatten_class_methods(dat.classes, clazz): |
| 317 | i.add_func(Func(meth, clazz.name, 'virtual')) |
| 318 | for iface in flatten_interfaces(dat, clazz): |
| 319 | for meth in flatten_interface_methods(dat, iface): |
| 320 | i.add_func(Func(meth, clazz.name, 'virtual')) |
| 321 | i.add_func(Func(meth, iface.name, 'interface')) |
| 322 | return m |
| 323 | |
| 324 | class TestData(namedtuple("TestData", ['classes', 'interfaces'])): |
| 325 | """ |
| 326 | A class representing the classes.xml document. |
| 327 | """ |
| 328 | pass |
| 329 | |
| 330 | class Clazz(namedtuple("Clazz", ["name", "methods", "super_class", "implements"])): |
| 331 | """ |
| 332 | A class representing a class element in the classes.xml document. |
| 333 | """ |
| 334 | pass |
| 335 | |
| 336 | class IFace(namedtuple("IFace", ["name", "methods", "super_class", "implements"])): |
| 337 | """ |
| 338 | A class representing an interface element in the classes.xml document. |
| 339 | """ |
| 340 | pass |
| 341 | |
| 342 | def parse_xml(xml): |
| 343 | """ |
| 344 | Parse the xml description of this test. |
| 345 | """ |
| 346 | classes = dict() |
| 347 | ifaces = dict() |
| 348 | root = ET.fromstring(xml) |
| 349 | for iface in root.find("interfaces"): |
| 350 | name = iface.attrib['name'] |
| 351 | implements = [a.text for a in iface.find("implements")] |
| 352 | methods = [a.text for a in iface.find("methods")] |
| 353 | ifaces[name] = IFace(name = name, |
| 354 | super_class = iface.attrib['super'], |
| 355 | methods = methods, |
| 356 | implements = implements) |
| 357 | for clazz in root.find('classes'): |
| 358 | name = clazz.attrib['name'] |
| 359 | implements = [a.text for a in clazz.find("implements")] |
| 360 | methods = [a.text for a in clazz.find("methods")] |
| 361 | classes[name] = Clazz(name = name, |
| 362 | super_class = clazz.attrib['super'], |
| 363 | methods = methods, |
| 364 | implements = implements) |
| 365 | return TestData(classes, ifaces) |
| 366 | |
| 367 | def main(argv): |
| 368 | smali_dir = Path(argv[1]) |
| 369 | if not smali_dir.exists() or not smali_dir.is_dir(): |
| 370 | print("{} is not a valid smali dir".format(smali_dir), file=sys.stderr) |
| 371 | sys.exit(1) |
| 372 | class_data = parse_xml((smali_dir / "classes.xml").open().read()) |
| 373 | make_main_class(class_data).dump(smali_dir) |
| 374 | |
| 375 | if __name__ == '__main__': |
| 376 | main(sys.argv) |