1 /* 2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* 25 * @test 26 * @summary Tests for API validator. 27 * @library /test/lib 28 * @modules java.base/jdk.internal.misc 29 * jdk.compiler 30 * jdk.jartool 31 * @build MRTestBase 32 * @run testng/timeout=1200 ApiValidatorTest 33 */ 34 35 import jdk.test.lib.process.OutputAnalyzer; 36 import jdk.test.lib.util.FileUtils; 37 import org.testng.annotations.AfterMethod; 38 import org.testng.annotations.BeforeMethod; 39 import org.testng.annotations.DataProvider; 40 import org.testng.annotations.Test; 41 42 import java.io.IOException; 43 import java.lang.reflect.Method; 44 import java.nio.file.Files; 45 import java.nio.file.Path; 46 import java.nio.file.Paths; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 public class ApiValidatorTest extends MRTestBase { 51 52 static final Pattern MODULE_PATTERN = Pattern.compile("module (\\w+)"); 53 static final Pattern CLASS_PATTERN = Pattern.compile("package (\\w+).*public class (\\w+)"); 54 55 private Path root; 56 private Path classes; 57 58 @BeforeMethod 59 void testInit(Method method) { 60 root = Paths.get(method.getName()); 61 classes = root.resolve("classes"); 62 } 63 64 @AfterMethod 65 void testCleanup() throws IOException { 66 FileUtils.deleteFileTreeWithRetry(root); 67 } 68 69 70 @Test(dataProvider = "signatureChange") 71 public void changeMethodSignature(String sigBase, String sigV10, 72 boolean isAcceptable) throws Throwable { 73 74 String METHOD_SIG = "#SIG"; 75 String classTemplate = 76 "public class C { \n" + 77 " " + METHOD_SIG + "{ throw new RuntimeException(); };\n" + 78 "}\n"; 79 String base = classTemplate.replace(METHOD_SIG, sigBase); 80 String v10 = classTemplate.replace(METHOD_SIG, sigV10); 81 82 compileTemplate(classes.resolve("base"), base); 83 compileTemplate(classes.resolve("v10"), v10); 84 85 String jarfile = root.resolve("test.jar").toString(); 86 OutputAnalyzer result = jar("cf", jarfile, 87 "-C", classes.resolve("base").toString(), ".", 88 "--release", "10", "-C", classes.resolve("v10").toString(), 89 "."); 90 if (isAcceptable) { 91 result.shouldHaveExitValue(SUCCESS) 92 .shouldBeEmpty(); 93 } else { 94 result.shouldNotHaveExitValue(SUCCESS) 95 .shouldContain("contains a class with different api from earlier version"); 96 } 97 } 98 99 @DataProvider 100 Object[][] signatureChange() { 101 return new Object[][]{ 102 {"public int m()", "protected int m()", false}, 103 {"protected int m()", "public int m()", false}, 104 {"public int m()", "int m()", false}, 105 {"protected int m()", "private int m()", false}, 106 {"private int m()", "int m()", true}, 107 {"int m()", "private int m()", true}, 108 {"int m()", "private int m(boolean b)", true}, 109 {"public int m()", "public int m(int i)", false}, 110 {"public int m()", "public int k()", false}, 111 {"public int m()", "private int k()", false}, 112 // @ignore JDK-8172147 {"public int m()", "public boolean m()", false}, 113 // @ignore JDK-8172147 {"public boolean", "public Boolean", false}, 114 // @ignore JDK-8172147 {"public <T> T", "public <T extends String> T", false}, 115 }; 116 } 117 118 @Test(dataProvider = "publicAPI") 119 public void introducingPublicMembers(String publicAPI) throws Throwable { 120 String API = "#API"; 121 String classTemplate = 122 "public class C { \n" + 123 " " + API + "\n" + 124 " public void method(){ };\n" + 125 "}\n"; 126 String base = classTemplate.replace(API, ""); 127 String v10 = classTemplate.replace(API, publicAPI); 128 129 compileTemplate(classes.resolve("base"), base); 130 compileTemplate(classes.resolve("v10"), v10); 131 132 String jarfile = root.resolve("test.jar").toString(); 133 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 134 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 135 .shouldNotHaveExitValue(SUCCESS) 136 .shouldContain("contains a class with different api from earlier version"); 137 } 138 139 @DataProvider 140 Object[][] publicAPI() { 141 return new Object[][]{ 142 // @ignore JDK-8172148 {"protected class Inner { public void m(){ } } "}, // protected inner class 143 // @ignore JDK-8172148 {"public class Inner { public void m(){ } }"}, // public inner class 144 // @ignore JDK-8172148 {"public enum E { A; }"}, // public enum 145 {"public void m(){ }"}, // public method 146 {"protected void m(){ }"}, // protected method 147 }; 148 } 149 150 @Test(dataProvider = "privateAPI") 151 public void introducingPrivateMembers(String privateAPI) throws Throwable { 152 String API = "#API"; 153 String classTemplate = 154 "public class C { \n" + 155 " " + API + "\n" + 156 " public void method(){ };\n" + 157 "}\n"; 158 String base = classTemplate.replace(API, ""); 159 String v10 = classTemplate.replace(API, privateAPI); 160 161 compileTemplate(classes.resolve("base"), base); 162 compileTemplate(classes.resolve("v10"), v10); 163 164 String jarfile = root.resolve("test.jar").toString(); 165 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 166 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 167 .shouldHaveExitValue(SUCCESS); 168 // add release 169 jar("uf", jarfile, 170 "--release", "11", "-C", classes.resolve("v10").toString(), ".") 171 .shouldHaveExitValue(SUCCESS); 172 // replace release 173 jar("uf", jarfile, 174 "--release", "11", "-C", classes.resolve("v10").toString(), ".") 175 .shouldHaveExitValue(SUCCESS); 176 } 177 178 @DataProvider 179 Object[][] privateAPI() { 180 return new Object[][]{ 181 {"private class Inner { public void m(){ } } "}, // private inner class 182 {"class Inner { public void m(){ } }"}, // package private inner class 183 {"enum E { A; }"}, // package private enum 184 // Local class and private method 185 {"private void m(){ class Inner { public void m(){} } Inner i = null; }"}, 186 {"void m(){ }"}, // package private method 187 }; 188 } 189 190 private void compileTemplate(Path classes, String template) throws Throwable { 191 Path classSourceFile = Files.createDirectories( 192 classes.getParent().resolve("src").resolve(classes.getFileName())) 193 .resolve("C.java"); 194 Files.write(classSourceFile, template.getBytes()); 195 javac(classes, classSourceFile); 196 } 197 198 /* Modular multi-release checks */ 199 200 @Test 201 public void moduleNameHasChanged() throws Throwable { 202 203 compileModule(classes.resolve("base"), "module A { }"); 204 compileModule(classes.resolve("v10"), "module B { }"); 205 206 String jarfile = root.resolve("test.jar").toString(); 207 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 208 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 209 .shouldNotHaveExitValue(SUCCESS) 210 .shouldContain("incorrect name"); 211 212 // update module-info release 213 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 214 "--release", "10", "-C", classes.resolve("base").toString(), ".") 215 .shouldHaveExitValue(SUCCESS); 216 jar("uf", jarfile, 217 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 218 .shouldNotHaveExitValue(SUCCESS) 219 .shouldContain("incorrect name"); 220 } 221 222 // @Test @ignore 8173370 223 public void moduleBecomeOpen() throws Throwable { 224 225 compileModule(classes.resolve("base"), "module A { }"); 226 compileModule(classes.resolve("v10"), "open module A { }"); 227 228 String jarfile = root.resolve("test.jar").toString(); 229 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 230 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 231 .shouldNotHaveExitValue(SUCCESS) 232 .shouldContain("FIX ME"); 233 234 // update module-info release 235 jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".", 236 "--release", "10", "-C", classes.resolve("base").toString(), ".") 237 .shouldHaveExitValue(SUCCESS); 238 jar("uf", jarfile, 239 "--release", "10", "-C", classes.resolve("v10").toString(), ".") 240 .shouldNotHaveExitValue(SUCCESS) 241 .shouldContain("FIX ME"); 242 } 243 244 @Test 245 public void moduleRequires() throws Throwable { 246 247 String BASE_VERSION_DIRECTIVE = "requires jdk.compiler;"; 248 // add transitive flag 249 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 250 "requires transitive jdk.compiler;", 251 false, 252 "contains additional \"requires transitive\""); 253 // remove requires 254 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 255 "", 256 true, 257 ""); 258 // add requires 259 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 260 "requires jdk.compiler; requires jdk.jartool;", 261 true, 262 ""); 263 // add requires transitive 264 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 265 "requires jdk.compiler; requires transitive jdk.jartool;", 266 false, 267 "contains additional \"requires transitive\""); 268 } 269 270 @Test 271 public void moduleExports() throws Throwable { 272 273 String BASE_VERSION_DIRECTIVE = "exports pkg1; exports pkg2 to jdk.compiler;"; 274 // add export 275 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 276 BASE_VERSION_DIRECTIVE + " exports pkg3;", 277 false, 278 "contains different \"exports\""); 279 // change exports to qualified exports 280 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 281 "exports pkg1 to jdk.compiler; exports pkg2;", 282 false, 283 "contains different \"exports\""); 284 // remove exports 285 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 286 "exports pkg1;", 287 false, 288 "contains different \"exports\""); 289 // add qualified exports 290 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 291 BASE_VERSION_DIRECTIVE + " exports pkg3 to jdk.compiler;", 292 false, 293 "contains different \"exports\""); 294 } 295 296 @Test 297 public void moduleOpens() throws Throwable { 298 299 String BASE_VERSION_DIRECTIVE = "opens pkg1; opens pkg2 to jdk.compiler;"; 300 // add opens 301 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 302 BASE_VERSION_DIRECTIVE + " opens pkg3;", 303 false, 304 "contains different \"opens\""); 305 // change opens to qualified opens 306 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 307 "opens pkg1 to jdk.compiler; opens pkg2;", 308 false, 309 "contains different \"opens\""); 310 // remove opens 311 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 312 "opens pkg1;", 313 false, 314 "contains different \"opens\""); 315 // add qualified opens 316 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 317 BASE_VERSION_DIRECTIVE + " opens pkg3 to jdk.compiler;", 318 false, 319 "contains different \"opens\""); 320 } 321 322 @Test 323 public void moduleProvides() throws Throwable { 324 325 String BASE_VERSION_DIRECTIVE = "provides pkg1.A with pkg1.A;"; 326 // add provides 327 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 328 BASE_VERSION_DIRECTIVE + " provides pkg2.B with pkg2.B;", 329 false, 330 "contains different \"provides\""); 331 // change service impl 332 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 333 "provides pkg1.A with pkg2.B;", 334 false, 335 "contains different \"provides\""); 336 // remove provides 337 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 338 "", 339 false, 340 "contains different \"provides\""); 341 } 342 343 @Test 344 public void moduleUses() throws Throwable { 345 346 String BASE_VERSION_DIRECTIVE = "uses pkg1.A;"; 347 // add 348 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 349 BASE_VERSION_DIRECTIVE + " uses pkg2.B;", 350 true, 351 ""); 352 // replace 353 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 354 "uses pkg2.B;", 355 true, 356 ""); 357 // remove 358 moduleDirectivesCase(BASE_VERSION_DIRECTIVE, 359 "", 360 true, 361 ""); 362 } 363 364 private void moduleDirectivesCase(String baseDirectives, 365 String versionedDirectives, 366 boolean expectSuccess, 367 String expectedMessage) throws Throwable { 368 String[] moduleClasses = { 369 "package pkg1; public class A { }", 370 "package pkg2; public class B extends pkg1.A { }", 371 "package pkg3; public class C extends pkg2.B { }"}; 372 compileModule(classes.resolve("base"), 373 "module A { " + baseDirectives + " }", 374 moduleClasses); 375 compileModule(classes.resolve("v10"), 376 "module A { " + versionedDirectives + " }", 377 moduleClasses); 378 379 String jarfile = root.resolve("test.jar").toString(); 380 OutputAnalyzer output = jar("cf", jarfile, 381 "-C", classes.resolve("base").toString(), ".", 382 "--release", "10", "-C", classes.resolve("v10").toString(), "."); 383 if (expectSuccess) { 384 output.shouldHaveExitValue(SUCCESS); 385 } else { 386 output.shouldNotHaveExitValue(SUCCESS) 387 .shouldContain(expectedMessage); 388 } 389 } 390 391 private void compileModule(Path classes, String moduleSource, 392 String... classSources) throws Throwable { 393 Matcher moduleMatcher = MODULE_PATTERN.matcher(moduleSource); 394 moduleMatcher.find(); 395 String name = moduleMatcher.group(1); 396 Path moduleinfo = Files.createDirectories( 397 classes.getParent().resolve("src").resolve(name)) 398 .resolve("module-info.java"); 399 Files.write(moduleinfo, moduleSource.getBytes()); 400 401 Path[] sourceFiles = new Path[classSources.length + 1]; 402 sourceFiles[0] = moduleinfo; 403 404 for (int i = 0; i < classSources.length; i++) { 405 String classSource = classSources[i]; 406 Matcher classMatcher = CLASS_PATTERN.matcher(classSource); 407 classMatcher.find(); 408 String packageName = classMatcher.group(1); 409 String className = classMatcher.group(2); 410 411 Path packagePath = moduleinfo.getParent() 412 .resolve(packageName.replace('.', '/')); 413 Path sourceFile = Files.createDirectories(packagePath) 414 .resolve(className + ".java"); 415 Files.write(sourceFile, classSource.getBytes()); 416 417 sourceFiles[i + 1] = sourceFile; 418 } 419 420 javac(classes, sourceFiles); 421 } 422 } 423