Files
fragalytics/tests/Fragalytics.Web.Tests/Program.cs
T
2026-04-23 02:23:23 +00:00

687 lines
28 KiB
C#

using Fragalytics.Web;
using Fragalytics.Web.Infrastructure;
using Fragalytics.Web.Models;
using Fragalytics.Web.Services;
using Fragalytics.Web.Services.Parser;
using Fragalytics.Web.Services.Render;
using Fragalytics.Web.Services.Scoring;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
var failures = new List<string>();
try
{
RunScoringTest();
}
catch (Exception exception)
{
failures.Add($"Scoring test failed: {exception.Message}");
}
try
{
await RunFlowTestAsync();
}
catch (Exception exception)
{
failures.Add($"Flow test failed: {exception.Message}");
}
try
{
RunRetryFlowTest();
}
catch (Exception exception)
{
failures.Add($"Retry flow test failed: {exception.Message}");
}
try
{
RunPathResolutionTest();
}
catch (Exception exception)
{
failures.Add($"Path resolution test failed: {exception.Message}");
}
try
{
RunRenderJobContractDefaultsTest();
}
catch (Exception exception)
{
failures.Add($"Render contract test failed: {exception.Message}");
}
try
{
await RunRescoreReplacementTestAsync();
}
catch (Exception exception)
{
failures.Add($"Rescore replacement test failed: {exception.Message}");
}
try
{
await RunParserFailureTestAsync();
}
catch (Exception exception)
{
failures.Add($"Parser failure test failed: {exception.Message}");
}
try
{
await RunInvalidClipUploadTestAsync();
}
catch (Exception exception)
{
failures.Add($"Invalid clip upload test failed: {exception.Message}");
}
if (failures.Count > 0)
{
foreach (var failure in failures)
{
Console.Error.WriteLine(failure);
}
return 1;
}
Console.WriteLine("All tests passed.");
return 0;
static void RunScoringTest()
{
var rules = new HighlightScoringRules
{
Weights = new Dictionary<string, double>
{
["base_kill"] = 10,
["headshot"] = 8,
["opening_kill"] = 12,
["wallbang"] = 10,
["smoke_kill"] = 10,
["round_win"] = 6,
["multikill_2"] = 8,
["multikill_3"] = 18,
["multikill_4"] = 30,
["multikill_5"] = 45,
["clutch_1v2"] = 18,
["clutch_1v3"] = 28,
["clutch_1v4"] = 40,
["clutch_1v5"] = 55,
["impact_last_enemy"] = 12,
["impact_low_time"] = 4
},
Clip = new ClipWindowRules { PreTicks = 160, PostTicks = 96 },
Thresholds = new ScoreThresholdRules { HighlightScoreMin = 18 }
};
var scorer = new HighlightScorer(rules);
var round = new RoundRecord { Id = 1, Number = 3, WinnerSide = "CT" };
var players = new Dictionary<int, PlayerRecord>
{
[7] = new() { Id = 7, Name = "Alpha", Side = "CT" }
};
var first = new EventRecord { Id = 1, RoundId = 1, Tick = 100, ActorPlayerId = 7, EventType = EventTypes.Kill };
var target = new EventRecord
{
Id = 2,
RoundId = 1,
Tick = 450,
Second = 38,
ActorPlayerId = 7,
EventType = EventTypes.Kill,
IsHeadshot = true,
KillerAliveCount = 1,
EnemyAliveCount = 3
};
var result = scorer.ScoreKill(target, round, [first, target], players, 1);
Assert(result.Score >= 18, "Expected score above threshold.");
Assert(result.Tags.Contains("headshot"), "Expected headshot tag.");
Assert(result.Tags.Contains("multikill"), "Expected multikill tag.");
Assert(result.Tags.Contains("clutch"), "Expected clutch tag.");
Assert(result.Tags.Contains("1vX"), "Expected 1vX tag.");
Assert(result.StartTick == 290, "Expected clip window pre-roll to be applied.");
Assert(result.EndTick == 546, "Expected clip window post-roll to be applied.");
Assert(result.Explanation.Any(item => item.Reason == "headshot"), "Expected score explanation to include headshot.");
Assert(result.Explanation.Any(item => item.Reason == "clutch_1v3"), "Expected score explanation to include clutch weight.");
}
static async Task RunFlowTestAsync()
{
var root = Path.Combine(Path.GetTempPath(), "fragalytics-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "data"));
Directory.CreateDirectory(Path.Combine(root, "uploads"));
var environment = new FakeEnvironment(root);
var options = Options.Create(new FragalyticsOptions
{
DataDirectory = Path.Combine(root, "data"),
UploadDirectory = Path.Combine(root, "uploads"),
AutoStartWorker = false,
RenderWorker = new RenderWorkerOptions
{
MaxAttempts = 2,
RetryDelaySeconds = 30
}
});
var repoRoot = FindRepoRoot();
var scoringRulesSource = Path.Combine(repoRoot, "src", "Fragalytics.Web", "scoring-rules.json");
File.Copy(
scoringRulesSource,
Path.Combine(root, "scoring-rules.json"),
overwrite: true);
var store = new JsonAppStore(options);
var scoringProvider = new ScoringRulesProvider(environment);
var renderWorker = new RecordingRenderWorkerAdapter();
var scoring = new MatchScoringService(scoringProvider, renderWorker, options);
var ingest = new DemoIngestService(store, options, new SampleDemoParserAdapter(), scoring);
var clipMedia = new ClipMediaService(store, options);
await using var payload = new MemoryStream("fake demo bytes"u8.ToArray());
var demoId = await ingest.SaveUploadAsync("sample.dem", payload, CancellationToken.None);
Assert(demoId == 1, "Expected first demo id to be 1.");
await ingest.ProcessNextAsync(CancellationToken.None);
var state = await store.GetSnapshotAsync();
Assert(state.Demos.Count == 1, "Expected one demo.");
Assert(state.Demos[0].Status == DemoStatuses.Parsed, "Expected parsed demo status.");
Assert(state.Matches.Count == 1, "Expected one match.");
Assert(state.Highlights.Count > 0, "Expected highlights.");
Assert(state.ClipJobs.Count == state.Highlights.Count, "Expected one clip job per highlight.");
Assert(state.ClipJobs[0].AttemptCount == 0, "Expected new clip job to have no worker attempts yet.");
Assert(renderWorker.EnqueuedClipJobIds.Count == state.ClipJobs.Count, "Expected every created clip job to be enqueued.");
await using var clipPayload = new MemoryStream("fake rendered mp4 bytes"u8.ToArray());
await clipMedia.AttachClipAssetAsync(state.ClipJobs[0].Id, $"sample{ClipMediaFormats.Mp4Extension}", ClipMediaFormats.Mp4ContentType, clipPayload, CancellationToken.None);
state = await store.GetSnapshotAsync();
Assert(state.ClipAssets.Count == 1, "Expected one clip asset.");
Assert(state.ClipJobs[0].Status == ClipJobStatuses.Rendered, "Expected rendered clip status after attach.");
Assert(state.ClipJobs[0].Payload.TryGetValue("version", out var version) && version == RenderJobContract.CurrentVersion, "Expected versioned render payload.");
Assert(state.ClipJobs[0].Payload.TryGetValue("demoId", out var demoIdPayload) && demoIdPayload == "1", "Expected demo id in render payload.");
Assert(state.ClipJobs[0].Payload.ContainsKey("cameraMode"), "Expected render payload defaults on clip job.");
Assert(state.ClipJobs[0].Payload.Keys.All(key => key is "version" or "demoId" or "matchId" or "highlightId" or "sourceEventId" or "startTick" or "endTick" or "tickRate" or "cameraMode" or "outputFormat" or "resolution" or "fps"), "Expected payload to contain only render contract fields.");
var renderJob = RenderJobContract.FromPayload(state.ClipJobs[0].Payload);
Assert(renderJob.MatchId == state.Matches[0].Id, "Expected match id in typed render payload.");
Assert(renderJob.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
Assert(renderJob.TickRate == state.Matches[0].TickRate, "Expected match tick rate in typed render payload.");
await ingest.RescoreAllAsync(CancellationToken.None);
state = await store.GetSnapshotAsync();
Assert(state.Highlights.Count > 0, "Expected highlights after rescoring.");
var deleted = await ingest.DeleteDemoAsync(demoId, CancellationToken.None);
Assert(deleted, "Expected demo deletion to succeed.");
state = await store.GetSnapshotAsync();
Assert(state.Demos.Count == 0, "Expected demo to be removed after deletion.");
Assert(state.Matches.Count == 0, "Expected match graph to be removed after demo deletion.");
Assert(state.Highlights.Count == 0, "Expected highlights to be removed after demo deletion.");
Assert(state.ClipJobs.Count == 0, "Expected clip jobs to be removed after demo deletion.");
}
static void RunRetryFlowTest()
{
var firstJob = new ClipJobRecord
{
Id = 1,
HighlightId = 7,
Renderer = RenderBackendNames.FutureRenderer,
Status = ClipJobStatuses.Pending,
StartTick = 100,
EndTick = 200,
MaxAttempts = 2
};
Assert(ClipJobRetryPolicy.CanStartAttempt(firstJob, DateTime.UtcNow), "Expected pending job to be claimable before any attempts.");
ClipJobRetryPolicy.MarkAttemptStarted(firstJob, RenderBackendNames.SampleCopy, DateTime.UtcNow);
ClipJobRetryPolicy.MarkAttemptFailed(firstJob, "Simulated backend failure.", DateTime.UtcNow, TimeSpan.FromSeconds(30));
Assert(firstJob.Status == ClipJobStatuses.Pending, "Expected failed render with retries remaining to return to pending.");
Assert(firstJob.AttemptCount == 1, "Expected first failed attempt to increment attempt count.");
Assert(firstJob.RetryAfterUtc.HasValue, "Expected retry to be scheduled after a failure.");
Assert(!string.IsNullOrWhiteSpace(firstJob.ErrorMessage), "Expected failure message to be preserved for retry visibility.");
Assert(!ClipJobRetryPolicy.CanStartAttempt(firstJob, DateTime.UtcNow), "Expected retry window to block immediate reprocessing.");
firstJob.RetryAfterUtc = DateTime.UtcNow.AddSeconds(-1);
Assert(ClipJobRetryPolicy.CanStartAttempt(firstJob, DateTime.UtcNow), "Expected job to become claimable once retry delay passes.");
ClipJobRetryPolicy.MarkAttemptStarted(firstJob, RenderBackendNames.SampleCopy, DateTime.UtcNow);
firstJob.Status = ClipJobStatuses.Rendered;
firstJob.ErrorMessage = null;
firstJob.RetryAfterUtc = null;
firstJob.CompletedAtUtc = DateTime.UtcNow;
Assert(firstJob.Status == ClipJobStatuses.Rendered, "Expected retry success to render the clip.");
Assert(firstJob.AttemptCount == 2, "Expected successful retry to record the second attempt.");
Assert(firstJob.RetryAfterUtc is null, "Expected successful retry to clear the retry schedule.");
var terminalJob = new ClipJobRecord
{
Id = 2,
HighlightId = 8,
Status = ClipJobStatuses.Pending,
MaxAttempts = 1
};
ClipJobRetryPolicy.MarkAttemptStarted(terminalJob, RenderBackendNames.SampleCopy, DateTime.UtcNow);
ClipJobRetryPolicy.MarkAttemptFailed(terminalJob, "Permanent failure.", DateTime.UtcNow, TimeSpan.FromSeconds(30));
Assert(terminalJob.Status == ClipJobStatuses.Failed, "Expected job with no retries remaining to become failed.");
Assert(terminalJob.RetryAfterUtc is null, "Expected terminal failure to avoid scheduling another retry.");
}
static void RunPathResolutionTest()
{
var root = Path.Combine(FindRepoRoot(), "src", "Fragalytics.Web");
if (root.StartsWith("/mnt/", StringComparison.OrdinalIgnoreCase))
{
var mixedWindowsPath = @"Z:\mnt\z\" + Path.GetRelativePath("/mnt/z", root).Replace('/', '\\');
var resolvedFromMixedWindows = AppPathResolver.ResolveContentRoot(mixedWindowsPath, root);
Assert(resolvedFromMixedWindows == root, "Expected mixed Windows path to resolve to the real app root.");
}
var resolvedFromBaseDirectory = AppPathResolver.ResolveContentRoot(Path.Combine(root, "bin", "Debug", "net8.0"), Path.Combine(root, "bin", "Debug", "net8.0"));
Assert(resolvedFromBaseDirectory == root, "Expected app base directory traversal to find the project root.");
}
static void RunRenderJobContractDefaultsTest()
{
var minimalPayload = new Dictionary<string, string>
{
["demoId"] = "9",
["matchId"] = "17",
["highlightId"] = "23",
["sourceEventId"] = "42",
["startTick"] = "1500",
["endTick"] = "1800"
};
var contract = RenderJobContract.FromPayload(minimalPayload);
Assert(contract.Version == RenderJobContract.CurrentVersion, "Expected missing version to fall back to current contract version.");
Assert(contract.CameraMode == RenderJobContract.DefaultCameraMode, "Expected default camera mode.");
Assert(contract.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
Assert(contract.Resolution == RenderJobContract.DefaultResolution, "Expected default resolution.");
Assert(contract.Fps == RenderJobContract.DefaultFps, "Expected default fps.");
Assert(contract.TickRate == RenderJobContract.DefaultTickRate, "Expected default tick rate.");
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.Mp4Extension, "Expected mp4 extension for default output format.");
contract.OutputFormat = "webm";
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.WebMExtension, "Expected webm extension override.");
var messyPayload = new Dictionary<string, string>
{
["demoId"] = "9",
["matchId"] = "17",
["highlightId"] = "23",
["sourceEventId"] = "42",
["startTick"] = "1500",
["endTick"] = "1800",
["tickRate"] = "128",
["cameraMode"] = " killer_pov ",
["outputFormat"] = " WEBM ",
["resolution"] = " 1280X720 ",
["fps"] = "0"
};
var normalized = RenderJobContract.FromPayload(messyPayload);
Assert(normalized.CameraMode == RenderJobContract.DefaultCameraMode, "Expected camera mode to be trimmed.");
Assert(normalized.OutputFormat == "webm", "Expected output format to be normalized.");
Assert(normalized.Resolution == "1280x720", "Expected resolution to be normalized.");
Assert(normalized.Fps == RenderJobContract.DefaultFps, "Expected invalid fps to fall back to the default.");
Assert(normalized.TickRate == 128, "Expected tick rate to survive normalization.");
}
static async Task RunRescoreReplacementTestAsync()
{
var root = Path.Combine(Path.GetTempPath(), "fragalytics-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "data"));
Directory.CreateDirectory(Path.Combine(root, "uploads"));
var environment = new FakeEnvironment(root);
var options = Options.Create(new FragalyticsOptions
{
DataDirectory = Path.Combine(root, "data"),
UploadDirectory = Path.Combine(root, "uploads"),
AutoStartWorker = false,
RenderWorker = new RenderWorkerOptions
{
MaxAttempts = 3,
RetryDelaySeconds = 15
}
});
var scoringRules = """
{
"name": "test-rules",
"weights": {
"base_kill": 10,
"headshot": 8,
"opening_kill": 12,
"wallbang": 10,
"smoke_kill": 10,
"round_win": 6,
"multikill_2": 8,
"multikill_3": 18,
"multikill_4": 30,
"multikill_5": 45,
"clutch_1v2": 18,
"clutch_1v3": 28,
"clutch_1v4": 40,
"clutch_1v5": 55,
"impact_last_enemy": 12,
"impact_low_time": 4
},
"clip": {
"preTicks": 160,
"postTicks": 96
},
"thresholds": {
"highlightScoreMin": 18
}
}
""";
File.WriteAllText(Path.Combine(root, "scoring-rules.json"), scoringRules);
var store = new JsonAppStore(options);
var scoringProvider = new ScoringRulesProvider(environment);
var renderWorker = new RecordingRenderWorkerAdapter();
var parser = new StaticParserAdapter(BuildRescoreParsedDemo());
var scoring = new MatchScoringService(scoringProvider, renderWorker, options);
var ingest = new DemoIngestService(store, options, parser, scoring);
var clipMedia = new ClipMediaService(store, options);
await using var payload = new MemoryStream("rescore demo"u8.ToArray());
var demoId = await ingest.SaveUploadAsync("rescore.dem", payload, CancellationToken.None);
await ingest.ProcessNextAsync(CancellationToken.None);
var state = await store.GetSnapshotAsync();
Assert(state.Highlights.Count == 1, "Expected only one above-threshold highlight from the initial parse.");
Assert(state.ClipJobs.Count == 1, "Expected one clip job for one highlight.");
Assert(renderWorker.EnqueuedClipJobIds.Count == 1, "Expected initial highlight to be enqueued once.");
var initialHighlightId = state.Highlights[0].Id;
var initialClipJobId = state.ClipJobs[0].Id;
await using var clipPayload = new MemoryStream("first clip bytes"u8.ToArray());
await clipMedia.AttachClipAssetAsync(initialClipJobId, $"initial{ClipMediaFormats.Mp4Extension}", ClipMediaFormats.Mp4ContentType, clipPayload, CancellationToken.None);
state = await store.GetSnapshotAsync();
Assert(state.ClipAssets.Count == 1, "Expected one attached clip asset before rescoring.");
File.WriteAllText(
Path.Combine(root, "scoring-rules.json"),
scoringRules.Replace("\"highlightScoreMin\": 18", "\"highlightScoreMin\": 8", StringComparison.Ordinal));
var rescoredCount = await ingest.RescoreAllAsync(CancellationToken.None);
Assert(rescoredCount == 2, "Expected rescoring to create two highlights after lowering the threshold.");
state = await store.GetSnapshotAsync();
Assert(state.Highlights.Count == 2, "Expected rescoring to replace old highlights with the new set.");
Assert(state.Highlights.All(item => item.Id != initialHighlightId), "Expected rescoring to replace prior highlight records.");
Assert(state.ClipJobs.Count == 2, "Expected clip jobs to be regenerated on rescoring.");
Assert(state.ClipJobs.All(item => item.Id != initialClipJobId), "Expected prior clip jobs to be discarded on rescoring.");
Assert(state.ClipAssets.Count == 0, "Expected clip assets tied to old clip jobs to be removed during rescoring.");
Assert(renderWorker.EnqueuedClipJobIds.Count == 3, "Expected rescoring to enqueue the replacement clip jobs.");
Assert(state.ClipJobs.All(item => item.MaxAttempts == 3), "Expected regenerated clip jobs to preserve configured retry policy.");
Assert(state.ScoringProfiles.Count == 2, "Expected a new scoring profile version after rules changed.");
Assert(state.ScoringProfiles.Count(item => item.IsActive) == 1, "Expected only one active scoring profile.");
var rescoredHighlightsByEvent = state.Highlights.ToDictionary(item => item.SourceEventId);
Assert(rescoredHighlightsByEvent.Count == 2, "Expected each source event to map to one highlight after rescoring.");
Assert(rescoredHighlightsByEvent.Values.Any(item => item.Score < 18), "Expected rescoring to include a previously filtered low-score highlight.");
var regeneratedPayload = state.ClipJobs
.Select(item => RenderJobContract.FromPayload(item.Payload))
.ToList();
Assert(regeneratedPayload.All(item => item.DemoId == demoId), "Expected regenerated payloads to retain the demo id.");
Assert(regeneratedPayload.All(item => item.OutputFormat == RenderJobContract.DefaultOutputFormat), "Expected regenerated payloads to keep default render settings.");
}
static async Task RunParserFailureTestAsync()
{
var root = Path.Combine(Path.GetTempPath(), "fragalytics-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "data"));
Directory.CreateDirectory(Path.Combine(root, "uploads"));
var environment = new FakeEnvironment(root);
var options = Options.Create(new FragalyticsOptions
{
DataDirectory = Path.Combine(root, "data"),
UploadDirectory = Path.Combine(root, "uploads"),
AutoStartWorker = false
});
var repoRoot = FindRepoRoot();
File.Copy(
Path.Combine(repoRoot, "src", "Fragalytics.Web", "scoring-rules.json"),
Path.Combine(root, "scoring-rules.json"),
overwrite: true);
var store = new JsonAppStore(options);
var scoringProvider = new ScoringRulesProvider(environment);
var scoring = new MatchScoringService(scoringProvider, new RecordingRenderWorkerAdapter(), options);
var ingest = new DemoIngestService(store, options, new ThrowingParserAdapter("Simulated parser failure."), scoring);
await using var payload = new MemoryStream("broken demo"u8.ToArray());
var demoId = await ingest.SaveUploadAsync("broken.dem", payload, CancellationToken.None);
var processed = await ingest.ProcessNextAsync(CancellationToken.None);
Assert(processed == 0, "Expected failed parse to report no processed demo.");
var state = await store.GetSnapshotAsync();
Assert(state.Demos.Count == 1, "Expected failed parse to retain the demo record.");
Assert(state.Demos[0].Id == demoId, "Expected the failed demo to remain addressable.");
Assert(state.Demos[0].Status == DemoStatuses.Failed, "Expected parser failure to mark the demo as failed.");
Assert(state.Demos[0].ErrorMessage == "Simulated parser failure.", "Expected parser failure message to be recorded.");
Assert(state.Demos[0].ProcessedAtUtc.HasValue, "Expected failed parser attempt to stamp processed time.");
Assert(state.Matches.Count == 0, "Expected failed parser attempt to avoid creating a match.");
Assert(state.Events.Count == 0, "Expected failed parser attempt to avoid creating events.");
Assert(state.Highlights.Count == 0, "Expected failed parser attempt to avoid creating highlights.");
Assert(state.ClipJobs.Count == 0, "Expected failed parser attempt to avoid creating clip jobs.");
}
static async Task RunInvalidClipUploadTestAsync()
{
var root = Path.Combine(Path.GetTempPath(), "fragalytics-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "data"));
Directory.CreateDirectory(Path.Combine(root, "uploads"));
var environment = new FakeEnvironment(root);
var options = Options.Create(new FragalyticsOptions
{
DataDirectory = Path.Combine(root, "data"),
UploadDirectory = Path.Combine(root, "uploads"),
AutoStartWorker = false
});
var repoRoot = FindRepoRoot();
File.Copy(
Path.Combine(repoRoot, "src", "Fragalytics.Web", "scoring-rules.json"),
Path.Combine(root, "scoring-rules.json"),
overwrite: true);
var store = new JsonAppStore(options);
var scoringProvider = new ScoringRulesProvider(environment);
var scoring = new MatchScoringService(scoringProvider, new RecordingRenderWorkerAdapter(), options);
var ingest = new DemoIngestService(store, options, new SampleDemoParserAdapter(), scoring);
var clipMedia = new ClipMediaService(store, options);
await using var payload = new MemoryStream("valid demo"u8.ToArray());
await ingest.SaveUploadAsync("valid.dem", payload, CancellationToken.None);
await ingest.ProcessNextAsync(CancellationToken.None);
var state = await store.GetSnapshotAsync();
Assert(state.ClipJobs.Count > 0, "Expected parsed demo to create a clip job for invalid upload test.");
var clipJobId = state.ClipJobs[0].Id;
await using var invalidClip = new MemoryStream("not a video"u8.ToArray());
var exception = await AssertThrowsAsync<InvalidOperationException>(
() => clipMedia.AttachClipAssetAsync(clipJobId, "invalid.avi", "video/x-msvideo", invalidClip, CancellationToken.None),
"Expected unsupported clip extension to be rejected.");
Assert(exception.Message.Contains(ClipMediaFormats.Mp4Extension, StringComparison.Ordinal), "Expected invalid clip upload message to mention mp4 support.");
Assert(exception.Message.Contains(ClipMediaFormats.WebMExtension, StringComparison.Ordinal), "Expected invalid clip upload message to mention webm support.");
state = await store.GetSnapshotAsync();
Assert(state.ClipAssets.Count == 0, "Expected invalid clip upload to avoid creating clip assets.");
Assert(state.ClipJobs[0].Id == clipJobId, "Expected invalid clip upload to leave the same clip job in place.");
Assert(state.ClipJobs[0].Status == ClipJobStatuses.Pending, "Expected invalid clip upload to avoid changing clip job status.");
Assert(state.ClipJobs[0].ClipAssetId is null, "Expected invalid clip upload to avoid linking a clip asset.");
}
static ParsedDemo BuildRescoreParsedDemo()
{
return new ParsedDemo
{
MapName = "test_rescore_map",
ServerName = "Deterministic Test Parser",
TickRate = 64,
TeamCtName = "Counter-Terrorists",
TeamTName = "Terrorists",
Players =
[
new() { SteamId = "1001", Name = "Alpha", TeamName = "Counter-Terrorists", Side = "CT" },
new() { SteamId = "1002", Name = "Bravo", TeamName = "Counter-Terrorists", Side = "CT" },
new() { SteamId = "1003", Name = "Charlie", TeamName = "Terrorists", Side = "T" }
],
Rounds =
[
new() { Number = 1, StartTick = 0, EndTick = 600, WinnerSide = "CT", WinnerTeamName = "Counter-Terrorists", Reason = "elimination" }
],
KillEvents =
[
new() { Tick = 120, Second = 5.0, RoundNumber = 1, KillerSteamId = "1001", KillerName = "Alpha", VictimSteamId = "1003", VictimName = "Charlie", Weapon = "m4a1", IsHeadshot = true, KillerAliveCount = 2, EnemyAliveCount = 2 },
new() { Tick = 280, Second = 18.0, RoundNumber = 1, KillerSteamId = "1002", KillerName = "Bravo", VictimSteamId = "1003", VictimName = "Charlie", Weapon = "famas", KillerAliveCount = 2, EnemyAliveCount = 2 }
]
};
}
static string FindRepoRoot()
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrWhiteSpace(current))
{
if (File.Exists(Path.Combine(current, "Fragalytics.sln"))
&& File.Exists(Path.Combine(current, "Directory.Build.props")))
{
return current;
}
var parent = Path.GetDirectoryName(current);
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, current, StringComparison.OrdinalIgnoreCase))
{
break;
}
current = parent;
}
throw new InvalidOperationException("Could not locate repository root from test base directory.");
}
static void Assert(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
static async Task<TException> AssertThrowsAsync<TException>(Func<Task> action, string message)
where TException : Exception
{
try
{
await action();
}
catch (TException exception)
{
return exception;
}
throw new InvalidOperationException(message);
}
internal sealed class FakeEnvironment : IWebHostEnvironment
{
public FakeEnvironment(string rootPath)
{
ContentRootPath = rootPath;
ContentRootFileProvider = new PhysicalFileProvider(rootPath);
WebRootPath = rootPath;
WebRootFileProvider = new PhysicalFileProvider(rootPath);
ApplicationName = "Fragalytics.Web.Tests";
EnvironmentName = "Development";
}
public string ApplicationName { get; set; }
public IFileProvider WebRootFileProvider { get; set; }
public string WebRootPath { get; set; }
public string EnvironmentName { get; set; }
public string ContentRootPath { get; set; }
public IFileProvider ContentRootFileProvider { get; set; }
}
internal sealed class RecordingRenderWorkerAdapter : IRenderWorkerAdapter
{
public string Name => "recording-renderer";
public List<int> EnqueuedClipJobIds { get; } = [];
public void Enqueue(ClipJobRecord clipJob)
{
EnqueuedClipJobIds.Add(clipJob.Id);
}
}
internal sealed class StaticParserAdapter : IDemoParserAdapter
{
private readonly ParsedDemo _parsedDemo;
public StaticParserAdapter(ParsedDemo parsedDemo)
{
_parsedDemo = parsedDemo;
}
public string Name => "static-test";
public Task<ParsedDemo> ParseAsync(string demoPath, CancellationToken cancellationToken)
{
return Task.FromResult(_parsedDemo);
}
}
internal sealed class ThrowingParserAdapter : IDemoParserAdapter
{
private readonly string _message;
public ThrowingParserAdapter(string message)
{
_message = message;
}
public string Name => "throwing-test";
public Task<ParsedDemo> ParseAsync(string demoPath, CancellationToken cancellationToken)
{
throw new InvalidOperationException(_message);
}
}