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