From 3c67f278e8db45d9810430f198cd80210b993cf8 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Apr 2026 13:00:40 -0600 Subject: [PATCH 1/5] NCO-58: Add more functional testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scope-level streaming and blocking tests for the sync API - Add scope-level end-to-end test for the async API (start→poll→fetch) - Add async metadata verification test at cluster level - Provision bare dataverse in CI workflow for scope-level queries --- .github/workflows/functional-tests.yml | 9 ++ .../AsyncAnalyticsTests.cs | 90 +++++++++++++++++++ .../Fixtures/FixtureSettings.cs | 6 ++ .../Fixtures/SimpleFixture.cs | 2 + .../Internal/AnalyticsServiceTests.cs | 36 ++++++++ 5 files changed, 143 insertions(+) diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 38d6d67..dccc0be 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -179,6 +179,15 @@ jobs: EOF echo "settings.json written:" && cat tests/Couchbase.Analytics.FunctionalTests/settings.json + - name: Create test dataverse for scope-level queries + run: | + echo "Creating test dataverse testdb.testscope for scope-level functional tests" + curl -sSf -k -u "${CBDINO_USER}:${CBDINO_PASS}" \ + "${CBDINO_CONNSTR}/api/v1/request" \ + --data-urlencode 'statement=CREATE DATAVERSE testdb.testscope IF NOT EXISTS;' + echo "" + echo "Dataverse created successfully" + - name: Build and Run Functional Tests run: | dotnet test tests/Couchbase.Analytics.FunctionalTests/Couchbase.Analytics.FunctionalTests.csproj \ diff --git a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs index c2ccc2f..0be5731 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs @@ -144,4 +144,94 @@ await Assert.ThrowsAsync(async () => await resultHandle.FetchResultsAsync(new FetchResultsOptions()); }); } + + [Fact] + public async Task Test_AsyncAnalytics_EndToEnd_Scope() + { + var statement = "select i from array_range(1, 10) as i;"; + var queryOptions = new StartQueryOptions() + { + QueryTimeout = TimeSpan.FromSeconds(30) + }; + + // 1. Start the query via scope + var handle = await _simpleFixture.TestScope.StartQueryAsync(statement, queryOptions); + Assert.NotNull(handle); + + // 2. Poll for the query status + QueryStatus? queryStatus = null; + for (var i = 0; i < 20; i++) + { + queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); + if (queryStatus.ResultsReady) + { + break; + } + await Task.Delay(500); + } + + Assert.NotNull(queryStatus); + Assert.True(queryStatus!.ResultsReady); + + // 3. Fetch results + var resultHandle = queryStatus.ResultHandle(); + Assert.NotNull(resultHandle); + + var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + Assert.NotNull(results); + + var count = 0; + await foreach (var row in results.Rows) + { + count++; + } + + Assert.Equal(9, count); + Assert.Equal(9, results.MetaData.Metrics?.ResultCount); + } + + [Fact] + public async Task Test_AsyncAnalytics_Metadata_Cluster() + { + var statement = "select i from array_range(1, 100) as i;"; + var queryOptions = new StartQueryOptions() + { + QueryTimeout = TimeSpan.FromSeconds(30) + }; + + var handle = await _simpleFixture.Cluster.StartQueryAsync(statement, queryOptions); + Assert.NotNull(handle); + + QueryStatus? queryStatus = null; + for (var i = 0; i < 20; i++) + { + queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); + _outputHelper.WriteLine($"Status: {queryStatus}"); + if (queryStatus.ResultsReady) + { + break; + } + await Task.Delay(500); + } + + Assert.NotNull(queryStatus); + Assert.True(queryStatus!.ResultsReady); + + var resultHandle = queryStatus.ResultHandle(); + var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + + // Consume all rows + var count = 0; + await foreach (var row in results.Rows) + { + count++; + } + + // Verify metrics + Assert.NotNull(results.MetaData); + Assert.NotNull(results.MetaData.Metrics); + Assert.Equal(99, results.MetaData.Metrics!.ResultCount); + Assert.NotNull(results.MetaData.Metrics.ElapsedTime); + Assert.NotNull(results.MetaData.Metrics.ExecutionTime); + } } diff --git a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs index 305f6dc..fd489c5 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs @@ -36,4 +36,10 @@ public class FixtureSettings [JsonPropertyName("ClientKeyPath2")] public string? ClientKeyPath2 { get; set; } + + [JsonPropertyName("TestDatabase")] + public string TestDatabase { get; set; } = "testdb"; + + [JsonPropertyName("TestScope")] + public string TestScope { get; set; } = "testscope"; } diff --git a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/SimpleFixture.cs b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/SimpleFixture.cs index 961c265..0516b10 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/SimpleFixture.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/SimpleFixture.cs @@ -20,6 +20,8 @@ public SimpleFixture() public Credential Credential { get; private set; } + public Scope TestScope => Cluster.Database(FixtureSettings.TestDatabase).Scope(FixtureSettings.TestScope); + public string CapellaCaCert = "-----BEGIN CERTIFICATE-----\n" + "MIIFWzCCA0OgAwIBAgIBATANBgkqhkiG9w0BAQsFADA4MTYwNAYDVQQDEy1kaW5v\n" + diff --git a/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs b/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs index 0348d36..ef873ff 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs @@ -153,4 +153,40 @@ public async Task Test_Cancellation_Works_Blocking() await Assert.ThrowsAsync(async () => await task.ConfigureAwait(false)); } + + [Fact] + public async Task Test_Streaming_Query_Scope() + { + var statement = "select i from array_range(1, 10) as i;"; + + var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, + new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = true }); + + var count = 0; + await foreach (var row in result.Rows) + { + count++; + } + + Assert.Equal(9, count); + Assert.Equal(9, result.MetaData.Metrics!.ResultCount); + } + + [Fact] + public async Task Test_Blocking_Query_Scope() + { + var statement = "select i from array_range(1, 10) as i;"; + + var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, + new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = false }); + + var count = 0; + await foreach (var row in result.Rows) + { + count++; + } + + Assert.Equal(9, count); + Assert.Equal(9, result.MetaData.Metrics!.ResultCount); + } } From 5da08ab8effc60ae9c9d913ebc3367138aa1a30d Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Apr 2026 13:14:48 -0600 Subject: [PATCH 2/5] fix workflow --- .github/workflows/functional-tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index dccc0be..71fac5e 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -182,9 +182,10 @@ jobs: - name: Create test dataverse for scope-level queries run: | echo "Creating test dataverse testdb.testscope for scope-level functional tests" - curl -sSf -k -u "${CBDINO_USER}:${CBDINO_PASS}" \ - "${CBDINO_CONNSTR}/api/v1/request" \ - --data-urlencode 'statement=CREATE DATAVERSE testdb.testscope IF NOT EXISTS;' + curl -sS -k -u "${CBDINO_USER}:${CBDINO_PASS}" \ + -H "Content-Type: application/json" \ + -d '{"statement": "CREATE DATAVERSE testdb.testscope IF NOT EXISTS;"}' \ + "${CBDINO_CONNSTR}/api/v1/request" echo "" echo "Dataverse created successfully" From 2e4f854568936c7ee624db0919974c40521918a4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Apr 2026 13:20:08 -0600 Subject: [PATCH 3/5] create dataverse correctly --- .github/workflows/functional-tests.yml | 4 ++-- .../Fixtures/FixtureSettings.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 71fac5e..4f0f8f2 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -181,10 +181,10 @@ jobs: - name: Create test dataverse for scope-level queries run: | - echo "Creating test dataverse testdb.testscope for scope-level functional tests" + echo "Creating test dataverse 'testscope' (under Default database) for scope-level functional tests" curl -sS -k -u "${CBDINO_USER}:${CBDINO_PASS}" \ -H "Content-Type: application/json" \ - -d '{"statement": "CREATE DATAVERSE testdb.testscope IF NOT EXISTS;"}' \ + -d '{"statement": "CREATE DATAVERSE testscope IF NOT EXISTS;"}' \ "${CBDINO_CONNSTR}/api/v1/request" echo "" echo "Dataverse created successfully" diff --git a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs index fd489c5..c2083e3 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs @@ -38,7 +38,7 @@ public class FixtureSettings public string? ClientKeyPath2 { get; set; } [JsonPropertyName("TestDatabase")] - public string TestDatabase { get; set; } = "testdb"; + public string TestDatabase { get; set; } = "Default"; [JsonPropertyName("TestScope")] public string TestScope { get; set; } = "testscope"; From 29d427fb3a40d992517fc703fa1fa5294f7351be Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Apr 2026 13:35:02 -0600 Subject: [PATCH 4/5] add readiness probe before starting functional tests --- .github/workflows/functional-tests.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 4f0f8f2..4f269b8 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -179,6 +179,27 @@ jobs: EOF echo "settings.json written:" && cat tests/Couchbase.Analytics.FunctionalTests/settings.json + - name: Wait for analytics service to be ready + run: | + echo "Polling analytics service until it accepts queries..." + for i in $(seq 1 60); do + HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" -k \ + -u "${CBDINO_USER}:${CBDINO_PASS}" \ + -H "Content-Type: application/json" \ + -d '{"statement": "SELECT 1;"}' \ + "${CBDINO_CONNSTR}/api/v1/request" 2>/dev/null || echo "000") + if [ "$HTTP_CODE" = "200" ]; then + echo "Analytics service is ready (attempt $i)" + break + fi + echo "Attempt $i/60: HTTP $HTTP_CODE — waiting 5s..." + sleep 5 + done + if [ "$HTTP_CODE" != "200" ]; then + echo "ERROR: Analytics service did not become ready after 5 minutes" + exit 1 + fi + - name: Create test dataverse for scope-level queries run: | echo "Creating test dataverse 'testscope' (under Default database) for scope-level functional tests" From 3ad752e564a93b3e6d0f55ea90507c2cd61e19a3 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 27 Apr 2026 21:00:13 -0600 Subject: [PATCH 5/5] PR feedback --- .github/workflows/functional-tests.yml | 13 ++- .../AsyncAnalyticsTests.cs | 82 ++++++++----------- .../Internal/AnalyticsServiceTests.cs | 10 +-- 3 files changed, 50 insertions(+), 55 deletions(-) diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 4f269b8..6985875 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -202,12 +202,19 @@ jobs: - name: Create test dataverse for scope-level queries run: | - echo "Creating test dataverse 'testscope' (under Default database) for scope-level functional tests" - curl -sS -k -u "${CBDINO_USER}:${CBDINO_PASS}" \ + echo "Creating test dataverse 'testscope' for scope-level functional tests" + HTTP_CODE=$(curl -sS -o /tmp/dataverse-response.json -w "%{http_code}" -k \ + -u "${CBDINO_USER}:${CBDINO_PASS}" \ -H "Content-Type: application/json" \ -d '{"statement": "CREATE DATAVERSE testscope IF NOT EXISTS;"}' \ - "${CBDINO_CONNSTR}/api/v1/request" + "${CBDINO_CONNSTR}/api/v1/request") + echo "HTTP status: $HTTP_CODE" + cat /tmp/dataverse-response.json echo "" + if [ "$HTTP_CODE" != "200" ]; then + echo "ERROR: Dataverse creation failed with HTTP $HTTP_CODE" + exit 1 + fi echo "Dataverse created successfully" - name: Build and Run Functional Tests diff --git a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs index 0be5731..52feb12 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/AsyncAnalyticsTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Couchbase.AnalyticsClient.Async; using Couchbase.AnalyticsClient.Exceptions; using Couchbase.AnalyticsClient.FunctionalTests.Fixtures; @@ -33,17 +34,7 @@ public async Task Test_AsyncAnalytics_EndToEnd_Cluster() Assert.NotNull(handle); // 2. Poll for the query status - QueryStatus? queryStatus = null; - for (var i = 0; i < 20; i++) - { - queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); - _outputHelper.WriteLine($"Status: {queryStatus}"); - if (queryStatus.ResultsReady) - { - break; - } - await Task.Delay(500); - } + var queryStatus = await PollUntilReadyAsync(handle, queryOptions.QueryTimeout ?? TimeSpan.FromSeconds(30)); Assert.NotNull(queryStatus); Assert.True(queryStatus!.ResultsReady); @@ -53,7 +44,7 @@ public async Task Test_AsyncAnalytics_EndToEnd_Cluster() Assert.NotNull(resultHandle); // 4. Fetch the results - var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + await using var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); Assert.NotNull(results); var count = 0; @@ -86,7 +77,8 @@ public async Task Test_AsyncAnalytics_Cancellation_Cluster() // It's possible the cancel takes a brief moment to process gracefully on the server. var ex = await Record.ExceptionAsync(async () => { - for (var i = 0; i < 20; i++) + var deadline = Stopwatch.StartNew(); + while (deadline.Elapsed < queryOptions.QueryTimeout) { var queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); if (queryStatus.ResultsReady) @@ -112,19 +104,11 @@ public async Task Test_AsyncAnalytics_Cancellation_Cluster() public async Task Test_AsyncAnalytics_DiscardResults_Cluster() { var statement = "select i from array_range(1, 5) as i;"; - var handle = await _simpleFixture.Cluster.StartQueryAsync(statement, new StartQueryOptions()); + var queryOptions = new StartQueryOptions(); + var handle = await _simpleFixture.Cluster.StartQueryAsync(statement, queryOptions); // Poll for the query status - QueryStatus? queryStatus = null; - for (var i = 0; i < 20; i++) - { - queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); - if (queryStatus.ResultsReady) - { - break; - } - await Task.Delay(500); - } + var queryStatus = await PollUntilReadyAsync(handle, TimeSpan.FromSeconds(30)); Assert.NotNull(queryStatus); Assert.True(queryStatus!.ResultsReady); @@ -159,16 +143,7 @@ public async Task Test_AsyncAnalytics_EndToEnd_Scope() Assert.NotNull(handle); // 2. Poll for the query status - QueryStatus? queryStatus = null; - for (var i = 0; i < 20; i++) - { - queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); - if (queryStatus.ResultsReady) - { - break; - } - await Task.Delay(500); - } + var queryStatus = await PollUntilReadyAsync(handle, queryOptions.QueryTimeout ?? TimeSpan.FromSeconds(30)); Assert.NotNull(queryStatus); Assert.True(queryStatus!.ResultsReady); @@ -177,7 +152,7 @@ public async Task Test_AsyncAnalytics_EndToEnd_Scope() var resultHandle = queryStatus.ResultHandle(); Assert.NotNull(resultHandle); - var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + await using var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); Assert.NotNull(results); var count = 0; @@ -202,23 +177,13 @@ public async Task Test_AsyncAnalytics_Metadata_Cluster() var handle = await _simpleFixture.Cluster.StartQueryAsync(statement, queryOptions); Assert.NotNull(handle); - QueryStatus? queryStatus = null; - for (var i = 0; i < 20; i++) - { - queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); - _outputHelper.WriteLine($"Status: {queryStatus}"); - if (queryStatus.ResultsReady) - { - break; - } - await Task.Delay(500); - } + var queryStatus = await PollUntilReadyAsync(handle, queryOptions.QueryTimeout ?? TimeSpan.FromSeconds(30)); Assert.NotNull(queryStatus); Assert.True(queryStatus!.ResultsReady); var resultHandle = queryStatus.ResultHandle(); - var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + await using var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); // Consume all rows var count = 0; @@ -227,6 +192,9 @@ public async Task Test_AsyncAnalytics_Metadata_Cluster() count++; } + // Verify row count matches metrics + Assert.Equal(99, count); + // Verify metrics Assert.NotNull(results.MetaData); Assert.NotNull(results.MetaData.Metrics); @@ -234,4 +202,24 @@ public async Task Test_AsyncAnalytics_Metadata_Cluster() Assert.NotNull(results.MetaData.Metrics.ElapsedTime); Assert.NotNull(results.MetaData.Metrics.ExecutionTime); } + + /// + /// Polls the query handle until results are ready or the deadline is reached. + /// + private async Task PollUntilReadyAsync(QueryHandle handle, TimeSpan timeout) + { + var deadline = Stopwatch.StartNew(); + QueryStatus? queryStatus = null; + while (deadline.Elapsed < timeout) + { + queryStatus = await handle.FetchStatusAsync(new FetchStatusOptions()); + _outputHelper.WriteLine($"Status: {queryStatus}"); + if (queryStatus.ResultsReady) + { + break; + } + await Task.Delay(500); + } + return queryStatus; + } } diff --git a/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs b/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs index ef873ff..5e55a59 100644 --- a/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs +++ b/tests/Couchbase.Analytics.FunctionalTests/Internal/AnalyticsServiceTests.cs @@ -42,7 +42,7 @@ public async Task Test_Streaming_Query() { var statement = "select i from array_range(1, 100) as i;"; - var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, + await using var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = true }); await foreach (var row in result.Rows) @@ -63,7 +63,7 @@ public async Task Test_Blocking_Query() { var statement = "select i from array_range(1, 100) as i;"; - var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, + await using var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = false }); await foreach (var row in result.Rows) @@ -96,7 +96,7 @@ public async Task Test_Query_Metadata_And_Metrics() var statement = "select i from array_range(1, 100) as i;"; - var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, + await using var result = await _simpleFixture.Cluster.ExecuteQueryAsync(statement, new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = false }); Assert.NotNull(result.MetaData); @@ -159,7 +159,7 @@ public async Task Test_Streaming_Query_Scope() { var statement = "select i from array_range(1, 10) as i;"; - var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, + await using var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = true }); var count = 0; @@ -177,7 +177,7 @@ public async Task Test_Blocking_Query_Scope() { var statement = "select i from array_range(1, 10) as i;"; - var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, + await using var result = await _simpleFixture.TestScope.ExecuteQueryAsync(statement, new QueryOptions() { Timeout = TimeSpan.FromSeconds(10), AsStreaming = false }); var count = 0;