Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/Turnierplan.Core.Test.Regression/IRegressionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Turnierplan.Core.Test.Regression;

internal interface IRegressionTest
{
string Result { get; }
}
60 changes: 60 additions & 0 deletions src/Turnierplan.Core.Test.Regression/RegressionTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Text;

namespace Turnierplan.Core.Test.Regression;

internal abstract class RegressionTestBase<TSubject> : IRegressionTest
where TSubject : class
{
private readonly StringBuilder _builder = new();
private TSubject? _subject;
private int _saveStateIndex;

public string Result => _builder.ToString();

protected void Subject(TSubject subject)
{
if (_subject is not null)
{
throw new InvalidOperationException($"The '{nameof(Subject)}()' method may only be called once.");
}

_subject = subject;

AddSaveState();
}

protected void Step(Action<TSubject> step)
{
if (_subject is null)
{
throw new InvalidOperationException($"Call '{nameof(Subject)}()' first to initialize the test subject.");
}

step(_subject);

AddSaveState();
}

protected TResult Step<TResult>(Func<TSubject, TResult> step)
{
if (_subject is null)
{
throw new InvalidOperationException($"Call '{nameof(Subject)}()' first to initialize the test subject.");
}

var result = step(_subject);

AddSaveState();

return result;
}

private void AddSaveState()
{
_builder.Append('[');
_builder.Append(++_saveStateIndex);
_builder.Append(']');
_builder.AppendLine();
_builder.AppendSubject(_subject);
}
}
79 changes: 79 additions & 0 deletions src/Turnierplan.Core.Test.Regression/RegressionTestRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Reflection;
using FluentAssertions;
using Xunit;
using Xunit.Sdk;
using Xunit.v3;

namespace Turnierplan.Core.Test.Regression;

public sealed class RegressionTestRunner(ITestOutputHelper output)
{
private static readonly string __outputFolderName = Path.GetFullPath($"test_output/{DateTime.UtcNow.Ticks}/");
private static readonly Type __selfType = typeof(RegressionTestRunner);

static RegressionTestRunner()
{
if (!Directory.Exists(__outputFolderName))
{
Directory.CreateDirectory(__outputFolderName);
}
}

[Theory]
[RegressionTestData]
public async Task RegressionTest___When_Executed___Produces_Expected_Result(string regressionTestName)
{
var typeName = $"{__selfType.Namespace}.Tests.{regressionTestName}";
var type = __selfType.Assembly.GetType(typeName) ?? throw new InvalidOperationException($"Test type not found: '{typeName}'");

if (Activator.CreateInstance(type) is not IRegressionTest regressionTest)
{
throw new InvalidOperationException($"Test type could not be instantiated found: '{typeName}'");
}

output.WriteLine("The test type {0} was successfully instantiated.", typeName);

var regressionTestOutput = regressionTest.Result.TrimEnd();
output.WriteLine("The actual test result length is {0}", regressionTestOutput.Length);

var outputFileName = Path.Join(__outputFolderName, $"{regressionTestName}.out");
output.WriteLine("Writing test output to: '{0}'", outputFileName);
await File.WriteAllTextAsync(outputFileName, regressionTestOutput, TestContext.Current.CancellationToken);

var expectedFileResourceName = $"{__selfType.Namespace}.Expected.{regressionTestName}.out";
output.WriteLine("Expected test output file: {0}", expectedFileResourceName);
await using var expectedFileStream = __selfType.Assembly.GetManifestResourceStream(expectedFileResourceName);

if (expectedFileStream is null)
{
throw new InvalidOperationException($"Missing expected test output: '{expectedFileResourceName}'");
}

using var streamReader = new StreamReader(expectedFileStream);
var expectedTestOutput = (await streamReader.ReadToEndAsync(TestContext.Current.CancellationToken)).TrimEnd();
output.WriteLine("Expected test output was read and has a length of {0}", expectedTestOutput.Length);

regressionTestOutput.Should().Be(expectedTestOutput);
}

[AttributeUsage(AttributeTargets.Method)]
private sealed class RegressionTestDataAttribute : DataAttribute
{
public override ValueTask<IReadOnlyCollection<ITheoryDataRow>> GetData(MethodInfo testMethod, DisposalTracker disposalTracker)
{
var interfaceType = typeof(IRegressionTest);

return ValueTask.FromResult<IReadOnlyCollection<ITheoryDataRow>>(__selfType.Assembly
.GetTypes()
.Where(type => type.IsAssignableTo(interfaceType) && type is { IsClass: true, IsAbstract: false })
.Select(type => type.Name)
.Select(name => new TheoryDataRow(name))
.ToList());
}

public override bool SupportsDiscoveryEnumeration()
{
return true;
}
}
}
196 changes: 196 additions & 0 deletions src/Turnierplan.Core.Test.Regression/SubjectSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Text;
using Turnierplan.Core.Tournament.TeamSelectors;

