Skip to content

Commit 6079c7d

Browse files
authored
Refactor Runtime to support end-to-end testing (#3)
* Add end-to-end tests * Refactor of Log * More tests
1 parent 4237eb7 commit 6079c7d

File tree

17 files changed

+532
-109
lines changed

17 files changed

+532
-109
lines changed

gradle/libs.versions.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ kotlin-date-time = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
2020
kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinest-test" }
2121
junit = { group = "junit", name = "junit", version.ref = "junit" }
2222
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
23-
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
24-
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
25-
ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" }
2623
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
24+
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
2725
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
2826
ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
2927
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }

lambda-runtime/build.gradle.kts

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import dev.mokkery.MockMode
2+
13
plugins {
24
alias(libs.plugins.kotlin.multiplatform)
35
alias(libs.plugins.kotlin.serialization)
46
alias(libs.plugins.mokkery)
5-
alias(libs.plugins.allopen)
7+
alias(libs.plugins.kotlinx.resources)
68
}
79

810
kotlin {
@@ -29,15 +31,15 @@ kotlin {
2931
}
3032

3133
nativeTest.dependencies {
34+
implementation(projects.lambdaEvents)
3235
implementation(libs.kotlin.test)
36+
implementation(libs.kotlin.coroutines.test)
37+
implementation(libs.ktor.client.mock)
38+
implementation(libs.kotlinx.resources)
3339
}
3440
}
3541
}
3642

37-
fun isTestingTask(name: String) = name.endsWith("Test")
38-
val isTesting = gradle.startParameter.taskNames.any(::isTestingTask)
39-
40-
if (isTesting) allOpen {
41-
annotation("kotlin.Metadata")
43+
mokkery {
44+
defaultMockMode.set(MockMode.autoUnit)
4245
}
43-

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Ext.kt renamed to lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.github.trueangle.knative.lambda.runtime.log
1+
package io.github.trueangle.knative.lambda.runtime
22

33
internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildString {
44
append("An exception occurred:\n")
@@ -9,4 +9,6 @@ internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildStr
99
append("Stack Trace:\n")
1010
append(stackTraceToString())
1111
}
12-
}
12+
}
13+
14+
internal fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt

+33-13
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,44 @@ import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVari
1111
import kotlinx.cinterop.ExperimentalForeignApi
1212
import kotlinx.cinterop.toKString
1313
import platform.posix.getenv
14+
import kotlin.system.exitProcess
1415

