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