<lambda>null1 package leakcanary
2 
3 import android.os.Bundle
4 import android.view.LayoutInflater
5 import android.view.View
6 import android.view.ViewGroup
7 import androidx.fragment.app.Fragment
8 import androidx.lifecycle.ViewModel
9 import com.squareup.leakcanary.instrumentation.test.R
10 import leakcanary.TestUtils.assertLeak
11 import org.assertj.core.api.Assertions.assertThat
12 import org.junit.After
13 import org.junit.Before
14 import org.junit.Rule
15 import org.junit.Test
16 import shark.LeakTraceObject.LeakingStatus
17 
18 class LifecycleLeaksTest : HasActivityTestRule<TestActivity> {
19 
20   class TestViewModel : ViewModel()
21 
22   class TestFragment : Fragment() {
23     override fun onCreateView(
24       inflater: LayoutInflater,
25       container: ViewGroup?,
26       savedInstanceState: Bundle?
27     ): View {
28       return View(context)
29     }
30   }
31 
32   class FragmentHoldingLeaky : Fragment() {
33     val leaky = Any()
34   }
35 
36   @get:Rule
37   override val activityRule = activityTestRule<TestActivity>(
38     initialTouchMode = false,
39     launchActivity = false
40   )
41 
42   @Before fun setUp() {
43     AppWatcher.objectWatcher
44       .clearWatchedObjects()
45   }
46 
47   @After fun tearDown() {
48     AppWatcher.objectWatcher
49       .clearWatchedObjects()
50   }
51 
52   @Test fun activityLeakDetected() {
53     triggersOnActivityCreated {
54       activityRule.launchActivity(null)
55     }
56 
57     activity retained {
58       triggersOnActivityDestroyed {
59         activityRule.finishActivity()
60       }
61       assertLeak(TestActivity::class.java)
62     }
63   }
64 
65   @Test fun activityViewModelLeakDetected() {
66     triggersOnActivityCreated {
67       activityRule.launchActivity(null)
68     }
69 
70     val viewModel = getOnMainSync {
71       activity.installViewModel(TestViewModel::class)
72     }
73 
74     viewModel retained {
75       triggersOnActivityDestroyed {
76         activityRule.finishActivity()
77       }
78       assertLeak(TestViewModel::class.java)
79     }
80   }
81 
82   @Test fun fragmentViewModelLeakDetected() {
83     triggersOnActivityCreated {
84       activityRule.launchActivity(null)
85     }
86 
87     val viewModel = getOnMainSync {
88       val fragment = Fragment()
89       activity.addFragmentNow(fragment)
90       val viewModel = fragment.installViewModel(TestViewModel::class)
91       activity.removeFragmentNow(fragment)
92       viewModel
93     }
94 
95     viewModel retained {
96       assertLeak(TestViewModel::class.java)
97     }
98   }
99 
100   @Test
101   fun fragmentLeakDetected() {
102     triggersOnActivityCreated {
103       activityRule.launchActivity(null)
104     }
105 
106     val fragment = getOnMainSync {
107       val fragment = Fragment()
108       activity.addFragmentNow(fragment)
109       activity.removeFragmentNow(fragment)
110       fragment
111     }
112 
113     fragment retained {
114       val expectedLeakClass = Fragment::class.java
115       assertLeak { (heapAnalysis, leakTrace) ->
116         val className = leakTrace.leakingObject.className
117         assertThat(className)
118           .describedAs("$heapAnalysis")
119           .isEqualTo(expectedLeakClass.name)
120         assertThat(leakTrace.leakingObject.leakingStatusReason)
121           .describedAs("$heapAnalysis")
122           .contains("Fragment.mLifecycleRegistry.state is DESTROYED")
123       }
124     }
125   }
126 
127   @Test
128   fun fragmentNotLeakingDetected() {
129     triggersOnActivityCreated {
130       activityRule.launchActivity(null)
131     }
132 
133     getOnMainSync {
134       val fragment = FragmentHoldingLeaky()
135       activity.addFragmentNow(fragment)
136       AppWatcher.objectWatcher.expectWeaklyReachable(fragment.leaky, "leaky leaks")
137     }
138 
139     assertLeak { (heapAnalysis, leakTrace) ->
140       val refToLeaky = leakTrace.referencePath.last()
141       assertThat(refToLeaky.referenceName)
142         .describedAs("$heapAnalysis")
143         .isEqualTo("leaky")
144       val fragment = refToLeaky.originObject
145       // AssertJ uses lambdas when comparing enum values, which fails on older Android versions.
146       if (fragment.leakingStatus != LeakingStatus.NOT_LEAKING) {
147         throw AssertionError(
148           "${fragment.leakingStatus} should be ${LeakingStatus.NOT_LEAKING}"
149         )
150       }
151       assertThat(fragment.leakingStatusReason).isEqualTo(
152         "Fragment.mLifecycleRegistry.state is RESUMED"
153       )
154     }
155   }
156 
157   @Test
158   fun fragmentViewLeakDetected() {
159     triggersOnActivityCreated {
160       activityRule.launchActivity(null)
161     }
162 
163     val fragment = triggersOnFragmentCreated {
164       getOnMainSync {
165         val fragment = TestFragment()
166         activity.replaceWithBackStack(fragment, R.id.fragments)
167         fragment
168       }
169     }
170 
171     val fragmentView = getOnMainSync {
172       fragment.view!!
173     }
174 
175     triggersOnFragmentViewDestroyed {
176       runOnMainSync {
177         // Add a new fragment again, which destroys the view of the previous fragment and puts
178         // the first fragment in the backstack.
179         activity.replaceWithBackStack(Fragment(), R.id.fragments)
180       }
181     }
182 
183     fragmentView retained {
184       assertLeak(View::class.java)
185     }
186   }
187 }
188