15-
@OptIn(ExperimentalForeignApi::class)
1616
@PublishedApi
17-
internal object LambdaEnvironment {
18-
val FUNCTION_MEMORY_SIZE = getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128
19-
val LOG_GROUP_NAME: String = getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty()
20-
val LOG_STREAM_NAME: String = getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty()
21-
val LAMBDA_LOG_LEVEL: String? = getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString()
22-
val LAMBDA_LOG_FORMAT: String? = getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString()
23-
val FUNCTION_NAME: String = getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty()
24-
val FUNCTION_VERSION: String = getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty()
25-
26-
val RUNTIME_API: String = requireNotNull(getenv(AWS_LAMBDA_RUNTIME_API)?.toKString()) {
27-
"Can't find AWS_LAMBDA_RUNTIME_API env variable"
17+
internal open class LambdaEnvironment {
18+
// open due to Mokkery limits
19+
open fun terminate(): Nothing = exitProcess(1)
20+
21+
@OptIn(ExperimentalForeignApi::class)
22+
@PublishedApi
23+
internal companion object Variables {
24+
val FUNCTION_MEMORY_SIZE by unsafeLazy {
25+
getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128
26+
}
27+
val LOG_GROUP_NAME by unsafeLazy {
28+
getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty()
29+
}
30+
val LOG_STREAM_NAME by unsafeLazy {
31+
getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty()
32+
}
33+
val LAMBDA_LOG_LEVEL by unsafeLazy {
34+
getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() ?: "INFO"
35+
}
36+
val LAMBDA_LOG_FORMAT by unsafeLazy {
37+
getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() ?: "TEXT"
38+
}
39+
val FUNCTION_NAME by unsafeLazy {
40+
getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty()
41+
}
42+
val FUNCTION_VERSION by unsafeLazy {
43+
getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty()
44+
}
45+
val RUNTIME_API by unsafeLazy {
46+
getenv(AWS_LAMBDA_RUNTIME_API)?.toKString()
47+
}
2848
}
2949
}
3050

31-
private object ReservedRuntimeEnvironmentVariables {
51+
internal object ReservedRuntimeEnvironmentVariables {
3252
/**
3353
* The handler location configured on the function.
3454
*/

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt

+55-33
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ package io.github.trueangle.knative.lambda.runtime
33
import io.github.trueangle.knative.lambda.runtime.LambdaEnvironmentException.NonRecoverableStateException
44
import io.github.trueangle.knative.lambda.runtime.api.Context
55
import io.github.trueangle.knative.lambda.runtime.api.LambdaClient
6+
import io.github.trueangle.knative.lambda.runtime.api.LambdaClientImpl
67
import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler
78
import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler
89
import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler
910
import io.github.trueangle.knative.lambda.runtime.log.KtorLogger
11+
import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger
1012
import io.github.trueangle.knative.lambda.runtime.log.Log
13+
import io.github.trueangle.knative.lambda.runtime.log.debug
14+
import io.github.trueangle.knative.lambda.runtime.log.error
15+
import io.github.trueangle.knative.lambda.runtime.log.fatal
16+
import io.github.trueangle.knative.lambda.runtime.log.info
17+
import io.github.trueangle.knative.lambda.runtime.log.warn
1118
import io.ktor.client.HttpClient
19+
import io.ktor.client.engine.HttpClientEngine
1220
import io.ktor.client.engine.curl.Curl
1321
import io.ktor.client.plugins.HttpTimeout
1422
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
@@ -22,13 +30,20 @@ import io.ktor.utils.io.writeStringUtf8
2230
import kotlinx.coroutines.runBlocking
2331
import kotlinx.serialization.ExperimentalSerializationApi
2432
import kotlinx.serialization.json.Json
25-
import kotlin.system.exitProcess
2633

2734
object LambdaRuntime {
2835
@OptIn(ExperimentalSerializationApi::class)
2936
internal val json = Json { explicitNulls = false }
3037

31-
private val httpClient = HttpClient(Curl) {
38+
inline fun <reified I, reified O> run(crossinline initHandler: () -> LambdaHandler<I, O>) = runBlocking {
39+
val curlHttpClient = createHttpClient(Curl.create())
40+
val lambdaClient = LambdaClientImpl(curlHttpClient)
41+
42+
Runner(client = lambdaClient, log = Log).run(false, initHandler)
43+
}
44+
45+
@PublishedApi
46+
internal fun createHttpClient(engine: HttpClientEngine) = HttpClient(engine) {
3247
install(HttpTimeout)
3348
install(ContentNegotiation) { json(json) }
3449
install(Logging) {
@@ -39,86 +54,93 @@ object LambdaRuntime {
3954
filter { !it.headers.contains("Lambda-Runtime-Function-Response-Mode", "streaming") }
4055
}
4156
}
57+
}
4258

43-
@PublishedApi
44-
internal val client = LambdaClient(httpClient)
45-
46-
inline fun <reified I, reified O> run(crossinline initHandler: () -> LambdaHandler<I, O>) = runBlocking {
59+
@PublishedApi
60+
internal class Runner(
61+
val client: LambdaClient,
62+
val log: LambdaLogger,
63+
val env: LambdaEnvironment = LambdaEnvironment()
64+
) {
65+
suspend inline fun <reified I, reified O> run(singleEventMode: Boolean = false, crossinline initHandler: () -> LambdaHandler<I, O>) {
4766
val handler = try {
48-
Log.info("Initializing Kotlin Native Lambda Runtime")
67+
log.info("Initializing Kotlin Native Lambda Runtime")
4968

5069
initHandler()
5170
} catch (e: Exception) {
52-
Log.fatal(e)
71+
log.fatal(e)
5372

5473
client.reportError(e.asInitError())
55-
exitProcess(1)
74+
75+
env.terminate()
5676
}
5777

5878
val handlerName = handler::class.simpleName
5979
val inputTypeInfo = typeInfo<I>()
6080
val outputTypeInfo = typeInfo<O>()
6181

62-
while (true) {
82+
var shouldExit = false
83+
while (!shouldExit) {
6384
try {
64-
Log.info("Runtime is ready for a new event")
85+
log.info("Runtime is ready for a new event")
6586

6687
val (event, context) = client.retrieveNextEvent<I>(inputTypeInfo)
6788

68-
with(Log) {
89+
with(log) {
6990
setContext(context)
7091

7192
debug(event)
7293
debug(context)
94+
info("$handlerName invocation started")
7395
}
7496

75-
Log.info("$handlerName invocation started")
76-
7797
if (handler is LambdaStreamHandler<I, *>) {
7898
val response = streamingResponse { handler.handleRequest(event, it, context) }
7999

80-
Log.info("$handlerName started response streaming")
100+
log.info("$handlerName started response streaming")
81101

82102
client.streamResponse(context, response)
83103
} else {
84104
handler as LambdaBufferedHandler<I, O>
85105
val response = bufferedResponse(context) { handler.handleRequest(event, context) }
86106

87-
Log.info("$handlerName invocation completed")
88-
Log.debug(response)
107+
log.info("$handlerName invocation completed")
108+
log.debug(response)
89109

90110
client.sendResponse(context, response, outputTypeInfo)
91111
}
92112
} catch (e: LambdaRuntimeException) {
93-
Log.error(e)
113+
log.error(e)
114+
94115
client.reportError(e)
95116
} catch (e: LambdaEnvironmentException) {
96117
when (e) {
97118
is NonRecoverableStateException -> {
98-
Log.fatal(e)
119+
log.fatal(e)
99120

100-
exitProcess(1)
121+
env.terminate()
101122
}
102123

103-
else -> Log.error(e)
124+
else -> log.error(e)
104125
}
105126
} catch (e: Throwable) {
106-
Log.fatal(e)
127+
log.fatal(e)
128+
129+
env.terminate()
130+
}
107131

108-
exitProcess(1)
132+
if (singleEventMode) {
133+
shouldExit = singleEventMode
109134
}
110135
}
111136
}
112-
}
113137

114-
@PublishedApi
115-
internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) =
116-
object : WriteChannelContent() {
138+
inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) = object : WriteChannelContent() {
117139
override suspend fun writeTo(channel: ByteWriteChannel) {
118140
try {
119141
handler(channel)
120142
} catch (e: Exception) {
121-
Log.warn("Exception occurred on streaming: " + e.message)
143+
log.warn("Exception occurred on streaming: " + e.message)
122144

123145
channel.writeStringUtf8(e.toTrailer())
124146
}
@@ -128,9 +150,9 @@ internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteCha
128150
"Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: ${stackTraceToString().encodeBase64()}\r\n"
129151
}
130152

131-
@PublishedApi
132-
internal inline fun <T, R> T.bufferedResponse(context: Context, block: T.() -> R): R = try {
133-
block()
134-
} catch (e: Exception) {
135-
throw e.asHandlerError(context)
153+
inline fun <T, R> T.bufferedResponse(context: Context, block: T.() -> R): R = try {
154+
block()
155+
} catch (e: Exception) {
156+
throw e.asHandlerError(context)
157+
}
136158
}

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt

+23-10
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ import kotlin.time.Duration.Companion.minutes
2727
import io.ktor.http.ContentType.Application.Json as ContentTypeJson
2828

2929
@PublishedApi
30-
internal class LambdaClient(private val httpClient: HttpClient) {
31-
private val invokeUrl = "http://${LambdaEnvironment.RUNTIME_API}/2018-06-01/runtime"
30+
internal interface LambdaClient {
31+
suspend fun <T> retrieveNextEvent(bodyType: TypeInfo): Pair<T, Context>
32+
suspend fun <T> sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse
33+
suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse
34+
suspend fun reportError(error: LambdaRuntimeException)
35+
}
36+
37+
@PublishedApi
38+
internal class LambdaClientImpl(private val httpClient: HttpClient): LambdaClient {
39+
private val baseUrl = requireNotNull(LambdaEnvironment.RUNTIME_API) {
40+
"Can't find AWS_LAMBDA_RUNTIME_API env variable"
41+
}
42+
private val invokeUrl = "http://$baseUrl/2018-06-01/runtime"
3243
private val requestTimeout = 15.minutes.inWholeMilliseconds
3344

34-
suspend fun <T> retrieveNextEvent(bodyType: TypeInfo): Pair<T, Context> {
45+
override suspend fun <T> retrieveNextEvent(bodyType: TypeInfo): Pair<T, Context> {
3546
val response = httpClient.get {
3647
url("${invokeUrl}/invocation/next")
3748

@@ -47,7 +58,7 @@ internal class LambdaClient(private val httpClient: HttpClient) {
4758
return body to context
4859
}
4960

50-
suspend fun <T> sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse {
61+
override suspend fun <T> sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse {
5162
val response = httpClient.post {
5263
url("${invokeUrl}/invocation/${event.awsRequestId}/response")
5364
contentType(ContentTypeJson)
@@ -64,7 +75,7 @@ internal class LambdaClient(private val httpClient: HttpClient) {
6475
return validateResponse(response)
6576
}
6677

67-
suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse {
78+
override suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse {
6879
val response = httpClient.post {
6980
url("${invokeUrl}/invocation/${event.awsRequestId}/response")
7081

@@ -89,13 +100,15 @@ internal class LambdaClient(private val httpClient: HttpClient) {
89100
return response
90101
}
91102

92-
suspend fun reportError(error: LambdaRuntimeException) {
93-
val response = when (error) {
103+
override suspend fun reportError(error: LambdaRuntimeException) {
104+
when (error) {
94105
is LambdaRuntimeException.Init -> sendInitError(error)
95-
is LambdaRuntimeException.Invocation -> sendInvocationError(error)
96-
}
106+
is LambdaRuntimeException.Invocation -> {
107+
val response = sendInvocationError(error)
97108

98-
validateResponse(response)
109+
validateResponse(response)
110+
}
111+
}
99112
}
100113

101114
private suspend fun sendInvocationError(

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt

-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ package io.github.trueangle.knative.lambda.runtime.api.dto
22

33
import io.github.trueangle.knative.lambda.runtime.log.LogLevel
44
import kotlinx.serialization.Contextual
5-
import kotlinx.serialization.KSerializer
65
import kotlinx.serialization.SerialName
76
import kotlinx.serialization.Serializable
8-
import kotlinx.serialization.encoding.Decoder
9-
import kotlinx.serialization.encoding.Encoder
107

118
@Serializable
129
internal data class LogMessageDto<T>(

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.trueangle.knative.lambda.runtime.log
22

33
import io.github.trueangle.knative.lambda.runtime.api.Context
44
import io.github.trueangle.knative.lambda.runtime.api.dto.LogMessageDto
5+
import io.github.trueangle.knative.lambda.runtime.prettyPrint
56
import io.ktor.util.reflect.TypeInfo
67
import kotlinx.datetime.Clock
78
import kotlinx.serialization.SerializationException

0 commit comments

Comments
 (0)