From 8f92cd0a1bb5dcb690646cfb6b8bb82c23eabcee Mon Sep 17 00:00:00 2001 From: Andrey Stolyarov Date: Tue, 25 Nov 2025 21:31:17 +0300 Subject: [PATCH] Refactoring LogViewerStream --- .../jsonlogviewer/FilterComponents.scala | 41 ++++ .../d10xa/jsonlogviewer/FilterPipeline.scala | 59 ++++++ .../jsonlogviewer/LogLineParserFactory.scala | 30 +++ .../d10xa/jsonlogviewer/LogViewerStream.scala | 125 ++----------- .../jsonlogviewer/FilterComponentsTest.scala | 84 +++++++++ .../jsonlogviewer/FilterPipelineTest.scala | 177 ++++++++++++++++++ .../LogLineParserFactoryTest.scala | 71 +++++++ .../jsonlogviewer/TestResolvedConfig.scala | 33 ++++ 8 files changed, 508 insertions(+), 112 deletions(-) create mode 100644 json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterComponents.scala create mode 100644 json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterPipeline.scala create mode 100644 json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogLineParserFactory.scala create mode 100644 json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterComponentsTest.scala create mode 100644 json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterPipelineTest.scala create mode 100644 json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/LogLineParserFactoryTest.scala create mode 100644 json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/TestResolvedConfig.scala diff --git a/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterComponents.scala b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterComponents.scala new file mode 100644 index 0000000..6972699 --- /dev/null +++ b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterComponents.scala @@ -0,0 +1,41 @@ +package ru.d10xa.jsonlogviewer + +import ru.d10xa.jsonlogviewer.config.ResolvedConfig +import ru.d10xa.jsonlogviewer.decline.Config +import ru.d10xa.jsonlogviewer.formatout.{ColorLineFormatter, RawFormatter} + +final case class FilterComponents( + timestampFilter: TimestampFilter, + parseResultKeys: ParseResultKeys, + logLineFilter: LogLineFilter, + fuzzyFilter: FuzzyFilter, + outputLineFormatter: OutputLineFormatter +) + +object FilterComponents { + + def fromConfig(resolvedConfig: ResolvedConfig): FilterComponents = { + val timestampFilter = TimestampFilter() + val parseResultKeys = ParseResultKeys(resolvedConfig) + val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys) + val fuzzyFilter = new FuzzyFilter(resolvedConfig) + + val outputLineFormatter = resolvedConfig.formatOut match { + case Some(Config.FormatOut.Raw) => RawFormatter() + case Some(Config.FormatOut.Pretty) | None => + ColorLineFormatter( + resolvedConfig, + resolvedConfig.feedName, + resolvedConfig.excludeFields + ) + } + + FilterComponents( + timestampFilter, + parseResultKeys, + logLineFilter, + fuzzyFilter, + outputLineFormatter + ) + } +} diff --git a/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterPipeline.scala b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterPipeline.scala new file mode 100644 index 0000000..c258b7e --- /dev/null +++ b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/FilterPipeline.scala @@ -0,0 +1,59 @@ +package ru.d10xa.jsonlogviewer + +import cats.effect.IO +import fs2.Stream +import ru.d10xa.jsonlogviewer.config.ResolvedConfig +import scala.util.{Failure, Success, Try} + +object FilterPipeline { + + // Filter order: rawFilter -> parse -> grep -> query -> fuzzy -> timestamp -> format + def applyFilters( + stream: Stream[IO, String], + parser: LogLineParser, + components: FilterComponents, + resolvedConfig: ResolvedConfig + ): Stream[IO, String] = { + stream + .filter( + rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude) + ) + .map(parser.parse) + .filter(components.logLineFilter.grep) + .filter(components.logLineFilter.logLineQueryPredicate) + .filter(components.fuzzyFilter.test) + .through( + components.timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter) + ) + .through( + components.timestampFilter.filterTimestampBefore( + resolvedConfig.timestampBefore + ) + ) + .map(formatWithSafety(_, components.outputLineFormatter)) + } + + private def rawFilter( + str: String, + include: Option[List[String]], + exclude: Option[List[String]] + ): Boolean = { + import scala.util.matching.Regex + val includeRegexes: List[Regex] = include.getOrElse(Nil).map(_.r) + val excludeRegexes: List[Regex] = exclude.getOrElse(Nil).map(_.r) + val includeMatches = includeRegexes.isEmpty || includeRegexes.exists( + _.findFirstIn(str).isDefined + ) + val excludeMatches = excludeRegexes.forall(_.findFirstIn(str).isEmpty) + includeMatches && excludeMatches + } + + private def formatWithSafety( + parseResult: ParseResult, + formatter: OutputLineFormatter + ): String = + Try(formatter.formatLine(parseResult)) match { + case Success(formatted) => formatted.toString + case Failure(_) => parseResult.raw + } +} diff --git a/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogLineParserFactory.scala b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogLineParserFactory.scala new file mode 100644 index 0000000..91291f2 --- /dev/null +++ b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogLineParserFactory.scala @@ -0,0 +1,30 @@ +package ru.d10xa.jsonlogviewer + +import ru.d10xa.jsonlogviewer.config.ResolvedConfig +import ru.d10xa.jsonlogviewer.csv.CsvLogLineParser +import ru.d10xa.jsonlogviewer.decline.Config.FormatIn +import ru.d10xa.jsonlogviewer.logfmt.LogfmtLogLineParser + +object LogLineParserFactory { + + def createNonCsvParser(resolvedConfig: ResolvedConfig): LogLineParser = { + val jsonPrefixPostfix = JsonPrefixPostfix(JsonDetector()) + resolvedConfig.formatIn match { + case Some(FormatIn.Logfmt) => + LogfmtLogLineParser(resolvedConfig) + case Some(FormatIn.Csv) => + throw new IllegalStateException( + "CSV format requires header line, use createCsvParser instead" + ) + case Some(FormatIn.Json) | None => + JsonLogLineParser(resolvedConfig, jsonPrefixPostfix) + } + } + + def createCsvParser( + resolvedConfig: ResolvedConfig, + headerLine: String + ): LogLineParser = { + CsvLogLineParser(resolvedConfig, headerLine) + } +} diff --git a/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogViewerStream.scala b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogViewerStream.scala index 267bd17..bcb1560 100644 --- a/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogViewerStream.scala +++ b/json-log-viewer/shared/src/main/scala/ru/d10xa/jsonlogviewer/LogViewerStream.scala @@ -6,19 +6,11 @@ import fs2.* import fs2.Pull import ru.d10xa.jsonlogviewer.config.ConfigResolver import ru.d10xa.jsonlogviewer.config.ResolvedConfig -import ru.d10xa.jsonlogviewer.csv.CsvLogLineParser import ru.d10xa.jsonlogviewer.decline.yaml.ConfigYaml import ru.d10xa.jsonlogviewer.decline.Config import ru.d10xa.jsonlogviewer.decline.Config.FormatIn -import ru.d10xa.jsonlogviewer.formatout.ColorLineFormatter -import ru.d10xa.jsonlogviewer.formatout.RawFormatter -import ru.d10xa.jsonlogviewer.logfmt.LogfmtLogLineParser import ru.d10xa.jsonlogviewer.shell.Shell import ru.d10xa.jsonlogviewer.shell.ShellImpl -import scala.util.matching.Regex -import scala.util.Failure -import scala.util.Success -import scala.util.Try object LogViewerStream { @@ -116,43 +108,18 @@ object LogViewerStream { ) ) } else { - IO.pure(makeNonCsvLogLineParser(resolvedConfig)) + IO.pure(LogLineParserFactory.createNonCsvParser(resolvedConfig)) } Stream.eval(getParser).flatMap { parser => - val timestampFilter = TimestampFilter() - val parseResultKeys = ParseResultKeys(resolvedConfig) - val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys) - val fuzzyFilter = new FuzzyFilter(resolvedConfig) - - val outputLineFormatter = resolvedConfig.formatOut match { - case Some(Config.FormatOut.Raw) => RawFormatter() - case Some(Config.FormatOut.Pretty) | None => - ColorLineFormatter( - resolvedConfig, - resolvedConfig.feedName, - resolvedConfig.excludeFields - ) - } - - Stream - .emit(line) - .filter( - rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude) - ) - .map(parser.parse) - .filter(logLineFilter.grep) - .filter(logLineFilter.logLineQueryPredicate) - .filter(fuzzyFilter.test) - .through( - timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter) - ) - .through( - timestampFilter.filterTimestampBefore( - resolvedConfig.timestampBefore - ) - ) - .map(formatWithSafety(_, outputLineFormatter)) + val components = FilterComponents.fromConfig(resolvedConfig) + + FilterPipeline.applyFilters( + Stream.emit(line), + parser, + components, + resolvedConfig + ) } } @@ -162,80 +129,14 @@ object LogViewerStream { ): Stream[IO, String] = lines.pull.uncons1.flatMap { case Some((headerLine, rest)) => - val csvHeaderParser = CsvLogLineParser(resolvedConfig, headerLine) - - val timestampFilter = TimestampFilter() - val parseResultKeys = ParseResultKeys(resolvedConfig) - val logLineFilter = LogLineFilter(resolvedConfig, parseResultKeys) - val fuzzyFilter = new FuzzyFilter(resolvedConfig) - - val outputLineFormatter = resolvedConfig.formatOut match { - case Some(Config.FormatOut.Raw) => RawFormatter() - case Some(Config.FormatOut.Pretty) | None => - ColorLineFormatter( - resolvedConfig, - resolvedConfig.feedName, - resolvedConfig.excludeFields - ) - } + val csvHeaderParser = LogLineParserFactory.createCsvParser(resolvedConfig, headerLine) + val components = FilterComponents.fromConfig(resolvedConfig) - rest - .filter( - rawFilter(_, resolvedConfig.rawInclude, resolvedConfig.rawExclude) - ) - .map(csvHeaderParser.parse) - .filter(logLineFilter.grep) - .filter(logLineFilter.logLineQueryPredicate) - .filter(fuzzyFilter.test) - .through( - timestampFilter.filterTimestampAfter(resolvedConfig.timestampAfter) - ) - .through( - timestampFilter.filterTimestampBefore( - resolvedConfig.timestampBefore - ) - ) - .map(formatWithSafety(_, outputLineFormatter)) + FilterPipeline + .applyFilters(rest, csvHeaderParser, components, resolvedConfig) .pull .echo case None => Pull.done }.stream - - private def formatWithSafety( - parseResult: ParseResult, - formatter: OutputLineFormatter - ): String = - Try(formatter.formatLine(parseResult)) match { - case Success(formatted) => formatted.toString - case Failure(_) => parseResult.raw - } - - def makeNonCsvLogLineParser( - resolvedConfig: ResolvedConfig - ): LogLineParser = { - val jsonPrefixPostfix = JsonPrefixPostfix(JsonDetector()) - resolvedConfig.formatIn match { - case Some(FormatIn.Logfmt) => LogfmtLogLineParser(resolvedConfig) - case Some(FormatIn.Csv) => - throw new IllegalStateException( - "method makeNonCsvLogLineParser does not support csv" - ) - case _ => JsonLogLineParser(resolvedConfig, jsonPrefixPostfix) - } - } - - def rawFilter( - str: String, - include: Option[List[String]], - exclude: Option[List[String]] - ): Boolean = { - val includeRegexes: List[Regex] = include.getOrElse(Nil).map(_.r) - val excludeRegexes: List[Regex] = exclude.getOrElse(Nil).map(_.r) - val includeMatches = includeRegexes.isEmpty || includeRegexes.exists( - _.findFirstIn(str).isDefined - ) - val excludeMatches = excludeRegexes.forall(_.findFirstIn(str).isEmpty) - includeMatches && excludeMatches - } } diff --git a/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterComponentsTest.scala b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterComponentsTest.scala new file mode 100644 index 0000000..3bc9628 --- /dev/null +++ b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterComponentsTest.scala @@ -0,0 +1,84 @@ +package ru.d10xa.jsonlogviewer + +import munit.FunSuite +import ru.d10xa.jsonlogviewer.decline.{Config, FieldNamesConfig} +import ru.d10xa.jsonlogviewer.formatout.{ColorLineFormatter, RawFormatter} + +class FilterComponentsTest extends FunSuite with TestResolvedConfig { + + override def baseResolvedConfig = super.baseResolvedConfig.copy( + feedName = Some("test-feed") + ) + + test("fromConfig should create all filter components") { + val components = FilterComponents.fromConfig(baseResolvedConfig) + + assertNotEquals(components.timestampFilter, null, "TimestampFilter should be created") + assertNotEquals(components.parseResultKeys, null, "ParseResultKeys should be created") + assertNotEquals(components.logLineFilter, null, "LogLineFilter should be created") + assertNotEquals(components.fuzzyFilter, null, "FuzzyFilter should be created") + assertNotEquals(components.outputLineFormatter, null, "OutputLineFormatter should be created") + } + + test("fromConfig should create RawFormatter when formatOut is Raw") { + val config = baseResolvedConfig.copy(formatOut = Some(Config.FormatOut.Raw)) + val components = FilterComponents.fromConfig(config) + + assert( + components.outputLineFormatter.isInstanceOf[RawFormatter], + "Should create RawFormatter for Raw format" + ) + } + + test("fromConfig should create ColorLineFormatter when formatOut is Pretty") { + val config = baseResolvedConfig.copy(formatOut = Some(Config.FormatOut.Pretty)) + val components = FilterComponents.fromConfig(config) + + assert( + components.outputLineFormatter.isInstanceOf[ColorLineFormatter], + "Should create ColorLineFormatter for Pretty format" + ) + } + + test("fromConfig should create ColorLineFormatter when formatOut is None") { + val config = baseResolvedConfig.copy(formatOut = None) + val components = FilterComponents.fromConfig(config) + + assert( + components.outputLineFormatter.isInstanceOf[ColorLineFormatter], + "Should create ColorLineFormatter when formatOut is None (default)" + ) + } + + test("fromConfig should initialize ParseResultKeys with correct config") { + val config = baseResolvedConfig.copy( + fieldNames = FieldNamesConfig( + timestampFieldName = "custom_ts", + levelFieldName = "custom_level", + messageFieldName = "custom_msg", + stackTraceFieldName = "custom_stack", + loggerNameFieldName = "custom_logger", + threadNameFieldName = "custom_thread" + ) + ) + val components = FilterComponents.fromConfig(config) + + // Verify that ParseResultKeys was created (opaque object, can't test internals) + assertNotEquals( + components.parseResultKeys, + null, + "ParseResultKeys should be created with custom config" + ) + } + + test("fromConfig should create components multiple times with same config") { + val components1 = FilterComponents.fromConfig(baseResolvedConfig) + val components2 = FilterComponents.fromConfig(baseResolvedConfig) + + // Components should be created each time (not cached) + assert( + components1 ne components2, + "Each call should create new FilterComponents" + ) + } +} diff --git a/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterPipelineTest.scala b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterPipelineTest.scala new file mode 100644 index 0000000..c31f7b7 --- /dev/null +++ b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/FilterPipelineTest.scala @@ -0,0 +1,177 @@ +package ru.d10xa.jsonlogviewer + +import cats.effect.IO +import fs2.Stream +import munit.CatsEffectSuite + +class FilterPipelineTest extends CatsEffectSuite with TestResolvedConfig { + + val testLogLine = + """{"@timestamp":"2023-01-01T10:00:00Z","level":"INFO","message":"Test message","logger_name":"TestLogger","thread_name":"main"}""" + + test("applyFilters should process a single log line") { + val parser = JsonLogLineParser( + baseResolvedConfig, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(baseResolvedConfig) + val stream = Stream.emit(testLogLine) + + FilterPipeline + .applyFilters(stream, parser, components, baseResolvedConfig) + .compile + .toList + .map { result => + assert(result.nonEmpty, "Should produce output") + assert(result.head.contains("INFO"), "Output should contain log level") + } + } + + test("applyFilters should filter out lines by rawExclude") { + val configWithExclude = baseResolvedConfig.copy( + rawExclude = Some(List("ERROR")) + ) + val parser = JsonLogLineParser( + configWithExclude, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(configWithExclude) + + val errorLine = + """{"@timestamp":"2023-01-01T10:00:00Z","level":"ERROR","message":"Error message"}""" + val infoLine = testLogLine + + val stream = Stream.emits(Seq(errorLine, infoLine)) + + FilterPipeline + .applyFilters(stream, parser, components, configWithExclude) + .compile + .toList + .map { result => + // ERROR line should be filtered out by rawExclude + assertEquals(result.length, 1, "Should only pass INFO line") + assert(result.head.contains("INFO"), "Passed line should be INFO") + } + } + + test("applyFilters should include lines by rawInclude") { + val configWithInclude = baseResolvedConfig.copy( + rawInclude = Some(List("INFO")) + ) + val parser = JsonLogLineParser( + configWithInclude, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(configWithInclude) + + val errorLine = + """{"@timestamp":"2023-01-01T10:00:00Z","level":"ERROR","message":"Error message"}""" + val infoLine = testLogLine + + val stream = Stream.emits(Seq(errorLine, infoLine)) + + FilterPipeline + .applyFilters(stream, parser, components, configWithInclude) + .compile + .toList + .map { result => + // Only INFO line should pass rawInclude filter + assertEquals(result.length, 1, "Should only pass INFO line") + assert(result.head.contains("INFO"), "Passed line should be INFO") + } + } + + test("applyFilters should process multiple lines") { + val parser = JsonLogLineParser( + baseResolvedConfig, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(baseResolvedConfig) + + val lines = List.fill(5)(testLogLine) + val stream = Stream.emits(lines) + + FilterPipeline + .applyFilters(stream, parser, components, baseResolvedConfig) + .compile + .toList + .map { result => + assertEquals(result.length, 5, "Should process all 5 lines") + } + } + + test("applyFilters should handle malformed JSON gracefully") { + val parser = JsonLogLineParser( + baseResolvedConfig, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(baseResolvedConfig) + + val malformedLine = """not a json line""" + val validLine = testLogLine + + val stream = Stream.emits(Seq(malformedLine, validLine)) + + FilterPipeline + .applyFilters(stream, parser, components, baseResolvedConfig) + .compile + .toList + .map { result => + // Both lines should pass through (malformed as raw, valid as formatted) + assertEquals(result.length, 2, "Should process both lines") + } + } + + test("applyFilters should work with empty stream") { + val parser = JsonLogLineParser( + baseResolvedConfig, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(baseResolvedConfig) + + val stream = Stream.empty + + FilterPipeline + .applyFilters(stream, parser, components, baseResolvedConfig) + .compile + .toList + .map { result => + assertEquals(result.length, 0, "Should produce no output for empty stream") + } + } + + test("applyFilters should apply all filters in correct order") { + // This test verifies the pipeline order: + // 1. rawFilter (before parsing) + // 2. parse + // 3. grep + // 4. query + // 5. fuzzy + // 6. timestamp + // 7. format + + val configWithFuzzy = baseResolvedConfig.copy( + fuzzyInclude = Some(List("INFO")) // Fuzzy filter for INFO + ) + val parser = JsonLogLineParser( + configWithFuzzy, + JsonPrefixPostfix(JsonDetector()) + ) + val components = FilterComponents.fromConfig(configWithFuzzy) + + val warnLine = + """{"@timestamp":"2023-01-01T10:00:00Z","level":"WARN","message":"Warning message"}""" + + val stream = Stream.emits(Seq(testLogLine, warnLine)) + + FilterPipeline + .applyFilters(stream, parser, components, configWithFuzzy) + .compile + .toList + .map { result => + // Only INFO line should pass fuzzy filter + assertEquals(result.length, 1, "Should only pass INFO line through fuzzy filter") + assert(result.head.contains("INFO"), "Passed line should be INFO") + } + } +} diff --git a/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/LogLineParserFactoryTest.scala b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/LogLineParserFactoryTest.scala new file mode 100644 index 0000000..532ecbd --- /dev/null +++ b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/LogLineParserFactoryTest.scala @@ -0,0 +1,71 @@ +package ru.d10xa.jsonlogviewer + +import munit.FunSuite +import ru.d10xa.jsonlogviewer.csv.CsvLogLineParser +import ru.d10xa.jsonlogviewer.decline.Config +import ru.d10xa.jsonlogviewer.logfmt.LogfmtLogLineParser + +class LogLineParserFactoryTest extends FunSuite with TestResolvedConfig { + + test("createNonCsvParser should create JsonLogLineParser for Json format") { + val config = baseResolvedConfig.copy(formatIn = Some(Config.FormatIn.Json)) + val parser = LogLineParserFactory.createNonCsvParser(config) + + assert( + parser.isInstanceOf[JsonLogLineParser], + "Should create JsonLogLineParser for Json format" + ) + } + + test("createNonCsvParser should create JsonLogLineParser when formatIn is None") { + val config = baseResolvedConfig.copy(formatIn = None) + val parser = LogLineParserFactory.createNonCsvParser(config) + + assert( + parser.isInstanceOf[JsonLogLineParser], + "Should create JsonLogLineParser when formatIn is None (default)" + ) + } + + test("createNonCsvParser should create LogfmtLogLineParser for Logfmt format") { + val config = baseResolvedConfig.copy(formatIn = Some(Config.FormatIn.Logfmt)) + val parser = LogLineParserFactory.createNonCsvParser(config) + + assert( + parser.isInstanceOf[LogfmtLogLineParser], + "Should create LogfmtLogLineParser for Logfmt format" + ) + } + + test("createNonCsvParser should throw IllegalStateException for Csv format") { + val config = baseResolvedConfig.copy(formatIn = Some(Config.FormatIn.Csv)) + + intercept[IllegalStateException] { + LogLineParserFactory.createNonCsvParser(config) + } + } + + test("createCsvParser should create CsvLogLineParser") { + val headerLine = "timestamp,level,message" + val parser = LogLineParserFactory.createCsvParser(baseResolvedConfig, headerLine) + + assert( + parser.isInstanceOf[CsvLogLineParser], + "Should create CsvLogLineParser" + ) + } + + test("createCsvParser should use provided header line") { + val headerLine = "ts,severity,msg" + val parser = LogLineParserFactory.createCsvParser(baseResolvedConfig, headerLine) + + // Parse a CSV line to verify header was used + val csvLine = "2023-01-01T10:00:00Z,INFO,Test message" + val result = parser.parse(csvLine) + + assert( + result.parsed.isDefined, + "Should parse CSV line using provided header" + ) + } +} diff --git a/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/TestResolvedConfig.scala b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/TestResolvedConfig.scala new file mode 100644 index 0000000..bbe348e --- /dev/null +++ b/json-log-viewer/shared/src/test/scala/ru/d10xa/jsonlogviewer/TestResolvedConfig.scala @@ -0,0 +1,33 @@ +package ru.d10xa.jsonlogviewer + +import ru.d10xa.jsonlogviewer.config.ResolvedConfig +import ru.d10xa.jsonlogviewer.decline.{Config, FieldNamesConfig} + +trait TestResolvedConfig { + + def baseResolvedConfig: ResolvedConfig = ResolvedConfig( + feedName = None, + commands = List.empty, + inlineInput = None, + filter = None, + formatIn = Some(Config.FormatIn.Json), + formatOut = Some(Config.FormatOut.Raw), + fieldNames = FieldNamesConfig( + timestampFieldName = "@timestamp", + levelFieldName = "level", + messageFieldName = "message", + stackTraceFieldName = "stack_trace", + loggerNameFieldName = "logger_name", + threadNameFieldName = "thread_name" + ), + rawInclude = None, + rawExclude = None, + fuzzyInclude = None, + fuzzyExclude = None, + excludeFields = None, + timestampAfter = None, + timestampBefore = None, + grep = List.empty, + showEmptyFields = false + ) +}