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