Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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