Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/verify_python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ uv pip install "./client[dev]"
echo "Checking client files (including smoke tests)..."
${VENV_DIR}/bin/python -m mypy client
echo "Checking tests files..."
python -m mypy tests
python -m mypy tests --explicit-package-bases
cleanup_venv

create_venv
Expand Down
35 changes: 35 additions & 0 deletions client/src/cbltest/api/couchbaseserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,41 @@ def run_query(

return list(dict(result) for result in query_obj.execute())

def upsert_document(
self,
bucket: str,
doc_id: str,
document: dict,
scope: str = "_default",
collection: str = "_default",
) -> None:
"""
Inserts a document into the specified bucket.scope.collection.

:param bucket: The bucket name.
:param scope: The scope name.
:param collection: The collection name.
:param doc_id: The document ID.
:param document: The document content (a dictionary).
"""
with self.__tracer.start_as_current_span(
"insert_document",
attributes={
"cbl.bucket.name": bucket,
"cbl.scope.name": scope,
"cbl.collection.name": collection,
"cbl.document.id": doc_id,
},
):
try:
bucket_obj = _try_n_times(10, 1, False, self.__cluster.bucket, bucket)
coll = bucket_obj.scope(scope).collection(collection)
coll.upsert(doc_id, document)
except Exception as e:
raise CblTestError(
f"Failed to insert document '{doc_id}' into {bucket}.{scope}.{collection}: {e}"
)

def start_xdcr(self, target: "CouchbaseServer", bucket_name: str) -> None:
"""
Starts an XDCR replication from this cluster to the target cluster
Expand Down
146 changes: 146 additions & 0 deletions client/src/cbltest/api/json_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import random
import sys
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Callable, Dict, List, Optional


class JSONGenerator:
"""
Utility class to generate and update reproducible JSON documents for testing.

Usage:
gen = JSONGenerator(size=1000, format="json")
docs = gen.generate_all_documents()
updated_docs = gen.update_all_documents(docs)

Parameters:
seed (int, optional): Random seed for reproducibility (default: random int).
size (int, optional): Number of documents to generate (default: 60000).
format (str, optional): Output format - "json" (dict) or "key-value" (list of dicts/documents).
To insert/update in CB-server/SGW/ Edge-server : use format "json".
To insert into test-server use format "key-value" .
"""

def __init__(
self,
seed: int = random.randint(0, sys.maxsize),
size: int = 60000,
format: str = "json",
):
self.seed = seed
self.size = size
self.format = format

def generate_document(self, doc_id: str) -> Dict[str, Any]:
"""Generate a single JSON document with reproducible random data"""
random.seed(self.seed + int(doc_id.split("-")[0], 16))
if self.format == "json":
return {
doc_id: {
"data": {
"temperature": random.uniform(-20, 40),
"humidity": random.randint(0, 100),
"status": random.choice(["active", "inactive", "maintenance"]),
},
"metadata": {
"version": 1,
"created_at": int(time.time()),
"modified_at": int(time.time()),
},
}
}
else:
return {
doc_id: [
{
"data": {
"temperature": random.uniform(-20, 40),
"humidity": random.randint(0, 100),
"status": random.choice(
["active", "inactive", "maintenance"]
),
}
},
{
"metadata": {
"version": 1,
"created_at": int(time.time()),
"modified_at": int(time.time()),
}
},
]
}

def update_document(self, doc: Any, doc_id: str) -> Dict[str, Any]:
"""Update a document with reproducible modifications"""
offset = int(doc_id.split("-")[0], 16)
random.seed(self.seed + offset)
if self.format == "json":
doc["data"]["temperature"] += random.uniform(-5, 5)
doc["data"]["humidity"] = (
doc["data"]["humidity"] + random.randint(-10, 10)
) % 100
doc["data"]["status"] = random.choice(["active", "inactive", "maintenance"])
doc["metadata"]["version"] = doc["metadata"]["version"] + 1
doc["metadata"]["modified_at"] = int(time.time())

else:
doc[0]["data"] = {
"temperature": random.uniform(-20, 40),
"humidity": random.randint(0, 100),
"status": random.choice(["active", "inactive", "maintenance"]),
}
doc[1]["metadata"]["version"] = doc[1]["metadata"]["version"] + 1
doc[1]["metadata"]["modified_at"] = int(time.time())
return {doc_id: doc}

def batch_process(
self,
process_fn: Callable,
items_ids: List[Any],
items_doc: Optional[Dict[str, Any]] = None,
batch_size: int = 1000,
) -> Dict[Any, Any]:
"""Generic batch processing function with threading"""
results = {}

def process_batch(batch):
result = {}
for item in batch:
output = (
process_fn(items_doc[item], item)
if items_doc is not None
else process_fn(item)
)
result.update(output)
return result

with ThreadPoolExecutor() as executor:
futures = [
executor.submit(process_batch, items_ids[i : i + batch_size])
for i in range(0, len(items_ids), batch_size)
]
for future in futures:
results.update(future.result())

return results

def generate_all_documents(self, size=None) -> Dict[str, Any]:
"""Generate all documents using parallel processing"""
if size is None:
size = self.size

doc_ids = [str(uuid.uuid4()) for _ in range(size)]
documents = self.batch_process(self.generate_document, doc_ids)
return documents

def update_all_documents(
self, documents: Dict[str, Any]
) -> Dict[str, Dict[str, Any]]:
"""Update all documents with consistent modifications"""

doc_ids = list(documents.keys())
updated = self.batch_process(self.update_document, doc_ids, documents)
return updated
49 changes: 34 additions & 15 deletions jenkins/pipelines/QE/multiplatform/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pipeline {
parameters {
string(
name: 'PLATFORM_VERSIONS',
defaultValue: 'ios:3.2.3 android:3.2.4',
defaultValue: 'ios:3.3.0 android:3.3.0',
description: 'Platform versions in two supported formats:\n' +
'1. Auto-fetch (recommended): platform1:version1 platform2:version2\n' +
' Example: "ios:3.2.3 android:3.2.4"\n' +
Expand All @@ -23,11 +23,22 @@ pipeline {
defaultValue: 'test_no_conflicts::TestNoConflicts::test_multiple_cbls_updates_concurrently_with_pull',
description: 'Name of the test to run, leave empty to run all tests, or just mention a directory name[::class name] to run tests in that directory[::class]'
)
string(
name: 'TOPOLOGY_FILE',
defaultValue: 'topology.json',
description: 'Multiplatform Topology file in JSON format'
)
booleanParam(
name: 'DISABLE_AUTO_FETCH',
defaultValue: false,
description: 'Disable automatic fetching of latest successful builds (requires explicit build numbers in PLATFORM_VERSIONS)'
)
booleanParam(
name: 'DISABLE_PREBUILD',
defaultValue: false,
description: 'Disable automatic prebuilding of testserver)'
)

}
stages {
stage('Init') {
Expand Down Expand Up @@ -111,17 +122,19 @@ pipeline {
parallelBuilds[platform] = {
// For multiplatform, we'll use a generic version since each platform may have different versions
// The actual version assignment happens during test setup

build job: 'prebuild-test-server',
parameters: [
string(name: 'TS_PLATFORM', value: platform),
string(name: 'CBL_VERSION', value: '3.2.3'), // Generic version for prebuild
string(name: 'CBL_VERSION', value: '3.3.0'), // Generic version for prebuild
string(name: 'CBL_BUILD', value: ''),
],
wait: true,
propagate: true
}
}

if (parallelBuilds.size() > 0) {
if (parallelBuilds.size() > 0 && !params.DISABLE_PREBUILD) {
parallel parallelBuilds
} else {
echo "No test servers to prebuild"
Expand All @@ -130,25 +143,31 @@ pipeline {
}
}
stage('Setup and Run Tests') {
agent { label 'mac-mini-new' }
agent { label 'mob-e2e-mac-01' }
environment {
KEYCHAIN_PASSWORD = credentials('mobile-qe-keychain')
PATH = "/opt/homebrew/opt/python@3.10/bin:/opt/homebrew/bin:/usr/local/bin:${env.PATH}"
AWS_PROFILE = "mobile-for-now"
KEYCHAIN_PASSWORD = credentials('mob-e2e-mac-01-keychain-password')
DOTNET_ROOT = "/opt/homebrew/opt/dotnet/libexec"
PATH = "/Users/qe_mobile_india/.local/bin:/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/Users/qe_mobile_india/.dotnet/tools:/opt/homebrew/opt/dotnet:${DOTNET_ROOT}:/Users/qe_mobile_india/Library/Android/sdk/cmdline-tools/latest/bin:/Users/qe_mobile_india/Library/Android/sdk/platform-tools:/Users/qe_mobile_india/Library/Android/sdk/emulator:${PATH}"
AWS_PROFILE = "default"
ANDROID_HOME = "/Users/qe_mobile_india/Library/Android/sdk"

}
steps {
// Unlock keychain:
sh 'security unlock-keychain -p ${KEYCHAIN_PASSWORD} ~/Library/Keychains/login.keychain-db'
echo "Run Multiplatform Test"
timeout(time: 60, unit: 'MINUTES') {
sh "jenkins/pipelines/QE/multiplatform/test_multiplatform.sh ${params.PLATFORM_VERSIONS} ${params.SGW_VERSION} ${params.CBL_TEST_NAME}"
timeout(time: 25, unit: 'HOURS') {
sh "jenkins/pipelines/QE/multiplatform/test_multiplatform.sh ${params.PLATFORM_VERSIONS} ${params.SGW_VERSION} ${params.CBL_TEST_NAME} ${params.TOPOLOGY_FILE}"
}
}
post {
always {
timeout(time: 5, unit: 'MINUTES') {
sh 'jenkins/pipelines/QE/multiplatform/teardown.sh'
}
archiveArtifacts artifacts: 'tests/QE/session.log', fingerprint: true, allowEmptyArchive: true
archiveArtifacts artifacts: 'tests/QE/http_log/*', fingerprint: true, allowEmptyArchive: true
}
}
}
post {
failure {
mail bcc: '', body: "Project: <a href='${env.BUILD_URL}'>${env.JOB_NAME}</a> has failed!", cc: '', charset: 'UTF-8', from: 'jenkins@couchbase.com', mimeType: 'text/html', replyTo: 'no-reply@couchbase.com', subject: "${env.JOB_NAME} failed", to: "vipul.bhardwaj@couchbase.com";
}
}
}
}
5 changes: 0 additions & 5 deletions jenkins/pipelines/QE/multiplatform/config_multiplatform.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
{
"$schema": "https://packages.couchbase.com/couchbase-lite/testserver.schema.json",
"test-servers": [],
"sync-gateways": [{"hostname": "{{test-client-ip}}", "tls": true}],
"couchbase-servers": [{"hostname": "{{test-client-ip}}"}],
"logslurp": "{{test-client-ip}}:8180",
"greenboard": {"hostname": "jenkins.mobiledev.couchbase.com", "username": "writer", "password": "couchbase2" },
"api-version": 1
}
Loading
Loading