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