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
114 lines
4.2 KiB
C#
114 lines
4.2 KiB
C#
// Unit test for btcpayserver-0002: AppService gap-fill O(D*S) series.All inside foreach
|
|
// Demonstrates quadratic vs linear behavior when filling date gaps in sales stats.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
namespace BTCPayServer.Tests.CWE407
|
|
{
|
|
public class SalesItem
|
|
{
|
|
public DateTime Date { get; set; }
|
|
public string Label { get; set; }
|
|
public int SalesCount { get; set; }
|
|
}
|
|
|
|
public static class BtcPayServer0002Test
|
|
{
|
|
// --- ORIGINAL (defective): O(D * S) ---
|
|
static void FillGapsOriginal(List<SalesItem> series, int numberOfDays)
|
|
{
|
|
foreach (var i in Enumerable.Range(0, numberOfDays))
|
|
{
|
|
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
|
if (series.All(e => e.Date != date))
|
|
{
|
|
series.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd") });
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- PATCHED: O(D + S) ---
|
|
static void FillGapsPatched(List<SalesItem> series, int numberOfDays)
|
|
{
|
|
var existingDates = new HashSet<DateTime>(series.Select(e => e.Date));
|
|
foreach (var i in Enumerable.Range(0, numberOfDays))
|
|
{
|
|
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
|
if (!existingDates.Contains(date))
|
|
{
|
|
series.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd") });
|
|
existingDates.Add(date);
|
|
}
|
|
}
|
|
}
|
|
|
|
static List<SalesItem> MakeSparse(int days, int gapEvery)
|
|
{
|
|
var list = new List<SalesItem>();
|
|
for (int i = 0; i < days; i++)
|
|
{
|
|
if (i % gapEvery != 0) continue;
|
|
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
|
list.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd"), SalesCount = 1 });
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public static void Main(string[] args)
|
|
{
|
|
int D = 3000; // exaggerated for benchmark
|
|
int gapEvery = 5;
|
|
|
|
// Warmup
|
|
var warmup1 = MakeSparse(D, gapEvery);
|
|
FillGapsOriginal(warmup1, D);
|
|
var warmup2 = MakeSparse(D, gapEvery);
|
|
FillGapsPatched(warmup2, D);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
for (int run = 0; run < 3; run++)
|
|
{
|
|
var s = MakeSparse(D, gapEvery);
|
|
FillGapsOriginal(s, D);
|
|
}
|
|
sw.Stop();
|
|
long originalMs = sw.ElapsedMilliseconds;
|
|
|
|
sw.Restart();
|
|
for (int run = 0; run < 3; run++)
|
|
{
|
|
var s = MakeSparse(D, gapEvery);
|
|
FillGapsPatched(s, D);
|
|
}
|
|
sw.Stop();
|
|
long patchedMs = sw.ElapsedMilliseconds;
|
|
|
|
double ratio = (double)originalMs / Math.Max(1, patchedMs);
|
|
|
|
Console.WriteLine($"D = {D}, gap every {gapEvery} days");
|
|
Console.WriteLine($"Original (List.All): {originalMs} ms");
|
|
Console.WriteLine($"Patched (HashSet): {patchedMs} ms");
|
|
Console.WriteLine($"Speedup ratio: {ratio:F1}x");
|
|
|
|
// Correctness
|
|
var orig = MakeSparse(D, gapEvery);
|
|
FillGapsOriginal(orig, D);
|
|
var patched = MakeSparse(D, gapEvery);
|
|
FillGapsPatched(patched, D);
|
|
|
|
bool correct = orig.Count == patched.Count && orig.Count == D;
|
|
Console.WriteLine($"Count match: {(correct ? "PASS" : "FAIL")} (orig={orig.Count}, patched={patched.Count})");
|
|
|
|
// Verify no duplicate dates
|
|
bool noDupsOrig = orig.Select(e => e.Date).Distinct().Count() == orig.Count;
|
|
bool noDupsPatched = patched.Select(e => e.Date).Distinct().Count() == patched.Count;
|
|
Console.WriteLine($"No dups (original): {(noDupsOrig ? "PASS" : "FAIL")}");
|
|
Console.WriteLine($"No dups (patched): {(noDupsPatched ? "PASS" : "FAIL")}");
|
|
|
|
if (!correct || !noDupsOrig || !noDupsPatched)
|
|
Environment.Exit(1);
|
|
}
|
|
}
|
|
}
|