1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package util.build; 18 19 import com.android.dex.util.FileUtils; 20 21 import dot.junit.AllTests; 22 23 import junit.framework.TestCase; 24 import junit.framework.TestResult; 25 import junit.framework.TestSuite; 26 import junit.textui.TestRunner; 27 28 import java.io.BufferedWriter; 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.FileOutputStream; 32 import java.io.FileReader; 33 import java.io.IOException; 34 import java.io.OutputStreamWriter; 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.HashSet; 39 import java.util.Iterator; 40 import java.util.LinkedHashMap; 41 import java.util.List; 42 import java.util.Scanner; 43 import java.util.Set; 44 import java.util.TreeSet; 45 import java.util.Map.Entry; 46 import java.util.regex.MatchResult; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 /** 51 * Main class to generate data from the test suite to later run from a shell 52 * script. the project's home folder.<br> 53 * <project-home>/src must contain the java sources<br> 54 * <project-home>/src/<for-each-package>/Main_testN1.java will be generated<br> 55 * (one Main class for each test method in the Test_... class 56 */ 57 public class BuildDalvikSuite { 58 59 public static boolean DEBUG = true; 60 61 private static String JAVASRC_FOLDER = ""; 62 private static String MAIN_SRC_OUTPUT_FOLDER = ""; 63 64 // the folder for the generated junit-files for the cts host (which in turn 65 // execute the real vm tests using adb push/shell etc) 66 private static String HOSTJUNIT_SRC_OUTPUT_FOLDER = ""; 67 private static String OUTPUT_FOLDER = ""; 68 private static String COMPILED_CLASSES_FOLDER = ""; 69 70 private static String CLASSES_OUTPUT_FOLDER = ""; 71 private static String HOSTJUNIT_CLASSES_OUTPUT_FOLDER = ""; 72 73 private static String CLASS_PATH = ""; 74 75 private static String restrictTo = null; // e.g. restrict to "opcodes.add_double" 76 77 private static final String TARGET_JAR_ROOT_PATH = "/data/local/tmp/vm-tests"; 78 79 private int testClassCnt = 0; 80 private int testMethodsCnt = 0; 81 82 /* 83 * using a linked hashmap to keep the insertion order for iterators. 84 * the junit suite/tests adding order is used to generate the order of the 85 * report. 86 * a map. key: fully qualified class name, value: a list of test methods for 87 * the given class 88 */ 89 private LinkedHashMap<String, List<String>> map = new LinkedHashMap<String, 90 List<String>>(); 91 92 private class MethodData { 93 String methodBody, constraint, title; 94 } 95 96 /** 97 * @param args 98 * args 0 must be the project root folder (where src, lib etc. 99 * resides) 100 * @throws IOException 101 */ main(String[] args)102 public static void main(String[] args) throws IOException { 103 104 if (args.length > 5) { 105 JAVASRC_FOLDER = args[0]; 106 OUTPUT_FOLDER = args[1]; 107 CLASS_PATH = args[2]; 108 MAIN_SRC_OUTPUT_FOLDER = args[3]; 109 CLASSES_OUTPUT_FOLDER = MAIN_SRC_OUTPUT_FOLDER + "/classes"; 110 111 COMPILED_CLASSES_FOLDER = args[4]; 112 113 HOSTJUNIT_SRC_OUTPUT_FOLDER = args[5]; 114 HOSTJUNIT_CLASSES_OUTPUT_FOLDER = HOSTJUNIT_SRC_OUTPUT_FOLDER + "/classes"; 115 116 if (args.length > 6) { 117 // optional: restrict to e.g. "opcodes.add_double" 118 restrictTo = args[6]; 119 System.out.println("restricting build to: " + restrictTo); 120 } 121 122 } else { 123 System.out.println("usage: java-src-folder output-folder classpath " + 124 "generated-main-files compiled_output generated-main-files " + 125 "[restrict-to-opcode]"); 126 System.exit(-1); 127 } 128 129 long start = System.currentTimeMillis(); 130 BuildDalvikSuite cat = new BuildDalvikSuite(); 131 cat.compose(); 132 long end = System.currentTimeMillis(); 133 134 System.out.println("elapsed seconds: " + (end - start) / 1000); 135 } 136 compose()137 public void compose() throws IOException { 138 System.out.println("Collecting all junit tests..."); 139 new TestRunner() { 140 @Override 141 protected TestResult createTestResult() { 142 return new TestResult() { 143 @Override 144 protected void run(TestCase test) { 145 addToTests(test); 146 } 147 148 }; 149 } 150 }.doRun(AllTests.suite()); 151 152 // for each combination of TestClass and method, generate a Main_testN1 153 // class in the respective package. 154 // for the report make sure all N... tests are called first, then B, 155 // then E, then VFE test methods. 156 // e.g. dxc.junit.opcodes.aaload.Test_aaload - testN1() -> 157 // File Main_testN1.java in package dxc.junit.opcodes.aaload. 158 // 159 handleTests(); 160 } 161 162 private void addToTests(TestCase test) { 163 164 String packageName = test.getClass().getPackage().getName(); 165 packageName = packageName.substring(packageName.lastIndexOf('.')); 166 167 168 String method = test.getName(); // e.g. testVFE2 169 String fqcn = test.getClass().getName(); // e.g. 170 // dxc.junit.opcodes.iload_3.Test_iload_3 171 172 // ignore all tests not belonging to the given restriction 173 if (restrictTo != null && !fqcn.contains(restrictTo)) return; 174 175 testMethodsCnt++; 176 List<String> li = map.get(fqcn); 177 if (li == null) { 178 testClassCnt++; 179 li = new ArrayList<String>(); 180 map.put(fqcn, li); 181 } 182 li.add(method); 183 } 184 private String curJunitFileName = null; 185 private String curJunitFileData = ""; 186 187 private JavacBuildStep javacHostJunitBuildStep; 188 189 private void flushHostJunitFile() { 190 if (curJunitFileName != null) { 191 File toWrite = new File(curJunitFileName); 192 String absPath = toWrite.getAbsolutePath(); 193 // add to java source files for later compilation 194 javacHostJunitBuildStep.addSourceFile(absPath); 195 // write file 196 curJunitFileData += "\n}\n"; 197 writeToFileMkdir(toWrite, curJunitFileData); 198 curJunitFileName = null; 199 curJunitFileData = ""; 200 } 201 } 202 203 private void openCTSHostFileFor(String pName, String classOnlyName) { 204 // flush previous JunitFile 205 flushHostJunitFile(); 206 String sourceName = "JUnit_" + classOnlyName; 207 208 // prepare current testcase-file 209 curJunitFileName = HOSTJUNIT_SRC_OUTPUT_FOLDER + "/" + pName.replaceAll("\\.","/") + "/" + 210 sourceName + ".java"; 211 curJunitFileData = getWarningMessage() + 212 "package " + pName + ";\n" + 213 "import java.io.IOException;\n" + 214 "import com.android.tradefed.testtype.DeviceTestCase;\n" + 215 "\n" + 216 "public class " + sourceName + " extends DeviceTestCase {\n"; 217 } 218 219 private String getShellExecJavaLine(String classpath, String mainclass) { 220 String cmd = String.format("ANDROID_DATA=%s dalvikvm -Xint:portable -Xmx512M -Xss32K " + 221 "-Djava.io.tmpdir=%s -classpath %s %s", TARGET_JAR_ROOT_PATH, TARGET_JAR_ROOT_PATH, 222 classpath, mainclass); 223 return "String res = getDevice().executeShellCommand(\""+ cmd + "\");\n" + 224 "// A sucessful adb shell command returns an empty string.\n" + 225 "assertEquals(\"" + cmd + "\", \"\", res);"; 226 } 227 228 private String getWarningMessage() { 229 return "//Autogenerated code by " + this.getClass().getName() + "; do not edit.\n"; 230 } 231 232 private void addCTSHostMethod(String pName, String method, MethodData md, 233 Set<String> dependentTestClassNames) { 234 curJunitFileData += "public void " + method + "() throws Exception {\n"; 235 final String targetCoreJarPath = String.format("%s/dot/junit/dexcore.jar", 236 TARGET_JAR_ROOT_PATH); 237 238 // push class with Main jar. 239 String mjar = "Main_" + method + ".jar"; 240 String pPath = pName.replaceAll("\\.","/"); 241 String mainJar = String.format("%s/%s/%s", TARGET_JAR_ROOT_PATH, pPath, mjar); 242 243 String cp = String.format("%s:%s", targetCoreJarPath, mainJar); 244 for (String depFqcn : dependentTestClassNames) { 245 int lastDotPos = depFqcn.lastIndexOf('.'); 246 String sourceName = depFqcn.replaceAll("\\.", "/") + ".jar"; 247 String targetName= String.format("%s/%s", TARGET_JAR_ROOT_PATH, 248 sourceName); 249 cp += ":" + targetName; 250 // dot.junit.opcodes.invoke_interface_range.ITest 251 // -> dot/junit/opcodes/invoke_interface_range/ITest.jar 252 } 253 254 //"dot.junit.opcodes.add_double_2addr.Main_testN2"; 255 String mainclass = pName + ".Main_" + method; 256 curJunitFileData += " " + getShellExecJavaLine(cp, mainclass); 257 curJunitFileData += "}\n\n"; 258 } 259 260 private void handleTests() throws IOException { 261 System.out.println("collected " + testMethodsCnt + " test methods in " + 262 testClassCnt + " junit test classes"); 263 Set<BuildStep> targets = new TreeSet<BuildStep>(); 264 265 javacHostJunitBuildStep = new JavacBuildStep(HOSTJUNIT_CLASSES_OUTPUT_FOLDER, CLASS_PATH); 266 267 268 JavacBuildStep javacBuildStep = new JavacBuildStep( 269 CLASSES_OUTPUT_FOLDER, CLASS_PATH); 270 271 for (Entry<String, List<String>> entry : map.entrySet()) { 272 273 String fqcn = entry.getKey(); 274 int lastDotPos = fqcn.lastIndexOf('.'); 275 String pName = fqcn.substring(0, lastDotPos); 276 String classOnlyName = fqcn.substring(lastDotPos + 1); 277 String instPrefix = "new " + classOnlyName + "()"; 278 279 openCTSHostFileFor(pName, classOnlyName); 280 281 List<String> methods = entry.getValue(); 282 Collections.sort(methods, new Comparator<String>() { 283 public int compare(String s1, String s2) { 284 // TODO sort according: test ... N, B, E, VFE 285 return s1.compareTo(s2); 286 } 287 }); 288 for (String method : methods) { 289 // e.g. testN1 290 if (!method.startsWith("test")) { 291 throw new RuntimeException("no test method: " + method); 292 } 293 294 // generate the Main_xx java class 295 296 // a Main_testXXX.java contains: 297 // package <packagenamehere>; 298 // public class Main_testxxx { 299 // public static void main(String[] args) { 300 // new dxc.junit.opcodes.aaload.Test_aaload().testN1(); 301 // } 302 // } 303 MethodData md = parseTestMethod(pName, classOnlyName, method); 304 String methodContent = md.methodBody; 305 306 Set<String> dependentTestClassNames = parseTestClassName(pName, 307 classOnlyName, methodContent); 308 309 addCTSHostMethod(pName, method, md, dependentTestClassNames); 310 311 312 if (dependentTestClassNames.isEmpty()) { 313 continue; 314 } 315 316 317 String content = getWarningMessage() + 318 "package " + pName + ";\n" + 319 "import " + pName + ".d.*;\n" + 320 "import dot.junit.*;\n" + 321 "public class Main_" + method + " extends DxAbstractMain {\n" + 322 " public static void main(String[] args) throws Exception {" + 323 methodContent + "\n}\n"; 324 325 String fileName = getFileName(pName, method, ".java"); 326 File sourceFile = getFileFromPackage(pName, method); 327 328 File classFile = new File(CLASSES_OUTPUT_FOLDER + "/" + 329 getFileName(pName, method, ".class")); 330 // if (sourceFile.lastModified() > classFile.lastModified()) { 331 writeToFile(sourceFile, content); 332 javacBuildStep.addSourceFile(sourceFile.getAbsolutePath()); 333 334 BuildStep dexBuildStep = generateDexBuildStep( 335 CLASSES_OUTPUT_FOLDER, getFileName(pName, method, "")); 336 targets.add(dexBuildStep); 337 // } 338 339 generateBuildStepFor(pName, method, dependentTestClassNames, 340 targets); 341 } 342 343 344 } 345 346 // write latest HOSTJUNIT generated file. 347 flushHostJunitFile(); 348 349 if (!javacHostJunitBuildStep.build()) { 350 System.out.println("main javac cts-host-hostjunit-classes build step failed"); 351 System.exit(1); 352 } 353 354 if (javacBuildStep.build()) { 355 for (BuildStep buildStep : targets) { 356 if (!buildStep.build()) { 357 System.out.println("building failed. buildStep: " + 358 buildStep.getClass().getName() + ", " + buildStep); 359 System.exit(1); 360 } 361 } 362 } else { 363 System.out.println("main javac dalvik-cts-buildutil build step failed"); 364 System.exit(1); 365 } 366 } 367 368 private void generateBuildStepFor(String pName, String method, 369 Set<String> dependentTestClassNames, Set<BuildStep> targets) { 370 371 372 for (String dependentTestClassName : dependentTestClassNames) { 373 generateBuildStepForDependant(dependentTestClassName, targets); 374 } 375 } 376 377 private void generateBuildStepForDependant(String dependentTestClassName, 378 Set<BuildStep> targets) { 379 380 File sourceFolder = new File(JAVASRC_FOLDER); 381 String fileName = dependentTestClassName.replace('.', '/').trim(); 382 383 if (new File(sourceFolder, fileName + ".dfh").exists()) { 384 385 BuildStep.BuildFile inputFile = new BuildStep.BuildFile( 386 JAVASRC_FOLDER, fileName + ".dfh"); 387 BuildStep.BuildFile dexFile = new BuildStep.BuildFile( 388 OUTPUT_FOLDER, fileName + ".dex"); 389 390 DFHBuildStep buildStep = new DFHBuildStep(inputFile, dexFile); 391 392 BuildStep.BuildFile jarFile = new BuildStep.BuildFile( 393 OUTPUT_FOLDER, fileName + ".jar"); 394 JarBuildStep jarBuildStep = new JarBuildStep(dexFile, 395 "classes.dex", jarFile, true); 396 jarBuildStep.addChild(buildStep); 397 398 targets.add(jarBuildStep); 399 return; 400 } 401 402 if (new File(sourceFolder, fileName + ".d").exists()) { 403 404 BuildStep.BuildFile inputFile = new BuildStep.BuildFile( 405 JAVASRC_FOLDER, fileName + ".d"); 406 BuildStep.BuildFile dexFile = new BuildStep.BuildFile( 407 OUTPUT_FOLDER, fileName + ".dex"); 408 409 DasmBuildStep buildStep = new DasmBuildStep(inputFile, dexFile); 410 411 BuildStep.BuildFile jarFile = new BuildStep.BuildFile( 412 OUTPUT_FOLDER, fileName + ".jar"); 413 414 JarBuildStep jarBuildStep = new JarBuildStep(dexFile, 415 "classes.dex", jarFile, true); 416 jarBuildStep.addChild(buildStep); 417 targets.add(jarBuildStep); 418 return; 419 } 420 421 if (new File(sourceFolder, fileName + ".java").exists()) { 422 423 BuildStep dexBuildStep = generateDexBuildStep( 424 COMPILED_CLASSES_FOLDER, fileName); 425 targets.add(dexBuildStep); 426 return; 427 } 428 429 try { 430 if (Class.forName(dependentTestClassName) != null) { 431 432 BuildStep dexBuildStep = generateDexBuildStep( 433 COMPILED_CLASSES_FOLDER, fileName); 434 targets.add(dexBuildStep); 435 return; 436 } 437 } catch (ClassNotFoundException e) { 438 // do nothing 439 } 440 441 throw new RuntimeException("neither .dfh,.d,.java file of dependant test class found : " + 442 dependentTestClassName + ";" + fileName); 443 } 444 445 private BuildStep generateDexBuildStep(String classFileFolder, 446 String classFileName) { 447 BuildStep.BuildFile classFile = new BuildStep.BuildFile( 448 classFileFolder, classFileName + ".class"); 449 450 BuildStep.BuildFile tmpJarFile = new BuildStep.BuildFile(OUTPUT_FOLDER, 451 classFileName + "_tmp.jar"); 452 453 JarBuildStep jarBuildStep = new JarBuildStep(classFile, classFileName + 454 ".class", tmpJarFile, false); 455 456 BuildStep.BuildFile outputFile = new BuildStep.BuildFile(OUTPUT_FOLDER, 457 classFileName + ".jar"); 458 459 DexBuildStep dexBuildStep = new DexBuildStep(tmpJarFile, outputFile, 460 true); 461 462 dexBuildStep.addChild(jarBuildStep); 463 return dexBuildStep; 464 465 } 466 467 /** 468 * @param pName 469 * @param classOnlyName 470 * @param methodSource 471 * @return testclass names 472 */ 473 private Set<String> parseTestClassName(String pName, String classOnlyName, 474 String methodSource) { 475 Set<String> entries = new HashSet<String>(); 476 String opcodeName = classOnlyName.substring(5); 477 478 Scanner scanner = new Scanner(methodSource); 479 480 String[] patterns = new String[] {"new\\s(T_" + opcodeName + "\\w*)", 481 "(T_" + opcodeName + "\\w*)", "new\\s(T\\w*)"}; 482 483 String token = null; 484 for (String pattern : patterns) { 485 token = scanner.findWithinHorizon(pattern, methodSource.length()); 486 if (token != null) { 487 break; 488 } 489 } 490 491 if (token == null) { 492 System.err.println("warning: failed to find dependent test class name: " + pName + 493 ", " + classOnlyName + " in methodSource:\n" + methodSource); 494 return entries; 495 } 496 497 MatchResult result = scanner.match(); 498 499 entries.add((pName + ".d." + result.group(1)).trim()); 500 501 // search additional @uses directives 502 Pattern p = Pattern.compile("@uses\\s+(.*)\\s+", Pattern.MULTILINE); 503 Matcher m = p.matcher(methodSource); 504 while (m.find()) { 505 String res = m.group(1); 506 entries.add(res.trim()); 507 } 508 509 // lines with the form @uses 510 // dot.junit.opcodes.add_double.jm.T_add_double_2 511 // one dependency per one @uses 512 // TODO 513 514 return entries; 515 } 516 517 private MethodData parseTestMethod(String pname, String classOnlyName, 518 String method) { 519 520 String path = pname.replaceAll("\\.", "/"); 521 String absPath = JAVASRC_FOLDER + "/" + path + "/" + classOnlyName + ".java"; 522 File f = new File(absPath); 523 524 Scanner scanner; 525 try { 526 scanner = new Scanner(f); 527 } catch (FileNotFoundException e) { 528 throw new RuntimeException("error while reading to file: " + e.getClass().getName() + 529 ", msg:" + e.getMessage()); 530 } 531 532 String methodPattern = "public\\s+void\\s+" + method + "[^\\{]+\\{"; 533 534 String token = scanner.findWithinHorizon(methodPattern, (int) f.length()); 535 if (token == null) { 536 throw new RuntimeException("cannot find method source of 'public void " + method + 537 "' in file '" + absPath + "'"); 538 } 539 540 MatchResult result = scanner.match(); 541 result.start(); 542 result.end(); 543 544 StringBuilder builder = new StringBuilder(); 545 //builder.append(token); 546 547 try { 548 FileReader reader = new FileReader(f); 549 reader.skip(result.end()); 550 551 char currentChar; 552 int blocks = 1; 553 while ((currentChar = (char) reader.read()) != -1 && blocks > 0) { 554 switch (currentChar) { 555 case '}': { 556 blocks--; 557 builder.append(currentChar); 558 break; 559 } 560 case '{': { 561 blocks++; 562 builder.append(currentChar); 563 break; 564 } 565 default: { 566 builder.append(currentChar); 567 break; 568 } 569 } 570 } 571 if (reader != null) { 572 reader.close(); 573 } 574 } catch (Exception e) { 575 throw new RuntimeException("failed to parse", e); 576 } 577 578 // find the @title/@constraint in javadoc comment for this method 579 // using platform's default charset 580 String all = new String(FileUtils.readFile(f)); 581 // System.out.println("grepping javadoc found for method " + method + 582 // " in " + pname + "," + classOnlyName); 583 String commentPattern = "/\\*\\*([^{]*)\\*/\\s*" + methodPattern; 584 Pattern p = Pattern.compile(commentPattern, Pattern.DOTALL); 585 Matcher m = p.matcher(all); 586 String title = null, constraint = null; 587 if (m.find()) { 588 String res = m.group(1); 589 // System.out.println("res: " + res); 590 // now grep @title and @constraint 591 Matcher titleM = Pattern.compile("@title (.*)", Pattern.DOTALL) 592 .matcher(res); 593 if (titleM.find()) { 594 title = titleM.group(1).replaceAll("\\n \\*", ""); 595 title = title.replaceAll("\\n", " "); 596 title = title.trim(); 597 // System.out.println("title: " + title); 598 } else { 599 System.err.println("warning: no @title found for method " + method + " in " + pname + 600 "," + classOnlyName); 601 } 602 // constraint can be one line only 603 Matcher constraintM = Pattern.compile("@constraint (.*)").matcher( 604 res); 605 if (constraintM.find()) { 606 constraint = constraintM.group(1); 607 constraint = constraint.trim(); 608 // System.out.println("constraint: " + constraint); 609 } else if (method.contains("VFE")) { 610 System.err 611 .println("warning: no @constraint for for a VFE method:" + method + " in " + 612 pname + "," + classOnlyName); 613 } 614 } else { 615 System.err.println("warning: no javadoc found for method " + method + " in " + pname + 616 "," + classOnlyName); 617 } 618 MethodData md = new MethodData(); 619 md.methodBody = builder.toString(); 620 md.constraint = constraint; 621 md.title = title; 622 if (scanner != null) { 623 scanner.close(); 624 } 625 return md; 626 } 627 628 private void writeToFileMkdir(File file, String content) { 629 File parent = file.getParentFile(); 630 if (!parent.exists() && !parent.mkdirs()) { 631 throw new RuntimeException("failed to create directory: " + parent.getAbsolutePath()); 632 } 633 writeToFile(file, content); 634 } 635 636 private void writeToFile(File file, String content) { 637 try { 638 if (file.length() == content.length()) { 639 FileReader reader = new FileReader(file); 640 char[] charContents = new char[(int) file.length()]; 641 reader.read(charContents); 642 reader.close(); 643 String contents = new String(charContents); 644 if (contents.equals(content)) { 645 // System.out.println("skipping identical: " 646 // + file.getAbsolutePath()); 647 return; 648 } 649 } 650 651 //System.out.println("writing file " + file.getAbsolutePath()); 652 653 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( 654 new FileOutputStream(file), "utf-8")); 655 bw.write(content); 656 bw.close(); 657 } catch (Exception e) { 658 throw new RuntimeException("error while writing to file: " + e.getClass().getName() + 659 ", msg:" + e.getMessage()); 660 } 661 } 662 663 private File getFileFromPackage(String pname, String methodName) 664 throws IOException { 665 // e.g. dxc.junit.argsreturns.pargsreturn 666 String path = getFileName(pname, methodName, ".java"); 667 String absPath = MAIN_SRC_OUTPUT_FOLDER + "/" + path; 668 File dirPath = new File(absPath); 669 File parent = dirPath.getParentFile(); 670 if (!parent.exists() && !parent.mkdirs()) { 671 throw new IOException("failed to create directory: " + absPath); 672 } 673 return dirPath; 674 } 675 676 private String getFileName(String pname, String methodName, 677 String extension) { 678 String path = pname.replaceAll("\\.", "/"); 679 return new File(path, "Main_" + methodName + extension).getPath(); 680 } 681 } 682