-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Bulk Load CDK: Add integration test using in-memory mock destination #45634
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 |
---|---|---|
|
@@ -61,6 +61,12 @@ allprojects { | |
} | ||
} | ||
|
||
tasks.register('bulkCdkIntegrationTest').configure { | ||
// findByName returns the task, or null if no such task exists. | ||
// we need this because not all submodules have an integrationTest task. | ||
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. FYI, alternatively, |
||
dependsOn allprojects.collect {it.tasks.findByName('integrationTest')}.findAll {it != null} | ||
} | ||
|
||
if (buildNumberFile.exists()) { | ||
tasks.register('bulkCdkBuild').configure { | ||
dependsOn allprojects.collect {it.tasks.named('build')} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,16 @@ | ||
// simply declaring the source sets is sufficient to populate them with | ||
// src/integrationTest/java+resources + src/integrationTest/kotlin. | ||
sourceSets { | ||
integrationTest { | ||
} | ||
} | ||
kotlin { | ||
sourceSets { | ||
testIntegration { | ||
} | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation project(':airbyte-cdk:bulk:core:bulk-cdk-core-base') | ||
implementation 'org.apache.commons:commons-lang3:3.17.0' | ||
|
@@ -10,3 +23,18 @@ dependencies { | |
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") | ||
implementation "org.jetbrains.kotlin:kotlin-reflect:2.0.20" | ||
} | ||
|
||
task integrationTest(type: Test) { | ||
description = 'Runs the integration tests.' | ||
group = 'verification' | ||
testClassesDirs = sourceSets.integrationTest.output.classesDirs | ||
classpath = sourceSets.integrationTest.runtimeClasspath | ||
useJUnitPlatform() | ||
mustRunAfter tasks.check | ||
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. what's this for? why not have this run as part of 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. this really tripped me up; did you in fact mean to have 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. iirc I copypasted this from stackoverflow without reading it :P I did in fact want to have check depend on integrationTest (which I think is what I did later on, with the 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. Thanks! FYI chatgpt is great at generating gradle scripts |
||
} | ||
configurations { | ||
integrationTestImplementation.extendsFrom testImplementation | ||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly | ||
} | ||
// These tests are lightweight enough to run on every PR. | ||
rootProject.check.dependsOn(integrationTest) | ||
edgao marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.cdk.mock_integration_test | ||
|
||
import io.airbyte.cdk.test.util.NoopDestinationCleaner | ||
import io.airbyte.cdk.test.util.NoopExpectedRecordMapper | ||
import io.airbyte.cdk.test.util.NoopNameMapper | ||
import io.airbyte.cdk.test.write.BasicFunctionalityIntegrationTest | ||
|
||
class MockBasicFunctionalityIntegrationTest : | ||
BasicFunctionalityIntegrationTest( | ||
MockDestinationSpecification(), | ||
MockDestinationDataDumper, | ||
NoopDestinationCleaner, | ||
NoopExpectedRecordMapper, | ||
NoopNameMapper | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.cdk.mock_integration_test | ||
|
||
import io.airbyte.cdk.test.util.DestinationDataDumper | ||
import io.airbyte.cdk.test.util.OutputRecord | ||
import java.util.concurrent.ConcurrentHashMap | ||
|
||
object MockDestinationBackend { | ||
private val files: MutableMap<String, MutableList<OutputRecord>> = ConcurrentHashMap() | ||
|
||
fun insert(filename: String, vararg records: OutputRecord) { | ||
getFile(filename).addAll(records) | ||
} | ||
|
||
fun readFile(filename: String): List<OutputRecord> { | ||
return getFile(filename) | ||
} | ||
|
||
private fun getFile(filename: String): MutableList<OutputRecord> { | ||
return files.getOrPut(filename) { mutableListOf() } | ||
} | ||
} | ||
|
||
object MockDestinationDataDumper : DestinationDataDumper { | ||
override fun dumpRecords(streamName: String, streamNamespace: String?): List<OutputRecord> { | ||
return MockDestinationBackend.readFile( | ||
MockStreamLoader.getFilename(streamNamespace, streamName) | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.cdk.mock_integration_test | ||
|
||
import io.airbyte.cdk.check.DestinationChecker | ||
import javax.inject.Singleton | ||
|
||
@Singleton | ||
class MockDestinationChecker : DestinationChecker<MockDestinationConfiguration> { | ||
override fun check(config: MockDestinationConfiguration) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.cdk.mock_integration_test | ||
|
||
import io.airbyte.cdk.command.ConfigurationSpecification | ||
import io.airbyte.cdk.command.DestinationConfiguration | ||
import io.airbyte.cdk.command.DestinationConfigurationFactory | ||
import io.micronaut.context.annotation.Factory | ||
import jakarta.inject.Singleton | ||
|
||
class MockDestinationConfiguration : DestinationConfiguration() | ||
|
||
@Singleton class MockDestinationSpecification : ConfigurationSpecification() | ||
|
||
@Singleton | ||
class MockDestinationConfigurationFactory : | ||
DestinationConfigurationFactory<MockDestinationSpecification, MockDestinationConfiguration> { | ||
edgao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
override fun makeWithoutExceptionHandling( | ||
pojo: MockDestinationSpecification | ||
): MockDestinationConfiguration { | ||
return MockDestinationConfiguration() | ||
} | ||
} | ||
|
||
@Factory | ||
class MockDestinationConfigurationProvider(private val config: DestinationConfiguration) { | ||
edgao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@Singleton | ||
fun get(): MockDestinationConfiguration { | ||
return config as MockDestinationConfiguration | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* | ||
* Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.cdk.mock_integration_test | ||
|
||
import io.airbyte.cdk.command.DestinationStream | ||
import io.airbyte.cdk.data.ObjectValue | ||
import io.airbyte.cdk.message.Batch | ||
import io.airbyte.cdk.message.DestinationRecord | ||
import io.airbyte.cdk.message.SimpleBatch | ||
import io.airbyte.cdk.test.util.OutputRecord | ||
import io.airbyte.cdk.write.DestinationWriter | ||
import io.airbyte.cdk.write.StreamLoader | ||
import java.time.Instant | ||
import java.util.UUID | ||
import javax.inject.Singleton | ||
|
||
@Singleton | ||
class MockDestinationWriter : DestinationWriter { | ||
override fun createStreamLoader(stream: DestinationStream): StreamLoader { | ||
return MockStreamLoader(stream) | ||
} | ||
} | ||
|
||
class MockStreamLoader(override val stream: DestinationStream) : StreamLoader { | ||
data class LocalBatch(val records: List<DestinationRecord>) : Batch { | ||
override val state = Batch.State.LOCAL | ||
} | ||
data class PersistedBatch(val records: List<DestinationRecord>) : Batch { | ||
override val state = Batch.State.PERSISTED | ||
} | ||
|
||
override suspend fun processRecords( | ||
edgao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
records: Iterator<DestinationRecord>, | ||
totalSizeBytes: Long | ||
): Batch { | ||
return LocalBatch(records.asSequence().toList()) | ||
} | ||
|
||
override suspend fun processBatch(batch: Batch): Batch { | ||
return when (batch) { | ||
is LocalBatch -> { | ||
batch.records.forEach { | ||
MockDestinationBackend.insert( | ||
getFilename(it.stream), | ||
OutputRecord( | ||
UUID.randomUUID(), | ||
Instant.ofEpochMilli(it.emittedAtMs), | ||
Instant.ofEpochMilli(System.currentTimeMillis()), | ||
stream.generationId, | ||
it.data as ObjectValue, | ||
OutputRecord.Meta(changes = it.meta?.changes, syncId = stream.syncId), | ||
) | ||
) | ||
} | ||
PersistedBatch(batch.records) | ||
} | ||
is PersistedBatch -> SimpleBatch(state = Batch.State.COMPLETE) | ||
else -> throw IllegalStateException("Unexpected batch type: $batch") | ||
} | ||
} | ||
|
||
companion object { | ||
fun getFilename(stream: DestinationStream.Descriptor) = | ||
getFilename(stream.namespace, stream.name) | ||
fun getFilename(namespace: String?, name: String) = "(${namespace},${name})" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# This is a minimal metadata.yaml that allows a destination connector to run. | ||
# A real metadata.yaml obviously contains much more stuff, but we don't strictly | ||
# need any of it at runtime. | ||
data: | ||
dockerRepository: "airbyte/fake-destination" |
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.
there are some read-only flags that you can set here, not sure if they're actually helpful
in any case none of this seems wrong