From 2dacdb70d76490ba6268bb0dab2bdf4d0e65d9fe Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 17:25:06 +0900 Subject: [PATCH 1/6] Support various log format of chipmunk --- .../jerryjeon/logjerry/parse/DefaultParser.kt | 168 ++++++++++++++++-- .../jerryjeon/logjerry/parse/ParserFactory.kt | 10 ++ .../logjerry/source/SourceManager.kt | 28 ++- .../logjerry/parse/DefaultParserTest.kt | 110 ++++++++++++ .../jerryjeon/logjerry/parse/ParserTest.kt | 7 +- src/test/resources/junit-platform.properties | 1 + 6 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 src/main/kotlin/com/jerryjeon/logjerry/parse/ParserFactory.kt create mode 100644 src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt create mode 100644 src/test/resources/junit-platform.properties diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt index c6462e9..cf19128 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt @@ -1,9 +1,102 @@ package com.jerryjeon.logjerry.parse import com.jerryjeon.logjerry.log.Log +import java.time.LocalDate +import java.time.LocalTime import java.util.concurrent.atomic.AtomicInteger -class DefaultParser : LogParser { +class DefaultParser( + // Log format configuration before AS Chipmunk version + val includeDateTime: Boolean, + val includePidTid: Boolean, + val includePackageName: Boolean, + val includeTag: Boolean +) : LogParser { + + companion object : ParserFactory { + + private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') + override fun create(sample: String): LogParser? { + try { + val split = sample.split(" ", limit = 5) + val iterator = split.listIterator() + + var currentToken = iterator.next() + + val includeDate = try { + LocalDate.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + val includeTime = try { + LocalTime.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + // Only supports both exist or not exist at all + if (includeDate xor includeTime) return null + + val pidTidRegex = Regex("\\d*[-/]\\d*") + val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") + + // pit-tid/packageName + var includePidTid: Boolean = false + var includePackageName: Boolean = false + if (currentToken.contains("/")) { + // both exist + val tokens = currentToken.split("/") + if (tokens[0].matches(pidTidRegex) && (tokens[1] == "?" || tokens[1].matches(packageNameRegex))) { + includePidTid = true + includePackageName = true + currentToken = iterator.next() + } + } else { + if (currentToken.matches(pidTidRegex)) { + includePidTid = true + includePackageName = false + currentToken = iterator.next() + } else if (currentToken == "?" || currentToken.matches(packageNameRegex)) { + includePidTid = false + includePackageName = true + currentToken = iterator.next() + } + } + + var includeTag = false + if (currentToken.contains("/")) { + // both exist + val tokens = currentToken.split("/") + // Check what's faster: list and regex + if (tokens[0].length == 1 && tokens[0][0] in priorityChars) { + includeTag = true + } else { + // invalid + return null + } + } else if (currentToken.length == 1 && currentToken[0] in priorityChars) { + includeTag = false + } + + if (currentToken.last() != ':') { + return null + } + + if (!iterator.hasNext()) { + return null + } + + return DefaultParser(includeDate, includePidTid, includePackageName, includeTag) + } catch (e: Exception) { + return null + } + } + } private val number = AtomicInteger(1) override fun canParse(raw: String): Boolean { @@ -42,22 +135,71 @@ class DefaultParser : LogParser { return ParseResult(logs, invalidSentences) } private fun parseSingleLineLog(raw: String): Log { - val split = raw.split(" ") + var segmentCount = 5 + if (!includeDateTime) segmentCount -= 2 + if (!includePidTid && !includePackageName) segmentCount-- - val date = split[0] - val time = split[1] + val split = raw.split(" ", limit = segmentCount) - val thirdSegment = split[2].split("-", "/") - val pid = thirdSegment[0].toLong() - val tid = thirdSegment[1].toLong() - val packageName = thirdSegment[2].takeIf { it != "?" } + var currentIndex = 0 - val fourthSegment = split[3].split("/") - val priority = fourthSegment[0] - val tag = fourthSegment[1].removeSuffix(":") + // TODO Change these to nullable + val date: String + val time: String + if (includeDateTime) { + date = split[currentIndex++] + time = split[currentIndex++] + } else { + date = "" + time = "" + } - val originalLog = split.subList(4, split.size).joinToString(separator = " ") + // TODO Change these to nullable + val pid: Long + val tid: Long + val packageName: String? + when { + includePidTid && includePackageName -> { + val thirdSegment = split[currentIndex++].split("-", "/") + pid = thirdSegment[0].toLong() + tid = thirdSegment[1].toLong() + packageName = thirdSegment[2].takeIf { it != "?" } + } + includePidTid -> { + val thirdSegment = split[currentIndex++].split("-") + pid = thirdSegment[0].toLong() + tid = thirdSegment[1].toLong() + packageName = null + } + includePackageName -> { + pid = 0L + tid = 0L + packageName = split[currentIndex++] + } + else -> { + pid = 0L + tid = 0L + packageName = null + } + } + + val priorityText: String + val tag: String + if (includeTag) { + val fourthSegment = split[currentIndex++].split("/") + priorityText = fourthSegment[0] + tag = fourthSegment[1].removeSuffix(":") + } else { + priorityText = split[currentIndex++].removeSuffix(":") + tag = "" + } + + val originalLog = split[currentIndex] + + return Log(number.getAndIncrement(), date, time, pid, tid, packageName, priorityText, tag, originalLog) + } - return Log(number.getAndIncrement(), date, time, pid, tid, packageName, priority, tag, originalLog) + override fun toString(): String { + return "DefaultParser(includeDateTime=$includeDateTime, includePidTid=$includePidTid, includePackageName=$includePackageName, includeTag=$includeTag)" } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/ParserFactory.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/ParserFactory.kt new file mode 100644 index 0000000..a952f6d --- /dev/null +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/ParserFactory.kt @@ -0,0 +1,10 @@ +package com.jerryjeon.logjerry.parse + +interface ParserFactory { + + /** + * If the parser can't handle the sample then return null + */ + fun create(sample: String): LogParser? + +} \ No newline at end of file diff --git a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt index 586ff9f..fcfa4c8 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt @@ -1,17 +1,12 @@ package com.jerryjeon.logjerry.source -import com.jerryjeon.logjerry.filter.FilterManager import com.jerryjeon.logjerry.log.LogManager import com.jerryjeon.logjerry.parse.DefaultParser import com.jerryjeon.logjerry.parse.ParseStatus import com.jerryjeon.logjerry.preferences.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.* import okio.FileSystem import okio.Path.Companion.toOkioPath import okio.Path.Companion.toPath @@ -19,7 +14,12 @@ import okio.openZip class SourceManager(private val preferences: Preferences) { private val sourceScope = CoroutineScope(Dispatchers.Default) - private val parser = DefaultParser() + private val defaultParser = DefaultParser( + includeDateTime = true, + includePidTid = true, + includePackageName = true, + includeTag = true + ) val sourceFlow: MutableStateFlow = MutableStateFlow(Source.None) val parseStatusFlow: StateFlow = sourceFlow.map { when (it) { @@ -28,18 +28,28 @@ class SourceManager(private val preferences: Preferences) { val zipFileSystem = fileSystem.openZip(it.file.toOkioPath()) val files = zipFileSystem.listOrNull("/".toPath()) ?: return@map ParseStatus.NotStarted val content = zipFileSystem.read(files.first()) { readUtf8() }.split("\n") + // Prefer second line because the first line breaks often because of the buffer + val sample = content.getOrNull(1) ?: content.first() + val parser = DefaultParser.create(sample) ?: defaultParser val parseResult = parser.parse(content) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } is Source.File -> { + val lines = it.file.readLines() + // Prefer second line because the first line breaks often because of the buffer + val sample = lines.getOrNull(1) ?: lines.first() + val parser = DefaultParser.create(sample) ?: defaultParser val parseResult = parser.parse(it.file.readLines()) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } is Source.Text -> { - val parseResult = parser.parse(it.text.split("\n")) - val filterManager = FilterManager() + val lines = it.text.split("\n") + // Prefer second line because the first line breaks often because of the buffer + val sample = lines.getOrNull(1) ?: lines.first() + val parser = DefaultParser.create(sample) ?: defaultParser + val parseResult = parser.parse(lines) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } Source.None -> { diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt new file mode 100644 index 0000000..1a52b6a --- /dev/null +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt @@ -0,0 +1,110 @@ +package com.jerryjeon.logjerry.parse + +import io.kotest.assertions.asClue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +internal class DefaultParserTest { + + @Nested + inner class FactoryTest { + + @ParameterizedTest + @MethodSource("logAndIncludeSettings") + fun `Factory can be created for all include settings`(input: String, expected: IncludeSettings) { + val parser = DefaultParser.create(input) + parser.shouldBeInstanceOf() + .asClue { + it.includeDateTime shouldBe expected.includeDateTime + it.includePidTid shouldBe expected.includePidTid + it.includePackageName shouldBe expected.includePackageName + it.includeTag shouldBe expected.includeTag + } + + parser.parse(listOf(input)).invalidSentences.shouldBeEmpty() + } + + private fun logAndIncludeSettings(): Stream { + return Stream.of( + Arguments.of( + "I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = false) + ), + Arguments.of( + "I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = true) + ), + Arguments.of( + "? I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = false) + ), + Arguments.of( + "? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = true) + ), + Arguments.of( + "178-178 I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = false) + ), + Arguments.of( + "178-178 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = true) + ), + Arguments.of( + "178-178/? I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = false) + ), + Arguments.of( + "178-178/? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = true) + ), + Arguments.of( + "2022-10-24 08:50:35.786 I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = false) + ), + Arguments.of( + "2022-10-24 08:50:35.786 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = true) + ), + Arguments.of( + "2022-10-24 09:31:55.786 ? I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = false) + ), + Arguments.of( + "2022-10-24 09:31:55.786 ? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = true) + ), + Arguments.of( + "2022-10-24 09:31:55.786 178-178 I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = false) + ), + Arguments.of( + "2022-10-24 09:31:55.786 178-178 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = true) + ), + Arguments.of( + "2022-10-24 09:31:55.786 178-178/? I: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = false) + ), + Arguments.of( + "2022-10-24 09:31:55.786 178-178/? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", + IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = true) + ), + ) + } + } + + data class IncludeSettings( + val includeDateTime: Boolean, + val includePidTid: Boolean, + val includePackageName: Boolean, + val includeTag: Boolean, + ) +} diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt index 7bf5e27..f03fe29 100644 --- a/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt @@ -9,7 +9,12 @@ internal class ParserTest { @Test fun `Given raw login, parse well`() { - val parseResult = DefaultParser().parse( + val parseResult = DefaultParser( + includeDateTime = true, + includePidTid = true, + includePackageName = true, + includeTag = true + ).parse( listOf("2021-12-19 23:05:36.664 165-165/? I/hwservicemanager: Since android.hardware.media.omx@1.0::IOmxStore/default is not registered, trying to start it as a lazy HAL.") ) diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..d265fd8 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class From 1f7e6562715ddbff4cbad1e2a546e1f15c5c9056 Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 17:26:18 +0900 Subject: [PATCH 2/6] Rename: DefaultParser -> StudioLogcatBelowChipmunkParser --- ...ultParser.kt => StudioLogcatBelowChipmunkParser.kt} | 4 ++-- .../com/jerryjeon/logjerry/source/SourceManager.kt | 10 +++++----- .../kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt | 2 +- ...rTest.kt => StudioLogcatBelowChipmunkParserTest.kt} | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) rename src/main/kotlin/com/jerryjeon/logjerry/parse/{DefaultParser.kt => StudioLogcatBelowChipmunkParser.kt} (97%) rename src/test/kotlin/com/jerryjeon/logjerry/parse/{DefaultParserTest.kt => StudioLogcatBelowChipmunkParserTest.kt} (96%) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt similarity index 97% rename from src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt rename to src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt index cf19128..4f22c08 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/DefaultParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.LocalTime import java.util.concurrent.atomic.AtomicInteger -class DefaultParser( +class StudioLogcatBelowChipmunkParser( // Log format configuration before AS Chipmunk version val includeDateTime: Boolean, val includePidTid: Boolean, @@ -91,7 +91,7 @@ class DefaultParser( return null } - return DefaultParser(includeDate, includePidTid, includePackageName, includeTag) + return StudioLogcatBelowChipmunkParser(includeDate, includePidTid, includePackageName, includeTag) } catch (e: Exception) { return null } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt index fcfa4c8..d60697e 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt @@ -1,8 +1,8 @@ package com.jerryjeon.logjerry.source import com.jerryjeon.logjerry.log.LogManager -import com.jerryjeon.logjerry.parse.DefaultParser import com.jerryjeon.logjerry.parse.ParseStatus +import com.jerryjeon.logjerry.parse.StudioLogcatBelowChipmunkParser import com.jerryjeon.logjerry.preferences.Preferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -14,7 +14,7 @@ import okio.openZip class SourceManager(private val preferences: Preferences) { private val sourceScope = CoroutineScope(Dispatchers.Default) - private val defaultParser = DefaultParser( + private val studioLogcatBelowChipmunkParser = StudioLogcatBelowChipmunkParser( includeDateTime = true, includePidTid = true, includePackageName = true, @@ -30,7 +30,7 @@ class SourceManager(private val preferences: Preferences) { val content = zipFileSystem.read(files.first()) { readUtf8() }.split("\n") // Prefer second line because the first line breaks often because of the buffer val sample = content.getOrNull(1) ?: content.first() - val parser = DefaultParser.create(sample) ?: defaultParser + val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser val parseResult = parser.parse(content) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } @@ -39,7 +39,7 @@ class SourceManager(private val preferences: Preferences) { val lines = it.file.readLines() // Prefer second line because the first line breaks often because of the buffer val sample = lines.getOrNull(1) ?: lines.first() - val parser = DefaultParser.create(sample) ?: defaultParser + val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser val parseResult = parser.parse(it.file.readLines()) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } @@ -48,7 +48,7 @@ class SourceManager(private val preferences: Preferences) { val lines = it.text.split("\n") // Prefer second line because the first line breaks often because of the buffer val sample = lines.getOrNull(1) ?: lines.first() - val parser = DefaultParser.create(sample) ?: defaultParser + val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser val parseResult = parser.parse(lines) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt index f03fe29..255f87c 100644 --- a/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/ParserTest.kt @@ -9,7 +9,7 @@ internal class ParserTest { @Test fun `Given raw login, parse well`() { - val parseResult = DefaultParser( + val parseResult = StudioLogcatBelowChipmunkParser( includeDateTime = true, includePidTid = true, includePackageName = true, diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt similarity index 96% rename from src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt rename to src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt index 1a52b6a..c4567e3 100644 --- a/src/test/kotlin/com/jerryjeon/logjerry/parse/DefaultParserTest.kt +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt @@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream -internal class DefaultParserTest { +internal class StudioLogcatBelowChipmunkParserTest { @Nested inner class FactoryTest { @@ -19,8 +19,8 @@ internal class DefaultParserTest { @ParameterizedTest @MethodSource("logAndIncludeSettings") fun `Factory can be created for all include settings`(input: String, expected: IncludeSettings) { - val parser = DefaultParser.create(input) - parser.shouldBeInstanceOf() + val parser = StudioLogcatBelowChipmunkParser.create(input) + parser.shouldBeInstanceOf() .asClue { it.includeDateTime shouldBe expected.includeDateTime it.includePidTid shouldBe expected.includePidTid From 2206044b4af3304cbf65ba9d510e025df2488656 Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 17:42:01 +0900 Subject: [PATCH 3/6] Make some fields nullable --- .../kotlin/com/jerryjeon/logjerry/filter/TextFilter.kt | 2 +- src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt | 10 +++++----- src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/filter/TextFilter.kt b/src/main/kotlin/com/jerryjeon/logjerry/filter/TextFilter.kt index 55a81b0..b64e444 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/filter/TextFilter.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/filter/TextFilter.kt @@ -10,7 +10,7 @@ data class TextFilter( override fun filter(log: Log): Boolean { return when (columnType) { ColumnType.PackageName -> text in (log.packageName ?: "") - ColumnType.Tag -> text in log.tag + ColumnType.Tag -> if (log.tag == null) true else text in log.tag ColumnType.Log -> text in log.log else -> throw NotImplementedError("Not supported filter : $columnType") } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt b/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt index 3a1a8e3..6f1bc8e 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt @@ -1,13 +1,13 @@ package com.jerryjeon.logjerry.log data class Log( val number: Int, - val date: String, - val time: String, - val pid: Long, - val tid: Long, + val date: String?, + val time: String?, + val pid: Long?, + val tid: Long?, val packageName: String?, val priorityText: String, - val tag: String, + val tag: String?, val log: String ) { val priority = Priority.find(priorityText) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt index f86ea6b..c08ae7c 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt @@ -103,7 +103,7 @@ private fun RowScope.NumberCell(preferences: Preferences, number: ColumnInfo, lo @Composable private fun RowScope.DateCell(preferences: Preferences, date: ColumnInfo, log: Log) { Text( - text = log.date, + text = log.date ?: "", style = MaterialTheme.typography.body2.copy( fontSize = preferences.fontSize, color = preferences.colorByPriority().getValue(log.priority) @@ -115,7 +115,7 @@ private fun RowScope.DateCell(preferences: Preferences, date: ColumnInfo, log: L @Composable private fun RowScope.TimeCell(preferences: Preferences, time: ColumnInfo, log: Log) { Text( - text = log.time, + text = log.time ?: "", style = MaterialTheme.typography.body2.copy( fontSize = preferences.fontSize, color = preferences.colorByPriority().getValue(log.priority) @@ -175,7 +175,7 @@ private fun RowScope.PriorityCell(preferences: Preferences, priority: ColumnInfo @Composable private fun RowScope.TagCell(preferences: Preferences, tag: ColumnInfo, log: Log) { Text( - text = log.tag, + text = log.tag ?: "", style = MaterialTheme.typography.body2.copy( fontSize = preferences.fontSize, color = preferences.colorByPriority().getValue(log.priority) From c68fa7c188530539104bf103372c91f8714b1d3f Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 17:43:03 +0900 Subject: [PATCH 4/6] Remove canParse function --- src/main/kotlin/com/jerryjeon/logjerry/parse/LogParser.kt | 1 - .../logjerry/parse/StudioLogcatBelowChipmunkParser.kt | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/LogParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/LogParser.kt index 1bebe7f..6f34e2e 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/LogParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/LogParser.kt @@ -1,6 +1,5 @@ package com.jerryjeon.logjerry.parse interface LogParser { - fun canParse(raw: String): Boolean fun parse(rawLines: List): ParseResult } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt index 4f22c08..bfca5bf 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt @@ -99,11 +99,6 @@ class StudioLogcatBelowChipmunkParser( } private val number = AtomicInteger(1) - override fun canParse(raw: String): Boolean { - // TODO check - return true - } - override fun parse(rawLines: List): ParseResult { val logs = mutableListOf() val invalidSentences = mutableListOf>() From fca7130a414eb4f5ea0021fcebf10a46232b869d Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 21:02:28 +0900 Subject: [PATCH 5/6] Support Dolphin version logcat --- .../parse/StudioLogcatAboveDolphinParser.kt | 207 ++++++++++++++++++ .../parse/StudioLogcatBelowChipmunkParser.kt | 34 +-- .../logjerry/source/SourceManager.kt | 24 +- .../StudioLogcatAboveDolphinParserTest.kt | 91 ++++++++ 4 files changed, 331 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt create mode 100644 src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt new file mode 100644 index 0000000..0e98acb --- /dev/null +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt @@ -0,0 +1,207 @@ +package com.jerryjeon.logjerry.parse + +import com.jerryjeon.logjerry.log.Log +import java.time.LocalDate +import java.time.LocalTime +import java.util.concurrent.atomic.AtomicInteger + +data class StudioLogcatAboveDolphinParser( + // Log format configuration after AS Dolphin version + val includeDate: Boolean, + val includeTime: Boolean, + val includePid: Boolean, + val includeTid: Boolean, + val includeTag: Boolean, + val includePackageName: Boolean +) : LogParser { + + companion object : ParserFactory { + + private val packageNameRegex2 = Regex("^pid-\\d*$") + private val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") + private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') + + private fun String.isPriority(): Boolean { + return length == 1 && first() in priorityChars + } + + override fun create(sample: String): LogParser? { + try { + val split = sample.split(" ").filter { it.isNotBlank() } + val iterator = split.listIterator() + + var currentToken = iterator.next() + + val includeDate = try { + LocalDate.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + val includeTime = try { + LocalTime.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + val includePid: Boolean? + val includeTid: Boolean? + when { + currentToken.matches(Regex("\\d*")) -> { // only pid + includePid = true + includeTid = false + currentToken = iterator.next() + } + currentToken.matches(Regex("\\d*-\\d*")) -> { // pid-tid + includePid = true + includeTid = true + currentToken = iterator.next() + } + else -> { + includePid = false + includeTid = false + } + } + + if (currentToken.isPriority()) { + return StudioLogcatAboveDolphinParser(includeDate, includeTime, includePid, includeTid, includeTag = false, includePackageName = false) + } + + // Check package first, because for the tag there's no way to validate it + + // TODO Find more cleaner way + if (currentToken.isPackageName()) { + currentToken = iterator.next() + return if (currentToken.isPriority() && iterator.hasNext()) { + StudioLogcatAboveDolphinParser(includeDate, includeTime, includePid, includeTid, includeTag = false, includePackageName = true) + } else { + null + } + } else { + currentToken = iterator.next() + if (currentToken.isPriority()) { + return if (currentToken.isPriority() && iterator.hasNext()) { + StudioLogcatAboveDolphinParser(includeDate, includeTime, includePid, includeTid, includeTag = true, includePackageName = false) + } else { + null + } + } else if (currentToken.isPackageName()) { + currentToken = iterator.next() + return if (currentToken.isPriority() && iterator.hasNext()) { + StudioLogcatAboveDolphinParser(includeDate, includeTime, includePid, includeTid, includeTag = true, includePackageName = true) + } else { + null + } + } + } + + return null + } catch (e: Exception) { + return null + } + } + + private fun String.isPackageName(): Boolean { + return this == "system_process" + || this.matches(packageNameRegex) + || this.matches(packageNameRegex2) + } + } + + private val number = AtomicInteger(1) + + override fun parse(rawLines: List): ParseResult { + val logs = mutableListOf() + val invalidSentences = mutableListOf>() + var lastLog: Log? = null + rawLines.forEachIndexed { index, s -> + lastLog = try { + val log = parseSingleLineLog(s) + + // Custom continuation + if (log.log.startsWith("Cont(")) { + lastLog?.let { + it.copy(log = "${it.log}${log.log.substringAfter(") ")}") + } ?: log + } else { + lastLog?.let { logs.add(it) } + log + } + } catch (e: Exception) { + val continuedLog = if (lastLog == null) { + invalidSentences.add(index to s) + return@forEachIndexed + } else { + lastLog!! + } + continuedLog.copy(log = "${continuedLog.log}\n$s") + } + } + lastLog?.let { logs.add(it) } + return ParseResult(logs, invalidSentences) + } + + // The algorithm is inefficient. From my machine it's ok for 5000 lines. Improve later if there's an issue + private fun parseSingleLineLog(raw: String): Log { + val split = raw.split(" ").filter { it.isNotBlank() } + + var currentIndex = 0 + + val date = if (includeDate) { + split[currentIndex++] + } else { + null + } + + val time = if(includeTime) { + split[currentIndex++] + } else { + null + } + + val pid: Long? + val tid: Long? + when { + includePid && includeTid -> { + val ids = split[currentIndex++].split("-") + pid = ids[0].toLong() + tid = ids[1].toLong() + } + includePid -> { + pid = split[currentIndex++].toLong() + tid = null + } + else -> { + pid = null + tid = null + } + } + + val tag = if (includeTag) { + split[currentIndex++] + } else { + null + } + + val packageName = if(includePackageName) { + split[currentIndex++] + } else { + null + } + + val priorityText = split[currentIndex++] + + val originalLog = split.drop(currentIndex).joinToString(separator = " ") + + return Log(number.getAndIncrement(), date, time, pid, tid, packageName, priorityText, tag, originalLog) + } + + override fun toString(): String { + return "StudioLogcatAboveDolphinParser(includeDate=$includeDate, includeTime=$includeTime, includePid=$includePid, includeTid=$includeTid, includeTag=$includeTag, includePackageName=$includePackageName, number=$number)" + } +} + diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt index bfca5bf..0ed72b3 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt @@ -16,6 +16,10 @@ class StudioLogcatBelowChipmunkParser( companion object : ParserFactory { private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') + + private fun String.isPriority(): Boolean { + return length == 1 && first() in priorityChars + } override fun create(sample: String): LogParser? { try { val split = sample.split(" ", limit = 5) @@ -73,13 +77,13 @@ class StudioLogcatBelowChipmunkParser( // both exist val tokens = currentToken.split("/") // Check what's faster: list and regex - if (tokens[0].length == 1 && tokens[0][0] in priorityChars) { + if (tokens[0].isPriority()) { includeTag = true } else { // invalid return null } - } else if (currentToken.length == 1 && currentToken[0] in priorityChars) { + } else if (currentToken.isPriority()) { includeTag = false } @@ -138,20 +142,18 @@ class StudioLogcatBelowChipmunkParser( var currentIndex = 0 - // TODO Change these to nullable - val date: String - val time: String + val date: String? + val time: String? if (includeDateTime) { date = split[currentIndex++] time = split[currentIndex++] } else { - date = "" - time = "" + date = null + time = null } - // TODO Change these to nullable - val pid: Long - val tid: Long + val pid: Long? + val tid: Long? val packageName: String? when { includePidTid && includePackageName -> { @@ -167,26 +169,26 @@ class StudioLogcatBelowChipmunkParser( packageName = null } includePackageName -> { - pid = 0L - tid = 0L + pid = null + tid = null packageName = split[currentIndex++] } else -> { - pid = 0L - tid = 0L + pid = null + tid = null packageName = null } } val priorityText: String - val tag: String + val tag: String? if (includeTag) { val fourthSegment = split[currentIndex++].split("/") priorityText = fourthSegment[0] tag = fourthSegment[1].removeSuffix(":") } else { priorityText = split[currentIndex++].removeSuffix(":") - tag = "" + tag = null } val originalLog = split[currentIndex] diff --git a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt index d60697e..0f47213 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/source/SourceManager.kt @@ -1,7 +1,9 @@ package com.jerryjeon.logjerry.source import com.jerryjeon.logjerry.log.LogManager +import com.jerryjeon.logjerry.parse.LogParser import com.jerryjeon.logjerry.parse.ParseStatus +import com.jerryjeon.logjerry.parse.StudioLogcatAboveDolphinParser import com.jerryjeon.logjerry.parse.StudioLogcatBelowChipmunkParser import com.jerryjeon.logjerry.preferences.Preferences import kotlinx.coroutines.CoroutineScope @@ -20,6 +22,7 @@ class SourceManager(private val preferences: Preferences) { includePackageName = true, includeTag = true ) + val sourceFlow: MutableStateFlow = MutableStateFlow(Source.None) val parseStatusFlow: StateFlow = sourceFlow.map { when (it) { @@ -28,27 +31,21 @@ class SourceManager(private val preferences: Preferences) { val zipFileSystem = fileSystem.openZip(it.file.toOkioPath()) val files = zipFileSystem.listOrNull("/".toPath()) ?: return@map ParseStatus.NotStarted val content = zipFileSystem.read(files.first()) { readUtf8() }.split("\n") - // Prefer second line because the first line breaks often because of the buffer - val sample = content.getOrNull(1) ?: content.first() - val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser + val parser = chooseParser(content) val parseResult = parser.parse(content) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } is Source.File -> { val lines = it.file.readLines() - // Prefer second line because the first line breaks often because of the buffer - val sample = lines.getOrNull(1) ?: lines.first() - val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser + val parser = chooseParser(lines) val parseResult = parser.parse(it.file.readLines()) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } is Source.Text -> { val lines = it.text.split("\n") - // Prefer second line because the first line breaks often because of the buffer - val sample = lines.getOrNull(1) ?: lines.first() - val parser = StudioLogcatBelowChipmunkParser.create(sample) ?: studioLogcatBelowChipmunkParser + val parser = chooseParser(lines) val parseResult = parser.parse(lines) ParseStatus.Completed(parseResult, LogManager(parseResult.logs, preferences)) } @@ -58,6 +55,15 @@ class SourceManager(private val preferences: Preferences) { } }.stateIn(sourceScope, SharingStarted.Lazily, ParseStatus.NotStarted) + private fun chooseParser(lines: List): LogParser { + // Prefer second line because the first line breaks often because of the buffer + val sample = lines.getOrNull(1) ?: lines.first() + + return StudioLogcatBelowChipmunkParser.create(sample) + ?: StudioLogcatAboveDolphinParser.create(sample) + ?: studioLogcatBelowChipmunkParser + } + fun changeSource(source: Source) { this.sourceFlow.value = source } diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt new file mode 100644 index 0000000..54ea528 --- /dev/null +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt @@ -0,0 +1,91 @@ +package com.jerryjeon.logjerry.parse + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +internal class StudioLogcatAboveDolphinParserTest { + + @Nested + inner class FactoryTest { + + @ParameterizedTest + @MethodSource("logAndIncludeSettings") + fun `Factory can be created for all include settings`(input: String, expected: StudioLogcatAboveDolphinParser) { + val parser = StudioLogcatAboveDolphinParser.create(input) + parser.shouldBeInstanceOf() shouldBe expected + + parser.parse(listOf(input)).invalidSentences.shouldBeEmpty() + } + + // TODO add tests for various formats + private fun logAndIncludeSettings(): Stream { + return Stream.of( + Arguments.of( + "2022-10-30 20:23:38.484 19086-19156 Gralloc4 com.example.myapplication I mapper 4.x is not supported", + StudioLogcatAboveDolphinParser( + includeDate = true, + includeTime = true, + includePid = true, + includeTid = true, + includeTag = true, + includePackageName = true + ) + ), + Arguments.of( + "2022-09-26 23:45:28.054 321-16228 resolv pid-321 D res_nmkquery: (QUERY, IN, A)", + StudioLogcatAboveDolphinParser( + includeDate = true, + includeTime = true, + includePid = true, + includeTid = true, + includeTag = true, + includePackageName = true + ) + ), + Arguments.of( + "2022-09-26 23:46:30.454 543-599 VerityUtils system_process E Failed to measure fs-verity, errno 1: /data/app/~~byIkOUX0heIwtiImACXFMg==/com.sendbird.android.test-e2Yic0CCjvMq4lmW7z0IAA==/base.apk", + StudioLogcatAboveDolphinParser( + includeDate = true, + includeTime = true, + includePid = true, + includeTid = true, + includeTag = true, + includePackageName = true + ) + ), + + // Compact view + Arguments.of( + "20:58:04.567 W Failed to initialize 101010-2 format, error = EGL_SUCCESS", + StudioLogcatAboveDolphinParser( + includeDate = false, + includeTime = true, + includePid = false, + includeTid = false, + includeTag = false, + includePackageName = false + ) + ), + + Arguments.of( + "20:58:04.567 Tag W Failed to initialize 101010-2 format, error = EGL_SUCCESS", + StudioLogcatAboveDolphinParser( + includeDate = false, + includeTime = true, + includePid = false, + includeTid = false, + includeTag = true, + includePackageName = false + ) + ), + ) + } + } +} \ No newline at end of file From 15ac7d947811d4a65358b5e2617bad2d3bd5202b Mon Sep 17 00:00:00 2001 From: Jerry Jeon Date: Sun, 30 Oct 2022 21:04:33 +0900 Subject: [PATCH 6/6] Cleanup codes --- .../parse/StudioLogcatAboveDolphinParser.kt | 187 +++++++++--------- .../parse/StudioLogcatBelowChipmunkParser.kt | 179 +++++++++-------- .../StudioLogcatAboveDolphinParserTest.kt | 4 +- .../StudioLogcatBelowChipmunkParserTest.kt | 56 ++---- 4 files changed, 206 insertions(+), 220 deletions(-) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt index 0e98acb..567082a 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParser.kt @@ -15,11 +15,103 @@ data class StudioLogcatAboveDolphinParser( val includePackageName: Boolean ) : LogParser { + private val number = AtomicInteger(1) + + override fun parse(rawLines: List): ParseResult { + val logs = mutableListOf() + val invalidSentences = mutableListOf>() + var lastLog: Log? = null + rawLines.forEachIndexed { index, s -> + lastLog = try { + val log = parseSingleLineLog(s) + + // Custom continuation + if (log.log.startsWith("Cont(")) { + lastLog?.let { + it.copy(log = "${it.log}${log.log.substringAfter(") ")}") + } ?: log + } else { + lastLog?.let { logs.add(it) } + log + } + } catch (e: Exception) { + val continuedLog = if (lastLog == null) { + invalidSentences.add(index to s) + return@forEachIndexed + } else { + lastLog!! + } + continuedLog.copy(log = "${continuedLog.log}\n$s") + } + } + lastLog?.let { logs.add(it) } + return ParseResult(logs, invalidSentences) + } + + // The algorithm is inefficient. From my machine it's ok for 5000 lines. Improve later if there's an issue + private fun parseSingleLineLog(raw: String): Log { + val split = raw.split(" ").filter { it.isNotBlank() } + + var currentIndex = 0 + + val date = if (includeDate) { + split[currentIndex++] + } else { + null + } + + val time = if(includeTime) { + split[currentIndex++] + } else { + null + } + + val pid: Long? + val tid: Long? + when { + includePid && includeTid -> { + val ids = split[currentIndex++].split("-") + pid = ids[0].toLong() + tid = ids[1].toLong() + } + includePid -> { + pid = split[currentIndex++].toLong() + tid = null + } + else -> { + pid = null + tid = null + } + } + + val tag = if (includeTag) { + split[currentIndex++] + } else { + null + } + + val packageName = if(includePackageName) { + split[currentIndex++] + } else { + null + } + + val priorityText = split[currentIndex++] + + val originalLog = split.drop(currentIndex).joinToString(separator = " ") + + return Log(number.getAndIncrement(), date, time, pid, tid, packageName, priorityText, tag, originalLog) + } + + override fun toString(): String { + return "StudioLogcatAboveDolphinParser(includeDate=$includeDate, includeTime=$includeTime, includePid=$includePid, includeTid=$includeTid, includeTag=$includeTag, includePackageName=$includePackageName, number=$number)" + } + companion object : ParserFactory { - private val packageNameRegex2 = Regex("^pid-\\d*$") - private val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') + private val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") + private val packageNameRegex2 = Regex("^pid-\\d*$") private fun String.isPriority(): Boolean { return length == 1 && first() in priorityChars @@ -112,96 +204,5 @@ data class StudioLogcatAboveDolphinParser( } } - private val number = AtomicInteger(1) - - override fun parse(rawLines: List): ParseResult { - val logs = mutableListOf() - val invalidSentences = mutableListOf>() - var lastLog: Log? = null - rawLines.forEachIndexed { index, s -> - lastLog = try { - val log = parseSingleLineLog(s) - - // Custom continuation - if (log.log.startsWith("Cont(")) { - lastLog?.let { - it.copy(log = "${it.log}${log.log.substringAfter(") ")}") - } ?: log - } else { - lastLog?.let { logs.add(it) } - log - } - } catch (e: Exception) { - val continuedLog = if (lastLog == null) { - invalidSentences.add(index to s) - return@forEachIndexed - } else { - lastLog!! - } - continuedLog.copy(log = "${continuedLog.log}\n$s") - } - } - lastLog?.let { logs.add(it) } - return ParseResult(logs, invalidSentences) - } - - // The algorithm is inefficient. From my machine it's ok for 5000 lines. Improve later if there's an issue - private fun parseSingleLineLog(raw: String): Log { - val split = raw.split(" ").filter { it.isNotBlank() } - - var currentIndex = 0 - - val date = if (includeDate) { - split[currentIndex++] - } else { - null - } - - val time = if(includeTime) { - split[currentIndex++] - } else { - null - } - - val pid: Long? - val tid: Long? - when { - includePid && includeTid -> { - val ids = split[currentIndex++].split("-") - pid = ids[0].toLong() - tid = ids[1].toLong() - } - includePid -> { - pid = split[currentIndex++].toLong() - tid = null - } - else -> { - pid = null - tid = null - } - } - - val tag = if (includeTag) { - split[currentIndex++] - } else { - null - } - - val packageName = if(includePackageName) { - split[currentIndex++] - } else { - null - } - - val priorityText = split[currentIndex++] - - val originalLog = split.drop(currentIndex).joinToString(separator = " ") - - return Log(number.getAndIncrement(), date, time, pid, tid, packageName, priorityText, tag, originalLog) - } - - override fun toString(): String { - return "StudioLogcatAboveDolphinParser(includeDate=$includeDate, includeTime=$includeTime, includePid=$includePid, includeTid=$includeTid, includeTag=$includeTag, includePackageName=$includePackageName, number=$number)" - } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt index 0ed72b3..b67bcba 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParser.kt @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.LocalTime import java.util.concurrent.atomic.AtomicInteger -class StudioLogcatBelowChipmunkParser( +data class StudioLogcatBelowChipmunkParser( // Log format configuration before AS Chipmunk version val includeDateTime: Boolean, val includePidTid: Boolean, @@ -13,95 +13,6 @@ class StudioLogcatBelowChipmunkParser( val includeTag: Boolean ) : LogParser { - companion object : ParserFactory { - - private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') - - private fun String.isPriority(): Boolean { - return length == 1 && first() in priorityChars - } - override fun create(sample: String): LogParser? { - try { - val split = sample.split(" ", limit = 5) - val iterator = split.listIterator() - - var currentToken = iterator.next() - - val includeDate = try { - LocalDate.parse(currentToken) - currentToken = iterator.next() - true - } catch (e: Exception) { - false - } - - val includeTime = try { - LocalTime.parse(currentToken) - currentToken = iterator.next() - true - } catch (e: Exception) { - false - } - - // Only supports both exist or not exist at all - if (includeDate xor includeTime) return null - - val pidTidRegex = Regex("\\d*[-/]\\d*") - val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") - - // pit-tid/packageName - var includePidTid: Boolean = false - var includePackageName: Boolean = false - if (currentToken.contains("/")) { - // both exist - val tokens = currentToken.split("/") - if (tokens[0].matches(pidTidRegex) && (tokens[1] == "?" || tokens[1].matches(packageNameRegex))) { - includePidTid = true - includePackageName = true - currentToken = iterator.next() - } - } else { - if (currentToken.matches(pidTidRegex)) { - includePidTid = true - includePackageName = false - currentToken = iterator.next() - } else if (currentToken == "?" || currentToken.matches(packageNameRegex)) { - includePidTid = false - includePackageName = true - currentToken = iterator.next() - } - } - - var includeTag = false - if (currentToken.contains("/")) { - // both exist - val tokens = currentToken.split("/") - // Check what's faster: list and regex - if (tokens[0].isPriority()) { - includeTag = true - } else { - // invalid - return null - } - } else if (currentToken.isPriority()) { - includeTag = false - } - - if (currentToken.last() != ':') { - return null - } - - if (!iterator.hasNext()) { - return null - } - - return StudioLogcatBelowChipmunkParser(includeDate, includePidTid, includePackageName, includeTag) - } catch (e: Exception) { - return null - } - } - } - private val number = AtomicInteger(1) override fun parse(rawLines: List): ParseResult { val logs = mutableListOf() @@ -199,4 +110,92 @@ class StudioLogcatBelowChipmunkParser( override fun toString(): String { return "DefaultParser(includeDateTime=$includeDateTime, includePidTid=$includePidTid, includePackageName=$includePackageName, includeTag=$includeTag)" } + companion object : ParserFactory { + + private val priorityChars = setOf('V', 'D', 'I', 'W', 'E', 'A') + + private fun String.isPriority(): Boolean { + return length == 1 && first() in priorityChars + } + override fun create(sample: String): LogParser? { + try { + val split = sample.split(" ", limit = 5) + val iterator = split.listIterator() + + var currentToken = iterator.next() + + val includeDate = try { + LocalDate.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + val includeTime = try { + LocalTime.parse(currentToken) + currentToken = iterator.next() + true + } catch (e: Exception) { + false + } + + // Only supports both exist or not exist at all + if (includeDate xor includeTime) return null + + val pidTidRegex = Regex("\\d*[-/]\\d*") + val packageNameRegex = Regex("^([A-Za-z][A-Za-z\\d_]*\\.)+[A-Za-z][A-Za-z\\d_]*$") + + // pit-tid/packageName + var includePidTid: Boolean = false + var includePackageName: Boolean = false + if (currentToken.contains("/")) { + // both exist + val tokens = currentToken.split("/") + if (tokens[0].matches(pidTidRegex) && (tokens[1] == "?" || tokens[1].matches(packageNameRegex))) { + includePidTid = true + includePackageName = true + currentToken = iterator.next() + } + } else { + if (currentToken.matches(pidTidRegex)) { + includePidTid = true + includePackageName = false + currentToken = iterator.next() + } else if (currentToken == "?" || currentToken.matches(packageNameRegex)) { + includePidTid = false + includePackageName = true + currentToken = iterator.next() + } + } + + var includeTag = false + if (currentToken.contains("/")) { + // both exist + val tokens = currentToken.split("/") + // Check what's faster: list and regex + if (tokens[0].isPriority()) { + includeTag = true + } else { + // invalid + return null + } + } else if (currentToken.isPriority()) { + includeTag = false + } + + if (currentToken.last() != ':') { + return null + } + + if (!iterator.hasNext()) { + return null + } + + return StudioLogcatBelowChipmunkParser(includeDate, includePidTid, includePackageName, includeTag) + } catch (e: Exception) { + return null + } + } + } } diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt index 54ea528..c8cecfc 100644 --- a/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatAboveDolphinParserTest.kt @@ -1,8 +1,8 @@ package com.jerryjeon.logjerry.parse import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest @@ -19,7 +19,7 @@ internal class StudioLogcatAboveDolphinParserTest { @MethodSource("logAndIncludeSettings") fun `Factory can be created for all include settings`(input: String, expected: StudioLogcatAboveDolphinParser) { val parser = StudioLogcatAboveDolphinParser.create(input) - parser.shouldBeInstanceOf() shouldBe expected + parser.shouldNotBeNull() shouldBe expected parser.parse(listOf(input)).invalidSentences.shouldBeEmpty() } diff --git a/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt index c4567e3..98a81fb 100644 --- a/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt +++ b/src/test/kotlin/com/jerryjeon/logjerry/parse/StudioLogcatBelowChipmunkParserTest.kt @@ -1,9 +1,8 @@ package com.jerryjeon.logjerry.parse -import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest @@ -17,94 +16,81 @@ internal class StudioLogcatBelowChipmunkParserTest { inner class FactoryTest { @ParameterizedTest - @MethodSource("logAndIncludeSettings") - fun `Factory can be created for all include settings`(input: String, expected: IncludeSettings) { + @MethodSource("logAndStudioLogcatBelowChipmunkParser") + fun `Factory can be created for all include settings`(input: String, expected: StudioLogcatBelowChipmunkParser) { val parser = StudioLogcatBelowChipmunkParser.create(input) - parser.shouldBeInstanceOf() - .asClue { - it.includeDateTime shouldBe expected.includeDateTime - it.includePidTid shouldBe expected.includePidTid - it.includePackageName shouldBe expected.includePackageName - it.includeTag shouldBe expected.includeTag - } + parser.shouldNotBeNull() shouldBe expected parser.parse(listOf(input)).invalidSentences.shouldBeEmpty() } - private fun logAndIncludeSettings(): Stream { + private fun logAndStudioLogcatBelowChipmunkParser(): Stream { return Stream.of( Arguments.of( "I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = false) ), Arguments.of( "I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = false, includePackageName = false, includeTag = true) ), Arguments.of( "? I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = false) ), Arguments.of( "? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = false, includePackageName = true, includeTag = true) ), Arguments.of( "178-178 I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = false) ), Arguments.of( "178-178 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = true, includePackageName = false, includeTag = true) ), Arguments.of( "178-178/? I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = false) ), Arguments.of( "178-178/? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = false, includePidTid = true, includePackageName = true, includeTag = true) ), Arguments.of( "2022-10-24 08:50:35.786 I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = false) ), Arguments.of( "2022-10-24 08:50:35.786 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = false, includePackageName = false, includeTag = true) ), Arguments.of( "2022-10-24 09:31:55.786 ? I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = false) ), Arguments.of( "2022-10-24 09:31:55.786 ? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = false, includePackageName = true, includeTag = true) ), Arguments.of( "2022-10-24 09:31:55.786 178-178 I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = false) ), Arguments.of( "2022-10-24 09:31:55.786 178-178 I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = true, includePackageName = false, includeTag = true) ), Arguments.of( "2022-10-24 09:31:55.786 178-178/? I: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = false) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = false) ), Arguments.of( "2022-10-24 09:31:55.786 178-178/? I/servicemanager: Tried to unregister apexservice, but there is about to be a client.", - IncludeSettings(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = true) + StudioLogcatBelowChipmunkParser(includeDateTime = true, includePidTid = true, includePackageName = true, includeTag = true) ), ) } } - - data class IncludeSettings( - val includeDateTime: Boolean, - val includePidTid: Boolean, - val includePackageName: Boolean, - val includeTag: Boolean, - ) }