Skip to content

Commit b1ca65c

Browse files
committed
Add end-to-end tests
1 parent 4237eb7 commit b1ca65c

File tree

11 files changed

+151
-34
lines changed

11 files changed

+151
-34
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 3 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ kotlin {
3030

3131
nativeTest.dependencies {
3232
implementation(libs.kotlin.test)
33+
implementation(libs.kotlin.coroutines.test)
34+
implementation(libs.ktor.client.mock)
3335
}
3436
}
3537
}
Lines changed: 4 additions & 2 deletions
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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,33 @@ import platform.posix.getenv
1515
@OptIn(ExperimentalForeignApi::class)
1616
@PublishedApi
1717
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"
18+
val FUNCTION_MEMORY_SIZE by unsafeLazy {
19+
getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128
20+
}
21+
val LOG_GROUP_NAME by unsafeLazy {
22+
getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty()
23+
}
24+
val LOG_STREAM_NAME by unsafeLazy {
25+
getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty()
26+
}
27+
val LAMBDA_LOG_LEVEL by unsafeLazy {
28+
getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() ?: "INFO"
29+
}
30+
val LAMBDA_LOG_FORMAT by unsafeLazy {
31+
getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() ?: "TEXT"
32+
}
33+
val FUNCTION_NAME by unsafeLazy {
34+
getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty()
35+
}
36+
val FUNCTION_VERSION by unsafeLazy {
37+
getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty()
38+
}
39+
val RUNTIME_API by unsafeLazy {
40+
getenv(AWS_LAMBDA_RUNTIME_API)?.toKString()
2841
}
2942
}
3043

