main

square/leakcanary

Last updated at: 29/12/2023 09:39

HeapAnalyzerTest.kt

TLDR

The file HeapAnalyzerTest.kt is part of the Shark project and contains test cases for the HeapAnalyzer class. It tests various memory leak scenarios and verifies the correctness of the analysis performed by the HeapAnalyzer.

Methods

setUp

Initializes the test environment by creating a temporary hprof file.

singlePathToInstance

Tests the scenario where there is a single path from the root to a leaking instance in the heap. Verifies that the analysis correctly detects the leak.

pathToString

Tests the scenario where there is a path from the root to a String instance in the heap. Verifies that the analysis correctly identifies the leaking object as a String.

pathToCharArray

Tests the scenario where there is a path from the root to a char array instance in the heap. Verifies that the analysis correctly identifies the leaking object as a char array.

pathToTwoCharArrays

Tests the scenario where there are two paths from the root to different char array instances in the heap. Verifies that the analysis correctly identifies both instances as leaks.

shortestPath

Tests the scenario where there is a single path from the root to a leaking instance in the heap, and there are no other paths to the same instance from any other objects. Verifies that the analysis correctly detects the leak and identifies the shortest path.

noPathToInstance

Tests the scenario where there is no path from the root to a leaking instance in the heap. Verifies that the analysis correctly reports no leaks.

weakRefCleared

Tests the scenario where a weak reference to an object is cleared before the leak analysis is performed. Verifies that the analysis correctly reports no leaks.

failsNoRetainedKeys

Tests the scenario where multiple activity leaks are written to the hprof file, but there are no retained keys. Verifies that the analysis correctly reports no leaks.

findMultipleIdenticalLeaks

Tests the scenario where multiple identical leaks are written to the hprof file. Verifies that the analysis correctly detects and reports only one leak with multiple leak traces.

localVariableLeakThreadSubclass

Tests the scenario where there is a local variable leak of a thread subclass. Verifies that the analysis correctly identifies the leaking object and the reference path.

localVariableLeak

Tests the scenario where there is a local variable leak of a thread. Verifies that the analysis correctly identifies the leaking object and the reference path.

localVariableLeakShortestPathGoesLast

Tests the scenario where there is a local variable leak of a thread subclass with a shorter reference path going last. Verifies that the analysis correctly identifies the leaking object and the reference path.

threadFieldLeak

Tests the scenario where there is a leak through a field of a thread. Verifies that the analysis correctly identifies the leaking object and the reference path.

nativeGlobalVariableApplicationLeak

Tests the scenario where there is a native global variable application leak. Verifies that the analysis correctly identifies the leaking object.

Classes

HeapAnalyzerTest

This class contains the test cases for the HeapAnalyzer. It utilizes different scenarios to test the leak analysis performed by the HeapAnalyzer.

package shark

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import shark.GcRoot.JniGlobal
import shark.GcRoot.ThreadObject
import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
import shark.LeakTraceReference.ReferenceType.LOCAL
import shark.ValueHolder.ReferenceHolder
import java.io.File

class HeapAnalyzerTest {

  @get:Rule
  var testFolder = TemporaryFolder()
  private lateinit var hprofFile: File

  @Before
  fun setUp() {
    hprofFile = testFolder.newFile("temp.hprof")
  }

  @Test fun singlePathToInstance() {
    hprofFile.writeSinglePathToInstance()
    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    assertThat(analysis.applicationLeaks[0]).isInstanceOf(Leak::class.java)
  }

  @Test fun pathToString() {
    hprofFile.writeSinglePathToString()
    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    val leak = analysis.applicationLeaks[0]

    assertThat(leak.leakTraces.first().leakingObject.className).isEqualTo("java.lang.String")
  }

