Coverage Summary for Class: LogStackTraceExtractor (kr.open.library.simple_ui.core.logcat.internal.extractor)

Class Method, % Branch, % Line, % Instruction, %
LogStackTraceExtractor 87.5% (7/8) 57.7% (15/26) 85.4% (35/41) 84.6% (176/208)
LogStackTraceExtractor$Companion 100% (1/1) 100% (3/3) 100% (11/11)
LogStackTraceExtractor$Companion$extractorPool$1 100% (2/2) 100% (2/2) 100% (6/6)
Total 90.9% (10/11) 57.7% (15/26) 87% (40/46) 85.8% (193/225)


 package kr.open.library.simple_ui.core.logcat.internal.extractor
 
 /**
  * 현재 스레드의 스택 트레이스를 분석해 로그용 프레임을 추출합니다.
  *
  * Extracts log-friendly stack frames from the current thread's stack trace.
  * <br><br>
  * 현재 스레드의 스택을 분석하여 현재/부모 프레임을 추출합니다.
  *
  * @param skipPackages 로그 프레임 탐색에서 제외할 패키지 접두사 목록.
  */
 internal class LogStackTraceExtractor(
     skipPackages: Set<String>,
 ) {
     private var skipPackages: Set<String> = skipPackages
 
     /**
      * 탐색 시작 인덱스를 결정하는 헬퍼입니다.
      *
      * Resolves the start index for stack trace scanning.
      * <br><br>
      * 스택 탐색 시작 지점을 계산합니다.
      */
     private var startResolver: LogStackTraceStartResolver = createStartResolver(skipPackages)
 
     /**
      * 스택 프레임 필터링 규칙 모음입니다.
      *
      * Frame filter rules for skipping internal frames.
      * <br><br>
      * 내부 프레임을 건너뛰기 위한 필터입니다.
      */
     private var frameFilter: LogStackTraceFrameFilter = LogStackTraceFrameFilter(skipPackages)
 
     /**
      * 스킵 패키지 목록을 갱신합니다.<br><br>
      * skipPackages를 업데이트하고 내부 헬퍼를 재구성합니다.<br>
      *
      * @param newSkipPackages 새 스킵 패키지 목록.<br><br>
      *                        새 스킵 패키지 목록.<br>
      */
     fun updateSkipPackages(newSkipPackages: Set<String>) {
         if (skipPackages == newSkipPackages) return
         skipPackages = newSkipPackages
         startResolver = createStartResolver(newSkipPackages)
         frameFilter = LogStackTraceFrameFilter(newSkipPackages)
     }
 
     /**
      * 현재/부모 프레임을 추출해 반환합니다.
      *
      * Extracts and returns current and parent frames.
      * <br><br>
      * 현재/부모 프레임을 찾아 반환합니다.
      */
     fun extract(): LogStackFrames {
         val stackTrace = Thread.currentThread().stackTrace
         if (stackTrace.isEmpty()) {
             return LogStackFrames(createFallbackFrame(), null)
         }
 
         val lastIndex = stackTrace.lastIndex
         val searchStart = startResolver.resolve(stackTrace)
 
         var currentElement: StackTraceElement? = null
         var parentElement: StackTraceElement? = null
 
         for (i in searchStart..lastIndex) {
             val element = stackTrace[i]
             if (frameFilter.isSkipped(element) || frameFilter.isSynthetic(element)) continue
             if (currentElement == null) {
                 currentElement = element
             } else {
                 parentElement = element
                 break
             }
         }
 
         val fallbackElement = currentElement ?: stackTrace.getOrNull(searchStart) ?: stackTrace.firstOrNull()
         val currentFrame = toFrame(fallbackElement) ?: createFallbackFrame()
         val parentFrame = toFrame(parentElement)
 
         return LogStackFrames(currentFrame, parentFrame)
     }
 
     /**
      * 스택 트레이스 요소를 StackFrame으로 변환합니다.
      *
      * Converts a stack trace element into a StackFrame.
      * <br><br>
      * 스택 요소를 로그 프레임 모델로 변환합니다.
      *
      * @param element 변환 대상 스택 요소.
      */
     private fun toFrame(element: StackTraceElement?): LogStackFrame? {
         if (element == null) return null
         return LogStackFrame(
             fileName = element.fileName ?: LogStackTraceConstants.UNKNOWN_FILE_NAME,
             lineNumber = element.lineNumber,
             methodName = element.methodName,
             className = element.className,
         )
     }
 
     /**
      * 프레임을 찾지 못했을 때 사용할 기본 프레임입니다.
      *
      * Returns a fallback frame used when none could be extracted.
      * <br><br>
      * 추출 실패 시 사용할 기본 프레임을 반환합니다.
      */
     private fun createFallbackFrame(): LogStackFrame = LogStackFrame(
         fileName = LogStackTraceConstants.UNKNOWN_FILE_NAME,
         lineNumber = LogStackTraceConstants.FALLBACK_LINE_NUMBER,
         methodName = LogStackTraceConstants.UNKNOWN_METHOD,
         className = LogStackTraceConstants.UNKNOWN_CLASS,
     )
 
     private fun isCustomStartPrefix(prefix: String): Boolean {
         if (prefix.isBlank()) return false
         return LogStackTraceConstants.START_PREFIX_EXCLUDES.none { excluded -> prefix.startsWith(excluded) }
     }
 
     private fun createStartResolver(packages: Set<String>): LogStackTraceStartResolver =
         LogStackTraceStartResolver(
             additionalPrefixes = packages.filter { isCustomStartPrefix(it) }.toSet(),
         )
 
     companion object {
         private val extractorPool = object : ThreadLocal<LogStackTraceExtractor>() {
             override fun initialValue(): LogStackTraceExtractor = LogStackTraceExtractor(emptySet())
         }
 
         /**
          * ThreadLocal 기반으로 StackTraceExtractor를 재사용합니다.<br><br>
          * 스레드별 추출기를 재사용해 불필요한 객체 생성을 줄입니다.<br>
          *
          * @param skipPackages 스킵 패키지 목록.<br><br>
          *                     스킵 패키지 목록.<br>
          */
         internal fun extract(skipPackages: Set<String>): LogStackFrames {
             val extractor = extractorPool.get()!!
             extractor.updateSkipPackages(skipPackages)
             return extractor.extract()
         }
     }
 }