-
Notifications
You must be signed in to change notification settings - Fork 55
test: allow e2e service tests to be checked in #283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
| package aws.sdk.kotlin.runtime.testing | ||
|
|
||
| import java.io.IOException | ||
| import java.io.InputStream | ||
| import kotlin.random.Random | ||
|
|
||
| /** | ||
| * Test utility InputStream implementation that generates random ASCII data when | ||
| * read, up to the size specified when constructed. | ||
| */ | ||
| public class RandomInputStream constructor( | ||
| /** The requested amount of data contained in this random stream. */ | ||
| private val lengthInBytes: Long, | ||
|
|
||
| /** Flag controlling whether binary or character data is used. */ | ||
| private val binaryData: Boolean = false | ||
| ) : InputStream() { | ||
|
|
||
| /** The number of bytes of data remaining in this random stream. */ | ||
| protected var remainingBytes: Long = lengthInBytes | ||
|
|
||
| public val bytesRead: Long | ||
| get() = lengthInBytes - remainingBytes | ||
|
|
||
| @Throws(IOException::class) | ||
| override fun read(b: ByteArray, off: Int, len: Int): Int { | ||
| // Signal that we're out of data if we've hit our limit | ||
| if (remainingBytes <= 0) { | ||
| return -1 | ||
| } | ||
| var bytesToRead = len | ||
| if (bytesToRead > remainingBytes) { | ||
| bytesToRead = remainingBytes.toInt() | ||
| } | ||
| remainingBytes -= bytesToRead.toLong() | ||
| if (binaryData) { | ||
| val endExclusive = off + bytesToRead | ||
| Random.nextBytes(b, off, endExclusive) | ||
| } else { | ||
| for (i in 0 until bytesToRead) { | ||
| b[off + i] = Random.nextInt(MIN_CHAR_CODE, MAX_CHAR_CODE + 1).toByte() | ||
| } | ||
| } | ||
| return bytesToRead | ||
| } | ||
|
|
||
| @Throws(IOException::class) | ||
| override fun read(): Int { | ||
| // Signal that we're out of data if we've hit our limit | ||
| if (remainingBytes <= 0) { | ||
| return -1 | ||
| } | ||
| remainingBytes-- | ||
| return if (binaryData) { | ||
| Random.nextInt() | ||
| } else { | ||
| Random.nextInt(MIN_CHAR_CODE, MAX_CHAR_CODE + 1) | ||
| } | ||
| } | ||
|
|
||
| public companion object { | ||
| /** The minimum ASCII code contained in the data in this stream. */ | ||
| private const val MIN_CHAR_CODE = 32 | ||
|
|
||
| /** The maximum ASCII code contained in the data in this stream. */ | ||
| private const val MAX_CHAR_CODE = 125 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
| package aws.sdk.kotlin.runtime.testing | ||
|
|
||
| import java.io.BufferedOutputStream | ||
| import java.io.File | ||
| import java.io.FileOutputStream | ||
| import java.io.IOException | ||
| import java.util.* | ||
|
|
||
| /** | ||
| * Extension of File that creates a temporary file with a specified name in | ||
| * Java's temporary directory, as declared in the JRE's system properties. The | ||
| * file is immediately filled with a specified amount of random ASCII data. | ||
| * | ||
| * @see RandomInputStream | ||
| */ | ||
| public class RandomTempFile : File { | ||
| /** Flag controlling whether binary or character data is used. */ | ||
| private val binaryData: Boolean | ||
|
|
||
| /** | ||
| * Creates, and fills, a temp file with a randomly generated name and specified size of random ASCII data. | ||
| * | ||
| * @param sizeInBytes The amount of random ASCII data, in bytes, for the new temp | ||
| * file. | ||
| * @throws IOException If any problems were encountered creating the new temp file. | ||
| */ | ||
| public constructor(sizeInBytes: Long) : this(UUID.randomUUID().toString(), sizeInBytes, false) | ||
|
|
||
| /** | ||
| * Creates, and fills, a temp file with the specified name and specified | ||
| * size of random data. | ||
| * | ||
| * @param filename The name for the new temporary file, within the Java temp | ||
| * directory as declared in the JRE's system properties. | ||
| * @param sizeInBytes The amount of random ASCII data, in bytes, for the new temp | ||
| * file. | ||
| * @param binaryData Whether to fill the file with binary or character data. | ||
| * | ||
| * @throws IOException | ||
| * If any problems were encountered creating the new temp file. | ||
| */ | ||
| public constructor(filename: String, sizeInBytes: Long, binaryData: Boolean = false) : super( | ||
| TEMP_DIR + separator + System.currentTimeMillis().toString() + "-" + filename | ||
| ) { | ||
| this.binaryData = binaryData | ||
| createFile(sizeInBytes) | ||
| } | ||
|
|
||
| @Throws(IOException::class) | ||
| public fun createFile(sizeInBytes: Long) { | ||
| deleteOnExit() | ||
| FileOutputStream(this).use { outputStream -> | ||
| BufferedOutputStream(outputStream).use { bufferedOutputStream -> | ||
| RandomInputStream(sizeInBytes, binaryData).use { inputStream -> | ||
| inputStream.copyTo(bufferedOutputStream) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun delete(): Boolean { | ||
| if (!super.delete()) { | ||
| throw RuntimeException("Could not delete: $absolutePath") | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| public companion object { | ||
| private val TEMP_DIR: String = System.getProperty("java.io.tmpdir") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,33 @@ subprojects { | |
| } | ||
|
|
||
| apply(from = rootProject.file("gradle/publish.gradle")) | ||
|
|
||
| if (project.file("e2eTest").exists()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question what (if any) is the difference between putting tests in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah there are definitely differences. If we put these into the unit test sourceSet they would run with the rest of the (unit) tests of course which is not what we want. This allows us to separate out these kinds of tests as an independent compilation and setup test tasks dedicated to them. |
||
|
|
||
| kotlin.target.compilations { | ||
| val main by getting | ||
| val e2eTest by creating { | ||
| defaultSourceSet { | ||
| kotlin.srcDir("e2eTest") | ||
| dependencies { | ||
| implementation(main.compileDependencyFiles + main.runtimeDependencyFiles + main.output.classesDirs) | ||
|
|
||
| implementation(kotlin("test")) | ||
| implementation(kotlin("test-junit5")) | ||
| implementation(project(":aws-runtime:testing")) | ||
| } | ||
| } | ||
|
|
||
| tasks.register<Test>("e2eTest") { | ||
| description = "Run e2e service tests" | ||
| group = "verification" | ||
| classpath = compileDependencyFiles + runtimeDependencyFiles | ||
| testClassesDirs = output.classesDirs | ||
| useJUnitPlatform() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
| package aws.sdk.kotlin.e2etest | ||
|
|
||
| import aws.sdk.kotlin.runtime.testing.RandomTempFile | ||
| import aws.sdk.kotlin.runtime.testing.runSuspendTest | ||
| import aws.sdk.kotlin.services.s3.S3Client | ||
| import aws.sdk.kotlin.services.s3.model.* | ||
| import aws.smithy.kotlin.runtime.content.ByteStream | ||
| import aws.smithy.kotlin.runtime.content.decodeToString | ||
| import aws.smithy.kotlin.runtime.content.fromFile | ||
| import kotlinx.coroutines.runBlocking | ||
| import kotlinx.coroutines.withTimeout | ||
| import org.junit.jupiter.api.AfterAll | ||
| import org.junit.jupiter.api.BeforeAll | ||
| import org.junit.jupiter.api.TestInstance | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.time.Duration | ||
| import kotlin.time.ExperimentalTime | ||
|
|
||
| /** | ||
| * Tests for bucket operations | ||
| */ | ||
| @TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question for my understanding is this annotation property an optimization or necessary? if the latter why?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It changes the default behavior to create a single instance for all of the test methods instead of creating a new one per test. I did this to only create the bucket once and re-use it for all of the tests. See https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle for more info
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So to answer your question directly, probably more of an optimization. |
||
| class S3BucketOpsIntegrationTest { | ||
| companion object { | ||
| const val DEFAULT_REGION = "us-east-2" | ||
| } | ||
|
|
||
| val client = S3Client { | ||
| region = DEFAULT_REGION | ||
| } | ||
|
|
||
| lateinit var testBucket: String | ||
|
|
||
| @BeforeAll | ||
| private fun createResources(): Unit = runBlocking { | ||
| testBucket = S3TestUtils.getTestBucket(client) | ||
| } | ||
|
|
||
| @AfterAll | ||
| private fun cleanup() = runBlocking { | ||
| S3TestUtils.deleteBucketAndAllContents(client, testBucket) | ||
| } | ||
|
|
||
| @Test | ||
| fun testPutObjectFromMemory() = runSuspendTest { | ||
| val contents = """ | ||
| A lep is a ball. | ||
| A tay is a hammer. | ||
| A korf is a tiger. | ||
| A flix is a comb. | ||
| A wogsin is a gift. | ||
| """.trimIndent() | ||
|
|
||
| val keyName = "put-obj-from-memory.txt" | ||
|
|
||
| client.putObject { | ||
| bucket = testBucket | ||
| key = keyName | ||
| body = ByteStream.fromString(contents) | ||
| } | ||
|
|
||
| val req = GetObjectRequest { | ||
| bucket = testBucket | ||
| key = keyName | ||
| } | ||
| val roundTrippedContents = client.getObject(req) { it.body?.decodeToString() } | ||
|
|
||
| assertEquals(contents, roundTrippedContents) | ||
| } | ||
|
|
||
| @OptIn(ExperimentalTime::class) | ||
| @Test | ||
| fun testPutObjectFromFile() = runSuspendTest { | ||
| val tempFile = RandomTempFile(1024) | ||
| val keyName = "put-obj-from-file.txt" | ||
|
|
||
| // This test fails sporadically (by never completing) | ||
| // see https://github.com/awslabs/aws-sdk-kotlin/issues/282 | ||
| withTimeout(Duration.seconds(5)) { | ||
| client.putObject { | ||
| bucket = testBucket | ||
| key = keyName | ||
| body = ByteStream.fromFile(tempFile) | ||
| } | ||
| } | ||
|
|
||
| val req = GetObjectRequest { | ||
| bucket = testBucket | ||
| key = keyName | ||
| } | ||
| val roundTrippedContents = client.getObject(req) { it.body?.decodeToString() } | ||
|
|
||
| val contents = tempFile.readText() | ||
| assertEquals(contents, roundTrippedContents) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will throw a cancellation exception if the request context was cancelled. It doesn't fix the issue but will keep us from looping endlessly if the outer context is cancelled...