blob: 42b9649311adebe6caffc6c943bf916842729e44 [file] [log] [blame]
/*
* Copyright (c) 2015, 2021, 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.
*/
import java.io.File;
import java.io.InputStream;
import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import toolbox.JavacTask;
import toolbox.Task;
import toolbox.Task.Expect;
import toolbox.ToolBox;
import build.tools.symbolgenerator.CreateSymbols;
import build.tools.symbolgenerator.CreateSymbols.ClassDescription;
import build.tools.symbolgenerator.CreateSymbols.ClassList;
import build.tools.symbolgenerator.CreateSymbols.ExcludeIncludeList;
import build.tools.symbolgenerator.CreateSymbols.VersionDescription;
public class CreateSymbolsTestImpl {
static final String CREATE_SYMBOLS_NAME = "symbolgenerator.CreateSymbols";
public static void main(String... args) throws Exception {
new CreateSymbolsTestImpl().doTest();
}
void doTest() throws Exception {
boolean testRun = false;
for (Method m : CreateSymbolsTestImpl.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
m.invoke(this);
testRun = true;
}
}
if (!testRun) {
throw new IllegalStateException("No tests found.");
}
}
@Test
void testMethodRemoved() throws Exception {
doTest("package t; public class T { public void m() { } }",
"package t; public class T { }",
"package t; public class Test { { T t = null; t.m(); } }",
Expect.SUCCESS,
Expect.FAIL);
doTest("package t; public class T { public void b() { } public void m() { } public void a() { } }",
"package t; public class T { public void b() { } public void a() { } }",
"package t; public class Test { { T t = null; t.b(); t.a(); } }",
Expect.SUCCESS,
Expect.SUCCESS);
//with additional attribute (need to properly skip the member):
doTest("package t; public class T { public void m() throws IllegalStateException { } public void a() { } }",
"package t; public class T { public void a() { } }",
"package t; public class Test { { T t = null; t.a(); } }",
Expect.SUCCESS,
Expect.SUCCESS);
}
@Test
void testMethodAdded() throws Exception {
doTest("package t; public class T { }",
"package t; public class T { public void m() { } }",
"package t; public class Test { { T t = null; t.m(); } }",
Expect.FAIL,
Expect.SUCCESS);
doTest("package t; public class T { public void b() { } public void a() { } }",
"package t; public class T { public void b() { } public void m() { } public void a() { } }",
"package t; public class Test { { T t = null; t.b(); t.a(); } }",
Expect.SUCCESS,
Expect.SUCCESS);
}
//verify fields added/modified/removed
@Test
void testClassAdded() throws Exception {
doTest("class Dummy {}",
"package t; public class T { }",
"package t; public class Test { { T t = new T(); } }",
Expect.FAIL,
Expect.SUCCESS);
}
@Test
void testClassModified() throws Exception {
doTest("package t; public class T { public void m() { } }",
"package t; public class T implements java.io.Serializable { public void m() { } }",
"package t; public class Test { { java.io.Serializable t = new T(); } }",
Expect.FAIL,
Expect.SUCCESS);
}
@Test
void testClassRemoved() throws Exception {
doTest("package t; public class T { }",
"class Dummy {}",
"package t; public class Test { { T t = new T(); } }",
Expect.SUCCESS,
Expect.FAIL);
}
@Test
void testInnerClassAttributes() throws Exception {
doTest("package t; public class T { public static class Inner { } }",
"package t; public class T { public static class Inner { } public void extra() {} }",
"package t; import t.T.Inner; public class Test { Inner i; }",
Expect.SUCCESS,
Expect.SUCCESS);
}
@Test
void testConstantAdded() throws Exception {
doTest("package t; public class T { }",
"package t; public class T { public static final int A = 0; }",
"package t; public class Test { void t(int i) { switch (i) { case T.A: break;} } }",
Expect.FAIL,
Expect.SUCCESS);
}
@Test
void testAnnotationAttributeDefaultvalue() throws Exception {
//TODO: this only verifies that there is *some* value, but we should also verify there is a specific value:
doTest("package t; public @interface T { }",
"package t;\n" +
"public @interface T {\n" +
" public boolean booleanValue() default true;\n" +
" public byte byteValue() default 1;\n" +
" public char charValue() default 2;\n" +
" public short shortValue() default 3;\n" +
" public int intValue() default 4;\n" +
" public long longValue() default 5;\n" +
" public float floatValue() default 6;\n" +
" public double doubleValue() default 7;\n" +
" public String stringValue() default \"8\";\n" +
" public java.lang.annotation.RetentionPolicy enumValue() default java.lang.annotation.RetentionPolicy.RUNTIME;\n" +
" public Class classValue() default Number.class;\n" +
" public int[] arrayValue() default {1, 2};\n" +
" public SuppressWarnings annotationValue() default @SuppressWarnings(\"cast\");\n" +
"}\n",
"package t; public @T class Test { }",
Expect.SUCCESS,
Expect.SUCCESS);
}
@Test
void testConstantTest() throws Exception {
//XXX: other constant types (String in particular) - see testStringConstant
doPrintElementTest("package t; public class T { public static final int A = 1; }",
"package t; public class T { public static final int A = 2; }",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final int A = 1;\n\n" +
" public T();\n" +
"}\n",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final int A = 2;\n\n" +
" public T();\n" +
"}\n");
doPrintElementTest("package t; public class T { public static final boolean A = false; }",
"package t; public class T { public static final boolean A = true; }",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final boolean A = false;\n\n" +
" public T();\n" +
"}\n",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final boolean A = true;\n\n" +
" public T();\n" +
"}\n");
}
@Test
void testAnnotations() throws Exception {
Set<String> extraAnnotations = Set.of("Ljava/lang/annotation/Retention;");
CreateSymbols.HARDCODED_ANNOTATIONS.addAll(extraAnnotations);
try {
doPrintElementTest("package t;" +
"import java.lang.annotation.*;" +
"public @Visible @Invisible class T { public void extra() { } }" +
"@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" +
"@Retention(RetentionPolicy.CLASS) @interface Invisible { }",
"package t;" +
"import java.lang.annotation.*;" +
"public @Visible @Invisible class T { }" +
"@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" +
"@Retention(RetentionPolicy.CLASS) @interface Invisible { }",
"t.T",
"package t;\n\n" +
"@t.Invisible\n" +
"@t.Visible\n" +
"public class T {\n\n" +
" public T();\n\n" +
" public void extra();\n" +
"}\n",
"t.Visible",
"package t;\n\n" +
"@java.lang.annotation.Retention(RUNTIME)\n" +
"@interface Visible {\n" +
"}\n");
doPrintElementTest("package t;" +
"import java.lang.annotation.*;" +
"import java.util.*;" +
"public class T {" +
" public void test(int h, @Invisible int i, @Visible List<String> j, int k) { }" +
"}" +
"@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" +
"@Retention(RetentionPolicy.CLASS) @interface Invisible { }",
"package t;" +
"import java.lang.annotation.*;" +
"import java.util.*;" +
"public class T {" +
" public void test(int h, @Invisible int i, @Visible List<String> j, int k) { }" +
" public void extra() { }" +
"}" +
"@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" +
"@Retention(RetentionPolicy.CLASS) @interface Invisible { }",
"t.T",
"package t;\n\n" +
"public class T {\n\n" +
" public T();\n\n" +
" public void test(int arg0,\n" +
" @t.Invisible int arg1,\n" +
" @t.Visible java.util.List<java.lang.String> arg2,\n" +
" int arg3);\n" +
"}\n",
"t.Visible",
"package t;\n\n" +
"@java.lang.annotation.Retention(RUNTIME)\n" +
"@interface Visible {\n" +
"}\n");
doPrintElementTest("package t;" +
"import java.lang.annotation.*;" +
"public class T {" +
" public void test(@Ann(v=\"url\", dv=\"\\\"\\\"\") String str) { }" +
"}" +
"@Retention(RetentionPolicy.RUNTIME) @interface Ann {" +
" public String v();" +
" public String dv();" +
"}",
"package t;" +
"public class T { }",
"t.T",
"package t;\n\n" +
"public class T {\n\n" +
" public T();\n\n" +
" public void test(@t.Ann(dv=\"\\\"\\\"\", v=\"url\") java.lang.String arg0);\n" +
"}\n",
"t.T",
"package t;\n\n" +
"public class T {\n\n" +
" public T();\n" +
"}\n");
} finally {
CreateSymbols.HARDCODED_ANNOTATIONS.removeAll(extraAnnotations);
}
}
@Test
void testStringConstant() throws Exception {
doTest("package t; public class T { public static final String C = \"\"; }",
"package t; public class T { public static final String C = \"\"; public void extra() { } }",
"package t; public class Test { { System.err.println(T.C); } }",
Expect.SUCCESS,
Expect.SUCCESS);
}
@Test
void testCopyProfileAnnotation() throws Exception {
String oldProfileAnnotation = CreateSymbols.PROFILE_ANNOTATION;
try {
CreateSymbols.PROFILE_ANNOTATION = "Lt/Ann;";
doTestEquivalence("package t; public @Ann class T { public void t() {} } @interface Ann { }",
"package t; public class T { public void t() {} }",
"t.T");
} finally {
CreateSymbols.PROFILE_ANNOTATION = oldProfileAnnotation;
}
}
@Test
void testParseAnnotation() throws Exception {
CreateSymbols.parseAnnotations("@Lsun/Proprietary+Annotation;@Ljdk/Profile+Annotation;(value=I1)", new int[1]);
CreateSymbols.parseAnnotations("@Ltest;(value={\"\"})", new int[1]);
CreateSymbols.parseAnnotations("@Ljava/beans/ConstructorProperties;(value={\"path\"})", new int[1]);
CreateSymbols.parseAnnotations("@Ljava/beans/ConstructorProperties;(value=I-2)", new int[1]);
}
@Test
void testStringCharLiterals() throws Exception {
doPrintElementTest("package t;" +
"public class T {" +
" public static final String STR = \"\\u0000\\u0001\\uffff\";" +
" public static final String EMPTY = \"\";" +
" public static final String AMP = \"&amp;&&lt;<&gt;>&apos;'\";" +
"}",
"package t;" +
" public class T {" +
" public static final char c = '\\uffff';" +
"}",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final java.lang.String STR = \"\\u0000\\u0001\\uffff\";\n" +
" public static final java.lang.String EMPTY = \"\";\n" +
" public static final java.lang.String AMP = \"&amp;&&lt;<&gt;>&apos;\\'\";\n\n" +
" public T();\n" +
"}\n",
"t.T",
"package t;\n\n" +
"public class T {\n" +
" public static final char c = '\\uffff';\n\n" +
" public T();\n" +
"}\n");
}
@Test
void testGenerification() throws Exception {
doTest("package t; public class T { public class TT { public Object t() { return null; } } }",
"package t; public class T<E> { public class TT { public E t() { return null; } } }",
"package t; public class Test { { T.TT tt = null; tt.t(); } }",
Expect.SUCCESS,
Expect.SUCCESS);
}
@Test
void testClearMissingAnnotations() throws Exception {
doPrintElementTest(new String[] {
"""
package t;
import t.impl.HC;
import t.impl.HR;
@HC @HR public class T {
@HC @HR public static final int i = 0;
@HC @HR public void t() {}
}
""",
"""
package t.impl;
import java.lang.annotation.*;
@Retention(RetentionPolicy.CLASS)
public @interface HC {
}
""",
"""
package t.impl;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
public @interface HR {
}
"""
},
new String[] {
"""
package t;
public class T {
public static final int i = 0;
}
"""
},
"t.T",
"""
package t;
public class T {
public static final int i = 0;
public T();
public void t();
}
""",
"t.T",
"""
package t;
public class T {
public static final int i = 0;
public T();
}
""");
}
int i = 0;
void doTest(String code7, String code8, String testCode, Expect result7, Expect result8) throws Exception {
ToolBox tb = new ToolBox();
Path classes = prepareVersionedCTSym(new String[] {code7}, new String[] {code8});
Path output = classes.getParent();
Path scratch = output.resolve("scratch");
Files.createDirectories(scratch);
new JavacTask(tb)
.sources(testCode)
.options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "7"))
.run(result7)
.writeAll();
new JavacTask(tb)
.sources(testCode)
.options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "8"))
.run(result8)
.writeAll();
}
private static String computeClassPath(Path classes, String version) throws IOException {
try (Stream<Path> elements = Files.list(classes)) {
return elements.filter(el -> el.getFileName().toString().contains(version))
.map(el -> el.resolve("java.base"))
.map(el -> el.toAbsolutePath().toString())
.collect(Collectors.joining(File.pathSeparator));
}
}
void doPrintElementTest(String code7, String code8, String className7, String printed7, String className8, String printed8) throws Exception {
doPrintElementTest(new String[] {code7}, new String[] {code8}, className7, printed7, className8, printed8);
}
void doPrintElementTest(String[] code7, String[] code8, String className7, String printed7, String className8, String printed8) throws Exception {
ToolBox tb = new ToolBox();
Path classes = prepareVersionedCTSym(code7, code8);
Path output = classes.getParent();
Path scratch = output.resolve("scratch");
Files.createDirectories(scratch);
String out;
out = new JavacTask(tb, Task.Mode.CMDLINE)
.options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "7"), "-Xprint", className7)
.run(Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT)
.replaceAll("\\R", "\n");
if (!out.equals(printed7)) {
throw new AssertionError("out=" + out + "; printed7=" + printed7);
}
out = new JavacTask(tb, Task.Mode.CMDLINE)
.options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "8"), "-Xprint", className8)
.run(Expect.SUCCESS)
.getOutput(Task.OutputKind.STDOUT)
.replaceAll("\\R", "\n");
if (!out.equals(printed8)) {
throw new AssertionError("out=" + out + "; printed8=" + printed8);
}
}
void doTestEquivalence(String code7, String code8, String testClass) throws Exception {
Path classes = prepareVersionedCTSym(new String[] {code7}, new String[] {code8});
Path classfile = classes.resolve("78").resolve("java.base").resolve(testClass.replace('.', '/') + ".class");
if (!Files.isReadable(classfile)) {
throw new AssertionError("Cannot find expected class.");
}
}
@Test
void testIncluded() throws Exception {
doTestIncluded("package t;\n" +
"public class Test extends PP1<PP2> implements PP3<PP4>, PP5<PP6> {\n" +
" public PP7 m1(PP8 p) { return null;}\n" +
" public PP9<PPA> m2(PPB<PPC> p) { return null;}\n" +
" public PPD f1;\n" +
" public PPE<PPF> f2;\n" +
" public Test2 aux;\n" +
"}\n" +
"class Test2 extends PPG implements PPH, PPI {\n" +
"}\n" +
"class PP1<T> {}\n" +
"class PP2 {}\n" +
"interface PP3<T> {}\n" +
"class PP4 {}\n" +
"interface PP5<T> {}\n" +
"class PP6 {}\n" +
"class PP7 {}\n" +
"class PP8 {}\n" +
"class PP9<T> {}\n" +
"class PPA {}\n" +
"class PPB<T> {}\n" +
"class PPC {}\n" +
"class PPD {}\n" +
"class PPE<T> {}\n" +
"class PPF {}\n" +
"class PPG {}\n" +
"interface PPH {}\n" +
"interface PPI {}\n",
"t.Test",
"t.Test2",
"t.PP1",
"t.PP2",
"t.PP3",
"t.PP4",
"t.PP5",
"t.PP6",
"t.PP7",
"t.PP8",
"t.PP9",
"t.PPA",
"t.PPB",
"t.PPC",
"t.PPD",
"t.PPE",
"t.PPF",
"t.PPG",
"t.PPH",
"t.PPI");
}
@Test
void testRecords() throws Exception {
doPrintElementTest("package t;" +
"public class T {" +
" public record R(int i, java.util.List<String> l) { }" +
"}",
"package t;" +
"public class T {" +
" public record R(@Ann int i, long j, java.util.List<String> l) { }" +
" public @interface Ann {} " +
"}",
"t.T$R",
"""
public static record R(int i, java.util.List<java.lang.String> l) {
public R(int i,
java.util.List<java.lang.String> l);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object arg0);
public int i();
public java.util.List<java.lang.String> l();
}
""",
"t.T$R",
"""
public static record R(@t.T.Ann int i, long j, java.util.List<java.lang.String> l) {
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object arg0);
public java.util.List<java.lang.String> l();
public R(@t.T.Ann int i,
long j,
java.util.List<java.lang.String> l);
@t.T.Ann
public int i();
public long j();
}
""");
doPrintElementTest("package t;" +
"public record R() {" +
"}",
"package t;" +
"public record R(int i) {" +
"}",
"t.R",
"""
package t;
\n\
public record R() {
\n\
public R();
\n\
public final java.lang.String toString();
\n\
public final int hashCode();
\n\
public final boolean equals(java.lang.Object arg0);
}
""",
"t.R",
"""
package t;
\n\
public record R(int i) {
\n\
public final java.lang.String toString();
\n\
public final int hashCode();
\n\
public final boolean equals(java.lang.Object arg0);
\n\
public R(int i);
\n\
public int i();
}
""");
}
void doTestIncluded(String code, String... includedClasses) throws Exception {
boolean oldIncludeAll = includeAll;
try {
includeAll = false;
Path classes = prepareVersionedCTSym(new String[] {code}, new String[] {"package other; public class Other {}"});
Path root = classes.resolve("7").resolve("java.base");
try (Stream<Path> classFiles = Files.walk(root)) {
Set<String> names = classFiles.map(p -> root.relativize(p))
.map(p -> p.toString())
.map(n -> {System.err.println("n= " + n); return n;})
.filter(n -> n.endsWith(".class"))
.map(n -> n.substring(0, n.lastIndexOf('.')))
.map(n -> n.replace(File.separator, "."))
.collect(Collectors.toSet());
if (!names.equals(new HashSet<>(Arrays.asList(includedClasses))))
throw new AssertionError("Expected classes not included: " + names);
}
} finally {
includeAll = oldIncludeAll;
}
}
Path prepareVersionedCTSym(String[] code7, String[] code8) throws Exception {
String testClasses = System.getProperty("test.classes");
Path output = Paths.get(testClasses, "test-data" + i++);
deleteRecursively(output);
Files.createDirectories(output);
Path ver7Jar = output.resolve("7.jar");
compileAndPack(output, ver7Jar, code7);
Path ver8Jar = output.resolve("8.jar");
compileAndPack(output, ver8Jar, code8);
Path classes = output.resolve("classes.zip");
Path ctSym = output.resolve("ct.sym");
deleteRecursively(ctSym);
CreateSymbols.ALLOW_NON_EXISTING_CLASSES = true;
CreateSymbols.EXTENSION = ".class";
testGenerate(ver7Jar, ver8Jar, ctSym, "8", classes.toAbsolutePath().toString());
Path classesDir = output.resolve("classes");
try (JarFile jf = new JarFile(classes.toFile())) {
Enumeration<JarEntry> en = jf.entries();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
if (je.isDirectory()) continue;
Path target = classesDir.resolve(je.getName());
Files.createDirectories(target.getParent());
Files.copy(jf.getInputStream(je), target);
}
}
return classesDir;
}
boolean includeAll = true;
void testGenerate(Path jar7, Path jar8, Path descDest, String version, String classDest) throws IOException {
deleteRecursively(descDest);
List<VersionDescription> versions =
Arrays.asList(new VersionDescription(jar7.toAbsolutePath().toString(), "7", null),
new VersionDescription(jar8.toAbsolutePath().toString(), "8", "7"));
ExcludeIncludeList acceptAll = new ExcludeIncludeList(null, null) {
@Override public boolean accepts(String className) {
return true;
}
};
new CreateSymbols() {
@Override
protected boolean includeEffectiveAccess(ClassList classes, ClassDescription clazz) {
return includeAll ? true : super.includeEffectiveAccess(classes, clazz);
}
}.createBaseLine(versions, acceptAll, descDest, new String[0]);
Path symbolsDesc = descDest.resolve("symbols");
Path systemModules = descDest.resolve("systemModules");
Files.newBufferedWriter(systemModules).close();
try {
new CreateSymbols().createSymbols(null, symbolsDesc.toAbsolutePath().toString(), classDest, 0, "8", systemModules.toString());
} catch (Throwable t) {
t.printStackTrace();
throw t;
}
}
void compileAndPack(Path output, Path outputFile, String... code) throws Exception {
ToolBox tb = new ToolBox();
Path scratch = output.resolve("temp");
deleteRecursively(scratch);
Files.createDirectories(scratch);
System.err.println(Arrays.asList(code));
new JavacTask(tb).sources(code).options("-d", scratch.toAbsolutePath().toString()).run(Expect.SUCCESS);
List<String> classFiles = collectClassFile(scratch);
try (Writer out = Files.newBufferedWriter(outputFile)) {
for (String classFile : classFiles) {
try (InputStream in = Files.newInputStream(scratch.resolve(classFile))) {
int read;
while ((read = in.read()) != (-1)) {
out.write(String.format("%02x", read));
}
out.write("\n");
}
}
}
}
List<String> collectClassFile(Path root) throws IOException {
try (Stream<Path> files = Files.walk(root)) {
return files.filter(p -> Files.isRegularFile(p))
.filter(p -> p.getFileName().toString().endsWith(".class"))
.map(p -> root.relativize(p).toString())
.filter(p -> !p.contains("impl"))
.collect(Collectors.toList());
}
}
void deleteRecursively(Path dir) throws IOException {
Files.walkFileTree(dir, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
@Retention(RetentionPolicy.RUNTIME)
@interface Test {
}
}