31-
private object ReservedRuntimeEnvironmentVariables {
44+
internal object ReservedRuntimeEnvironmentVariables {
3245
/**
3346
* The handler location configured on the function.
3447
*/

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler
99
import io.github.trueangle.knative.lambda.runtime.log.KtorLogger
1010
import io.github.trueangle.knative.lambda.runtime.log.Log
1111
import io.ktor.client.HttpClient
12+
import io.ktor.client.engine.HttpClientEngine
1213
import io.ktor.client.engine.curl.Curl
1314
import io.ktor.client.plugins.HttpTimeout
1415
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
@@ -28,7 +29,15 @@ object LambdaRuntime {
2829
@OptIn(ExperimentalSerializationApi::class)
2930
internal val json = Json { explicitNulls = false }
3031

31-
private val httpClient = HttpClient(Curl) {
32+
inline fun <reified I, reified O> run(crossinline initHandler: () -> LambdaHandler<I, O>) = runBlocking {
33+
val curlHttpClient = createHttpClient(Curl.create())
34+
val lambdaClient = LambdaClient(curlHttpClient)
35+
36+
Runner(lambdaClient).run(initHandler)
37+
}
38+
39+
@PublishedApi
40+
internal fun createHttpClient(engine: HttpClientEngine) = HttpClient(engine) {
3241
install(HttpTimeout)
3342
install(ContentNegotiation) { json(json) }
3443
install(Logging) {
@@ -39,11 +48,13 @@ object LambdaRuntime {
3948
filter { !it.headers.contains("Lambda-Runtime-Function-Response-Mode", "streaming") }
4049
}
4150
}
51+
}
4252

43-
@PublishedApi
44-
internal val client = LambdaClient(httpClient)
45-
46-
inline fun <reified I, reified O> run(crossinline initHandler: () -> LambdaHandler<I, O>) = runBlocking {
53+
@PublishedApi
54+
internal class Runner(
55+
val client: LambdaClient,
56+
) {
57+
suspend inline fun <reified I, reified O> run(crossinline initHandler: () -> LambdaHandler<I, O>) {
4758
val handler = try {
4859
Log.info("Initializing Kotlin Native Lambda Runtime")
4960

@@ -91,6 +102,7 @@ object LambdaRuntime {
91102
}
92103
} catch (e: LambdaRuntimeException) {
93104
Log.error(e)
105+
94106
client.reportError(e)
95107
} catch (e: LambdaEnvironmentException) {
96108
when (e) {
@@ -109,11 +121,8 @@ object LambdaRuntime {
109121
}
110122
}
111123
}
112-
}
113124

114-
@PublishedApi
115-
internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) =
116-
object : WriteChannelContent() {
125+
inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) = object : WriteChannelContent() {
117126
override suspend fun writeTo(channel: ByteWriteChannel) {
118127
try {
119128
handler(channel)
@@ -128,9 +137,9 @@ internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteCha
128137
"Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: ${stackTraceToString().encodeBase64()}\r\n"
129138
}
130139

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)
140+
inline fun <T, R> T.bufferedResponse(context: Context, block: T.() -> R): R = try {
141+
block()
142+
} catch (e: Exception) {
143+
throw e.asHandlerError(context)
144+
}
136145
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import io.ktor.http.ContentType.Application.Json as ContentTypeJson
2828

2929
@PublishedApi
3030
internal class LambdaClient(private val httpClient: HttpClient) {
31-
private val invokeUrl = "http://${LambdaEnvironment.RUNTIME_API}/2018-06-01/runtime"
31+
private val baseUrl = requireNotNull(LambdaEnvironment.RUNTIME_API) {
32+
"Can't find AWS_LAMBDA_RUNTIME_API env variable"
33+
}
34+
private val invokeUrl = "http://$baseUrl/2018-06-01/runtime"
3235
private val requestTimeout = 15.minutes.inWholeMilliseconds
3336

3437
suspend fun <T> retrieveNextEvent(bodyType: TypeInfo): Pair<T, Context> {

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

Lines changed: 1 addition & 0 deletions
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

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.github.trueangle.knative.lambda.runtime.log
33
import io.github.trueangle.knative.lambda.runtime.LambdaEnvironment
44
import io.github.trueangle.knative.lambda.runtime.LambdaRuntime
55
import io.github.trueangle.knative.lambda.runtime.api.Context
6-
import io.github.trueangle.knative.lambda.runtime.log.Log.write
76
import io.ktor.util.reflect.TypeInfo
87
import io.ktor.util.reflect.typeInfo
98

@@ -20,7 +19,7 @@ object Log {
2019
@PublishedApi
2120
internal val currentLogLevel = LogLevel.fromEnv()
2221
private val writer = StdoutLogWriter()
23-
private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT == "JSON") {
22+
private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT.equals("JSON", ignoreCase = true)) {
2423
JsonLogFormatter(LambdaRuntime.json)
2524
} else {
2625
TextLogFormatter()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.trueangle.knative.lambda.runtime.log
22

3+
import io.github.trueangle.knative.lambda.runtime.prettyPrint
34
import io.ktor.util.reflect.TypeInfo
45

56
internal class TextLogFormatter : LogFormatter {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.github.trueangle.knative.lambda.runtime
2+
3+
import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME
4+
import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION
5+
import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API
6+
import io.github.trueangle.knative.lambda.runtime.api.Context
7+
import io.github.trueangle.knative.lambda.runtime.api.LambdaClient
8+
import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler
9+
import io.ktor.client.engine.HttpClientEngine
10+
import io.ktor.client.engine.mock.MockEngine
11+
import io.ktor.client.engine.mock.respond
12+
import io.ktor.http.HttpHeaders
13+
import io.ktor.http.HttpStatusCode
14+
import io.ktor.http.Url
15+
import io.ktor.http.headers
16+
import io.ktor.http.headersOf
17+
import io.ktor.utils.io.ByteReadChannel
18+
import kotlinx.cinterop.ExperimentalForeignApi
19+
import kotlinx.cinterop.toKString
20+
import kotlinx.coroutines.launch
21+
import kotlinx.coroutines.test.runTest
22+
import platform.posix.getenv
23+
import platform.posix.setenv
24+
import kotlin.test.BeforeClass
25+
import kotlin.test.BeforeTest
26+
import kotlin.test.Test
27+
28+
class LambdaRunnerTest {
29+
30+
@BeforeTest
31+
fun setup() {
32+
mockEnvironment()
33+
}
34+
35+
@Test
36+
fun `GIVEN string event WHEN LambdaBufferedHandler THEN success invocation`() = runTest {
37+
val handlerResponse = "Response"
38+
val requestId = "156cb537-e2d4-11e8-9b34-d36013741fb9"
39+
val deadline = "1542409706888"
40+
41+
val lambdaRunner = createRunner(MockEngine { request ->
42+
when {
43+
request.url.encodedPath.contains("invocation/next") -> respond(
44+
content = ByteReadChannel("""Hello world"""),
45+
status = HttpStatusCode.OK,
46+
headers = headers {
47+
append(HttpHeaders.ContentType, "application/json")
48+
append("Lambda-Runtime-Aws-Request-Id", requestId)
49+
append("Lambda-Runtime-Deadline-Ms", deadline)
50+
append("Lambda-Runtime-Invoked-Function-Arn", "arn")
51+
52+
}
53+
)
54+
55+
else -> respond(
56+
content = ByteReadChannel("""{"ip":"127.0.0.1"}"""),
57+
status = HttpStatusCode.OK,
58+
headers = headersOf(HttpHeaders.ContentType, "application/json")
59+
)
60+
}
61+
})
62+
63+
val handler = object : LambdaBufferedHandler<String, String> {
64+
override suspend fun handleRequest(input: String, context: Context): String = handlerResponse
65+
}
66+
67+
lambdaRunner.run { handler }
68+
}
69+
70+
@OptIn(ExperimentalForeignApi::class)
71+
private fun mockEnvironment() {
72+
if (getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().isNullOrEmpty()) {
73+
setenv(AWS_LAMBDA_FUNCTION_NAME, "test", 1)
74+
}
75+
76+
if (getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().isNullOrEmpty()) {
77+
setenv(AWS_LAMBDA_FUNCTION_VERSION, "1", 1)
78+
}
79+
80+
if (getenv(AWS_LAMBDA_RUNTIME_API)?.toKString().isNullOrEmpty()) {
81+
setenv(AWS_LAMBDA_RUNTIME_API, "127.0.0.1", 1)
82+
}
83+
}
84+
85+
private fun createRunner(mockEngine: HttpClientEngine): Runner {
86+
val lambdaClient = LambdaClient(LambdaRuntime.createHttpClient(mockEngine))
87+
return Runner(lambdaClient)
88+
}
89+
}

0 commit comments

Comments
 (0)