Skip to content

Commit

Permalink
Add Kotlin/Wasm (JS browser) support (#388)
Browse files Browse the repository at this point in the history
Partially solves #306.

No Node.js support for now, as it requires a canary Node.js version (21.0.0-v8-canary202309143a48826a08 or newer).

No WASI support also. Considering the raw state of Kotlin WASI, this currently seems too hard to implement.

Overall, this is a fairly simple implementation based on the jsMain and jsTest modules. I hope this will be ok as a first solution, just to provide initial support for WASM.
  • Loading branch information
AzimMuradov authored Jan 15, 2024
1 parent d960f37 commit b0e682b
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 0 deletions.
19 changes: 19 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

plugins {
kotlin("multiplatform") version "1.9.22"
Expand Down Expand Up @@ -58,6 +59,16 @@ kotlin {
}
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
android {
publishLibraryVariants("release", "debug")
}
Expand Down Expand Up @@ -155,6 +166,14 @@ kotlin {
implementation(kotlin("test-js"))
}
}
val wasmJsMain by getting {
dependsOn(directMain)
}
val wasmJsTest by getting {
dependencies {
implementation(kotlin("test-wasm-js"))
}
}
val nativeMain by creating {
dependsOn(directMain)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.oshai.kotlinlogging

public class ConsoleOutputAppender : FormattingAppender() {
override fun logFormattedMessage(loggingEvent: KLoggingEvent, formattedMessage: Any?) {
when (loggingEvent.level) {
Level.TRACE -> consoleLog(formattedMessage.toString())
Level.DEBUG -> consoleLog(formattedMessage.toString())
Level.INFO -> consoleInfo(formattedMessage.toString())
Level.WARN -> consoleWarn(formattedMessage.toString())
Level.ERROR -> consoleError(formattedMessage.toString())
Level.OFF -> Unit
}
}
}

private fun consoleLog(message: String): Unit = js("console.log(message)")

private fun consoleInfo(message: String): Unit = js("console.info(message)")

private fun consoleWarn(message: String): Unit = js("console.warn(message)")

private fun consoleError(message: String): Unit = js("console.error(message)")
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.oshai.kotlinlogging

public actual object KotlinLoggingConfiguration {
public actual var logLevel: Level = Level.INFO
public actual var formatter: Formatter = DefaultMessageFormatter(includePrefix = true)
public actual var appender: Appender = ConsoleOutputAppender()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.oshai.kotlinlogging.internal

internal actual object KLoggerNameResolver {

internal actual fun name(func: () -> Unit): String {
var found = false
val exception = Exception()
for (line in exception.stackTraceToString().split("\n")) {
if (found) {
return line.substringBefore(".kt").substringAfterLast(".").substringAfterLast("/")
}
if (line.contains("at KotlinLogging")) {
found = true
}
}
return ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package io.github.oshai.kotlinlogging

import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

class ConsoleOutputAppenderTest {
private lateinit var defaultLogLevel: Level
private lateinit var defaultFormatter: Formatter
private lateinit var defaultAppender: Appender

private lateinit var testAppender: ConsoleOutputAppender

@BeforeTest
fun setup() {
defaultLogLevel = KotlinLoggingConfiguration.logLevel
defaultFormatter = KotlinLoggingConfiguration.formatter
defaultAppender = KotlinLoggingConfiguration.appender

testAppender = ConsoleOutputAppender()

KotlinLoggingConfiguration.logLevel = Level.TRACE
KotlinLoggingConfiguration.formatter = TestFormatter()
KotlinLoggingConfiguration.appender = testAppender

setupConsole()
}

@AfterTest
fun cleanup() {
KotlinLoggingConfiguration.logLevel = defaultLogLevel
KotlinLoggingConfiguration.formatter = defaultFormatter
KotlinLoggingConfiguration.appender = defaultAppender

cleanupConsole()
}

@Test
fun logTraceTest() {
testAppender.log(createTestEvent(Level.TRACE))

assertEquals(expected = "testing... TRACE", actual = getTestLog())
assertEquals("", getTestInfo())
assertEquals("", getTestWarn())
assertEquals("", getTestError())
}

@Test
fun logDebugTest() {
testAppender.log(createTestEvent(Level.DEBUG))

assertEquals(expected = "testing... DEBUG", actual = getTestLog())
assertEquals("", getTestInfo())
assertEquals("", getTestWarn())
assertEquals("", getTestError())
}

@Test
fun logInfoTest() {
testAppender.log(createTestEvent(Level.INFO))

assertEquals("", getTestLog())
assertEquals(expected = "testing... INFO", actual = getTestInfo())
assertEquals("", getTestWarn())
assertEquals("", getTestError())
}

@Test
fun logWarnTest() {
testAppender.log(createTestEvent(Level.WARN))

assertEquals("", getTestLog())
assertEquals("", getTestInfo())
assertEquals(expected = "testing... WARN", actual = getTestWarn())
assertEquals("", getTestError())
}

@Test
fun logErrorTest() {
testAppender.log(createTestEvent(Level.ERROR))

assertEquals("", getTestLog())
assertEquals("", getTestInfo())
assertEquals("", getTestWarn())
assertEquals(expected = "testing... ERROR", actual = getTestError())
}

@Test
fun logOffTest() {
testAppender.log(createTestEvent(Level.OFF))

assertEquals("", getTestLog())
assertEquals("", getTestInfo())
assertEquals("", getTestWarn())
assertEquals("", getTestError())
}

class TestFormatter : Formatter {
override fun formatMessage(loggingEvent: KLoggingEvent): String =
"testing... ${loggingEvent.level}"
}

private fun createTestEvent(level: Level) =
KLoggingEvent(
level = level,
marker = null,
loggerName = "test logger",
message = "test message",
cause = null,
payload = null,
)
}

// Access intercepted console.* test messages

private fun getTestLog(): String = js("""window.__testLog.toString()""")

private fun getTestInfo(): String = js("""window.__testInfo.toString()""")

private fun getTestWarn(): String = js("""window.__testWarn.toString()""")

private fun getTestError(): String = js("""window.__testError.toString()""")

private fun setupConsole() {
js(
"""
{
// Save standard console.*
window.__stdLog = console.log;
window.__stdInfo = console.info;
window.__stdWarn = console.warn;
window.__stdError = console.error;
// Define list containers for the intercepted messages
window.__testLog = [];
window.__testInfo = [];
window.__testWarn = [];
window.__testError = [];
// Intercept console.* calls and
// save all intercepted messages to respectful list containers
console.log = function (msg) {
window.__testLog.push(msg);
window.__stdLog.apply(console, arguments);
};
console.info = function (msg) {
window.__testInfo.push(msg);
window.__stdInfo.apply(console, arguments);
};
console.warn = function (msg) {
window.__testWarn.push(msg);
window.__stdWarn.apply(console, arguments);
};
console.error = function (msg) {
window.__testError.push(msg);
window.__stdError.apply(console, arguments);
};
}"""
)
}

private fun cleanupConsole() {
js(
"""
{
// Reset console.*
console.log = window.__stdLog;
console.info = window.__stdInfo;
console.warn = window.__stdWarn;
console.error = window.__stdError;
// Clear list containers
window.__testLog = [];
window.__testInfo = [];
window.__testWarn = [];
window.__testError = [];
}"""
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.oshai.kotlinlogging

import kotlin.test.*

private val logger = KotlinLogging.logger("SimpleWasmJsTest")

class SimpleWasmJsTest {
private lateinit var appender: SimpleAppender

@BeforeTest
fun setup() {
appender = createAppender()
KotlinLoggingConfiguration.appender = appender
}

@AfterTest
fun cleanup() {
KotlinLoggingConfiguration.appender = ConsoleOutputAppender()
KotlinLoggingConfiguration.logLevel = Level.INFO
}

@Test
fun simpleWasmJsTest() {
assertEquals("SimpleWasmJsTest", logger.name)
logger.info { "info msg" }
assertEquals("INFO: [SimpleWasmJsTest] info msg", appender.lastMessage)
assertEquals("info", appender.lastLevel)
}

@Test
fun offLevelWasmJsTest() {
KotlinLoggingConfiguration.logLevel = Level.OFF
assertTrue(logger.isLoggingOff())
logger.error { "error msg" }
assertEquals("NA", appender.lastMessage)
assertEquals("NA", appender.lastLevel)
}

private fun createAppender(): SimpleAppender = SimpleAppender()

class SimpleAppender : Appender {
var lastMessage: String = "NA"
var lastLevel: String = "NA"

override fun log(loggingEvent: KLoggingEvent) {
lastMessage = DefaultMessageFormatter(includePrefix = true).formatMessage(loggingEvent)
lastLevel = loggingEvent.level.name.lowercase()
}
}
}

0 comments on commit b0e682b

Please sign in to comment.