1 /*
  2  * Copyright (c) 2022, 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 8292817
 26  * @summary binary compatibility tests for value objects
 27  * @library /tools/lib
 28  * @modules jdk.compiler/com.sun.tools.javac.api
 29  *          jdk.compiler/com.sun.tools.javac.main
 30  *          jdk.compiler/com.sun.tools.javac.util
 31  *          jdk.compiler/com.sun.tools.javac.code
 32  *          jdk.jdeps/com.sun.tools.classfile
 33  * @build toolbox.ToolBox toolbox.JavacTask
 34  * @enablePreview
 35  * @run main ValueObjectsBinaryCompatibilityTests
 36  */
 37 
 38 import java.io.IOException;
 39 import java.nio.file.Files;
 40 import java.nio.file.Path;
 41 import java.nio.file.Paths;
 42 
 43 import toolbox.TestRunner;
 44 import toolbox.ToolBox;
 45 import toolbox.JavaTask;
 46 import toolbox.JavacTask;
 47 import toolbox.Task;
 48 
 49 public class ValueObjectsBinaryCompatibilityTests extends TestRunner {
 50     ToolBox tb;
 51 
 52     ValueObjectsBinaryCompatibilityTests() {
 53         super(System.err);
 54         tb = new ToolBox();
 55     }
 56 
 57     protected void runTests() throws Exception {
 58         runTests(m -> new Object[]{Paths.get(m.getName())});
 59     }
 60 
 61     public static void main(String... args) throws Exception {
 62         new ValueObjectsBinaryCompatibilityTests().runTests();
 63     }
 64 
 65     Path[] findJavaFiles(Path... paths) throws IOException {
 66         return tb.findJavaFiles(paths);
 67     }
 68 
 69     /* 1- compiles the first version of the source code, code1, along with the client source code
 70      * 2- executes the client class just to make sure that it works, sanity check
 71      * 3- compiles the second version, code2
 72      * 4- executes the client class and makes sure that the VM throws the expected error or not
 73      *    depending on the shouldFail argument
 74      */
 75     private void testCompatibilityAfterChange(
 76             Path base,
 77             String code1,
 78             String code2,
 79             String clientCode,
 80             boolean shouldFail,
 81             Class<?> expectedError) throws Exception {
 82         Path src = base.resolve("src");
 83         Path pkg = src.resolve("pkg");
 84         Path src1 = pkg.resolve("Test");
 85         Path client = pkg.resolve("Client");
 86 
 87         tb.writeJavaFiles(src1, code1);
 88         tb.writeJavaFiles(client, clientCode);
 89 
 90         Path out = base.resolve("out");
 91         Files.createDirectories(out);
 92 
 93         new JavacTask(tb)
 94                 .outdir(out)
 95                 .options("--enable-preview", "-source", String.valueOf(Runtime.version().feature()))
 96                 .files(findJavaFiles(pkg))
 97                 .run();
 98 
 99         // let's execute to check that it's working
100         String output = new JavaTask(tb)
101                 .classpath(out.toString())
102                 .classArgs("pkg.Client")
103                 .vmOptions("--enable-preview")
104                 .run()
105                 .writeAll()
106                 .getOutput(Task.OutputKind.STDOUT);
107 
108         // let's first check that it runs wo issues
109         if (!output.contains("Hello World!")) {
110             throw new AssertionError("execution of Client didn't finish");
111         }
112 
113         // now lets change the first class
114         tb.writeJavaFiles(src1, code2);
115 
116         new JavacTask(tb)
117                 .outdir(out)
118                 .files(findJavaFiles(src1))
119                 .options("--enable-preview", "-source", String.valueOf(Runtime.version().feature()))
120                 .run();
121 
122         if (shouldFail) {
123             // let's now check that we get the expected error
124             output = new JavaTask(tb)
125                     .classpath(out.toString())
126                     .classArgs("pkg.Client")
127                     .vmOptions("--enable-preview")
128                     .run(Task.Expect.FAIL)
129                     .writeAll()
130                     .getOutput(Task.OutputKind.STDERR);
131             if (!output.contains(expectedError.getName())) {
132                 throw new AssertionError(expectedError.getName() + " expected");
133             }
134         } else {
135             new JavaTask(tb)
136                     .classpath(out.toString())
137                     .classArgs("pkg.Client")
138                     .vmOptions("--enable-preview")
139                     .run(Task.Expect.SUCCESS);
140         }
141     }
142 
143     @Test
144     public void testAbstractValueToValueClass(Path base) throws Exception {
145         /* Removing the abstract modifier from a value class declaration has the side-effect of making the class final,
146          * with binary compatibility risks outlined in 13.4.2.3
147          */
148         testCompatibilityAfterChange(
149                 base,
150                 """
151                 package pkg;
152                 public abstract value class A {}
153                 """,
154                 """
155                 package pkg;
156                 public value class A {}
157                 """,
158                 """
159                 package pkg;
160                 public value class Client extends A {
161                     public static void main(String... args) {
162                         System.out.println("Hello World!");
163                     }
164                 }
165                 """,
166                 true,
167                 IncompatibleClassChangeError.class
168         );
169     }
170 
171     @Test
172     public void testAbstractOrFinalIdentityToValueClass(Path base) throws Exception {
173         /* Modifying an abstract or final identity class to be a value class does not break compatibility
174          * with pre-existing binaries
175          */
176         testCompatibilityAfterChange(
177                 base,
178                 """
179                 package pkg;
180                 public abstract class A {}
181                 """,
182                 """
183                 package pkg;
184                 public abstract value class A {}
185                 """,
186                 """
187                 package pkg;
188                 public class Client extends A {
189                     public static void main(String... args) {
190                         System.out.println("Hello World!");
191                     }
192                 }
193                 """,
194                 false,
195                 null
196         );
197 
198         testCompatibilityAfterChange(
199                 base,
200                 """
201                 package pkg;
202                 public final class A {}
203                 """,
204                 """
205                 package pkg;
206                 public final value class A {}
207                 """,
208                 """
209                 package pkg;
210                 public class Client {
211                     A a;
212                     public static void main(String... args) {
213                         System.out.println("Hello World!");
214                     }
215                 }
216                 """,
217                 false,
218                 null
219         );
220     }
221 
222     @Test
223     public void testAddingValueModifier(Path base) throws Exception {
224         /* Adding the value modifier to a non-abstract, non-final class declaration has the side-effect of making
225          * the class final, with binary compatibility risks outlined in 13.4.2.3
226          */
227         testCompatibilityAfterChange(
228                 base,
229                 """
230                 package pkg;
231                 public class A {}
232                 """,
233                 """
234                 package pkg;
235                 public value class A {}
236                 """,
237                 """
238                 package pkg;
239                 public class Client extends A {
240                     public static void main(String... args) {
241                         System.out.println("Hello World!");
242                     }
243                 }
244                 """,
245                 true,
246                 IncompatibleClassChangeError.class
247         );
248     }
249 
250     @Test
251     public void testValueToIdentityClass(Path base) throws Exception {
252         /* If a value class is changed to be an identity class, then an IncompatibleClassChangeError is thrown if a
253          * binary of a pre-existing value subclass of this class is loaded
254          */
255         testCompatibilityAfterChange(
256                 base,
257                 """
258                 package pkg;
259                 public abstract value class A {}
260                 """,
261                 """
262                 package pkg;
263                 public abstract class A {}
264                 """,
265                 """
266                 package pkg;
267                 public value class Client extends A {
268                     public static void main(String... args) {
269                         System.out.println("Hello World!");
270                     }
271                 }
272                 """,
273                 true,
274                 IncompatibleClassChangeError.class
275         );
276     }
277 }