  @Test fun pathToCharArray() {
    hprofFile.writeSinglePathsToCharArrays(listOf("Hello"))
    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()
    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.leakingObject.className).isEqualTo("char[]")
  }

  // Two char arrays to ensure we keep going after finding the first one
  @Test fun pathToTwoCharArrays() {
    hprofFile.writeSinglePathsToCharArrays(listOf("Hello", "World"))
    val analysis = hprofFile.checkForLeaks<HeapAnalysis>()
    assertThat(analysis).isInstanceOf(HeapAnalysisSuccess::class.java)
  }

  @Test fun shortestPath() {
    hprofFile.writeTwoPathsToInstance()

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.referencePath).hasSize(1)
    assertThat(leakTrace.referencePath[0].originObject.className).isEqualTo("GcRoot")
    assertThat(leakTrace.referencePath[0].referenceName).isEqualTo("shortestPath")
    assertThat(leakTrace.leakingObject.className).isEqualTo("Leaking")
  }

  @Test fun noPathToInstance() {
    hprofFile.writeNoPathToInstance()

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    assertThat(analysis.applicationLeaks).isEmpty()
  }

  @Test fun weakRefCleared() {
    hprofFile.writeWeakReferenceCleared()

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()
    assertThat(analysis.applicationLeaks).isEmpty()
  }

  @Test fun failsNoRetainedKeys() {
    hprofFile.writeMultipleActivityLeaks(0)

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    assertThat(analysis.applicationLeaks).isEmpty()
  }

  @Test fun findMultipleIdenticalLeaks() {
    hprofFile.writeMultipleActivityLeaks(5)

    val leaks = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    assertThat(leaks.applicationLeaks).hasSize(1)
    assertThat(leaks.applicationLeaks.first().leakTraces).hasSize(5)
  }

  @Test fun localVariableLeakThreadSubclass() {
    hprofFile.writeJavaLocalLeak(threadClass = "MyThread", threadName = "kroutine")

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.referencePath).hasSize(1)
    assertThat(leakTrace.referencePath[0].originObject.className).isEqualTo("MyThread")
    assertThat(leakTrace.referencePath[0].referenceType).isEqualTo(LOCAL)
    assertThat(leakTrace.leakingObject.className).isEqualTo("Leaking")
  }

  @Test fun localVariableLeak() {
    hprofFile.writeJavaLocalLeak()

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()
    println(analysis)

    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.referencePath).hasSize(1)
    assertThat(leakTrace.referencePath[0].originObject.className).isEqualTo(Thread::class.java.name)
    assertThat(leakTrace.referencePath[0].referenceType).isEqualTo(LOCAL)
    assertThat(leakTrace.leakingObject.className).isEqualTo("Leaking")
  }

  @Test fun localVariableLeakShortestPathGoesLast() {
    hprofFile.writeTwoPathJavaLocalShorterLeak(threadClass = "MyThread", threadName = "kroutine")

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()
    println(analysis)

    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.referencePath).hasSize(2)
    assertThat(leakTrace.referencePath[0].originObject.className).isEqualTo("GcRoot")
    assertThat(leakTrace.referencePath[0].referenceType).isEqualTo(STATIC_FIELD)
  }

  @Test fun threadFieldLeak() {
    hprofFile.dump {
      val threadClassId =
        clazz(className = "java.lang.Thread", fields = listOf("name" to ReferenceHolder::class))
      val myThreadClassId = clazz(
        className = "MyThread", superclassId = threadClassId,
        fields = listOf("leaking" to ReferenceHolder::class)
      )
      val threadInstance =
        instance(myThreadClassId, listOf("Leaking" watchedInstance {}, string("Thread Name")))
      gcRoot(
        ThreadObject(
          id = threadInstance.value, threadSerialNumber = 42, stackTraceSerialNumber = 0
        )
      )
    }

    val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>()

    val leakTrace = analysis.applicationLeaks[0].leakTraces.first()
    assertThat(leakTrace.referencePath).hasSize(1)
    assertThat(leakTrace.referencePath[0].originObject.className).isEqualTo("MyThread")
    assertThat(leakTrace.referencePath[0].referenceName).isEqualTo("leaking")
    assertThat(leakTrace.leakingObject.className).isEqualTo("Leaking")
  }

  @Test fun nativeGlobalVariableApplicationLeak() {
    hprofFile.dump {
      gcRoot(JniGlobal(id = "Leaking".watchedInstance {}.value, jniGlobalRefId = 42))
    }

    val leaks = hprofFile.checkForLeaks<HeapAnalysisSuccess>()
    assertThat(leaks.applicationLeaks).hasSize(1)
  }
}