namespace Turnierplan.Core.Test.Regression;

internal static class SubjectSerializer
{
public static void AppendSubject<TSubject>(this StringBuilder builder, TSubject subject)
{
switch (subject)
{
case Tournament.Tournament tournament:
AppendTournament(builder, tournament);
break;
default:
throw new InvalidOperationException($"Subject type '{typeof(TSubject).FullName}' currently not supported for serialization.");
}
}

private static void AppendTournament(StringBuilder builder, Tournament.Tournament tournament)
{
builder.Append(tournament.Name);
builder.Append(';');
builder.Append(tournament.Visibility);
builder.Append(';');
builder.Append(tournament.IsPublic);
builder.Append(';');
builder.Append(tournament.PublicPageViews);
builder.Append(';');
builder.Append(tournament.Teams.Count);
builder.Append(';');
builder.Append(tournament.Groups.Count);
builder.Append(';');
builder.Append(tournament.Matches.Count);
builder.Append(';');
builder.Append(tournament.RankingOverwrites.Count);
builder.Append(';');
builder.Append(tournament.Ranking.Count);
builder.AppendLine();

foreach (var team in tournament.Teams)
{
builder.Append('T');
builder.Append(team.Id);
builder.Append(';');
builder.Append(team.Name);
builder.Append(';');
builder.Append(team.EntryFeePaidAt?.Ticks);
builder.Append(';');
builder.Append(team.OutOfCompetition);
builder.Append(';');
builder.Append(team.Statistics.ScoreFor);
builder.Append(';');
builder.Append(team.Statistics.ScoreAgainst);
builder.Append(';');
builder.Append(team.Statistics.MatchesWon);
builder.Append(';');
builder.Append(team.Statistics.MatchesDrawn);
builder.Append(';');
builder.Append(team.Statistics.MatchesLost);
builder.Append(';');
builder.Append(team.Statistics.MatchesPlayed);
builder.AppendLine();
}

foreach (var group in tournament.Groups)
{
builder.Append('G');
builder.Append(group.Id);
builder.Append(';');
builder.Append(group.AlphabeticalId);
builder.Append(';');
builder.Append(group.DisplayName);
builder.Append(';');
builder.Append(group.Participants.Count);
builder.AppendLine();

foreach (var participant in group.Participants)
{
builder.Append('>');
builder.Append(participant.Team.Id);
builder.Append(';');
builder.Append(participant.Group.Id);
builder.Append(';');
builder.Append(participant.Order);
builder.Append(';');
builder.Append(participant.Priority);
builder.Append(';');
builder.Append(participant.Statistics.Position);
builder.Append(';');
builder.Append(participant.Statistics.ScoreFor);
builder.Append(';');
builder.Append(participant.Statistics.ScoreAgainst);
builder.Append(';');
builder.Append(participant.Statistics.MatchesWon);
builder.Append(';');
builder.Append(participant.Statistics.MatchesDrawn);
builder.Append(';');
builder.Append(participant.Statistics.MatchesLost);
builder.Append(';');
builder.Append(participant.Statistics.Points);
builder.Append(';');
builder.Append(participant.Statistics.ScoreDifference);
builder.Append(';');
builder.Append(participant.Statistics.MatchesPlayed);
builder.AppendLine();
}
}

foreach (var match in tournament.Matches)
{
builder.Append('M');
builder.Append(match.Id);
builder.Append(';');
builder.Append(match.Index);
builder.Append(';');
builder.Append(match.Court);
builder.Append(';');
builder.Append(match.Kickoff?.Ticks);
builder.Append(';');
builder.Append(ConvertTeamSelectorToString(match.TeamSelectorA));
builder.Append(';');
builder.Append(ConvertTeamSelectorToString(match.TeamSelectorB));
builder.Append(';');
builder.Append(match.TeamA?.Id);
builder.Append(';');
builder.Append(match.TeamB?.Id);
builder.Append(';');
builder.Append(match.Group?.Id);
builder.Append(';');
builder.Append(match.IsGroupMatch);
builder.Append(';');
builder.Append(match.IsDecidingMatch);
builder.Append(';');
builder.Append(match.PlayoffPosition);
builder.Append(';');
builder.Append(match.IsCurrentlyPlaying);
builder.Append(';');
builder.Append(match.ScoreA);
builder.Append(';');
builder.Append(match.ScoreB);
builder.Append(';');
builder.Append(match.OutcomeType);
builder.Append(';');
builder.Append(match.IsFinished);
builder.AppendLine();
}

foreach (var overwrite in tournament.RankingOverwrites)
{
builder.Append("RO");
builder.Append(overwrite.Id);
builder.Append(';');
builder.Append(overwrite.PlacementRank);
builder.Append(';');
builder.Append(overwrite.HideRanking);
builder.Append(';');
builder.Append(overwrite.AssignTeam?.Id);
builder.AppendLine();
}

foreach (var ranking in tournament.Ranking)
{
builder.Append('R');
builder.Append(ranking.Position);
builder.Append(';');
builder.Append(ranking.Reason);
builder.Append(';');
builder.Append(ranking.IsDefined);
builder.Append(';');
builder.Append(ranking.Team?.Id);
builder.AppendLine();
}
}

/// <remarks>Copied from the DAL project.</remarks>
private static string ConvertTeamSelectorToString(TeamSelectorBase input)
{
switch (input)
{
case GroupDefinitionSelector groupDefinitionSelector:
return $"G{groupDefinitionSelector.TargetGroupId}/{groupDefinitionSelector.TargetTeamIndex}";
case GroupResultsNthRankedSelector groupResultsNthRankedSelector:
var groupIds = string.Join(",", groupResultsNthRankedSelector.TargetGroupIds);
return $"N{groupIds}/{groupResultsNthRankedSelector.OrdinalNumber}/{groupResultsNthRankedSelector.PlacementRank}";
case GroupResultsSelector groupResultsSelector:
return $"R{groupResultsSelector.TargetGroupId}/{groupResultsSelector.TargetGroupPosition}";
case MatchSelector matchSelector:
return $"{(matchSelector.SelectionMode is MatchSelector.Mode.Winner ? "W" : "L")}{matchSelector.TargetMatchIndex}";
case StaticTeamSelector staticTeamSelector:
return $"T{staticTeamSelector.TargetTeamId}";
default:
return string.Empty;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Turnierplan.Core.Tournament;

namespace Turnierplan.Core.Test.Regression.Tests;

internal sealed class Tournament4Teams1Group2RoundsNoFinals : TournamentRegressionTestBase
{
public Tournament4Teams1Group2RoundsNoFinals()
{
var team1 = Step(t => t.AddTeam("Test 1"));
var team2 = Step(t => t.AddTeam("Test 2"));
var team3 = Step(t => t.AddTeam("Test 3"));
var team4 = Step(t => t.AddTeam("Test 4"));

var group1 = Step(t => t.AddGroup('A'));

Step(t => t.AddGroupParticipant(group1, team1));
Step(t => t.AddGroupParticipant(group1, team2));
Step(t => t.AddGroupParticipant(group1, team3));
Step(t => t.AddGroupParticipant(group1, team4));

Step(t => t.GenerateMatchPlan(new MatchPlanConfiguration
{
GroupRoundConfig = new GroupRoundConfig
{
GroupMatchOrder = GroupMatchOrder.Alternating,
GroupPhaseRounds = 2
},
FinalsRoundConfig = null,
ScheduleConfig = null
}));

Step(t => t.Compute());
Step(t => t.Matches[0].SetOutcome(false, 1, 0, MatchOutcomeType.Standard));
Step(t => t.Compute());
Step(t => t.Matches[1].SetOutcome(false, 2, 2, MatchOutcomeType.Standard));
Step(t => t.Matches[2].SetOutcome(false, 3, 1, MatchOutcomeType.Standard));
Step(t => t.Compute());
Step(t => t.Matches[3].SetOutcome(false, 4, 6, MatchOutcomeType.Standard));
Step(t => t.Matches[4].SetOutcome(false, 0, 0, MatchOutcomeType.Standard));
Step(t => t.Matches[5].SetOutcome(false, 2, 2, MatchOutcomeType.Standard));
Step(t => t.Matches[6].SetOutcome(false, 3, 2, MatchOutcomeType.Standard));
Step(t => t.Compute());
Step(t => t.Matches[7].SetOutcome(false, 1, 1, MatchOutcomeType.Standard));
Step(t => t.Matches[8].SetOutcome(false, 0, 3, MatchOutcomeType.Standard));
Step(t => t.Matches[9].SetOutcome(false, 2, 0, MatchOutcomeType.Standard));
Step(t => t.Matches[10].SetOutcome(false, 0, 2, MatchOutcomeType.Standard));
Step(t => t.Matches[11].SetOutcome(false, 2, 1, MatchOutcomeType.Standard));
Step(t => t.Compute());
}
}
Loading
Loading