/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.devtools.cyclefinder; import com.google.common.base.Joiner; import com.google.common.io.Files; import com.google.devtools.j2objc.util.ErrorUtil; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import junit.framework.TestCase; /** * System tests for the CycleFinder tool. * * @author Keith Stanger */ public class CycleFinderTest extends TestCase { File tempDir; List<String> inputFiles; List<List<Edge>> cycles; List<String> whitelistEntries; List<String> blacklistEntries; boolean printReferenceGraph; ReferenceGraph referenceGraph; static { // Prevents errors and warnings from being printed to the console. ErrorUtil.setTestMode(); } @Override protected void setUp() throws IOException { tempDir = createTempDir(); inputFiles = new ArrayList<>(); whitelistEntries = new ArrayList<>(); blacklistEntries = new ArrayList<>(); printReferenceGraph = false; referenceGraph = null; } @Override protected void tearDown() { ErrorUtil.reset(); } public void testEasyCycle() throws Exception { addSourceFile("A.java", "class A { B b; }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertCycle("LA;", "LB;"); } // TODO(nbraswell): Use com.google.j2objc.annotations.WeakOuter when transitioned to Java 8 private static String weakOuterAndInterface = "import java.lang.annotation.*;\n" + "@Target(ElementType.TYPE_USE) @interface WeakOuter {}" + "interface Simple { public int run(); }"; public void testAnonymousClassOuterRefCycle() throws Exception { addSourceFile("Simple.java", weakOuterAndInterface + "class Test { int member = 7; Simple o;" + "void f() { o = new Simple() { public int run() { return member; } }; } }"); findCycles(); // Assert that we have one cycle that contains LTest and a LTest anonymous class. assertEquals(1, cycles.size()); assertCycle("LTest;"); assertContains("LTest.1", printCyclesToString()); } public void testAnonymousClassWithWeakOuter() throws Exception { addSourceFile("Simple.java", weakOuterAndInterface + "class Test { int member = 7; Simple o;" + "void f() { new @WeakOuter Simple() { public int run() { return member; } }; } }"); findCycles(); assertNoCycles(); } public void testInnerClassWithWeakOuter() throws Exception { String source = "import com.google.j2objc.annotations.WeakOuter; " + "public class A { @WeakOuter class B { int test() { return o.hashCode(); }} B o; }"; addSourceFile("A.java", source); findCycles(); assertNoCycles(); } public void testInnerClassOuterRefCycle() throws Exception { String source = "import com.google.j2objc.annotations.WeakOuter; " + "public class A { class B {int test(){return o.hashCode();}} B o;}"; addSourceFile("A.java", source); findCycles(); assertCycle("LA;", "LA.B;"); } public void testWeakField() throws Exception { addSourceFile("A.java", "import com.google.j2objc.annotations.Weak; class A { @Weak B b; }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testRetainedWithField() throws Exception { addSourceFile("A.java", "import com.google.j2objc.annotations.RetainedWith; class A { @RetainedWith B b; }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testRecursiveTypeVariable() throws Exception { addSourceFile("A.java", "class A<T> { A<? extends T> a; }"); addSourceFile("B.java", "class B<T> { B<? extends B<T>> b; }"); addSourceFile("C.java", "class C<T> { C<java.util.List<T>> c; }"); findCycles(); // This test passes if it doesn't hang or crash due to infinite recursion. } public void testExtendsWildcard() throws Exception { addSourceFile("A.java", "class A { B<? extends C> b; }"); addSourceFile("B.java", "class B<T> { T t; }"); addSourceFile("C.java", "class C { A a; }"); findCycles(); assertCycle("LA;", "LB<+LC;>;", "+LC;"); } public void testWhitelistedField() throws Exception { addSourceFile("A.java", "class A { B b; }"); addSourceFile("B.java", "class B { A a; }"); whitelistEntries.add("FIELD A.b"); findCycles(); assertNoCycles(); } public void testWhitelistedType() throws Exception { addSourceFile("test/foo/A.java", "package test.foo; class A { C c; }"); addSourceFile("test/foo/B.java", "package test.foo; class B { A a; }"); addSourceFile("test/foo/C.java", "package test.foo; class C extends B { }"); whitelistEntries.add("TYPE test.foo.C"); findCycles(); assertNoCycles(); whitelistEntries.set(0, "TYPE test.foo.A"); findCycles(); assertNoCycles(); whitelistEntries.set(0, "TYPE test.foo.B"); findCycles(); assertCycle("Ltest/foo/C;", "Ltest/foo/A;"); } public void testWhitelistedLocalType() throws Exception { addSourceFile("test/foo/A.java", "package test.foo; class A { B b; void test() { " + "class Inner extends B { void foo() { A a = A.this; } } } }"); addSourceFile("test/foo/B.java", "package test.foo; class B {}"); whitelistEntries.add("TYPE test.foo.A.test.Inner"); findCycles(); assertNoCycles(); } public void testWhitelistedAnonymousType() throws Exception { addSourceFile("test/foo/A.java", "package test.foo; class A { B b; B test() { " + "return new B() { void foo() { A a = A.this; } }; } }"); addSourceFile("test/foo/B.java", "package test.foo; class B {}"); whitelistEntries.add("TYPE test.foo.A.test.$"); findCycles(); assertNoCycles(); } public void testSubtypeOfWhitelistedType() throws Exception { addSourceFile("test/foo/A.java", "package test.foo; class A { B b; }"); addSourceFile("test/foo/B.java", "package test.foo; class B { A a; }"); addSourceFile("test/foo/C.java", "package test.foo; class C extends B { }"); whitelistEntries.add("TYPE test.foo.B"); findCycles(); assertNoCycles(); } public void testWhitelistedPackage() throws Exception { addSourceFile("test/foo/A.java", "package test.foo; import test.bar.B; public class A { B b; }"); addSourceFile("test/bar/B.java", "package test.bar; import test.foo.A; public class B { A a; }"); whitelistEntries.add("NAMESPACE test.bar"); findCycles(); assertNoCycles(); } public void testWhitelistedSubtype() throws Exception { addSourceFile("A.java", "class A { B b; }"); addSourceFile("B.java", "class B {}"); addSourceFile("C.java", "class C extends B {}"); addSourceFile("D.java", "class D extends C { A a; }"); whitelistEntries.add("FIELD A.b C"); findCycles(); assertNoCycles(); } public void testWhitelistedOuterReference() throws Exception { addSourceFile("A.java", "class A { Inner i; class Inner { void test() { A a = A.this; } } }"); whitelistEntries.add("OUTER A.Inner"); findCycles(); assertNoCycles(); } public void testWhitelistComment() throws Exception { addSourceFile("A.java", "class A { B b; }"); addSourceFile("B.java", "class B { A a; }"); whitelistEntries.add("# FIELD A.b"); findCycles(); assertCycle("LA;", "LB;"); } public void testStaticField() throws Exception { addSourceFile("A.java", "class A { static B b; }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testArrayField() throws Exception { addSourceFile("A.java", "class A { B[] b; }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertCycle("LA;", "LB;"); } public void testNonStaticInnerInterface() throws Exception { addSourceFile("A.java", "class A { interface I {} I i;}"); findCycles(); assertNoCycles(); } public void testCapturedVariable() throws Exception { addSourceFile("A.java", "class A { void test() {" + " final B b = new B();" + " A a = new A() { void test() { b.hashCode(); } }; } }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertCycle("LB;"); } public void testCapturedVariableNotUsed() throws Exception { addSourceFile("A.java", "class A { void test() {" + " final B b = new B();" + " A a = new A() { void test() { } }; } }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testWeakCapturedVariable() throws Exception { addSourceFile("A.java", "import com.google.j2objc.annotations.Weak;" + "class A { void test() {" + " @Weak final B b = new B();" + " A a = new A() { void test() { b.hashCode(); } }; } }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testFinalVarAfterAnonymousClassNotCaptured() throws Exception { addSourceFile("A.java", "class A { void test() {" + " A a = new A() {};" + " final B b; } }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testOutOFScopeFinalVarNotCaptured() throws Exception { addSourceFile("A.java", "class A { void test() {" + " { final B b; }" + " A a = new A() {}; } }"); addSourceFile("B.java", "class B { A a; }"); findCycles(); assertNoCycles(); } public void testAnonymousClassAssignedToStaticField() throws Exception { addSourceFile("A.java", "class A { B b; static Runnable r = new Runnable() { public void run() {} }; }"); addSourceFile("B.java", "class B { Runnable r; }"); findCycles(); assertNoCycles(); } public void testAnonymousClassInStaticMethod() throws Exception { addSourceFile("A.java", "class A { B b; static void test() { " + "Runnable r = new Runnable() { public void run() {} }; } }"); addSourceFile("B.java", "class B { Runnable r; }"); findCycles(); assertNoCycles(); } public void testNoOuterReferenceIfNotNeeded() throws Exception { addSourceFile("A.java", "class A { Runnable r = new Runnable() { public void run() {} }; }"); findCycles(); assertNoCycles(); } public void testOuterReferenceToGenericClass() throws Exception { // B.java is added before A.java to test that the outer edge A<B>.C -> A<B> is still added // despite B being visited before A. The outer reference cannot be known until A is visited. addSourceFile("B.java", "class B { A<B>.C abc; }"); addSourceFile("A.java", "class A<T> { int i; T t; class C { void test() { i++; } } }"); findCycles(); assertCycle("LA<LB;>;", "LB;", "LA<LB;>.C;"); } public void testBlacklist() throws Exception { addSourceFile("A.java", "class A { B b; C c; }"); addSourceFile("B.java", "class B { A a; }"); addSourceFile("C.java", "class C { A a; }"); blacklistEntries.add("TYPE C"); findCycles(); assertEquals(1, cycles.size()); assertCycle("LA;", "LC;"); } public void testAnonymousLineNumbers() throws Exception { addSourceFile("Test.java", "class Test {\n" + " void dummy() {}\n" + " Runnable r = new Runnable() { public void run() { dummy(); } }; }"); findCycles(); assertEquals(1, cycles.size()); assertCycle("LTest;"); for (Edge e : cycles.get(0)) { assertContains("anonymous:3", e.toString()); } } public void testWhitelistedAnonymousTypesInClassScope() throws Exception { addSourceFile("bar/AbstractA.java", "package bar; public class AbstractA {}"); addSourceFile("bar/AbstractB.java", "package bar; public class AbstractB {}"); addSourceFile("foo/Test.java", "package foo; import bar.AbstractA; import bar.AbstractB;" + " class Test { AbstractA a = new AbstractA() { void dummyA() {}" + " AbstractB b = new AbstractB() { void dummyB() { dummyA(); } }; }; }"); whitelistEntries.add("NAMESPACE foo"); findCycles(); assertNoCycles(); } public void testIncompatibleStripping() throws Exception { addSourceFile("Test.java", "import com.google.j2objc.annotations.J2ObjCIncompatible; " + "import java.garbage.Foo; class Test { @J2ObjCIncompatible Foo foo; }"); // We just care that there are no compile errors. findCycles(); assertNoCycles(); } public void testIgnoreRawTypes() throws Exception { addSourceFile("A.java", "class A<T> { B b; }"); addSourceFile("B.java", "class B<T> { A a; }"); findCycles(); assertNoCycles(); } public void testSimpleLambdaWithCycle() throws Exception { addSourceFile("I.java", "interface I { int foo(); }"); addSourceFile("A.java", "class A { int j = 1; I i = () -> j; }"); findCycles(); assertCycle("LA.$Lambda$1;", "LA;"); } public void testMethodReferenceCycle() throws Exception { addSourceFile("I.java", "interface I { int foo(); }"); addSourceFile("A.java", "class A { int bar() { return 1; } I i = this::bar; }"); findCycles(); assertCycle("LA.$Lambda$1;", "LA;"); } public void testPrintReferenceGraph() throws Exception { addSourceFile("A.java", "class A { B<? extends C> b; }"); addSourceFile("B.java", "class B<T> { T t; }"); addSourceFile("C.java", "class C { A a; }"); printReferenceGraph = true; findCycles(); String graph = printReferenceGraphToString(); assertContains("class: LB<+LC;>;", graph); assertContains("A -> (field b with type B<? extends C>)", graph); assertContains("C -> (field a with type A)", graph); } private void assertContains(String substr, String str) { assertTrue("Expected \"" + substr + "\" within \"" + str + "\"", str.contains(substr)); } private void assertNoCycles() { assertNotNull(cycles); assertTrue("Expected no cycles: " + printCyclesToString(), cycles.isEmpty()); } private void assertCycle(String... types) { assertNotNull(cycles); outer: for (List<Edge> cycle : cycles) { List<String> cycleTypes = new ArrayList<>(); for (Edge e : cycle) { cycleTypes.add(e.getOrigin().getSignature()); } for (String type : types) { if (!cycleTypes.contains(type)) { continue outer; } } return; } fail("No cycle found with types: " + Joiner.on(", ").join(types) + "\n" + printCyclesToString()); } private String printCyclesToString() { ByteArrayOutputStream cyclesOut = new ByteArrayOutputStream(); CycleFinder.printCycles(cycles, new PrintStream(cyclesOut)); return cyclesOut.toString(); } private String printReferenceGraphToString() { ByteArrayOutputStream referenceGraphOut = new ByteArrayOutputStream(); referenceGraph.print(new PrintStream(referenceGraphOut)); return referenceGraphOut.toString(); } private void findCycles() throws IOException { Options options = new Options(); if (!whitelistEntries.isEmpty()) { File whitelistFile = new File(tempDir, "whitelist"); Files.write(Joiner.on("\n").join(whitelistEntries), whitelistFile, Charset.defaultCharset()); options.addWhitelistFile(whitelistFile.getAbsolutePath()); } if (!blacklistEntries.isEmpty()) { File blacklistFile = new File(tempDir, "type_filter"); Files.write(Joiner.on("\n").join(blacklistEntries), blacklistFile, Charset.defaultCharset()); options.addBlacklistFile(blacklistFile.getAbsolutePath()); } options.setSourceFiles(inputFiles); options.setClasspath(System.getProperty("java.class.path")); if (printReferenceGraph) { options.setPrintReferenceGraph(); } CycleFinder finder = new CycleFinder(options); finder.constructGraph(); cycles = finder.findCycles(); if (printReferenceGraph) { referenceGraph = finder.getReferenceGraph(); } if (ErrorUtil.errorCount() > 0) { fail("CycleFinder failed with errors:\n" + Joiner.on("\n").join(ErrorUtil.getErrorMessages())); } } private void addSourceFile(String fileName, String source) throws IOException { File file = new File(tempDir, fileName); file.getParentFile().mkdirs(); Files.write(source, file, Charset.defaultCharset()); inputFiles.add(file.getAbsolutePath()); } private File createTempDir() throws IOException { File tempDir = File.createTempFile("cyclefinder_testout", ""); tempDir.delete(); tempDir.mkdir(); return tempDir; } }