diff --git a/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift b/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift index a75bf3c..6a14997 100644 --- a/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift +++ b/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift @@ -1,5 +1,4 @@ import Benchmark -import ConsoleKit import ConsoleLogger import Logging @@ -26,10 +25,9 @@ let benchmarks: @Sendable () -> Void = { "Logging", configuration: .init( setup: { - let console = TestConsole() LoggingSystem.bootstrap( { label, provider in - ConsoleLogger(label: label, console: console) + ConsoleLogger(label: label) }, metadataProvider: .init { ["provided1": "from metadata provider", "provided2": "another metadata provider"] @@ -54,9 +52,7 @@ let benchmarks: @Sendable () -> Void = { "LoggerFragmentBuilder", configuration: .init( setup: { - let console = TestConsole() LoggingSystem.bootstrap( - console: console, metadataProvider: .init { ["provided1": "from metadata provider", "provided2": "another metadata provider"] } diff --git a/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift b/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift deleted file mode 100644 index a7e376f..0000000 --- a/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift +++ /dev/null @@ -1,47 +0,0 @@ -import ConsoleKit -import Synchronization - -/// A test console that captures input and output for testing purposes. -/// -/// > Warning: This class is a duplicate of the one in the main package, -/// remember to update all when making changes. -final class TestConsole: Console { - let _testInputQueue: Mutex<[String]> = Mutex([]) - - var testInputQueue: [String] { - get { self._testInputQueue.withLock { $0 } } - set { self._testInputQueue.withLock { $0 = newValue } } - } - - let _testOutputQueue: Mutex<[String]> = Mutex([]) - var testOutputQueue: [String] { - get { self._testOutputQueue.withLock { $0 } } - set { self._testOutputQueue.withLock { $0 = newValue } } - } - - let _userInfo: Mutex<[AnySendableHashable: any Sendable]> = Mutex([:]) - var userInfo: [AnySendableHashable: any Sendable] { - get { self._userInfo.withLock { $0 } } - set { self._userInfo.withLock { $0 = newValue } } - } - - init() { - self.testInputQueue = [] - self.testOutputQueue = [] - self.userInfo = [:] - } - - func input(isSecure: Bool) -> String { - return testInputQueue.popLast() ?? "" - } - - func output(_ text: ConsoleText, newLine: Bool) { - testOutputQueue.insert(text.description + (newLine ? "\n" : ""), at: 0) - } - - func report(error: String, newLine: Bool) {} - - func clear(_ type: ConsoleClear) {} - - var size: (width: Int, height: Int) { (width: 32, height: 0) } -} diff --git a/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.LoggerFragmentBuilder.p90.json b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.LoggerFragmentBuilder.p90.json index 304bfa3..cf3f2db 100644 --- a/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.LoggerFragmentBuilder.p90.json +++ b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.LoggerFragmentBuilder.p90.json @@ -1,4 +1,4 @@ { - "mallocCountTotal" : 52, - "peakMemoryResident" : 14278655 + "mallocCountTotal" : 38, + "peakMemoryResident" : 11042816 } \ No newline at end of file diff --git a/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json index 512a674..b17394c 100644 --- a/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json +++ b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json @@ -1,4 +1,4 @@ { - "mallocCountTotal" : 48, - "peakMemoryResident" : 13279231 + "mallocCountTotal" : 30, + "peakMemoryResident" : 11108352 } \ No newline at end of file diff --git a/Package.swift b/Package.swift index 358c921..c5f12fd 100644 --- a/Package.swift +++ b/Package.swift @@ -35,8 +35,7 @@ let package = Package( .target( name: "ConsoleLogger", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .target(name: "ConsoleKit"), + .product(name: "Logging", package: "swift-log") ], swiftSettings: swiftSettings ), @@ -50,7 +49,6 @@ let package = Package( .executableTarget( name: "ConsoleLoggerExample", dependencies: [ - .target(name: "ConsoleKit"), .target(name: "ConsoleLogger"), .product(name: "Logging", package: "swift-log"), ], diff --git a/Sources/ConsoleLogger/ANSIColor.swift b/Sources/ConsoleLogger/ANSIColor.swift new file mode 100644 index 0000000..854a3d9 --- /dev/null +++ b/Sources/ConsoleLogger/ANSIColor.swift @@ -0,0 +1,38 @@ +#if canImport(Darwin) +import Darwin.C +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(Android) +@preconcurrency import Android +#elseif os(WASI) +import WASILibc +#elseif os(Windows) +import CRT +#endif + +enum ANSIColor: String { + case red = "\u{001B}[31m" + case green = "\u{001B}[32m" + case yellow = "\u{001B}[33m" + case cyan = "\u{001B}[36m" + case brightRed = "\u{001B}[91m" +} + +extension String { + func colored(_ color: ANSIColor?) -> String { + guard supportsANSICommands, let color else { return self } + return color.rawValue + self + "\u{001B}[0m" + } +} + +private var supportsANSICommands: Bool { + #if Xcode + // Xcode output does not support ANSI commands + return false + #else + // If STDOUT is not an interactive terminal then omit ANSI commands + return isatty(STDOUT_FILENO) > 0 + #endif +} diff --git a/Sources/ConsoleLogger/ConsoleLogger.swift b/Sources/ConsoleLogger/ConsoleLogger.swift index 9dd6f2e..4980d41 100644 --- a/Sources/ConsoleLogger/ConsoleLogger.swift +++ b/Sources/ConsoleLogger/ConsoleLogger.swift @@ -1,7 +1,6 @@ -public import ConsoleKit public import Logging -/// Outputs logs to a `Console` via a ``LoggerFragment`` pipeline. +/// Outputs logs to console via a ``LoggerFragment`` pipeline. public struct ConsoleLogger: LogHandler, Sendable { public let label: String @@ -14,65 +13,65 @@ public struct ConsoleLogger: LogHandler, Sendable { /// See `LogHandler.logLevel`. public var logLevel: Logger.Level - /// The conosle that the messages will get logged to. - public let console: any Console - /// The ``LoggerFragment`` this logger outputs through. public var fragment: T + /// The printer used to output log messages. Used for testing purposes only. + private let printer: any ConsoleLoggerPrinter + /// Creates a new ``ConsoleLogger`` instance. /// /// - Parameters: /// - fragment: The ``LoggerFragment`` this logger outputs through. + /// - printer: The ``ConsoleLoggerPrinter`` used to output log messages. /// - label: Unique identifier for this logger. - /// - console: The console to log the messages to. /// - level: The minimum level of message that the logger will output. This defaults to `.debug`, the lowest level. /// - metadata: Extra metadata to log with the message. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to use for this logger. This defaults to `nil`. public init( fragment: T = .default, + printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), label: String, - console: any Console, level: Logger.Level = .debug, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil ) { self.fragment = fragment + self.printer = printer self.label = label self.metadata = metadata self.logLevel = level - self.console = console self.metadataProvider = metadataProvider } /// Creates a new ``ConsoleLogger`` instance. /// /// - Parameters: + /// - printer: The ``ConsoleLoggerPrinter`` used to output log messages. /// - label: Unique identifier for this logger. - /// - console: The console to log the messages to. /// - level: The minimum level of message that the logger will output. This defaults to `.debug`, the lowest level. /// - metadata: Extra metadata to log with the message. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to use for this logger. This defaults to `nil`. /// - fragment: The ``LoggerFragment`` this logger outputs through. public init( + printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), label: String, - console: any Console, level: Logger.Level = .debug, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, @LoggerFragmentBuilder fragment: () -> T ) { self.fragment = fragment() + self.printer = printer self.label = label self.metadata = metadata self.logLevel = level - self.console = console self.metadataProvider = metadataProvider } /// See `LogHandler[metadataKey:]`. /// - /// This just acts as a getter/setter for the `.metadata` property. + /// This just acts as a getter/setter for the ``ConsoleLogger/metadata`` property. public subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } set { self.metadata[key] = newValue } @@ -104,19 +103,19 @@ public struct ConsoleLogger: LogHandler, Sendable { ) self.fragment.write(&record, to: &output) - self.console.output(output.text) + self.printer.print(output.text) } } extension Logger.Level { /// Converts log level to console style - public var style: ConsoleStyle { + var style: ANSIColor? { switch self { - case .trace, .debug: .plain - case .info, .notice: .info - case .warning: .warning - case .error: .error - case .critical: ConsoleStyle(color: .brightRed) + case .trace, .debug: nil + case .info, .notice: .cyan + case .warning: .yellow + case .error: .red + case .critical: .brightRed } } diff --git a/Sources/ConsoleLogger/ConsoleLoggerPrinter.swift b/Sources/ConsoleLogger/ConsoleLoggerPrinter.swift new file mode 100644 index 0000000..d8d2d3f --- /dev/null +++ b/Sources/ConsoleLogger/ConsoleLoggerPrinter.swift @@ -0,0 +1,20 @@ +/// Defines a printer used by a ``ConsoleLogger`` to output log messages. +public protocol ConsoleLoggerPrinter: Sendable { + /// The method called by a ``ConsoleLogger`` to print a log message. + /// + /// - Parameter string: The string to print. + func print(_ string: String) +} + +/// The default ``ConsoleLoggerPrinter`` that prints to standard output. +public struct DefaultConsoleLoggerPrinter: ConsoleLoggerPrinter { + /// Prints the given string to standard output using `Swift.print(_:)`. + /// + /// - Parameter string: The string to print. + public func print(_ string: String) { + Swift.print(string) + } + + /// Creates a new ``DefaultConsoleLoggerPrinter``. + public init() {} +} diff --git a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md index d38f9a6..d3bf993 100644 --- a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md +++ b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md @@ -73,13 +73,10 @@ Keep in mind that you can also create your own custom fragments by conforming to ### Bootstrapping the LoggingSystem -Build a ``ConsoleLogger`` with a custom or default fragment and a `ConsoleKit` `Console` (usually a `Terminal`), and register it with the `LoggingSystem`: +Build a ``ConsoleLogger`` with a custom or default fragment and register it with the `LoggingSystem`: ```swift -LoggingSystem.bootstrap( - fragment: .timestampDefault(), - console: Terminal() -) +LoggingSystem.bootstrap(fragment: .timestampDefault()) // Prints "2023-08-21T00:00:00Z [ INFO ] Logged!" Logger(label: "EXAMPLE").info("Logged!") @@ -89,7 +86,7 @@ You can also create multiple loggers with different labels and fragments as need ```swift let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(label: label) { SpacedFragment { "ConsoleLogger" // This is equivalent to LiteralFragment("ConsoleLogger") LabelFragment() diff --git a/Sources/ConsoleLogger/Docs.docc/index.md b/Sources/ConsoleLogger/Docs.docc/index.md index 0675a70..ae36c72 100644 --- a/Sources/ConsoleLogger/Docs.docc/index.md +++ b/Sources/ConsoleLogger/Docs.docc/index.md @@ -16,7 +16,6 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - - ``ConsoleLogger`` -- ``LogRecord`` - ``ConsoleLogger/Logging`` ### Logger Fragments @@ -42,4 +41,10 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - ``SystemTimestampSource`` - ``TimestampFragment`` - ``DefaultLoggerFragmentType`` -- ``TimestampDefaultLoggerFragmentType`` \ No newline at end of file +- ``TimestampDefaultLoggerFragmentType`` + +### Miscellaneous + +- ``LogRecord`` +- ``ConsoleLoggerPrinter`` +- ``DefaultConsoleLoggerPrinter`` \ No newline at end of file diff --git a/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift index 10fd6db..0481e04 100644 --- a/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift +++ b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift @@ -1,4 +1,3 @@ -import ConsoleKit import Logging /// The type of the default ``LoggerFragment``. diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index 8b0fc0b..8d7c28d 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -1,4 +1,3 @@ -public import ConsoleKit public import Logging #if canImport(Darwin) @@ -17,13 +16,13 @@ public import CRT /// The output of a ``LoggerFragment``, including some intermediary state used for things like deduplicating separators. public struct FragmentOutput { - public var text = ConsoleText() + public var text = "" public var needsSeparator = false public init() {} - public static func += (lhs: inout FragmentOutput, rhs: ConsoleText) { - lhs.text = ConsoleText(fragments: lhs.text.fragments + rhs.fragments) + public static func += (lhs: inout FragmentOutput, rhs: String) { + lhs.text += rhs } } @@ -66,19 +65,19 @@ extension LoggerFragment { } /// Add a literal prefix to the current fragment. - public func prefixed(_ text: ConsoleText) -> AndFragment { + public func prefixed(_ text: String) -> AndFragment { AndFragment(LiteralFragment(text), self) } /// Add a literal suffix to the current fragment. - public func suffixed(_ text: ConsoleText) -> AndFragment { + public func suffixed(_ text: String) -> AndFragment { AndFragment(self, LiteralFragment(text)) } /// Appends the given separator text to the output before `self`'s output, as long as a separator is needed. /// /// If the wrapped fragment reports that it has no content, no separator will be inserted. - public func separated(_ text: ConsoleText) -> SeparatorFragment { + public func separated(_ text: String) -> SeparatorFragment { SeparatorFragment(text, fragment: self) } } @@ -160,9 +159,9 @@ public struct OptionalFragment: LoggerFragment { /// A fragment that combines multiple fragments of the same type. public struct ArrayFragment: LoggerFragment { public let fragments: [T] - public let separator: ConsoleText? + public let separator: String? - public init(_ fragments: [T], separator: ConsoleText? = nil) { + public init(_ fragments: [T], separator: String? = nil) { self.fragments = fragments self.separator = separator } @@ -187,7 +186,7 @@ public struct LabelFragment: LoggerFragment { public init() {} public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - output += "[ \(record.label) ]".consoleText() + output += "[ \(record.label) ]" output.needsSeparator = true } } @@ -197,16 +196,16 @@ public struct LevelFragment: LoggerFragment { public init() {} public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - output += "[ \(record.level.name) ]".consoleText(record.level.style) + output += "[ \(record.level.name) ]".colored(record.level.style) output.needsSeparator = true } } /// Writes the given text to the output. public struct LiteralFragment: LoggerFragment { - public let literal: ConsoleText + public let literal: String - public init(_ literal: ConsoleText) { + public init(_ literal: String) { self.literal = literal } @@ -223,15 +222,15 @@ public struct LiteralFragment: LoggerFragment { extension LiteralFragment: ExpressibleByStringLiteral { public init(stringLiteral value: String) { - self.literal = value.consoleText() + self.literal = value } } public struct SeparatorFragment: LoggerFragment { - public let literal: ConsoleText + public let literal: String public var fragment: T - public init(_ literal: ConsoleText, fragment: T) { + public init(_ literal: String, fragment: T) { self.literal = literal self.fragment = fragment } @@ -253,7 +252,7 @@ public struct MessageFragment: LoggerFragment { public init() {} public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - output += record.message.description.consoleText() + output += record.message.description output.needsSeparator = true } } @@ -273,7 +272,7 @@ public struct MetadataFragment: LoggerFragment { guard !allMetadata.isEmpty else { return } - output += allMetadata.sortedDescriptionWithoutQuotes.consoleText() + output += allMetadata.sortedDescriptionWithoutQuotes output.needsSeparator = true } } @@ -286,7 +285,7 @@ public struct SourceLocationFragment: LoggerFragment { public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { let file = record.file + ":" + record.line.description - output += "(" + file.consoleText() + ")" + output += "(" + file + ")" output.needsSeparator = true } } @@ -298,7 +297,7 @@ public struct LoggerSourceFragment: LoggerFragment { public init() {} public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - output += record.source.consoleText() + output += record.source output.needsSeparator = true } } @@ -336,7 +335,7 @@ public struct TimestampFragment: LoggerFragment { } public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - output += self.timestamp().consoleText() + output += self.timestamp() output.needsSeparator = true } diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift index 82c0cc6..2d73bbd 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift @@ -1,5 +1,3 @@ -import ConsoleKit - /// A result builder for creating logger fragments in a declarative way. /// /// This allows you to build complex logger fragment combinations using Swift's result builder syntax. @@ -12,7 +10,7 @@ public enum LoggerFragmentBuilder { /// Build an expression from a string literal, creating a ``LiteralFragment``. public static func buildExpression(_ literal: String) -> LiteralFragment { - LiteralFragment(literal.consoleText()) + LiteralFragment(literal) } /// Build a block from a single logger fragment. diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift index 0a2e01b..fbb5ccd 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift @@ -1,5 +1,3 @@ -import ConsoleKit - /// A result builder for creating logger fragments in a declarative way. /// /// This allows you to build complex logger fragment combinations using Swift's result builder syntax. diff --git a/Sources/ConsoleLogger/LoggingSystem+bootstrap.swift b/Sources/ConsoleLogger/LoggingSystem+bootstrap.swift index bcacb7b..47bca3a 100644 --- a/Sources/ConsoleLogger/LoggingSystem+bootstrap.swift +++ b/Sources/ConsoleLogger/LoggingSystem+bootstrap.swift @@ -1,28 +1,27 @@ -public import ConsoleKit public import Logging extension LoggingSystem { /// Bootstraps a ``ConsoleLogger`` to the `LoggingSystem`, so that logger will be used in `Logger.init(label:)`. /// /// ```swift - /// LoggingSystem.boostrap(console: console) + /// LoggingSystem.boostrap() /// ``` /// /// - Parameters: /// - fragment: The logger fragment which will be used to build the logged messages. - /// - console: The console the logger will log the messages to. + /// - printer: The ``ConsoleLoggerPrinter`` used to output log messages. /// - level: The minimum level of message that the logger will output. This defaults to `.info`. /// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to bootstrap the logging system with. public static func bootstrap( fragment: some LoggerFragment = .default, - console: any Console, + printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), level: Logger.Level = .info, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil ) { self.bootstrap( - { ConsoleLogger(fragment: fragment, label: $0, console: console, level: level, metadata: metadata, metadataProvider: $1) }, + { ConsoleLogger(fragment: fragment, printer: printer, label: $0, level: level, metadata: metadata, metadataProvider: $1) }, metadataProvider: metadataProvider ) } @@ -30,24 +29,24 @@ extension LoggingSystem { /// Bootstraps a ``ConsoleLogger`` to the `LoggingSystem`, so that logger will be used in `Logger.init(label:)`. /// /// ```swift - /// LoggingSystem.boostrap(console: console) { + /// LoggingSystem.boostrap() { /// TimestampFragment() /// } /// ``` /// /// - Parameters: - /// - console: The console the logger will log the messages to. + /// - print: The ``ConsoleLoggerPrinter`` used to output log messages. /// - level: The minimum level of message that the logger will output. This defaults to `.info`. /// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to bootstrap the logging system with. /// - fragment: The logger fragment which will be used to build the logged messages. public static func bootstrap( - console: any Console, + print: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), level: Logger.Level = .info, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, @LoggerFragmentBuilder fragment: () -> some LoggerFragment ) { - self.bootstrap(fragment: fragment(), console: console, level: level, metadata: metadata, metadataProvider: metadataProvider) + self.bootstrap(fragment: fragment(), level: level, metadata: metadata, metadataProvider: metadataProvider) } } diff --git a/Sources/ConsoleLoggerExample/entrypoint.swift b/Sources/ConsoleLoggerExample/entrypoint.swift index 170dd34..cd719ba 100644 --- a/Sources/ConsoleLoggerExample/entrypoint.swift +++ b/Sources/ConsoleLoggerExample/entrypoint.swift @@ -1,14 +1,10 @@ -import ConsoleKit import ConsoleLogger import Logging @main struct ConsoleLoggerExample { static func main() { - LoggingSystem.bootstrap( - fragment: .timestampDefault(), - console: Terminal() - ) + LoggingSystem.bootstrap(fragment: .timestampDefault()) // Prints "2023-08-21T00:00:00Z [ INFO ] Logged!" Logger(label: "EXAMPLE").info("Logged!") diff --git a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift index 45beb9e..dd370aa 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -1,4 +1,3 @@ -import ConsoleKit import ConsoleLogger import Logging import Testing @@ -7,10 +6,9 @@ import Testing struct LoggerFragmentBuilderTests { @Test("Simple Fragment") func simpleFragment() throws { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: printer, label: label) { SpacedFragment { "ConsoleLogger" LabelFragment() @@ -22,15 +20,14 @@ struct LoggerFragmentBuilderTests { logger.info("Test message") - #expect(console.testOutputQueue.first == "ConsoleLogger [ codes.vapor.console ] [ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "ConsoleLogger [ codes.vapor.console ] [ INFO ] Test message") } @Test("Conditional Fragment", arguments: [true, false]) func conditionalFragment(includeTimestamp: Bool) throws { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: printer, label: label) { SpacedFragment { if includeTimestamp { TimestampFragment() @@ -44,18 +41,17 @@ struct LoggerFragmentBuilderTests { logger.info("Test message") if includeTimestamp { - #expect(console.testOutputQueue.first?.contains("[ INFO ] Test message") == true) + #expect(printer.testOutputQueue.first?.contains("[ INFO ] Test message") == true) } else { - #expect(console.testOutputQueue.first == "[ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "[ INFO ] Test message") } } @Test("Array Fragment") func arrayFragment() throws { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: printer, label: label) { SpacedFragment { for i in 1...2 { "[PREFIX\(i)]" @@ -68,30 +64,28 @@ struct LoggerFragmentBuilderTests { logger.info("Test message") - #expect(console.testOutputQueue.first == "[PREFIX1] [ INFO ] [PREFIX2] [ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "[PREFIX1] [ INFO ] [PREFIX2] [ INFO ] Test message") } @Test("Empty Block") func emptyBlock() throws { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: printer, label: label) { // Empty block } } logger.info("Test message") - #expect(console.testOutputQueue.first == "\n") + #expect(printer.testOutputQueue.first == "") } @Test("Complex Conditional Fragment", arguments: [Logger.Level.error, .warning, .info]) func complexConditionalFragment(level: Logger.Level) throws { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: printer, label: label) { if level >= .error { "X" } else if level >= .warning { @@ -108,20 +102,19 @@ struct LoggerFragmentBuilderTests { logger.info("Test message") if level >= .error { - #expect(console.testOutputQueue.first == "X [ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "X [ INFO ] Test message") } else if level >= .warning { - #expect(console.testOutputQueue.first == "! [ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "! [ INFO ] Test message") } else { - #expect(console.testOutputQueue.first == "i [ INFO ] Test message\n") + #expect(printer.testOutputQueue.first == "i [ INFO ] Test message") } } @Test("Default built with LoggerFragmentBuilder") func defaultFragment() throws { - let console = TestConsole() - + let loggerBuilderPrinter = TestingConsoleLoggerPrinter() let loggerBuilder = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) { + ConsoleLogger(printer: loggerBuilderPrinter, label: label) { // This is the default logger fragment, but built using LoggerFragmentBuilder SpacedFragment { LabelFragment().maxLevel(.trace) @@ -133,14 +126,14 @@ struct LoggerFragmentBuilderTests { } } + let defaultLoggerPrinter = TestingConsoleLoggerPrinter() let defaultLogger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console) + ConsoleLogger(printer: defaultLoggerPrinter, label: label) } - loggerBuilder.info("Test message", metadata: ["key": "value"]) - defaultLogger.info("Test message", metadata: ["key": "value"]) + loggerBuilder.info("Test message", metadata: ["key": "value"], line: 1) + defaultLogger.info("Test message", metadata: ["key": "value"], line: 1) - // Drop the last 5 characters which are the source location line number that can differ - #expect(console.testOutputQueue[0].dropLast(5) == console.testOutputQueue[1].dropLast(5)) + #expect(loggerBuilderPrinter.testOutputQueue[0] == defaultLoggerPrinter.testOutputQueue[0]) } } diff --git a/Tests/ConsoleLoggerTests/LoggingTests.swift b/Tests/ConsoleLoggerTests/LoggingTests.swift index 75e427d..71b4a88 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -1,4 +1,3 @@ -import ConsoleKit import ConsoleLogger import Logging import Testing @@ -21,9 +20,8 @@ import CRT struct ConsoleLoggerTests { @Test("Log Handler Check") func logHandlerCheck() { - let console = TestConsole() var logger1 = Logger(label: "codes.vapor.console.1") { label in - ConsoleLogger(label: label, console: console) + ConsoleLogger(label: label) } logger1.logLevel = .debug logger1[metadataKey: "only-on"] = "first" @@ -40,48 +38,48 @@ struct ConsoleLoggerTests { @Test("Logging Levels") func loggingLevels() throws { - let console = TestConsole() + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console, level: .info) + ConsoleLogger(printer: printer, label: label, level: .info) } logger.trace("trace") - #expect(console.testOutputQueue.first == nil) + #expect(printer.testOutputQueue.first == nil) logger.debug("debug") - #expect(console.testOutputQueue.first == nil) + #expect(printer.testOutputQueue.first == nil) logger.info("info") - expect(console, logs: .info, message: "info") + expect(printer: printer, logs: .info, message: "info") logger.notice("notice") - expect(console, logs: .notice, message: "notice") + expect(printer: printer, logs: .notice, message: "notice") logger.warning("warning") - expect(console, logs: .warning, message: "warning") + expect(printer: printer, logs: .warning, message: "warning") logger.error("error") - expect(console, logs: .error, message: "error") + expect(printer: printer, logs: .error, message: "error") logger.critical("critical") - expect(console, logs: .critical, message: "critical") + expect(printer: printer, logs: .critical, message: "critical") } @Test("Metadata") func metadata() { - let console = TestConsole() + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console, level: .info, metadata: ["meta1": "test1"]) + ConsoleLogger(printer: printer, label: label, level: .info, metadata: ["meta1": "test1"]) } logger.info("info") - expect(console, logs: .info, message: "info [meta1: test1]") + expect(printer: printer, logs: .info, message: "info [meta1: test1]") logger.info("info", metadata: ["meta2": "test2"]) - expect(console, logs: .info, message: "info [meta1: test1, meta2: test2]") + expect(printer: printer, logs: .info, message: "info [meta1: test1, meta2: test2]") logger.info("info", metadata: ["meta1": "overridden"]) - expect(console, logs: .info, message: "info [meta1: overridden]") + expect(printer: printer, logs: .info, message: "info [meta1: overridden]") logger.info( "info", @@ -91,19 +89,21 @@ struct ConsoleLoggerTests { ] ) expect( - console, logs: .info, message: #"info [meta1: test1, meta2: "Missing command", meta3: [hello, wor"ld], meta4: [hello: wor"ld]]"# + printer: printer, + logs: .info, + message: #"info [meta1: test1, meta2: "Missing command", meta3: [hello, wor"ld], meta4: [hello: wor"ld]]"# ) } @Test("Source Location") func sourceLocation() { - let console = TestConsole() + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console, level: .debug) + ConsoleLogger(printer: printer, label: label, level: .debug) } logger.debug("debug", line: 1) - expect(console, logs: .debug, message: "debug (ConsoleLoggerTests/LoggingTests.swift:1)") + expect(printer: printer, logs: .debug, message: "debug (ConsoleLoggerTests/LoggingTests.swift:1)") } @Test("Metadata Providers") @@ -114,22 +114,20 @@ struct ConsoleLoggerTests { } return ["simple-trace-id": .string(traceID)] } - let console = TestConsole() + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger(label: label, console: console, metadataProvider: simpleTraceIDMetadataProvider) + ConsoleLogger(printer: printer, label: label, metadataProvider: simpleTraceIDMetadataProvider) } TraceNamespace.$simpleTraceID.withValue("1234-5678") { logger.debug("debug", line: 1) } - expect(console, logs: .debug, message: "debug [simple-trace-id: 1234-5678] (ConsoleLoggerTests/LoggingTests.swift:1)") + expect(printer: printer, logs: .debug, message: "debug [simple-trace-id: 1234-5678] (ConsoleLoggerTests/LoggingTests.swift:1)") } @Test("Timestamp Fragment") func timestampFragment() { - let console = TestConsole() - struct ConstantTimestampSource: TimestampSource, @unchecked Sendable { let time: tm @@ -138,6 +136,7 @@ struct ConsoleLoggerTests { } } + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in var time = tm() time.tm_sec = 1 @@ -149,14 +148,14 @@ struct ConsoleLoggerTests { return ConsoleLogger( fragment: .timestampDefault(timestampSource: ConstantTimestampSource(time: time)), - label: label, - console: console + printer: printer, + label: label ) } logger.info("logged", line: 1) - var logged = console.testOutputQueue.first! + var logged = printer.testOutputQueue.first! let expect = "2000-06-04T03:02:01" #expect(logged.hasPrefix(expect)) logged.removeFirst(expect.count) @@ -164,32 +163,36 @@ struct ConsoleLoggerTests { // Remove the timezone, since there doesn't appear to be a good way to mock it with strftime. while logged.removeFirst() != " " {} - #expect(logged == "[ \(Logger.Level.info.name) ] logged (ConsoleLoggerTests/LoggingTests.swift:1)\n") + #expect(logged == "[ \(Logger.Level.info.name) ] logged (ConsoleLoggerTests/LoggingTests.swift:1)") } @Test("Source Fragment") func sourceFragment() { - let console = TestConsole() - + let printer = TestingConsoleLoggerPrinter() let logger = Logger(label: "codes.vapor.console") { label in - ConsoleLogger( + return ConsoleLogger( fragment: LoggerSourceFragment().and(.default.separated(" ")), - label: label, - console: console + printer: printer, + label: label ) } logger.info("logged", line: 1) #expect( - console.testOutputQueue.first - == "ConsoleLoggerTests [ \(Logger.Level.info.name) ] logged (ConsoleLoggerTests/LoggingTests.swift:1)\n" + printer.testOutputQueue.first + == "ConsoleLoggerTests [ \(Logger.Level.info.name) ] logged (ConsoleLoggerTests/LoggingTests.swift:1)" ) } } -private func expect(_ console: TestConsole, logs level: Logger.Level, message: String, sourceLocation: SourceLocation = #_sourceLocation) { - #expect(console.testOutputQueue.first ?? "" == "[ \(level.name) ] \(message)\n", sourceLocation: sourceLocation) +private func expect( + printer: TestingConsoleLoggerPrinter, + logs level: Logger.Level, + message: String, + sourceLocation: SourceLocation = #_sourceLocation +) { + #expect(printer.testOutputQueue.first ?? "" == "[ \(level.name) ] \(message)", sourceLocation: sourceLocation) } enum TraceNamespace { diff --git a/Tests/ConsoleLoggerTests/TestConsole.swift b/Tests/ConsoleLoggerTests/TestConsole.swift deleted file mode 100644 index bf50500..0000000 --- a/Tests/ConsoleLoggerTests/TestConsole.swift +++ /dev/null @@ -1,47 +0,0 @@ -import ConsoleKit -import Synchronization - -/// A test console that captures input and output for testing purposes. -/// -/// > Warning: This class is a duplicate of the one in `ConsoleKitTests`, -/// remember to update both when making changes. -final class TestConsole: Console { - let _testInputQueue: Mutex<[String]> = Mutex([]) - - var testInputQueue: [String] { - get { self._testInputQueue.withLock { $0 } } - set { self._testInputQueue.withLock { $0 = newValue } } - } - - let _testOutputQueue: Mutex<[String]> = Mutex([]) - var testOutputQueue: [String] { - get { self._testOutputQueue.withLock { $0 } } - set { self._testOutputQueue.withLock { $0 = newValue } } - } - - let _userInfo: Mutex<[AnySendableHashable: any Sendable]> = Mutex([:]) - var userInfo: [AnySendableHashable: any Sendable] { - get { self._userInfo.withLock { $0 } } - set { self._userInfo.withLock { $0 = newValue } } - } - - init() { - self.testInputQueue = [] - self.testOutputQueue = [] - self.userInfo = [:] - } - - func input(isSecure: Bool) -> String { - return testInputQueue.popLast() ?? "" - } - - func output(_ text: ConsoleText, newLine: Bool) { - testOutputQueue.insert(text.description + (newLine ? "\n" : ""), at: 0) - } - - func report(error: String, newLine: Bool) {} - - func clear(_ type: ConsoleClear) {} - - var size: (width: Int, height: Int) { (width: 32, height: 0) } -} diff --git a/Tests/ConsoleLoggerTests/TestingConsoleLoggerPrinter.swift b/Tests/ConsoleLoggerTests/TestingConsoleLoggerPrinter.swift new file mode 100644 index 0000000..ac4e3e1 --- /dev/null +++ b/Tests/ConsoleLoggerTests/TestingConsoleLoggerPrinter.swift @@ -0,0 +1,14 @@ +import ConsoleLogger +import Synchronization + +final class TestingConsoleLoggerPrinter: ConsoleLoggerPrinter { + let _testOutputQueue: Mutex<[String]> = .init([]) + var testOutputQueue: [String] { + get { _testOutputQueue.withLock { $0 } } + set { _testOutputQueue.withLock { $0 = newValue } } + } + + func print(_ string: String) { + testOutputQueue.insert(string, at: 0) + } +}