Skip to content

Commit e811550

Browse files
authored
CSHARP-5930: Update replica-set discovery and reporting in tests (#1974)
See CSHARP-6008 for details about why this change is needed. There were multiple iterations getting to this point: ## Iteration 1 RequireServer.cs - ClusterType()/ClusterTypes(): Now use GetActualClusterType() which maps a directConnect RS member to ClusterType.ReplicaSet (instead of the misleading Standalone) - topology/topologies case: Uses actual server type — single matches only Standalone, replicaset matches IsReplicaSetMember() DriverTestConfiguration.cs - IsReplicaSet(): Checks server.Type.IsReplicaSetMember() instead of cluster.Description.Type == ClusterType.ReplicaSet, so a directConnect RS is correctly identified ReadPreferenceOnStandaloneTests.cs (from earlier) - Uses ServerType.Standalone instead of ClusterType.Standalone to decide whether $readPreference is expected ServerDiscoveryProseTests.cs - Missing secondary now throws SkipException instead of a plain Exception UnifiedTestSpecRunner.cs - Added IsDirectConnectionToReplicaSet() helper - ServerDiscoveryAndMonitoring: Skips logging-replicaset.json, replicaset-emit-topology-changed-before-close.json, rediscover-quickly-after-step-down.json when on directConnect RS (these require full multi-server topology discovery events) - ServerSelection: Skips replica-set.json when on directConnect RS (same reason) ## Iteration 2 - Root cause: My GetActualClusterType() change made RequireServer.ClusterType(ClusterType.ReplicaSet) pass for a directConnect RSPrimary. This caused Connection_pool_should_not_be_cleared_when_replSetStepDown_and_GetMore to run — a test that was previously always skipped on this setup. That test calls { replSetStepDown: 30, force: true }, which makes the single-node RS step down and refuse writes for up to 30 seconds. All subsequent write operations in other tests (like DropCollection in fixture setup) then fail with MongoNotPrimaryException. - Fix: Added RequireServer.ReplicaSetDataBearingMembers(int minimum) — a new check that counts how many data-bearing RS members the cluster has ## Iteration 3 - Applied .ReplicaSetDataBearingMembers(2) to Connection_pool_should_not_be_cleared_when_replSetStepDown_and_GetMore — the step-down test only makes sense (and is safe) with at least 2 RS members so a new primary can be elected after the step-down ## Iteration 4 - CausalConsistencyExamples.cs — Causal_Consistency_Example_3: Replaced the hardcoded "mongodb://localhost/?readPreference=secondaryPreferred" with settings built from CoreTestConfiguration.ConnectionString plus ReadPreference.SecondaryPreferred. The example intent is preserved. - ClientSideEncryption2Examples.cs — FLE2AutomaticEncryption: Added an explicit skip when CRYPT_SHARED_LIB_PATH is not set (CSFLE/auto-encryption requires it), and fixed two new MongoClient() calls to use CoreTestConfiguration.ConnectionString.ToString() instead of the default localhost:27017. Fix replica-set detection in tests to handle both directConnection=true and a normal connection to a single node replica set. - Renamed IsDirectConnectionToReplicaSet() → IsSingleNodeReplicaSet() — better reflects that it covers both connection modes. - Updated the logic: the old method required description.DirectConnection == true. The new method checks servers.Count == 1 && servers[0].Type.IsReplicaSetMember(), which is true for both: - directConnection=true to a RS member (old case) - replicaSet=rs0 on a single-node RS (new case) - Updated both call sites in ServerDiscoveryAndMonitoring and ServerSelection with updated comments. Update Causal_Consistency_Example_2 to skip because it needs two nodes CSHARP-5930: Address review feedback on replica-set topology helpers - GetActualClusterType: add SpinWait (10 s) in the DirectConnection path so the server type is known before reading it, avoiding spurious Unknown results on a fresh cluster. Throws InvalidOperationException if the timeout expires, matching the pattern in DriverTestConfiguration.IsReplicaSet. - IsRequirementSatisfied "topologies"/"topology" case: same SpinWait before reading the server type from the cluster description. - SkipNotSupportedTestCases: add optional `reason` parameter so callers can supply a specific message instead of the generic "not supported" text. - Single-node RS skips in UnifiedTestSpecRunner now emit messages that explicitly mention "single-node replica set" for searchable CI logs ("does not produce full RS topology discovery events" / "does not have a secondary"). CSHARP-5930: Address second-round review feedback - GetActualClusterType: replace InvalidOperationException with SkipException on spin-wait timeout, consistent with every other Require* helper that signals a skip rather than a hard failure when the topology is not as expected. - SkipNotSupportedTestCases: use StartsWith(operationName + ":") when operationName ends in ".json" so that a similarly-named spec file (e.g. "other-logging-replicaset.json") cannot accidentally match. Non-filename matches continue to use Contains as before. - ServerDiscoveryProseTests: add ReplicaSetDataBearingMembers(2) to the RequireServer chain so single-node RS is skipped at the require level. The secondary-not-found check below it is now a hard failure, because the require guard means we must be on a multi-node RS where a secondary is expected. CSHARP-5930: Address third-round review feedback - UnifiedTestSpecRunner: add CSHARP-6008 references to the single-node RS skip-block comments so the skips are auditable and findable. - UnifiedTestSpecRunner: split the dual-mode SkipNotSupportedTestCases into two clearly-named helpers: SkipFile (StartsWith match on filename, always requires an explicit reason) and SkipNotSupportedTestCases (Contains match on description substring, unchanged callers). - RequireServer.GetActualClusterType: add a one-line comment explaining why the non-direct-connection path reads Description.Type without a spin-wait. - RequireServer.IsTopologyMatch: add comment confirming that matching any IsReplicaSetMember() for "replicaset" is intentional per unified-spec semantics (Primary / Secondary / Arbiter / Other / Ghost all qualify). - DriverTestConfiguration.IsReplicaSet: add comment noting the broad RS- member match and directing callers that need a data-bearing member to use GetReplicaSetNumberOfDataBearingMembers instead. More review feedback [review-iter 1] Address review findings [review-iter 2] Address review findings [review-iter 3] Address review findings [review-iter 4] Address review findings [review-iter 5] Address review findings [review-iter 5] Address review findings Minor Copilot feedback - Copilot's concern is valid: if the spin times out, cluster.Description.Servers would likely be empty or all-Unknown, the factory would return false, and the Lazy<bool> would cache that false for the rest of the process — silently un-skipping logging-replicaset.json, replicaset-emit-topology-changed-before-close.json, rediscover-quickly-after-step-down.json, and replica-set.json on real single-node RS environments. - The fix captures the SpinWait.SpinUntil return and throws SkipException with the cluster description on timeout, mirroring the precedent in RequireServer.GetActualClusterType() at RequireServer.cs:325-328. The Lazy then caches the exception, so all subsequent calls in a broken-cluster process throw the same skip rather than spinning another 10s each. - Fix at RequireServer.cs:256-263: short-circuits when cluster.Description.DirectConnection && minimum > 1 so each affected test skips instantly instead of paying a 10-second timeout. The original directConnection comment is reworded into the new short-circuit branch. - Summary: ReplicaSetDataBearingMembers now no-ops on non-RS topologies instead of throwing. Effect on the three callers: - ConnectionsSurvivePrimaryStepDownTests.cs:80 — unchanged (already gated by .ClusterType(ReplicaSet)). - ServerDiscoveryProseTests.cs:55 — unchanged (already gated by .ClusterTypes(ReplicaSet)). - CausalConsistencyExamples.cs:78 — now correctly runs on Sharded (where mongos handles per-shard secondary routing for ReadPreference.Secondary) while still skipping on single-node RS. - Three changes in ServerDiscoveryProseTests.cs:62-77: 1. Wait on the right predicate: spin on Any(s => Connected && ReplicaSetSecondary) instead of ClusterState.Connected. The old check was satisfied by the primary alone and never actually waited for a secondary. 2. Longer timeout: 3s → 10s, matching the other SDAM spins in the codebase, reducing the slow-environment flakiness Copilot flagged. 3. Richer diagnostic: include clusterDescription in the failure message, so when this branch does fire it points at the real culprit (likely ReplicaSetDataBearingMembers undercounting). - Kept Exception rather than SkipException per the iter 2 reviewer's intent — added an inline comment so a future reader doesn't try to re-skip it. The stale "this case should be skipped" line in my old commit message was just out-of-date; iter 2 explicitly reverted that. CSHARP-5930: Address review feedback on replica-set topology helpers Consolidate the replica-set topology helpers into DriverTestConfiguration and remove fragile waits, per PR review. TL;DR - ReplicaSetDataBearingMembers now reuses DriverTestConfiguration.GetReplicaSetNumberOfDataBearingMembers, so it waits for the cluster to connect and then counts instead of spinning the full 10s when the required count can never be reached. - GetActualClusterType moved to DriverTestConfiguration and now throws InvalidOperationException (fail, not skip) when the cluster type cannot be determined. - Single-node-RS detection moved to DriverTestConfiguration.IsSingleNodeReplicaSet. - SkipFile identifies a test case's source spec file from the discoverer's "_fileName" value rather than parsing the composite test-case name. Details ReplicaSetDataBearingMembers (RequireServer): The previous implementation special-cased directConnection and then spun on `Servers.Count(IsDataBearing) >= minimum`, which always ran to the full timeout whenever the cluster simply did not have `minimum` data-bearing members (e.g. requiring 3 on a 2-member set, or any multi-member requirement under directConnection). It now delegates to DriverTestConfiguration.GetReplicaSetNumberOfDataBearingMembers, which waits for all servers to connect and returns the real count; the method then compares once and skips immediately. The directConnection case needs no special handling: a direct connection only ever sees its single server, so the count tops out at 1 and the comparison skips at once. GetActualClusterType moved to DriverTestConfiguration: The effective-cluster-type logic (mapping a directConnection server type to its real cluster type, with a short spin for early-startup races) now lives alongside the other cluster helpers in DriverTestConfiguration as an internal static taking the cluster. RequireServer keeps a thin delegating wrapper so its call sites are unchanged. When the type cannot be determined it now throws InvalidOperationException rather than SkipException, matching the existing IsReplicaSet/WaitForAllServersToBe- Connected convention: an undeterminable topology is an environment failure that should fail the test, not silently skip it. IsSingleNodeReplicaSet moved to DriverTestConfiguration: The single-node-RS detection used by the unified spec runner now lives in DriverTestConfiguration and builds on the existing IsReplicaSet spin (which throws if the type never resolves). The runner's process-lifetime Lazy cache is dropped; once the cluster is connected the answer is stable and the underlying spin returns immediately, so the cache added no value. SkipFile uses the discoverer's _fileName: UnifiedTestsDiscoverer already stamps each test case's shared document with "_fileName". SkipFile now matches on that value instead of StartsWith-parsing the composite "<file>:<description>" test-case name. The embedded-resource typo guard is retained so a misspelled file name throws loudly instead of silently skipping nothing. CSHARP-5930: Remove SkipFile embedded-resource typo guard SkipFile previously validated its fileName argument against an assembly-wide scan of embedded .json resources (the __embeddedJsonSpecResources Lazy) so a typo threw a precise error instead of silently skipping nothing. Now that SkipFile matches on the discoverer's "_fileName" value, drop the guard and the Lazy entirely. SkipFile only runs inside IsSingleNodeReplicaSet() blocks, so a mistyped fileName degrades to the file's tests running on single-node RS - the exact topology they are skipped for - which surfaces as a normal test failure rather than a silent pass. The reflection scan and ~18 lines of guard are not worth that marginally more precise diagnostic. Feedback.
1 parent 3a33efa commit e811550

7 files changed

Lines changed: 175 additions & 28 deletions

File tree

tests/MongoDB.Driver.Examples/CausalConsistencyExamples.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void Causal_Consistency_Example_1()
7575
[Fact]
7676
public void Causal_Consistency_Example_2()
7777
{
78-
RequireServer.Check().SupportsCausalConsistency();
78+
RequireServer.Check().SupportsCausalConsistency().ReplicaSetDataBearingMembers(2);
7979

8080
string testDatabaseName = "test";
8181
string itemsCollectionName = "items";
@@ -120,9 +120,10 @@ public void Causal_Consistency_Example_3()
120120
DropCollection(CreateClient(), "myDatabase", "myCollection");
121121

122122
// Start Tunable Consistency Controls Example
123-
var connectionString = "mongodb://localhost/?readPreference=secondaryPreferred";
123+
var settings = MongoClientSettings.FromConnectionString(CoreTestConfiguration.ConnectionString.ToString());
124+
settings.ReadPreference = ReadPreference.SecondaryPreferred;
124125

125-
var client = new MongoClient(connectionString);
126+
var client = new MongoClient(settings);
126127
var database = client.GetDatabase("myDatabase");
127128
var collection = database.GetCollection<BsonDocument>("myCollection");
128129

tests/MongoDB.Driver.Examples/ClientSideEncryption2Examples.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public class ClientSideEncryption2Examples
3838
public void FLE2AutomaticEncryption()
3939
{
4040
RequireServer.Check().Supports(Feature.Csfle2).ClusterTypes(ClusterType.ReplicaSet, ClusterType.Sharded, ClusterType.LoadBalanced);
41+
if (string.IsNullOrWhiteSpace(CoreTestConfiguration.GetCryptSharedLibPath()))
42+
{
43+
throw new Xunit.Sdk.SkipException("Test skipped because CRYPT_SHARED_LIB_PATH is not set.");
44+
}
4145

4246
var unencryptedClient = DriverTestConfiguration.Client;
4347

@@ -52,7 +56,7 @@ public void FLE2AutomaticEncryption()
5256
};
5357
kmsProviders.Add("local", localKey);
5458

55-
var keyVaultClient = new MongoClient();
59+
var keyVaultClient = new MongoClient(CoreTestConfiguration.ConnectionString.ToString());
5660

5761
// Create two data keys.
5862
var clientEncryptionOptions = new ClientEncryptionOptions(keyVaultClient, KeyVaultNamespace, kmsProviders);
@@ -92,7 +96,9 @@ public void FLE2AutomaticEncryption()
9296
};
9397

9498
var autoEncryptionOptions = new AutoEncryptionOptions(KeyVaultNamespace, kmsProviders, encryptedFieldsMap: encryptedFieldsMap);
95-
var encryptedClient = new MongoClient(new MongoClientSettings { AutoEncryptionOptions = autoEncryptionOptions });
99+
var encryptedClientSettings = MongoClientSettings.FromConnectionString(CoreTestConfiguration.ConnectionString.ToString());
100+
encryptedClientSettings.AutoEncryptionOptions = autoEncryptionOptions;
101+
var encryptedClient = new MongoClient(encryptedClientSettings);
96102

97103
// Create an FLE 2 collection.
98104
var database = encryptedClient.GetDatabase(CollectionNamespace.DatabaseNamespace.DatabaseName);

tests/MongoDB.Driver.TestHelpers/Core/XunitExtensions/RequireServer.cs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
using MongoDB.Driver.Core.Clusters;
2020
using MongoDB.Driver.Core.Misc;
2121
using MongoDB.Driver.Encryption;
22+
using MongoDB.Driver.Tests;
2223
using Xunit.Sdk;
24+
using ClusterTypeEnum = MongoDB.Driver.Core.Clusters.ClusterType;
2325

2426
namespace MongoDB.Driver.Core.TestHelpers.XunitExtensions
2527
{
@@ -67,7 +69,7 @@ internal RequireServer Cluster(Func<IClusterInternal, bool> condition, string be
6769

6870
public RequireServer ClusterType(ClusterType clusterType)
6971
{
70-
var actualClusterType = CoreTestConfiguration.Cluster.Description.Type;
72+
var actualClusterType = GetActualClusterType();
7173
if (actualClusterType == clusterType)
7274
{
7375
return this;
@@ -77,7 +79,7 @@ public RequireServer ClusterType(ClusterType clusterType)
7779

7880
public RequireServer ClusterTypes(params ClusterType[] clusterTypes)
7981
{
80-
var actualClusterType = CoreTestConfiguration.Cluster.Description.Type;
82+
var actualClusterType = GetActualClusterType();
8183
if (clusterTypes.Contains(actualClusterType))
8284
{
8385
return this;
@@ -172,13 +174,13 @@ public RequireServer Supports(params Feature[] features)
172174

173175
public RequireServer SupportsCausalConsistency()
174176
{
175-
return ClusterTypes(Clusters.ClusterType.Sharded, Clusters.ClusterType.ReplicaSet).SupportsSessions();
177+
return ClusterTypes(ClusterTypeEnum.Sharded, ClusterTypeEnum.ReplicaSet).SupportsSessions();
176178
}
177179

178180
public RequireServer SupportsSessions()
179181
{
180182
var clusterDescription = CoreTestConfiguration.Cluster.Description;
181-
if (clusterDescription.LogicalSessionTimeout != null || clusterDescription.Type == Clusters.ClusterType.LoadBalanced)
183+
if (clusterDescription.LogicalSessionTimeout != null || GetActualClusterType() == ClusterTypeEnum.LoadBalanced)
182184
{
183185
return this;
184186
}
@@ -220,8 +222,8 @@ public RequireServer Tls(bool required = true)
220222

221223
public RequireServer MultipleMongosesIfSharded(bool required)
222224
{
223-
var clusterType = CoreTestConfiguration.Cluster.Description.Type;
224-
if (clusterType == Clusters.ClusterType.Sharded || clusterType == Clusters.ClusterType.LoadBalanced)
225+
var clusterType = GetActualClusterType();
226+
if (clusterType == ClusterTypeEnum.Sharded || clusterType == ClusterTypeEnum.LoadBalanced)
225227
{
226228
MultipleMongoses(required);
227229
}
@@ -239,6 +241,31 @@ public RequireServer MultipleMongoses(bool required)
239241
throw new SkipException($"Test skipped because the cluster does not have multiple mongoses.");
240242
}
241243

244+
public RequireServer ReplicaSetDataBearingMembers(int minimum)
245+
{
246+
// Only meaningful on replica-set topologies; on sharded/standalone/load-balanced the
247+
// "data-bearing members" concept doesn't translate to a useful constraint. No-op on
248+
// non-RS so callers can chain this onto a multi-topology allowlist (e.g. after
249+
// SupportsCausalConsistency, which permits RS or Sharded) without inadvertently
250+
// skipping sharded runs. Callers who want RS-only must chain ClusterType(ReplicaSet)
251+
// explicitly.
252+
if (GetActualClusterType() != ClusterTypeEnum.ReplicaSet)
253+
{
254+
return this;
255+
}
256+
// GetReplicaSetNumberOfDataBearingMembers waits for the cluster to connect and then
257+
// counts, so it returns promptly with the real count rather than spinning the full
258+
// timeout when the count can never reach `minimum` (e.g. a directConnection only ever
259+
// sees its single targeted server, so the count tops out at 1).
260+
var cluster = CoreTestConfiguration.Cluster;
261+
var actualCount = DriverTestConfiguration.GetReplicaSetNumberOfDataBearingMembers(cluster);
262+
if (actualCount >= minimum)
263+
{
264+
return this;
265+
}
266+
throw new SkipException($"Test skipped because replica set has {actualCount} data-bearing member(s) and at least {minimum} are required: {cluster.Description}.");
267+
}
268+
242269
public RequireServer VersionGreaterThanOrEqualTo(SemanticVersion version)
243270
{
244271
var actualVersion = CoreTestConfiguration.ServerVersion;
@@ -285,6 +312,8 @@ public RequireServer VersionLessThanOrEqualTo(string version)
285312
}
286313

287314
// private methods
315+
private ClusterType GetActualClusterType() => DriverTestConfiguration.GetActualClusterType(CoreTestConfiguration.Cluster);
316+
288317
private bool CanRunOn(BsonDocument requirements)
289318
{
290319
return requirements.All(IsRequirementSatisfied);
@@ -335,9 +364,10 @@ private bool IsRequirementSatisfied(BsonElement requirement)
335364
return true;
336365
case "topologies":
337366
case "topology":
338-
var actualClusterType = CoreTestConfiguration.Cluster.Description.Type;
339-
var runOnClusterTypes = requirement.Value.AsBsonArray.Select(topology => MapTopologyToClusterType(topology.AsString)).ToList();
340-
return runOnClusterTypes.Contains(actualClusterType);
367+
{
368+
var actualClusterType = GetActualClusterType();
369+
return requirement.Value.AsBsonArray.Any(topology => IsTopologyMatch(topology.AsString, actualClusterType));
370+
}
341371
case "csfle":
342372
return IsCsfleRequirementSatisfied(requirement);
343373
default:
@@ -359,15 +389,15 @@ private bool IsCsfleRequirementSatisfied(BsonElement requirement)
359389
return SemanticVersionCompareToAsReleased(actualLibmongocryptVersion, minLibmongocryptVersion) >= 0;
360390
}
361391

362-
private ClusterType MapTopologyToClusterType(string topology)
392+
private bool IsTopologyMatch(string topology, ClusterTypeEnum clusterType)
363393
{
364394
switch (topology)
365395
{
366-
case "single": return Clusters.ClusterType.Standalone;
367-
case "replicaset": return Clusters.ClusterType.ReplicaSet;
396+
case "single": return clusterType == ClusterTypeEnum.Standalone;
397+
case "replicaset": return clusterType == ClusterTypeEnum.ReplicaSet;
368398
case "sharded-replicaset":
369-
case "sharded": return Clusters.ClusterType.Sharded;
370-
case "load-balanced": return Clusters.ClusterType.LoadBalanced;
399+
case "sharded": return clusterType == ClusterTypeEnum.Sharded;
400+
case "load-balanced": return clusterType == ClusterTypeEnum.LoadBalanced;
371401
default: throw new ArgumentException($"Invalid topology: \"{topology}\".", nameof(topology));
372402
}
373403
}

tests/MongoDB.Driver.TestHelpers/DriverTestConfiguration.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,19 @@ public static ExpressionTranslationOptions GetTranslationOptions()
223223
};
224224
}
225225

226+
// Returns true if any server reports an RS member type (Primary, Secondary, Arbiter,
227+
// Other, or Ghost). Callers that need specifically a data-bearing member should use
228+
// GetReplicaSetNumberOfDataBearingMembers instead.
226229
internal static bool IsReplicaSet(IClusterInternal cluster)
227230
{
228-
var clusterTypeIsKnown = SpinWait.SpinUntil(() => cluster.Description.Type != ClusterType.Unknown, TimeSpan.FromSeconds(10));
229-
if (!clusterTypeIsKnown)
231+
var serverIsKnown = SpinWait.SpinUntil(
232+
() => cluster.Description.Servers.Any(s => s.Type != ServerType.Unknown),
233+
TimeSpan.FromSeconds(10));
234+
if (!serverIsKnown)
230235
{
231236
throw new InvalidOperationException($"Unable to determine cluster type: {cluster.Description}.");
232237
}
233-
return cluster.Description.Type == ClusterType.ReplicaSet;
238+
return cluster.Description.Servers.Any(s => s.Type.IsReplicaSetMember());
234239
}
235240

236241
internal static int GetReplicaSetNumberOfDataBearingMembers(IClusterInternal cluster)
@@ -241,9 +246,58 @@ internal static int GetReplicaSetNumberOfDataBearingMembers(IClusterInternal clu
241246
}
242247

243248
WaitForAllServersToBeConnected(cluster);
249+
// Under a directConnect the description only includes the single node being targeted,
250+
// so this count reflects only that server. RequireServer.ReplicaSetDataBearingMembers(2)
251+
// therefore correctly skips tests that need multiple members when connected directly.
244252
return cluster.Description.Servers.Count(s => s.IsDataBearing);
245253
}
246254

255+
// Returns the effective cluster type. For direct connections Description.Type is always
256+
// Standalone regardless of the actual server type, so the server's reported type is used
257+
// instead. Throws (rather than skips) if the type cannot be determined: an undeterminable
258+
// topology is an environment failure, not a "this topology isn't applicable" skip.
259+
internal static ClusterType GetActualClusterType(IClusterInternal cluster)
260+
{
261+
var description = cluster.Description;
262+
if (description.DirectConnection)
263+
{
264+
var serverIsKnown = SpinWait.SpinUntil(
265+
() => cluster.Description.Servers.Any(s => s.Type != ServerType.Unknown),
266+
TimeSpan.FromSeconds(10));
267+
if (!serverIsKnown)
268+
{
269+
throw new InvalidOperationException($"Unable to determine cluster type: {cluster.Description}.");
270+
}
271+
return cluster.Description.Servers.First(s => s.Type != ServerType.Unknown).Type.ToClusterType();
272+
}
273+
// For non-direct connections the cluster machinery resolves Description.Type before tests
274+
// run, but spin briefly here as well so early-startup invocations don't race against the
275+
// initial SDAM rounds.
276+
var typeIsKnown = SpinWait.SpinUntil(
277+
() => cluster.Description.Type != ClusterType.Unknown,
278+
TimeSpan.FromSeconds(10));
279+
if (!typeIsKnown)
280+
{
281+
throw new InvalidOperationException($"Unable to determine cluster type: {cluster.Description}.");
282+
}
283+
return cluster.Description.Type;
284+
}
285+
286+
internal static bool IsSingleNodeReplicaSet(IClusterInternal cluster)
287+
{
288+
// IsReplicaSet spins until at least one server type is known (throwing if it never
289+
// resolves), so a transient empty / all-Unknown snapshot is never mistaken for "not a
290+
// replica set".
291+
if (!IsReplicaSet(cluster))
292+
{
293+
return false;
294+
}
295+
// Matches both a directConnection to a single RS member and a non-directConnection
296+
// single-node RS.
297+
var servers = cluster.Description.Servers;
298+
return servers.Count == 1 && servers[0].Type.IsReplicaSetMember();
299+
}
300+
247301
internal static void WaitForAllServersToBeConnected(IClusterInternal cluster)
248302
{
249303
var allServersAreConnected = SpinWait.SpinUntil(() => cluster.Description.Servers.All(s => s.State == ServerState.Connected), TimeSpan.FromSeconds(10));

tests/MongoDB.Driver.Tests/ConnectionsSurvivePrimaryStepDownTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public void Connection_pool_should_be_cleared_when_Shutdown_exceptions(
7676
[ParameterAttributeData]
7777
public void Connection_pool_should_not_be_cleared_when_replSetStepDown_and_GetMore([Values(false, true)] bool async)
7878
{
79-
RequireServer.Check().ClusterType(ClusterType.ReplicaSet);
79+
RequireServer.Check().ClusterType(ClusterType.ReplicaSet).ReplicaSetDataBearingMembers(2);
8080

8181
var eventCapturer = new EventCapturer().Capture<ConnectionPoolClearedEvent>();
8282
using (var client = CreateMongoClient(eventCapturer))

tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
using System;
1717
using System.Collections.Generic;
18+
using System.Linq;
1819
using FluentAssertions;
1920
using MongoDB.Bson.TestHelpers.JsonDrivenTests;
2021
using MongoDB.Driver.Core.Clusters;
@@ -210,11 +211,34 @@ public void RetryableWrites(JsonDrivenTestCase testCase)
210211
[Category("SDAM", "SupportLoadBalancing")]
211212
[UnifiedTestsTheory("server_discovery_and_monitoring.tests.unified")]
212213
public void ServerDiscoveryAndMonitoring(JsonDrivenTestCase testCase)
213-
=> Run(testCase, IsSdamLogValid, new SdamRunnerEventsProcessor(testCase.Name));
214+
{
215+
// These tests require full RS topology discovery (multiple topology change events).
216+
// A single-node RS (whether via directConnect or replicaSet= with one member) only fires 2 topology
217+
// events, never the full RS discovery sequence with secondary discovery. CSHARP-6008.
218+
if (IsSingleNodeReplicaSet())
219+
{
220+
const string singleNodeRsReason = "Test skipped because single-node replica set does not produce full RS topology discovery events (CSHARP-6008).";
221+
SkipFile(testCase, "logging-replicaset.json", singleNodeRsReason);
222+
SkipFile(testCase, "replicaset-emit-topology-changed-before-close.json", singleNodeRsReason);
223+
SkipFile(testCase, "rediscover-quickly-after-step-down.json", singleNodeRsReason);
224+
}
225+
226+
Run(testCase, IsSdamLogValid, new SdamRunnerEventsProcessor(testCase.Name));
227+
}
214228

215229
[Category("SupportLoadBalancing")]
216230
[UnifiedTestsTheory("server_selection.tests.logging")]
217-
public void ServerSelection(JsonDrivenTestCase testCase) => Run(testCase);
231+
public void ServerSelection(JsonDrivenTestCase testCase)
232+
{
233+
// replica-set.json expects full RS topology discovery events and a secondary, not available on a
234+
// single-node RS (whether via directConnect or replicaSet= with one member). CSHARP-6008.
235+
if (IsSingleNodeReplicaSet())
236+
{
237+
SkipFile(testCase, "replica-set.json", "Test skipped because single-node replica set does not have a secondary (CSHARP-6008).");
238+
}
239+
240+
Run(testCase);
241+
}
218242

219243
[UnifiedTestsTheory("sessions.tests")]
220244
public void Sessions(JsonDrivenTestCase testCase) => Run(testCase);
@@ -277,6 +301,25 @@ private void Run(JsonDrivenTestCase testCase, Predicate<LogEntry> loggingFilter
277301
private static void RequireKmsMock() =>
278302
RequireEnvironment.Check().EnvironmentVariable("KMS_MOCK_SERVERS_ENABLED");
279303

304+
private static bool IsSingleNodeReplicaSet() => DriverTestConfiguration.IsSingleNodeReplicaSet(CoreTestConfiguration.Cluster);
305+
306+
307+
/// <summary>
308+
/// Skip all tests sourced from a specific JSON spec file. The source file is identified via
309+
/// the "_fileName" value the discoverer stamps onto every test case (see
310+
/// UnifiedTestsDiscoverer), rather than by parsing the composite test-case name.
311+
/// </summary>
312+
private static void SkipFile(JsonDrivenTestCase testCase, string fileName, string reason)
313+
{
314+
if (testCase.Shared.GetValue("_fileName", null)?.AsString == fileName)
315+
{
316+
throw new SkipException(reason);
317+
}
318+
}
319+
320+
/// <summary>
321+
/// Skip tests whose name contains the given operation substring.
322+
/// </summary>
280323
private static void SkipNotSupportedTestCases(JsonDrivenTestCase testCase, string operationName)
281324
{
282325
if (testCase.Name.Contains(operationName))
@@ -285,7 +328,9 @@ private static void SkipNotSupportedTestCases(JsonDrivenTestCase testCase, strin
285328
}
286329
}
287330

288-
// used by SkippedTestsProvider property in UnifiedTests attribute.
331+
/// <summary>
332+
/// Used by SkippedTestsProvider property in UnifiedTests attribute.
333+
/// </summary>
289334
private static readonly HashSet<string> __ignoredTests = new(
290335
[
291336
// CMAP

0 commit comments

Comments
 (0)