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