diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 38d6d67..6985875 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -179,6 +179,44 @@ 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' 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") + 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 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..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); @@ -144,4 +128,98 @@ 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 + var queryStatus = await PollUntilReadyAsync(handle, queryOptions.QueryTimeout ?? TimeSpan.FromSeconds(30)); + + Assert.NotNull(queryStatus); + Assert.True(queryStatus!.ResultsReady); + + // 3. Fetch results + var resultHandle = queryStatus.ResultHandle(); + Assert.NotNull(resultHandle); + + await using 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); + + var queryStatus = await PollUntilReadyAsync(handle, queryOptions.QueryTimeout ?? TimeSpan.FromSeconds(30)); + + Assert.NotNull(queryStatus); + Assert.True(queryStatus!.ResultsReady); + + var resultHandle = queryStatus.ResultHandle(); + await using var results = await resultHandle.FetchResultsAsync(new FetchResultsOptions()); + + // Consume all rows + var count = 0; + await foreach (var row in results.Rows) + { + count++; + } + + // Verify row count matches metrics + Assert.Equal(99, 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); + } + + /// + /// 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/Fixtures/FixtureSettings.cs b/tests/Couchbase.Analytics.FunctionalTests/Fixtures/FixtureSettings.cs index 305f6dc..c2083e3 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; } = "Default"; + + [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..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); @@ -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;"; + + await using 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;"; + + await using 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); + } }