/* * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * @test * @summary test binary compatibility rules for sealed classes * @library /tools/lib * @enablePreview * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * jdk.compiler/com.sun.tools.javac.util * jdk.compiler/com.sun.tools.javac.code * java.base/jdk.internal.classfile.impl * @build toolbox.ToolBox toolbox.JavacTask * @run main BinaryCompatibilityTests */ import java.util.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.IntStream; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.util.Assert; import toolbox.TestRunner; import toolbox.ToolBox; import toolbox.JavaTask; import toolbox.JavacTask; import toolbox.Task; import toolbox.Task.OutputKind; public class BinaryCompatibilityTests extends TestRunner { ToolBox tb; BinaryCompatibilityTests() { super(System.err); tb = new ToolBox(); } protected void runTests() throws Exception { runTests(m -> new Object[]{Paths.get(m.getName())}); } public static void main(String... args) throws Exception { BinaryCompatibilityTests t = new BinaryCompatibilityTests(); t.runTests(); } Path[] findJavaFiles(Path... paths) throws IOException { return tb.findJavaFiles(paths); } @Test public void testCompatibilityAfterMakingSuperclassSealed(Path base) throws Exception { /* If a class that was not declared sealed is changed to be declared sealed, then an * IncompatibleClassChangeError is thrown if a binary of a pre-existing subclass of * this class is loaded that is not contained in its permits clause */ testCompatibilityAfterModifyingSupertype( base, true, """ package pkg; public class Super { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; public sealed class Super permits Sub1 { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } final class Sub1 extends Super {} """, """ package pkg; class Sub extends Super {} """ ); } @Test public void testCompatibilityAfterMakingSuperInterfaceSealed(Path base) throws Exception { // test similar to testCompatibilityAfterMakingSuperclassSealed but with interfaces testCompatibilityAfterModifyingSupertype( base, true, """ package pkg; public interface Super { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; public sealed interface Super permits Sub1 { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } final class Sub1 implements Super {} """, """ package pkg; class Sub implements Super {} """ ); } /* 1- compiles the first version of the superclass source code along with the subclass source code * 2- executes the super class just to make sure that it works * 3- compiles the second version of the super class along with the class file of the subclass * 4- executes the super class and makes sure that the VM throws IncompatibleClassChangeError or not * depending on the shouldFail argument */ private void testCompatibilityAfterModifyingSupertype( Path base, boolean shouldFail, String superClassCode1, String superClassCode2, String... subClassesCode) throws Exception { Path src = base.resolve("src"); Path pkg = src.resolve("pkg"); Path superClass = pkg.resolve("Super"); Path sub = pkg.resolve("Sub"); tb.writeJavaFiles(superClass, superClassCode1); tb.writeJavaFiles(sub, subClassesCode); Path out = base.resolve("out"); Files.createDirectories(out); new JavacTask(tb) .outdir(out) .files(findJavaFiles(pkg)) .run(); // let's execute to check that it's working String output = new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run() .writeAll() .getOutput(Task.OutputKind.STDOUT); // let's first check that it runs wo issues if (!output.contains("done")) { throw new AssertionError("execution of Super didn't finish"); } // now lets change the super class tb.writeJavaFiles(superClass, superClassCode2); new JavacTask(tb) .classpath(out) .outdir(out) .files(findJavaFiles(superClass)) .run(); if (shouldFail) { // let's now check that there is an IncompatibleClassChangeError output = new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run(Task.Expect.FAIL) .writeAll() .getOutput(Task.OutputKind.STDERR); if (!output.startsWith("Exception in thread \"main\" java.lang.IncompatibleClassChangeError")) { throw new AssertionError("java.lang.IncompatibleClassChangeError expected"); } } else { new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run(Task.Expect.SUCCESS); } } @Test public void testRemoveSealedModifierToClass(Path base) throws Exception { /* Changing a class that is declared sealed to no longer be declared * sealed does not break compatibility with pre-existing binaries. */ testCompatibilityAfterModifyingSupertype( base, false, """ package pkg; public sealed class Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; public class Super { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; final class Sub extends Super {} """ ); } @Test public void testRemoveSealedModifierToInterface(Path base) throws Exception { // same as testRemoveSealedModifierToClass but with an interface testCompatibilityAfterModifyingSupertype( base, false, """ package pkg; public sealed interface Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; public interface Super { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; final class Sub implements Super {} """ ); } @Test public void testAddNonSealedModifierToClass(Path base) throws Exception { /* Changing a class that is not declared non-sealed to be declared * non-sealed does not break compatibility with pre-existing binaries */ testCompatibilityOKAfterSubclassChange( base, """ package pkg; public sealed class Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; final class Sub extends Super {} """, """ package pkg; non-sealed class Sub extends Super {} """ ); } @Test public void testAddNonSealedModifierToInterface(Path base) throws Exception { // same as `testAddNonSealedModifierToClass` but with interfaces testCompatibilityOKAfterSubclassChange( base, """ package pkg; public sealed interface Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; final class Sub implements Super {} """, """ package pkg; non-sealed class Sub implements Super {} """ ); } @Test public void testRemoveNonSealedModifier(Path base) throws Exception { /* Changing a class that is declared non-sealed to no longer be declared * non-sealed does not break compatibility with pre-existing binaries */ testCompatibilityOKAfterSubclassChange( base, """ package pkg; public sealed class Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; non-sealed class Sub extends Super {} """, """ package pkg; final class Sub extends Super {} """ ); } @Test public void testRemoveNonSealedModifierFromInterface(Path base) throws Exception { // same as `testRemoveNonSealedModifier` but with interfaces testCompatibilityOKAfterSubclassChange( base, """ package pkg; public sealed interface Super permits pkg.Sub { public static void main(String... args) { pkg.Sub sub = new pkg.Sub(); System.out.println("done"); } } """, """ package pkg; non-sealed class Sub implements Super {} """, """ package pkg; final class Sub implements Super {} """ ); } /* 1- compiles the superclass source code along with the first version of the subclass source code * 2- executes the super class just to make sure that it works * 3- compiles the second version of the subclass along with the class file of the superclass * 4- executes the super class and makes sure that it executes successfully */ private void testCompatibilityOKAfterSubclassChange( Path base, String superClassCode, String subClassCode1, String subClassCode2) throws Exception { Path src = base.resolve("src"); Path pkg = src.resolve("pkg"); Path superClass = pkg.resolve("Super"); Path sub = pkg.resolve("Sub"); // super class initially sealed tb.writeJavaFiles(superClass, superClassCode); tb.writeJavaFiles(sub, subClassCode1); Path out = base.resolve("out"); Files.createDirectories(out); new JavacTask(tb) .outdir(out) .files(findJavaFiles(pkg)) .run(); // let's execute to check that it's working String output = new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run() .writeAll() .getOutput(Task.OutputKind.STDOUT); // let's first check that it runs wo issues if (!output.contains("done")) { throw new AssertionError("execution of Super didn't finish"); } // now lets remove the non-sealed modifier from class Sub tb.writeJavaFiles(sub, subClassCode2); new JavacTask(tb) .classpath(out) .outdir(out) .files(findJavaFiles(sub)) .run(); // should execute without issues output = new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run() .writeAll() .getOutput(Task.OutputKind.STDOUT); // let's first check that it runs wo issues if (!output.contains("done")) { throw new AssertionError("execution of Super didn't finish"); } } @Test public void testAfterChangingPermitsClause(Path base) throws Exception { /* If a class is removed from the set of permitted direct subclasses of * a sealed class then an IncompatibleClassChangeError is thrown if the * pre-existing binary of the removed class is loaded */ testCompatibilityAfterModifyingSupertype( base, true, """ package pkg; public sealed class Super permits pkg.Sub1, Sub2 { public static void main(String... args) { pkg.Sub1 sub = new pkg.Sub1(); System.out.println("done"); } } final class Sub2 extends Super {} """, """ package pkg; public sealed class Super permits Sub2 { public static void main(String... args) { pkg.Sub1 sub = new pkg.Sub1(); System.out.println("done"); } } final class Sub2 extends Super {} """, """ package pkg; final class Sub1 extends Super {} """ ); } @Test public void testAfterChangingPermitsClause2(Path base) throws Exception { /* If a class is removed from the set of permitted direct subclasses of * a sealed class then an IncompatibleClassChangeError is thrown if the * pre-existing binary of the removed class is loaded */ testCompatibilityAfterModifyingSupertype( base, true, """ package pkg; public sealed class Super permits pkg.Sub1, pkg.Sub2 { public static void main(String... args) { pkg.Sub1 sub1 = new pkg.Sub1(); pkg.Sub2 sub2 = new pkg.Sub2(); System.out.println("done"); } } """, """ package pkg; public sealed class Super permits pkg.Sub1 { public static void main(String... args) { pkg.Sub1 sub1 = new pkg.Sub1(); pkg.Sub2 sub2 = new pkg.Sub2(); System.out.println("done"); } } """, """ package pkg; final class Sub1 extends Super {} """, """ package pkg; final class Sub2 extends Super {} """ ); } @Test public void testAfterChangingPermitsClause3(Path base) throws Exception { /* Changing the set of permitted direct subclasses of a sealed class will * not break compatibility with pre-existing binaries, provided that the * total set of permitted direct subclasses of the sealed class loses no * members */ String superClassCode1 = """ package pkg; public sealed class Super permits pkg.Sub1 { public static void main(String... args) { pkg.Sub1 sub1 = new pkg.Sub1(); System.out.println("done"); } } """; String subClass1Code = """ package pkg; final class Sub1 extends Super {} """; String superClassCode2 = """ package pkg; public sealed class Super permits pkg.Sub1, pkg.Sub2 { public static void main(String... args) { pkg.Sub1 sub1 = new pkg.Sub1(); pkg.Sub2 sub2 = new pkg.Sub2(); System.out.println("done"); } } """; String subClass2Code = """ package pkg; final class Sub2 extends Super {} """; Path src = base.resolve("src"); Path pkg = src.resolve("pkg"); Path superClass = pkg.resolve("Super"); Path sub1 = pkg.resolve("Sub1"); tb.writeJavaFiles(superClass, superClassCode1); tb.writeJavaFiles(sub1, subClass1Code); Path out = base.resolve("out"); Files.createDirectories(out); new JavacTask(tb) .outdir(out) .files(findJavaFiles(pkg)) .run(); // let's execute to check that it's working String output = new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run() .writeAll() .getOutput(Task.OutputKind.STDOUT); // let's first check that it runs wo issues if (!output.contains("done")) { throw new AssertionError("execution of Super didn't finish"); } // now lets change the super class tb.writeJavaFiles(superClass, superClassCode2); Path sub2 = pkg.resolve("Sub2"); tb.writeJavaFiles(sub2, subClass2Code); new JavacTask(tb) .classpath(out) .outdir(out) .files(findJavaFiles(superClass)[0], findJavaFiles(sub2)[0]) .run(); new JavaTask(tb) .classpath(out.toString()) .classArgs("pkg.Super") .run(Task.Expect.SUCCESS); } }