Use CouchbaseMock instead of actual server

Seems way more reliable/consistent for test scenarios.
This commit is contained in:
Tyler Benson 2018-08-31 16:05:01 +10:00
parent 0655ef208f
commit 444cbecba5
6 changed files with 94 additions and 235 deletions

View File

@ -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

View File

@ -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+'

View File

@ -1,9 +1,7 @@
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()
}
}

View File

@ -1,6 +1,7 @@
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)

View File

@ -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)]
}

View File

@ -1,55 +1,32 @@
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
def couchbaseContainer
protected CouchbaseCluster couchbaseCluster
@Shared
protected CouchbaseCluster cluster
protected CouchbaseCluster memcacheCluster
@Shared
protected CouchbaseEnvironment environment
protected CouchbaseEnvironment couchbaseEnvironment
@Shared
protected ClusterManager manager
protected CouchbaseEnvironment memcacheEnvironment
@Shared
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"
}
manager = cluster.clusterManager(USERNAME, PASSWORD)
mock = new CouchbaseMock("127.0.0.1", port, 1, 1)
mock.httpServer.register("/query", new QueryServer())
mock.start()
println "CouchbaseMock listening on localhost:$port"
resetBucket(bucketCouchbase)
resetBucket(bucketMemcache)
resetBucket(bucketEphemeral)
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())
}
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
}
}