Use CouchbaseMock instead of actual server
Seems way more reliable/consistent for test scenarios.
MODIFY
.circleci/config.yml
.circleci/config.yml
@@ -64,8 +64,6 @@ jobs:
- image: *default_container
# This is used by spymemcached instrumentation tests
- image: memcached
- # This is used by couchbase instrumentation tests
- - image: couchbase/server
steps:
- checkout

MODIFY
dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle
dd-java-agent/instrumentation/couchbase-2.0/couchbase-2.0.gradle
@@ -32,13 +32,13 @@ dependencies {
annotationProcessor deps.autoservice
implementation deps.autoservice
- testCompile 'com.anotherchrisberry:spock-retry:0.6.4'
-
testCompile project(':dd-java-agent:testing')
- testCompile group: 'org.testcontainers', name: 'couchbase', version: '1.8.3'
+ testCompile group: 'com.couchbase.mock', name: 'CouchbaseMock', version: '1.5.19'
testCompile group: 'org.springframework.data', name: 'spring-data-couchbase', version: '2.0.0.RELEASE'
+ // Earliest version that seems to allow queries with CouchbaseMock:
+ testCompile group: 'com.couchbase.client', name: 'java-client', version: '2.5.0'
latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-couchbase', version: '3.+'
latestDepTestCompile group: 'com.couchbase.client', name: 'java-client', version: '2.6+'

MODIFY
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/CouchbaseClientTest.groovy
import com.couchbase.client.java.Bucket
import com.couchbase.client.java.document.JsonDocument
import com.couchbase.client.java.document.json.JsonObject
-import com.couchbase.client.java.query.N1qlParams
import com.couchbase.client.java.query.N1qlQuery
-import com.couchbase.client.java.query.consistency.ScanConsistency
import util.AbstractCouchbaseTest
import static datadog.trace.agent.test.asserts.ListWriterAssert.assertTraces
@@ -33,7 +31,7 @@ class CouchbaseClientTest extends AbstractCouchbaseTest {
when:
// Connect to the bucket and open it
- Bucket bkt = cluster.openBucket(bucketSettings.name())
+ Bucket bkt = cluster.openBucket(bucketSettings.name(), bucketSettings.password())
// Create a JSON document and store it with the ID "helloworld"
JsonObject content = JsonObject.create().put("hello", "world")
@@ -84,52 +82,26 @@ class CouchbaseClientTest extends AbstractCouchbaseTest {
where:
- bucketSettings << [bucketCouchbase, bucketMemcache, bucketEphemeral]
+ manager | cluster | bucketSettings
+ couchbaseManager | couchbaseCluster | bucketCouchbase
+ memcacheManager | memcacheCluster | bucketMemcache
+
type = bucketSettings.type().name()
}
def "test query"() {
- when:
- // Connect to the bucket and open it
- Bucket bkt = cluster.openBucket(bucketSettings.name())
-
- // Create a JSON document and store it with the ID "helloworld"
- JsonObject content = JsonObject.create().put("hello", "world")
- def inserted = bkt.upsert(JsonDocument.create("helloworld", content))
-
- then:
- inserted != null
- assertTraces(TEST_WRITER, 1) {
- trace(0, 1) {
- span(0) {
- serviceName "couchbase"
- resourceName "Bucket.upsert(${bkt.name()})"
- operationName "couchbase.call"
- errored false
- parent()
- tags {
- "bucket" bkt.name()
- defaultTags()
- }
- }
- }
- }
- TEST_WRITER.clear()
+ setup:
+ Bucket bkt = cluster.openBucket(bucketSettings.name(), bucketSettings.password())
when:
- def result = bkt.query(
- N1qlQuery.parameterized(
- "SELECT * FROM `${bkt.name()}` WHERE hello = \$hello",
- JsonObject.create()
- .put("hello", "world"),
- N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS)
- )
- )
+ // Mock expects this specific query.
+ // See com.couchbase.mock.http.query.QueryServer.handleString.
+ def result = bkt.query(N1qlQuery.simple("SELECT mockrow"))
then:
result.parseSuccess()
result.finalSuccess()
- result.first().value().get(bkt.name()).get("hello") == "world"
+ result.first().value().get("row") == "value"
and:
assertTraces(TEST_WRITER, 1) {
@@ -149,7 +121,10 @@ class CouchbaseClientTest extends AbstractCouchbaseTest {
}
where:
- bucketSettings << [bucketCouchbase] // Only couchbase buckets support queries.
+ manager | cluster | bucketSettings
+ couchbaseManager | couchbaseCluster | bucketCouchbase
+ // Only couchbase buckets support queries.
+
type = bucketSettings.type().name()
}
}

MODIFY
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringRepositoryTest.groovy
package springdata
-
+import com.couchbase.client.java.view.DefaultView
+import com.couchbase.client.java.view.DesignDocument
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.data.repository.CrudRepository
@@ -30,10 +31,24 @@ class CouchbaseSpringRepositoryTest extends AbstractCouchbaseTest {
DocRepository repo
def setupSpec() {
- CouchbaseConfig.setEnvironment(environment)
+
+ // Create view for SpringRepository's findAll()
+ couchbaseCluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password()).bucketManager()
+ .insertDesignDocument(
+ DesignDocument.create("doc", Collections.singletonList(DefaultView.create("all",
+ '''
+ function (doc, meta) {
+ if (doc._class == "springdata.Doc") {
+ emit(meta.id, null);
+ }
+ }
+ '''.stripIndent()
+ )))
+ )
+ CouchbaseConfig.setEnvironment(couchbaseEnvironment)
// Close all buckets and disconnect
- cluster.disconnect()
+ couchbaseCluster.disconnect()
applicationContext = new AnnotationConfigApplicationContext(CouchbaseConfig)
repo = applicationContext.getBean(DocRepository)

MODIFY
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/springdata/CouchbaseSpringTemplateTest.groovy
@@ -14,14 +14,11 @@ class CouchbaseSpringTemplateTest extends AbstractCouchbaseTest {
List<CouchbaseTemplate> templates
def setupSpec() {
- Bucket bucketCouchbase = cluster.openBucket(bucketCouchbase.name())
- Bucket bucketMemcache = cluster.openBucket(bucketMemcache.name())
- Bucket bucketEphemeral = cluster.openBucket(bucketEphemeral.name())
- def info = manager.info()
+ Bucket bucketCouchbase = couchbaseCluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password())
+ Bucket bucketMemcache = memcacheCluster.openBucket(bucketMemcache.name(), bucketMemcache.password())
- templates = [new CouchbaseTemplate(info, bucketCouchbase),
- new CouchbaseTemplate(info, bucketMemcache),
- new CouchbaseTemplate(info, bucketEphemeral)]
+ templates = [new CouchbaseTemplate(couchbaseManager.info(), bucketCouchbase),
+ new CouchbaseTemplate(memcacheManager.info(), bucketMemcache)]
}

MODIFY
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/util/AbstractCouchbaseTest.groovy
dd-java-agent/instrumentation/couchbase-2.0/src/test/groovy/util/AbstractCouchbaseTest.groovy
package util
-import com.anotherchrisberry.spock.extensions.retry.RetryOnFailure
-import com.couchbase.client.core.env.AbstractServiceConfig
-import com.couchbase.client.core.env.KeyValueServiceConfig
-import com.couchbase.client.core.env.QueryServiceConfig
-import com.couchbase.client.core.env.SearchServiceConfig
-import com.couchbase.client.core.env.ViewServiceConfig
+
import com.couchbase.client.core.metrics.DefaultLatencyMetricsCollectorConfig
import com.couchbase.client.core.metrics.DefaultMetricsCollectorConfig
-import com.couchbase.client.core.utils.Base64
-import com.couchbase.client.java.Bucket
import com.couchbase.client.java.CouchbaseCluster
import com.couchbase.client.java.bucket.BucketType
-import com.couchbase.client.java.cluster.AuthDomain
import com.couchbase.client.java.cluster.BucketSettings
import com.couchbase.client.java.cluster.ClusterManager
import com.couchbase.client.java.cluster.DefaultBucketSettings
-import com.couchbase.client.java.cluster.UserRole
-import com.couchbase.client.java.cluster.UserSettings
import com.couchbase.client.java.env.CouchbaseEnvironment
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment
-import com.couchbase.client.java.query.Index
-import com.couchbase.client.java.query.N1qlParams
-import com.couchbase.client.java.query.N1qlQuery
-import com.couchbase.client.java.query.consistency.ScanConsistency
-import com.couchbase.client.java.view.DefaultView
-import com.couchbase.client.java.view.DesignDocument
+import com.couchbase.mock.Bucket
+import com.couchbase.mock.BucketConfiguration
+import com.couchbase.mock.CouchbaseMock
+import com.couchbase.mock.http.query.QueryServer
import datadog.trace.agent.test.AgentTestRunner
-import datadog.trace.agent.test.utils.OkHttpUtils
-import okhttp3.FormBody
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody
-import org.testcontainers.couchbase.CouchbaseContainer
-import spock.lang.Requires
+import datadog.trace.agent.test.TestUtils
import spock.lang.Shared
import java.util.concurrent.TimeUnit
-// Do not run tests locally on Java7 since testcontainers are not compatible with Java7
-// It is fine to run on CI because CI provides couchbase externally, not through testcontainers
-@Requires({ "true" == System.getenv("CI") || jvm.java8Compatible })
-
-// Couchbase client sometimes throws com.couchbase.client.java.error.TemporaryFailureException.
-// Lets automatically retry to avoid the test from failing completely.
-@RetryOnFailure(times = 3, delaySeconds = 1)
abstract class AbstractCouchbaseTest extends AgentTestRunner {
private static final USERNAME = "Administrator"
private static final PASSWORD = "password"
- private static final OkHttpClient HTTP_CLIENT = OkHttpUtils.client()
+
+ @Shared
+ private int port = TestUtils.randomOpenPort()
@Shared
private String testBucketName = this.getClass().simpleName
@@ -73,177 +50,74 @@ abstract class AbstractCouchbaseTest extends AgentTestRunner {
.build()
@Shared
- protected bucketEphemeral = DefaultBucketSettings.builder()
- .enableFlush(true)
- .name("$testBucketName-emp")
- .password("test-pass")
- .type(BucketType.EPHEMERAL)
- .quota(100)
- .build()
-
- /*
- Note: type here has to stay undefined, otherwise tests will fail in CI in Java 7 because
- 'testcontainers' are built for Java 8 and Java 7 cannot load this class.
- */
+ CouchbaseMock mock
+ @Shared
+ protected CouchbaseCluster couchbaseCluster
@Shared
- def couchbaseContainer
+ protected CouchbaseCluster memcacheCluster
@Shared
- protected CouchbaseCluster cluster
+ protected CouchbaseEnvironment couchbaseEnvironment
@Shared
- protected CouchbaseEnvironment environment
+ protected CouchbaseEnvironment memcacheEnvironment
@Shared
- protected ClusterManager manager
+ protected ClusterManager couchbaseManager
+ @Shared
+ protected ClusterManager memcacheManager
def setupSpec() {
- /*
- CI will provide us with couchbase container running along side our build.
- When building locally, however, we need to take matters into our own hands
- and we use 'testcontainers' for this.
- */
- if (false && "true" != System.getenv("CI")) {
- couchbaseContainer = new CouchbaseContainer(
- clusterUsername: USERNAME,
- clusterPassword: PASSWORD)
- couchbaseContainer.start()
- environment = couchbaseContainer.getCouchbaseEnvironment()
- cluster = couchbaseContainer.getCouchbaseCluster()
- println "Couchbase container started"
- } else {
- initCluster()
- environment = envBuilder().build()
- cluster = CouchbaseCluster.create(environment)
- println "Using provided couchbase"
+ mock = new CouchbaseMock("127.0.0.1", port, 1, 1)
+ mock.httpServer.register("/query", new QueryServer())
+ mock.start()
+ println "CouchbaseMock listening on localhost:$port"
+
+ mock.createBucket(convert(bucketCouchbase))
+
+ couchbaseEnvironment = envBuilder(bucketCouchbase).build()
+ couchbaseCluster = CouchbaseCluster.create(couchbaseEnvironment, Arrays.asList("127.0.0.1"))
+ couchbaseManager = couchbaseCluster.clusterManager(USERNAME, PASSWORD)
+
+ mock.createBucket(convert(bucketMemcache))
+
+ memcacheEnvironment = envBuilder(bucketMemcache).build()
+ memcacheCluster = CouchbaseCluster.create(memcacheEnvironment, Arrays.asList("127.0.0.1"))
+ memcacheManager = memcacheCluster.clusterManager(USERNAME, PASSWORD)
+
+ // Cache buckets:
+ couchbaseCluster.openBucket(bucketCouchbase.name(), bucketCouchbase.password())
+ memcacheCluster.openBucket(bucketMemcache.name(), bucketMemcache.password())
}
- manager = cluster.clusterManager(USERNAME, PASSWORD)
- resetBucket(bucketCouchbase)
- resetBucket(bucketMemcache)
- resetBucket(bucketEphemeral)
+ private static BucketConfiguration convert(BucketSettings bucketSettings) {
+ def configuration = new BucketConfiguration()
+ configuration.name = bucketSettings.name()
+ configuration.password = bucketSettings.password()
+ configuration.type = Bucket.BucketType.valueOf(bucketSettings.type().name())
+ configuration.numNodes = 1
+ configuration.numReplicas = 0
+ return configuration
}
def cleanupSpec() {
- // Close all buckets and disconnect
- cluster?.disconnect()
+ couchbaseCluster?.disconnect()
+ memcacheCluster?.disconnect()
- couchbaseContainer?.stop()
+ mock?.stop()
}
- protected DefaultCouchbaseEnvironment.Builder envBuilder() {
- def timeout = TimeUnit.MINUTES.toMillis(1)
+ private DefaultCouchbaseEnvironment.Builder envBuilder(BucketSettings bucketSettings) {
+ def timeout = TimeUnit.SECONDS.toMillis(5)
return DefaultCouchbaseEnvironment.builder()
+ .bootstrapCarrierDirectPort(mock.getCarrierPort(bucketSettings.name()))
+ .bootstrapHttpDirectPort(port)
// settings to try to reduce variability in the tests:
- .configPollInterval(0)
- .configPollFloorInterval(0)
.runtimeMetricsCollectorConfig(DefaultMetricsCollectorConfig.create(0, TimeUnit.DAYS))
.networkLatencyMetricsCollectorConfig(DefaultLatencyMetricsCollectorConfig.create(0, TimeUnit.DAYS))
- .queryServiceConfig(QueryServiceConfig.create(10, 10, AbstractServiceConfig.NO_IDLE_TIME))
- .searchServiceConfig(SearchServiceConfig.create(10, 10, AbstractServiceConfig.NO_IDLE_TIME))
- .viewServiceConfig(ViewServiceConfig.create(10, 10, AbstractServiceConfig.NO_IDLE_TIME))
- .keyValueServiceConfig(KeyValueServiceConfig.create(10))
.computationPoolSize(1)
- .analyticsTimeout(timeout)
.connectTimeout(timeout)
.kvTimeout(timeout)
.managementTimeout(timeout)
.queryTimeout(timeout)
- .searchTimeout(timeout)
.viewTimeout(timeout)
}
-
-
- protected void initCluster() {
- callCouchbaseRestAPI("/pools/default", new FormBody.Builder().add("memoryQuota", "1000").add("indexMemoryQuota", "300").build())
- callCouchbaseRestAPI("/node/controller/setupServices", new FormBody.Builder().add("services", "kv,index,n1ql,fts").build(), ["cannot change node services after cluster is provisioned"])
- callCouchbaseRestAPI("/settings/web", new FormBody.Builder().add("username", USERNAME).add("password", PASSWORD).add("port", "8091").build())
- callCouchbaseRestAPI("/settings/indexes", RequestBody.create(FormBody.CONTENT_TYPE, "indexerThreads=0&logLevel=info&maxRollbackPoints=5&storageMode=memory_optimized"))
- }
-
- /**
- * Adapted from CouchbaseContainer.callCouchbaseRestAPI()
- */
- protected void callCouchbaseRestAPI(String url, RequestBody body, expectations = null) throws IOException {
- String authToken = Base64.encode((USERNAME + ":" + PASSWORD).getBytes("UTF-8"))
- def request = new Request.Builder()
- .url("http://localhost:8091$url")
- .header("Authorization", "Basic " + authToken)
- .post(body)
- .build()
- def response = HTTP_CLIENT.newCall(request).execute()
- try {
- if (expectations != null) {
- if (expectations instanceof Integer) {
- assert response.code() == expectations
- } else if (expectations instanceof String) {
- assert response.body().string() == expectations
- }
- } else {
- assert response.code() == 200
- }
- } finally {
- response.close()
- }
- }
-
- /**
- * Copied from CouchbaseContainer.callCouchbaseRestAPI()
- */
- protected void resetBucket(BucketSettings bucketSetting) {
- try {
- // Remove existing Bucket
- if (manager.hasBucket(bucketSetting.name())) {
- manager.removeBucket(bucketSetting.name())
- assert !manager.hasBucket(bucketSetting.name())
- }
-
- // Calling the rest api because the library is slow and creates a lot of traces.
- callCouchbaseRestAPI("/pools/default/buckets", new FormBody.Builder()
- .add("name", bucketSetting.name())
- .add("bucketType", bucketSetting.type().name().toLowerCase())
- .add("ramQuotaMB", String.valueOf(bucketSetting.quota()))
- .add("authType", "sasl")
- .add("saslPassword", bucketSetting.password())
- .build(), 202)
-
- // Insert Bucket admin user
- UserSettings userSettings = UserSettings.build().password(bucketSetting.password())
- .roles([new UserRole("bucket_full_access", bucketSetting.name())])
- manager.upsertUser(AuthDomain.LOCAL, bucketSetting.name(), userSettings)
-
- } catch (Exception e) {
- throw new RuntimeException(e)
- }
-
-
- Bucket bucket = cluster.openBucket(bucketSetting.name(), bucketSetting.password())
-
- if (BucketType.COUCHBASE.equals(bucketSetting.type())) {
- try {
- bucket.query(N1qlQuery.simple(Index.dropPrimaryIndex(bucketSetting.name()),
- N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS)))
-
- assert bucket.query(N1qlQuery.simple(Index.createPrimaryIndex().on(bucketSetting.name()),
- N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS))).finalSuccess()
-
- // Create view for SpringRepository's findAll()
- bucket.bucketManager().insertDesignDocument(
- DesignDocument.create("doc", Collections.singletonList(DefaultView.create("all",
- '''
- function (doc, meta) {
- if (doc._class == "springdata.Doc") {
- emit(meta.id, null);
- }
- }
- '''
- )))
- )
- TEST_WRITER.waitForTraces(13)
- } catch (Exception e) {
- throw new RuntimeException(e)
- }
- } else {
- TEST_WRITER.waitForTraces(4)
- }
- TEST_WRITER.clear() // remove traces generated by insertBucket
- }
}

Use CouchbaseMock instead of actual server

Seems way more reliable/consistent for test scenarios.