btcpayserver-0001: WalletTransactionInfo.Merge Attachments.Any O(A*B) MEDIUM btcpayserver-0002: AppService gap-fill series.All O(D*S) LOW-MEDIUM btcpayserver-0003: StringExtensions.IsValidFileName GetInvalidFileNameChars O(F*I) MEDIUM
100 lines
3.7 KiB
C#
100 lines
3.7 KiB
C#
// Unit test for btcpayserver-0001: WalletTransactionInfo.Merge O(A*B) attachment dedup
|
|
// Demonstrates quadratic vs linear behavior when merging wallet transaction attachments.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
namespace BTCPayServer.Tests.CWE407
|
|
{
|
|
// Minimal Attachment stand-in
|
|
public class Attachment
|
|
{
|
|
public string Id { get; set; }
|
|
public string Type { get; set; }
|
|
}
|
|
|
|
public static class BtcPayServer0001Test
|
|
{
|
|
// --- ORIGINAL (defective): O(A * B) ---
|
|
static List<Attachment> MergeOriginal(List<Attachment> existing, List<Attachment> incoming)
|
|
{
|
|
var result = new List<Attachment>(existing);
|
|
foreach (var va in incoming.Where(va =>
|
|
!existing.Any(a => a.Id == va.Id && a.Type == va.Type)))
|
|
{
|
|
result.Add(va);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- PATCHED: O(A + B) ---
|
|
static List<Attachment> MergePatched(List<Attachment> existing, List<Attachment> incoming)
|
|
{
|
|
var result = new List<Attachment>(existing);
|
|
var existingSet = new HashSet<(string, string)>(
|
|
existing.Select(a => (a.Id, a.Type)));
|
|
foreach (var va in incoming.Where(va =>
|
|
!existingSet.Contains((va.Id, va.Type))))
|
|
{
|
|
result.Add(va);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static List<Attachment> MakeAttachments(int count, string prefix)
|
|
{
|
|
var list = new List<Attachment>(count);
|
|
for (int i = 0; i < count; i++)
|
|
list.Add(new Attachment { Id = $"{prefix}-{i}", Type = "invoice" });
|
|
return list;
|
|
}
|
|
|
|
public static void Main(string[] args)
|
|
{
|
|
int N = 2000;
|
|
var existing = MakeAttachments(N, "existing");
|
|
var incoming = MakeAttachments(N, "incoming"); // all new, worst case
|
|
|
|
// Warmup
|
|
MergeOriginal(existing, incoming);
|
|
MergePatched(existing, incoming);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
for (int i = 0; i < 5; i++)
|
|
MergeOriginal(existing, incoming);
|
|
sw.Stop();
|
|
long originalMs = sw.ElapsedMilliseconds;
|
|
|
|
sw.Restart();
|
|
for (int i = 0; i < 5; i++)
|
|
MergePatched(existing, incoming);
|
|
sw.Stop();
|
|
long patchedMs = sw.ElapsedMilliseconds;
|
|
|
|
double ratio = (double)originalMs / Math.Max(1, patchedMs);
|
|
|
|
Console.WriteLine($"N = {N}");
|
|
Console.WriteLine($"Original (List.Any): {originalMs} ms");
|
|
Console.WriteLine($"Patched (HashSet): {patchedMs} ms");
|
|
Console.WriteLine($"Speedup ratio: {ratio:F1}x");
|
|
|
|
// Correctness: both produce same count
|
|
var origResult = MergeOriginal(existing, incoming);
|
|
var patchResult = MergePatched(existing, incoming);
|
|
bool correctness = origResult.Count == patchResult.Count &&
|
|
origResult.Count == N + N;
|
|
Console.WriteLine($"Correctness: {(correctness ? "PASS" : "FAIL")}");
|
|
|
|
// Dedup correctness: overlapping
|
|
var overlap = MakeAttachments(N, "existing"); // same IDs as existing
|
|
var origOverlap = MergeOriginal(existing, overlap);
|
|
var patchOverlap = MergePatched(existing, overlap);
|
|
bool dedupCorrect = origOverlap.Count == N && patchOverlap.Count == N;
|
|
Console.WriteLine($"Dedup correctness: {(dedupCorrect ? "PASS" : "FAIL")}");
|
|
|
|
if (!correctness || !dedupCorrect)
|
|
Environment.Exit(1);
|
|
}
|
|
}
|
|
}
|