docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ArtifactDiff"/>: the structured per-line Myers code diff
|
||||
/// embedded in <c>FieldDiffJson</c> (T20) and the M8 Site / DataConnection /
|
||||
/// embedded in <c>FieldDiffJson</c> (T20) and the Site / DataConnection /
|
||||
/// Instance compares. The diff records are private, so assertions parse the
|
||||
/// serialized <see cref="ImportPreviewItem.FieldDiffJson"/>.
|
||||
/// </summary>
|
||||
@@ -61,6 +61,7 @@ public sealed class ArtifactDiffTests
|
||||
|
||||
// ============ T20: structured per-line code diff ============
|
||||
|
||||
/// <summary>Verifies that a modified shared script emits a structured line diff.</summary>
|
||||
[Fact]
|
||||
public void SharedScript_ModifiedCode_EmitsStructuredLineDiff()
|
||||
{
|
||||
@@ -94,6 +95,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.DoesNotContain(hunks, h => h.Text.Contains("lines>"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a shared script with identical code reports no change.</summary>
|
||||
[Fact]
|
||||
public void SharedScript_IdenticalCode_NoChange()
|
||||
{
|
||||
@@ -156,7 +158,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal(ConflictKind.Identical, item.Kind);
|
||||
}
|
||||
|
||||
// ============ arch-review 2026-08-19 P7-1: TimeoutSeconds in the diff (row-27 shape) ============
|
||||
// ============ TimeoutSeconds in the diff ============
|
||||
|
||||
/// <summary>
|
||||
/// The bundle has always CARRIED TimeoutSeconds and BundleImporter has always APPLIED it
|
||||
@@ -220,6 +222,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal(ConflictKind.Identical, item.Kind);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a modified API method script emits a structured line diff.</summary>
|
||||
[Fact]
|
||||
public void ApiMethod_ModifiedScript_EmitsStructuredLineDiff()
|
||||
{
|
||||
@@ -244,6 +247,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Contains(("add", "log(\"b\");"), hunks);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that modified template script code emits a structured line diff.</summary>
|
||||
[Fact]
|
||||
public void TemplateScript_ModifiedCode_EmitsStructuredLineDiff()
|
||||
{
|
||||
@@ -265,8 +269,9 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Contains(("add", "new2"), hunks);
|
||||
}
|
||||
|
||||
// ============ #05-T23: placeholder-identity resolution via name maps ============
|
||||
// ============ placeholder-identity resolution via name maps ============
|
||||
|
||||
/// <summary>Verifies that templates with matching folder, base and composition names compare as identical.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_MatchingFolderBaseAndComposition_IsIdentical()
|
||||
{
|
||||
@@ -295,6 +300,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal(ConflictKind.Identical, item.Kind);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a divergent base template name makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_DivergentBaseTemplateName_IsModified()
|
||||
{
|
||||
@@ -311,8 +317,9 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal(ConflictKind.Modified, item.Kind);
|
||||
}
|
||||
|
||||
// ============ #05-T6: single-source child equality — diff sees the full field set ============
|
||||
// ============ single-source child equality — diff sees the full field set ============
|
||||
|
||||
/// <summary>Verifies that a change to an attribute's element data type makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_AttributeElementDataTypeChange_IsModified()
|
||||
{
|
||||
@@ -334,6 +341,7 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Attributes.L"); // ElementDataType diverged — previously missed.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a change to the locked-in-derived flag makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_LockedInDerivedChange_IsModified()
|
||||
{
|
||||
@@ -355,6 +363,7 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Attributes.L"); // LockedInDerived diverged — previously missed.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a change to a script's execution timeout makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_ScriptExecutionTimeoutChange_IsModified()
|
||||
{
|
||||
@@ -372,6 +381,7 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Scripts.S"); // ExecutionTimeoutSeconds diverged — previously missed.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a change to an alarm's on-trigger script makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_AlarmOnTriggerScriptChange_IsModified()
|
||||
{
|
||||
@@ -396,6 +406,7 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Alarms.A"); // OnTriggerScriptName diverged — previously missed.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a change to an alarm's MES-reportable flag makes the template compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareTemplate_AlarmIsMesReportableChange_IsModified()
|
||||
{
|
||||
@@ -418,6 +429,7 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Alarms.A"); // IsMesReportable diverged.
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a change to an instance alarm override's MES-reportable flag makes the instance compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_AlarmOverrideIsMesReportableChange_IsModified()
|
||||
{
|
||||
@@ -443,8 +455,9 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("<modified>", modified.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
// ============ M8: CompareSite ============
|
||||
// ============ CompareSite ============
|
||||
|
||||
/// <summary>Verifies that comparing a site against no existing site yields a new result.</summary>
|
||||
[Fact]
|
||||
public void CompareSite_ExistingNull_New()
|
||||
{
|
||||
@@ -457,6 +470,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Null(item.FieldDiffJson);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site with a differing field compares as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareSite_FieldDiffers_Modified()
|
||||
{
|
||||
@@ -471,6 +485,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("new", change.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a site with no changes compares as identical.</summary>
|
||||
[Fact]
|
||||
public void CompareSite_NoChanges_Identical()
|
||||
{
|
||||
@@ -490,8 +505,9 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Null(item.FieldDiffJson);
|
||||
}
|
||||
|
||||
// ============ M8: CompareDataConnection ============
|
||||
// ============ CompareDataConnection ============
|
||||
|
||||
/// <summary>Verifies that comparing a data connection against no existing connection yields a new result.</summary>
|
||||
[Fact]
|
||||
public void CompareDataConnection_ExistingNull_New()
|
||||
{
|
||||
@@ -503,6 +519,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("opc1", item.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a data connection with a different protocol compares as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareDataConnection_ProtocolDiffers_Modified()
|
||||
{
|
||||
@@ -517,6 +534,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("MxAccess", change.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a changed secret value with preserved presence is reported without echoing the secret.</summary>
|
||||
[Fact]
|
||||
public void CompareDataConnection_SecretValueChangesButPresencePreserved_NoSecretEcho()
|
||||
{
|
||||
@@ -541,6 +559,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.DoesNotContain("4840", item.FieldDiffJson ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flip in secret presence is shown only as a present marker, without the secret value.</summary>
|
||||
[Fact]
|
||||
public void CompareDataConnection_SecretPresenceFlips_ShowsPresentMarkerOnly()
|
||||
{
|
||||
@@ -577,6 +596,7 @@ public sealed class ArtifactDiffTests
|
||||
? new SecretsBlock(new Dictionary<string, string> { ["AuthToken"] = "wire-token" })
|
||||
: null);
|
||||
|
||||
/// <summary>Verifies that comparing an SMS configuration against no existing configuration yields a new result.</summary>
|
||||
[Fact]
|
||||
public void CompareSmsConfiguration_ExistingNull_New()
|
||||
{
|
||||
@@ -587,6 +607,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("AC1", item.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an SMS configuration whose fields and secret presence all match compares as identical.</summary>
|
||||
[Fact]
|
||||
public void CompareSmsConfiguration_AllFieldsAndPresenceMatch_Identical()
|
||||
{
|
||||
@@ -596,6 +617,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Null(item.FieldDiffJson);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an SMS configuration with a different from-number compares as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareSmsConfiguration_FromNumberDiffers_Modified()
|
||||
{
|
||||
@@ -607,6 +629,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("+15559999999", change.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a changed auth token value with preserved presence is reported without echoing the secret.</summary>
|
||||
[Fact]
|
||||
public void CompareSmsConfiguration_AuthTokenValueChangesButPresencePreserved_NoSecretEcho()
|
||||
{
|
||||
@@ -620,6 +643,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.DoesNotContain("wire-token", json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a flip in auth token presence is shown only as a present marker, without the secret value.</summary>
|
||||
[Fact]
|
||||
public void CompareSmsConfiguration_AuthTokenPresenceFlips_ShowsPresentMarkerOnly()
|
||||
{
|
||||
@@ -634,8 +658,9 @@ public sealed class ArtifactDiffTests
|
||||
Assert.DoesNotContain("stored-token", item.FieldDiffJson!);
|
||||
}
|
||||
|
||||
// ============ M8: CompareInstance ============
|
||||
// ============ CompareInstance ============
|
||||
|
||||
/// <summary>Verifies that comparing an instance against no existing instance yields a new result.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_ExistingNull_New()
|
||||
{
|
||||
@@ -647,6 +672,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("inst-1", item.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance with a differing state compares as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_StateDiffers_Modified()
|
||||
{
|
||||
@@ -661,6 +687,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("Enabled", change.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an instance with no changes compares as identical.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_NoChanges_Identical()
|
||||
{
|
||||
@@ -673,6 +700,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Null(item.FieldDiffJson);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that differing template and site names make the instance compare as modified.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_TemplateAndSiteNamesDiffer_Modified()
|
||||
{
|
||||
@@ -689,6 +717,7 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Equal("site-b", ChangeFor(item, "SiteIdentifier").GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that added, removed and modified child overrides are reported for an instance.</summary>
|
||||
[Fact]
|
||||
public void CompareInstance_ChildOverridesAddedRemovedModified()
|
||||
{
|
||||
|
||||
@@ -69,7 +69,10 @@ public sealed class BundleImporterLoadTests
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
/// <summary>Initializes the provider at the given start time.</summary>
|
||||
/// <param name="start">The initial UTC time.</param>
|
||||
public TestTimeProvider(DateTimeOffset start) { _now = start; }
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
}
|
||||
|
||||
@@ -159,6 +162,8 @@ public sealed class BundleImporterLoadTests
|
||||
return serializer.Pack(content, manifest, passphrase, encryptor);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading an unencrypted bundle returns a session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_returns_session_for_unencrypted_bundle()
|
||||
{
|
||||
@@ -176,6 +181,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.NotNull(rig.SessionStore.Get(session.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading an encrypted bundle with the correct passphrase returns a session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_returns_session_for_encrypted_bundle_with_correct_passphrase()
|
||||
{
|
||||
@@ -197,6 +204,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Equal("Pump", roundTripped.Templates[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading throws when the passphrase is wrong.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_throws_when_passphrase_wrong()
|
||||
{
|
||||
@@ -210,6 +219,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(stream, passphrase: "wrong"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the bundle is locked after three wrong passphrases, even across callers.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_locks_bundle_after_three_wrong_passphrases_even_across_callers()
|
||||
{
|
||||
@@ -245,6 +256,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(new MemoryStream(bundleBytes), passphrase: "correct"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading rejects a bundle with too many entries.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_rejects_bundle_with_too_many_entries()
|
||||
{
|
||||
@@ -261,6 +274,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(new MemoryStream(paddedBytes), passphrase: null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading rejects a bundle with an oversized entry.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_rejects_bundle_with_oversized_entry()
|
||||
{
|
||||
@@ -277,6 +292,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(new MemoryStream(oversizedBytes), passphrase: null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading rejects a bundle whose entry exceeds the compression ratio.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_rejects_bundle_whose_entry_exceeds_compression_ratio()
|
||||
{
|
||||
@@ -352,13 +369,15 @@ public sealed class BundleImporterLoadTests
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading rejects a bundle with a tampered manifest field even with the correct passphrase.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_rejects_bundle_with_tampered_manifest_field_even_with_correct_passphrase()
|
||||
{
|
||||
// T-005: a stolen bundle whose plaintext manifest fields (SourceEnvironment,
|
||||
// ExportedBy, …) have been edited must fail decryption with a tag mismatch.
|
||||
// Without AAD an attacker could rewrite the SourceEnvironment label and slip
|
||||
// past the Step-4 typo-resistant confirmation gate. We tamper the field by
|
||||
// past the typo-resistant confirmation gate. We tamper the field by
|
||||
// re-packing the manifest (with everything else, including the original
|
||||
// ciphertext, unchanged) into a fresh ZIP and verify decrypt fails.
|
||||
var rig = BuildRig();
|
||||
@@ -405,6 +424,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(tamperedZipBytes, passphrase: "correct"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading resets the unlock counter on the correct passphrase.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_resets_unlock_counter_on_correct_passphrase()
|
||||
{
|
||||
@@ -429,6 +450,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Equal(1, rig.SessionStore.GetUnlockFailureCount(session.Manifest.ContentHash));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading throws a not-supported error when the bundle format version is unsupported.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_throws_NotSupportedException_when_bundleFormatVersion_unsupported()
|
||||
{
|
||||
@@ -458,6 +481,8 @@ public sealed class BundleImporterLoadTests
|
||||
() => rig.Importer.LoadAsync(bundleStream, passphrase: null));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading throws an invalid-data error when the content hash does not match.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_throws_InvalidDataException_when_content_hash_mismatch()
|
||||
{
|
||||
@@ -488,6 +513,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Contains("hash", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that loading throws an invalid-operation error when the bundle exceeds the size cap.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_throws_InvalidOperationException_when_bundle_exceeds_size_cap()
|
||||
{
|
||||
@@ -508,6 +535,8 @@ public sealed class BundleImporterLoadTests
|
||||
// invoked, so these all passed-by-doing-nothing before the wiring.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that loading rejects unlock attempts past the per-IP hourly cap.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LoadAsync_rejects_unlock_attempts_past_the_per_ip_hourly_cap()
|
||||
{
|
||||
@@ -542,6 +571,8 @@ public sealed class BundleImporterLoadTests
|
||||
new MemoryStream(bundleBytes), passphrase: "correct", clientKey: "10.1.1.5"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throttled unlock is not counted as a passphrase failure and burns no strike.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Throttled_unlock_is_not_a_passphrase_failure_and_burns_no_strike()
|
||||
{
|
||||
@@ -571,6 +602,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Equal(1, rig.SessionStore.GetUnlockFailureCount(contentHash));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the unlock throttle is isolated per client.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unlock_throttle_is_isolated_per_client()
|
||||
{
|
||||
@@ -598,6 +631,8 @@ public sealed class BundleImporterLoadTests
|
||||
new MemoryStream(bundleBytes), passphrase: "wrong", clientKey: "10.1.1.8"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unattributed callers share one throttle bucket rather than bypassing it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unattributed_callers_share_one_throttle_bucket_rather_than_bypassing_it()
|
||||
{
|
||||
@@ -623,6 +658,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Equal(BundleImporter.UnattributedClientKey, throttled.ClientKey);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unencrypted bundles do not consume the unlock budget.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Unencrypted_bundles_do_not_consume_the_unlock_budget()
|
||||
{
|
||||
@@ -641,6 +678,8 @@ public sealed class BundleImporterLoadTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an already-locked bundle does not spend the client's hourly budget.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task An_already_locked_bundle_does_not_spend_the_clients_hourly_budget()
|
||||
{
|
||||
@@ -683,6 +722,8 @@ public sealed class BundleImporterLoadTests
|
||||
// Register row 43 — abandoned-session release.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that abandoning a session releases its slot and zeroes its decrypted content.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Abandon_releases_the_session_slot_and_zeroes_its_decrypted_content()
|
||||
{
|
||||
@@ -708,6 +749,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.All(decrypted, b => Assert.Equal(0, b));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that abandoning a session is idempotent and tolerates unknown ids.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Abandon_is_idempotent_and_tolerates_unknown_ids()
|
||||
{
|
||||
@@ -724,6 +767,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.Null(rig.SessionStore.Get(session.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that abandoning a session does not release another caller's session.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Abandon_does_not_release_another_callers_session()
|
||||
{
|
||||
@@ -743,6 +788,8 @@ public sealed class BundleImporterLoadTests
|
||||
Assert.NotEmpty(survivor!.DecryptedContent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that abandoning a session frees a concurrent import session slot.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Abandon_frees_a_concurrent_import_session_slot()
|
||||
{
|
||||
|
||||
+36
-1
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Register row 53: <c>MaxConcurrentImportSessions</c> had no observability — nothing
|
||||
/// counted open sessions and nothing logged a rejection at the cap, so the register-row-43
|
||||
/// counted open sessions and nothing logged a rejection at the cap, so the earlier
|
||||
/// session leak could only be inferred from rotating test failures. These tests pin the
|
||||
/// two signals that close that gap: the
|
||||
/// <c>scadabridge.central.transport.import_sessions.open</c> gauge and the Warning-level
|
||||
@@ -25,6 +25,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
// Warning log on cap rejection
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that opening a session at the cap logs a warning with the session count and the cap.</summary>
|
||||
[Fact]
|
||||
public void Open_AtCap_LogsWarningWithCountAndCap()
|
||||
{
|
||||
@@ -50,6 +51,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Contains("MaxConcurrentImportSessions", warning.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that opening a session under the cap logs nothing.</summary>
|
||||
[Fact]
|
||||
public void Open_UnderCap_LogsNothing()
|
||||
{
|
||||
@@ -64,6 +66,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Empty(logger.Entries);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that opening at a cap filled by expired sessions reclaims them and logs nothing.</summary>
|
||||
[Fact]
|
||||
public void Open_AtCapOfExpiredSessions_ReclaimsAndLogsNothing()
|
||||
{
|
||||
@@ -88,6 +91,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
// Rejection counter (register row 66)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that opening a session at the cap increments the rejection counter.</summary>
|
||||
[Fact]
|
||||
public void Open_AtCap_IncrementsTheRejectionCounter()
|
||||
{
|
||||
@@ -109,6 +113,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(1, delta);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated cap hits accumulate on the rejection counter.</summary>
|
||||
[Fact]
|
||||
public void Open_RepeatedCapHits_AccumulateOnTheRejectionCounter()
|
||||
{
|
||||
@@ -131,6 +136,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(3, delta);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that opening a session under the cap does not increment the rejection counter.</summary>
|
||||
[Fact]
|
||||
public void Open_UnderCap_DoesNotIncrementTheRejectionCounter()
|
||||
{
|
||||
@@ -145,6 +151,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(0, delta);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reclaiming expired sessions at the cap does not increment the rejection counter.</summary>
|
||||
[Fact]
|
||||
public void Open_AtCapOfExpiredSessions_ReclaimsWithoutIncrementingTheRejectionCounter()
|
||||
{
|
||||
@@ -166,6 +173,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(0, delta);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reopening an existing session id at the cap does not increment the rejection counter.</summary>
|
||||
[Fact]
|
||||
public void Open_ReopeningAnExistingSessionIdAtCap_DoesNotIncrementTheRejectionCounter()
|
||||
{
|
||||
@@ -190,6 +198,7 @@ public sealed class BundleSessionObservabilityTests
|
||||
// OpenSessionCount / gauge
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that the open session count excludes sessions that expired but have not been swept.</summary>
|
||||
[Fact]
|
||||
public void OpenSessionCount_ExcludesExpiredButUnsweptSessions()
|
||||
{
|
||||
@@ -211,6 +220,8 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(1, sut.OpenSessionCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the eviction service registers the gauge provider and clears it on stop.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EvictionService_RegistersTheGaugeProvider_AndClearsItOnStop()
|
||||
{
|
||||
@@ -250,6 +261,8 @@ public sealed class BundleSessionObservabilityTests
|
||||
Assert.Equal(0, afterClear);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that stopping an eviction service does not clobber a newer instance's gauge provider.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EvictionService_StopDoesNotClobberANewerInstancesProvider()
|
||||
{
|
||||
@@ -418,8 +431,13 @@ public sealed class BundleSessionObservabilityTests
|
||||
private sealed class TestClock : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
/// <summary>Initializes the clock at the given start time.</summary>
|
||||
/// <param name="start">The initial time.</param>
|
||||
public TestClock(DateTimeOffset start) { _now = start; }
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
/// <summary>Moves the clock forward.</summary>
|
||||
/// <param name="by">The amount of time to advance.</param>
|
||||
public void Advance(TimeSpan by) => _now = _now.Add(by);
|
||||
}
|
||||
|
||||
@@ -434,22 +452,39 @@ public sealed class BundleSessionObservabilityTests
|
||||
{
|
||||
public static readonly InertUnlockWindow Instance = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> TryRegisterAttemptAsync(
|
||||
string clientKey, int maxAttemptsPerWindow, TimeSpan window, CancellationToken ct = default)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PruneAsync(TimeSpan window, CancellationToken ct = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Captures formatted log entries so the cap-rejection Warning can be asserted.</summary>
|
||||
private sealed class CapturingLogger : ILogger<BundleSessionStore>
|
||||
{
|
||||
/// <summary>Gets the captured log entries in emission order.</summary>
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = [];
|
||||
|
||||
/// <summary>Starts no scope; the fake does not track scopes.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state, ignored.</param>
|
||||
/// <returns>No scope token; the fake does not track scopes.</returns>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
/// <summary>Reports every log level as enabled.</summary>
|
||||
/// <param name="logLevel">The log level being queried.</param>
|
||||
/// <returns>Always <see langword="true"/>.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
/// <param name="logLevel">The severity of the entry.</param>
|
||||
/// <param name="eventId">The event id of the entry.</param>
|
||||
/// <param name="state">The state object being logged.</param>
|
||||
/// <param name="exception">The exception associated with the entry, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message text.</param>
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
|
||||
@@ -29,10 +29,11 @@ public sealed class BundleSessionStoreTests
|
||||
Summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0),
|
||||
Contents: Array.Empty<ManifestContentEntry>());
|
||||
|
||||
/// <summary>Verifies that opening beyond the maximum concurrent import sessions throws, and removing one admits a new session.</summary>
|
||||
[Fact]
|
||||
public void Open_beyond_MaxConcurrentImportSessions_throws_and_removing_one_admits_a_new_session()
|
||||
{
|
||||
// #05-T24: the store bounds concurrently-open sessions (each pins a
|
||||
// The store bounds concurrently-open sessions (each pins a
|
||||
// decrypted bundle in memory). The (cap+1)th Open is rejected; freeing a
|
||||
// slot re-admits a new session.
|
||||
var clock = new TestTimeProvider(DateTimeOffset.UtcNow);
|
||||
@@ -59,6 +60,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.NotNull(sut.Get(overflow.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that opening a session and then getting it returns the session.</summary>
|
||||
[Fact]
|
||||
public void Open_then_Get_returns_session()
|
||||
{
|
||||
@@ -73,6 +75,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Same(session, fetched);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a session after its time-to-live returns null and evicts it.</summary>
|
||||
[Fact]
|
||||
public void Get_after_TTL_returns_null_and_evicts()
|
||||
{
|
||||
@@ -90,6 +93,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Null(sut.Get(session.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removing a session evicts it.</summary>
|
||||
[Fact]
|
||||
public void Remove_evicts_session()
|
||||
{
|
||||
@@ -103,6 +107,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Null(sut.Get(session.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that eviction removes every session past its time-to-live.</summary>
|
||||
[Fact]
|
||||
public void EvictExpired_removes_all_past_ttl()
|
||||
{
|
||||
@@ -125,6 +130,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Null(sut.Get(dropB.SessionId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the legacy failed-unlock-attempts field still round-trips through the shared reference.</summary>
|
||||
[Fact]
|
||||
public void Legacy_FailedUnlockAttempts_field_still_round_trips_via_shared_reference()
|
||||
{
|
||||
@@ -155,6 +161,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.True(refetch!.Locked);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the unlock failure count is tracked per bundle and resets.</summary>
|
||||
[Fact]
|
||||
public void UnlockFailureCount_TracksPerBundleAndResets()
|
||||
{
|
||||
@@ -179,6 +186,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Equal(0, sut.GetUnlockFailureCount(contentHashA));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unlock failures expire on the time-to-live and get returns zero.</summary>
|
||||
[Fact]
|
||||
public void UnlockFailures_ExpireOnTtlAndGetReturnsZero()
|
||||
{
|
||||
@@ -197,6 +205,7 @@ public sealed class BundleSessionStoreTests
|
||||
Assert.Equal(0, sut.GetUnlockFailureCount(contentHash));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that evicting expired entries clears stale unlock failure entries.</summary>
|
||||
[Fact]
|
||||
public void UnlockFailures_EvictExpired_ClearsStaleEntries()
|
||||
{
|
||||
@@ -220,8 +229,13 @@ public sealed class BundleSessionStoreTests
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
/// <summary>Initializes the provider with the given start time.</summary>
|
||||
/// <param name="start">The initial UTC time.</param>
|
||||
public TestTimeProvider(DateTimeOffset start) { _now = start; }
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
/// <summary>Advances the provider's current time.</summary>
|
||||
/// <param name="by">The amount of time to advance.</param>
|
||||
public void Advance(TimeSpan by) => _now = _now.Add(by);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Transport-004: <see cref="BundleUnlockRateLimiter"/> must enforce a per-key cap
|
||||
/// <see cref="BundleUnlockRateLimiter"/> must enforce a per-key cap
|
||||
/// over a trailing window — the design doc's "per-IP-per-hour" cap (§11). The
|
||||
/// limiter accepts any opaque caller key (typically a remote IP); these tests use
|
||||
/// IP-style strings to mirror the documented intent.
|
||||
@@ -13,11 +13,18 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
private sealed class TestClock : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
/// <summary>Initializes a new instance of the <c>TestClock</c> class.</summary>
|
||||
/// <param name="start">The start.</param>
|
||||
public TestClock(DateTimeOffset start) { _now = start; }
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
/// <summary>Advances the test clock by the given amount.</summary>
|
||||
/// <param name="delta">The delta.</param>
|
||||
public void Advance(TimeSpan delta) { _now += delta; }
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering an attempt under the limit returns true.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void TryRegisterAttempt_UnderLimit_ReturnsTrue()
|
||||
{
|
||||
@@ -36,6 +43,8 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
Assert.Equal(10, limiter.GetAttemptCount("10.0.0.1"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering an attempt once the limit is reached is rejected.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void TryRegisterAttempt_AtLimit_RejectsNextAttempt()
|
||||
{
|
||||
@@ -60,6 +69,8 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
Assert.Equal(10, limiter.GetAttemptCount("10.0.0.1"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that recorded attempts expire after the window elapses.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void TryRegisterAttempt_EntriesExpireAfterWindow()
|
||||
{
|
||||
@@ -83,6 +94,8 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
Assert.True(limiter.TryRegisterAttempt("10.0.0.1", maxAttemptsPerWindow: 10));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a partial expiry releases only the oldest slot.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void TryRegisterAttempt_PartialExpiry_ReleasesOldestSlotOnly()
|
||||
{
|
||||
@@ -118,6 +131,8 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
Assert.False(limiter.TryRegisterAttempt("10.0.0.1", maxAttemptsPerWindow: 10));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that attempts are counted independently per key.</summary>
|
||||
/// <returns>The result.</returns>
|
||||
[Fact]
|
||||
public void TryRegisterAttempt_PerKeyIsolation()
|
||||
{
|
||||
@@ -137,6 +152,9 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
Assert.Equal(10, limiter.GetAttemptCount("10.0.0.1"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering an attempt with a blank key throws.</summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The result.</returns>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -148,6 +166,9 @@ public sealed class BundleUnlockRateLimiterTests
|
||||
() => limiter.TryRegisterAttempt(key!, maxAttemptsPerWindow: 10));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering an attempt with a non-positive limit throws.</summary>
|
||||
/// <param name="limit">The limit.</param>
|
||||
/// <returns>The result.</returns>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
||||
|
||||
public sealed class LineDifferTests
|
||||
{
|
||||
/// <summary>Verifies that identical text produces no additions or removals and is not truncated.</summary>
|
||||
[Fact]
|
||||
public void IdenticalText_NoAddsOrRemoves_NotTruncated()
|
||||
{
|
||||
@@ -18,6 +19,7 @@ public sealed class LineDifferTests
|
||||
Assert.All(result.Lines, l => Assert.Equal(LineDiffOp.Context, l.Op));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing an empty old text against three new lines yields three additions numbered 1 to 3.</summary>
|
||||
[Fact]
|
||||
public void OldEmpty_NewThreeLines_ThreeAdds_NewLineNos1To3()
|
||||
{
|
||||
@@ -34,6 +36,7 @@ public sealed class LineDifferTests
|
||||
Assert.All(adds, a => Assert.Null(a.OldLineNo));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that diffing two old lines against an empty new text yields two removals numbered 1 to 2.</summary>
|
||||
[Fact]
|
||||
public void NewEmpty_OldTwoLines_TwoRemoves_OldLineNos1To2()
|
||||
{
|
||||
@@ -50,6 +53,7 @@ public sealed class LineDifferTests
|
||||
Assert.All(removes, r => Assert.Null(r.NewLineNo));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that changing one middle line yields one removal and one addition with the surrounding context preserved.</summary>
|
||||
[Fact]
|
||||
public void SingleMiddleLineChange_OneRemoveOneAdd_ContextPreserved()
|
||||
{
|
||||
@@ -83,6 +87,7 @@ public sealed class LineDifferTests
|
||||
Assert.Equal(3, ctxC.NewLineNo);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that differing CRLF and LF line endings are normalised and produce no changes.</summary>
|
||||
[Fact]
|
||||
public void CrlfNormalization_NoChanges()
|
||||
{
|
||||
@@ -93,6 +98,7 @@ public sealed class LineDifferTests
|
||||
Assert.False(result.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that lone carriage returns are normalised and produce no changes.</summary>
|
||||
[Fact]
|
||||
public void LoneCarriageReturnNormalization_NoChanges()
|
||||
{
|
||||
@@ -103,6 +109,7 @@ public sealed class LineDifferTests
|
||||
Assert.False(result.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null old text and a single new line yield one addition.</summary>
|
||||
[Fact]
|
||||
public void NullOld_NewSingleLine_OneAdd()
|
||||
{
|
||||
@@ -115,6 +122,7 @@ public sealed class LineDifferTests
|
||||
Assert.Equal(1, add.NewLineNo);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null new text and a single old line yield one removal.</summary>
|
||||
[Fact]
|
||||
public void NullNew_OldSingleLine_OneRemove()
|
||||
{
|
||||
@@ -127,6 +135,7 @@ public sealed class LineDifferTests
|
||||
Assert.Equal(1, remove.OldLineNo);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two null texts produce no changes.</summary>
|
||||
[Fact]
|
||||
public void BothNull_NoChanges()
|
||||
{
|
||||
@@ -137,6 +146,7 @@ public sealed class LineDifferTests
|
||||
Assert.False(result.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a thousand new lines against an empty old text are truncated to the maximum line count.</summary>
|
||||
[Fact]
|
||||
public void SizeCap_OldEmpty_ThousandNewLines_TruncatedToMax()
|
||||
{
|
||||
@@ -150,6 +160,7 @@ public sealed class LineDifferTests
|
||||
Assert.Equal(0, result.RemovedCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an interleaved change preserves the longest-common-subsequence context.</summary>
|
||||
[Fact]
|
||||
public void InterleavedCase_LcsContextPreserved()
|
||||
{
|
||||
@@ -182,6 +193,7 @@ public sealed class LineDifferTests
|
||||
Assert.True(idxCtx1 < idxRem2 && idxRem2 < idxCtx3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a trailing newline produces a trailing empty line.</summary>
|
||||
[Fact]
|
||||
public void TrailingNewline_ProducesTrailingEmptyLine()
|
||||
{
|
||||
@@ -192,6 +204,7 @@ public sealed class LineDifferTests
|
||||
Assert.Equal(0, result.RemovedCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that oversized inputs with no common lines degrade to a summary without running the Myers algorithm.</summary>
|
||||
[Fact]
|
||||
public void OversizedInputs_ZeroCommonLines_DegradesToSummary_WithoutRunningMyers()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user