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 }