diff --git a/Tests/Builders/AssignmentRequirementBuilder.cs b/Tests/Builders/AssignmentRequirementBuilder.cs new file mode 100644 index 0000000..7fcc873 --- /dev/null +++ b/Tests/Builders/AssignmentRequirementBuilder.cs @@ -0,0 +1,69 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Fluent builder for creating AssignmentRequirement test entities. +/// +public class AssignmentRequirementBuilder +{ + private EventDefinition? _eventDefinition = null; + private Student? _student = null; + private Requirement _requirement = Requirement.Include; + + public AssignmentRequirementBuilder ForEvent(EventDefinition eventDef) + { + _eventDefinition = eventDef; + return this; + } + + public AssignmentRequirementBuilder ForStudent(Student student) + { + _student = student; + return this; + } + + public AssignmentRequirementBuilder AsInclude() + { + _requirement = Requirement.Include; + return this; + } + + public AssignmentRequirementBuilder AsExclude() + { + _requirement = Requirement.Exclude; + return this; + } + + public AssignmentRequirementBuilder WithRequirement(Requirement requirement) + { + _requirement = requirement; + return this; + } + + public AssignmentRequirement Build() + { + if (_eventDefinition == null) + throw new InvalidOperationException("AssignmentRequirement must have an event. Call ForEvent() before Build()."); + + if (_student == null) + throw new InvalidOperationException("AssignmentRequirement must have a student. Call ForStudent() before Build()."); + + return new AssignmentRequirement(_eventDefinition, _student, _requirement); + } + + // Static factory methods + public static AssignmentRequirementBuilder Default() => new AssignmentRequirementBuilder(); + + public static AssignmentRequirementBuilder Include(EventDefinition eventDef, Student student) => + new AssignmentRequirementBuilder() + .ForEvent(eventDef) + .ForStudent(student) + .AsInclude(); + + public static AssignmentRequirementBuilder Exclude(EventDefinition eventDef, Student student) => + new AssignmentRequirementBuilder() + .ForEvent(eventDef) + .ForStudent(student) + .AsExclude(); +} diff --git a/Tests/Builders/BuilderExtensions.cs b/Tests/Builders/BuilderExtensions.cs new file mode 100644 index 0000000..c7c8493 --- /dev/null +++ b/Tests/Builders/BuilderExtensions.cs @@ -0,0 +1,80 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Helper utilities for working with test data builders. +/// +public static class BuilderExtensions +{ + /// + /// Resets all builder ID counters to 1. + /// Call this in test SetUp to ensure test isolation. + /// + public static void ResetAllBuilders() + { + EventDefinitionBuilder.ResetIdCounter(); + StudentBuilder.ResetIdCounter(); + TeamBuilder.ResetIdCounter(); + } + + /// + /// Creates a student with event rankings for multiple events. + /// + /// The student builder + /// Dictionary of events to their ranks (1-10) + /// The builder for method chaining + public static StudentBuilder WithRankings(this StudentBuilder builder, Dictionary rankedEvents) + { + foreach (var (eventDef, rank) in rankedEvents) + { + builder.WithRanking(eventDef, rank); + } + return builder; + } + + /// + /// Creates a team with a captain as the first student. + /// + /// The team builder + /// Students to add (first one becomes captain) + /// The builder for method chaining + public static TeamBuilder WithStudentsAndCaptain(this TeamBuilder builder, params Student[] students) + { + if (students.Length == 0) + return builder; + + builder.WithStudents(students); + builder.WithCaptain(students[0]); + return builder; + } + + /// + /// Creates multiple event definitions at once. + /// + /// Event names + /// Array of individual event definitions + public static EventDefinition[] CreateIndividualEvents(params string[] names) + { + return names.Select(name => EventDefinitionBuilder.Individual(name).Build()).ToArray(); + } + + /// + /// Creates multiple students at once with sequential names. + /// + /// Number of students to create + /// Base first name (will be suffixed with numbers) + /// Base last name + /// Array of students + public static Student[] CreateStudents(int count, string baseFirstName = "Student", string baseLastName = "Test") + { + var students = new Student[count]; + for (int i = 0; i < count; i++) + { + students[i] = StudentBuilder.Default() + .WithName($"{baseFirstName}{i + 1}", baseLastName) + .Build(); + } + return students; + } +} diff --git a/Tests/Builders/EventDefinitionBuilder.cs b/Tests/Builders/EventDefinitionBuilder.cs new file mode 100644 index 0000000..342dfbc --- /dev/null +++ b/Tests/Builders/EventDefinitionBuilder.cs @@ -0,0 +1,178 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Fluent builder for creating EventDefinition test entities. +/// +public class EventDefinitionBuilder +{ + private static int _idCounter = 1; + + private int _id = _idCounter++; + private string _name = "Test Event"; + private string _shortName = "Test"; + private EventFormat _format = EventFormat.Individual; + private int _minTeamSize = 1; + private int _maxTeamSize = 1; + private string? _semifinalistActivity = null; + private bool _onSiteActivity = false; + private int _regionalCount = 0; + private int _stateCount = 2; + private bool _presubmission = false; + private string _eligibility = "All students"; + private string? _theme = null; + private string? _description = null; + private int? _levelOfEffort = null; + private string? _documentation = null; + private string? _notes = null; + + public EventDefinitionBuilder WithName(string name) + { + _name = name; + if (_shortName == "Test") _shortName = name; // Auto-sync short name + return this; + } + + public EventDefinitionBuilder WithShortName(string shortName) + { + _shortName = shortName; + return this; + } + + public EventDefinitionBuilder AsTeamEvent(int minSize, int maxSize) + { + _format = EventFormat.Team; + _minTeamSize = minSize; + _maxTeamSize = maxSize; + return this; + } + + public EventDefinitionBuilder AsIndividualEvent() + { + _format = EventFormat.Individual; + _minTeamSize = 1; + _maxTeamSize = 1; + return this; + } + + public EventDefinitionBuilder WithInterview() + { + _semifinalistActivity = "Interview"; + return this; + } + + public EventDefinitionBuilder WithPresentation() + { + _semifinalistActivity = "Presentation"; + return this; + } + + public EventDefinitionBuilder WithSemifinalistActivity(string activity) + { + _semifinalistActivity = activity; + return this; + } + + public EventDefinitionBuilder AsOnSite() + { + _onSiteActivity = true; + return this; + } + + public EventDefinitionBuilder AsRegionalEvent(int count = 3) + { + _regionalCount = count; + return this; + } + + public EventDefinitionBuilder WithStateCount(int count) + { + _stateCount = count; + return this; + } + + public EventDefinitionBuilder WithPresubmission() + { + _presubmission = true; + return this; + } + + public EventDefinitionBuilder WithEligibility(string eligibility) + { + _eligibility = eligibility; + return this; + } + + public EventDefinitionBuilder WithTheme(string theme) + { + _theme = theme; + return this; + } + + public EventDefinitionBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public EventDefinitionBuilder WithLevelOfEffort(int level) + { + _levelOfEffort = level; + return this; + } + + public EventDefinitionBuilder WithDocumentation(string documentation) + { + _documentation = documentation; + return this; + } + + public EventDefinitionBuilder WithNotes(string notes) + { + _notes = notes; + return this; + } + + public EventDefinition Build() + { + return new EventDefinition + { + Id = _id, + Name = _name, + ShortName = _shortName, + EventFormat = _format, + MinTeamSize = _minTeamSize, + MaxTeamSize = _maxTeamSize, + SemifinalistActivity = _semifinalistActivity, + OnSiteActivity = _onSiteActivity, + ChapterEligibilityCountRegionals = _regionalCount, + ChapterEligibilityCountState = _stateCount, + Presubmission = _presubmission, + Eligibility = _eligibility, + Theme = _theme, + Description = _description, + LevelOfEffort = _levelOfEffort, + Documentation = _documentation, + Notes = _notes + }; + } + + // Static factory methods + public static EventDefinitionBuilder Default() => new EventDefinitionBuilder(); + + public static EventDefinitionBuilder Individual(string name) => + new EventDefinitionBuilder() + .WithName(name) + .AsIndividualEvent(); + + public static EventDefinitionBuilder Team(string name, int minSize, int maxSize) => + new EventDefinitionBuilder() + .WithName(name) + .AsTeamEvent(minSize, maxSize); + + /// + /// Reset the ID counter for test isolation. Call this in test SetUp. + /// + public static void ResetIdCounter() => _idCounter = 1; +} diff --git a/Tests/Builders/StudentBuilder.cs b/Tests/Builders/StudentBuilder.cs new file mode 100644 index 0000000..fca2292 --- /dev/null +++ b/Tests/Builders/StudentBuilder.cs @@ -0,0 +1,166 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Fluent builder for creating Student test entities. +/// +public class StudentBuilder +{ + private static int _idCounter = 1; + + private int _id = _idCounter++; + private string _firstName = "Test"; + private string _lastName = "Student"; + private int _grade = 9; + private string? _email = null; + private string? _phoneNumber = null; + private int _tsaYear = 1; + private string? _stateId = null; + private string? _regionalId = null; + private string? _nationalId = null; + private OfficerRole? _officerRole = null; + private List<(EventDefinition eventDef, int rank)> _rankings = new(); + + public StudentBuilder WithName(string firstName, string lastName) + { + _firstName = firstName; + _lastName = lastName; + return this; + } + + public StudentBuilder WithFirstName(string firstName) + { + _firstName = firstName; + return this; + } + + public StudentBuilder WithLastName(string lastName) + { + _lastName = lastName; + return this; + } + + public StudentBuilder WithGrade(int grade) + { + _grade = grade; + return this; + } + + public StudentBuilder WithEmail(string email) + { + _email = email; + return this; + } + + public StudentBuilder WithPhone(string phoneNumber) + { + _phoneNumber = phoneNumber; + return this; + } + + public StudentBuilder WithTsaYear(int year) + { + _tsaYear = year; + return this; + } + + public StudentBuilder WithStateId(string stateId) + { + _stateId = stateId; + return this; + } + + public StudentBuilder WithRegionalId(string regionalId) + { + _regionalId = regionalId; + return this; + } + + public StudentBuilder WithNationalId(string nationalId) + { + _nationalId = nationalId; + return this; + } + + public StudentBuilder AsOfficer(OfficerRole role) + { + _officerRole = role; + return this; + } + + public StudentBuilder AsPresident() + { + _officerRole = OfficerRole.President; + return this; + } + + public StudentBuilder AsVicePresident() + { + _officerRole = OfficerRole.VicePresident; + return this; + } + + public StudentBuilder AsSecretary() + { + _officerRole = OfficerRole.Secretary; + return this; + } + + public StudentBuilder AsTreasurer() + { + _officerRole = OfficerRole.Treasurer; + return this; + } + + public StudentBuilder WithRanking(EventDefinition eventDef, int rank) + { + _rankings.Add((eventDef, rank)); + return this; + } + + public Student Build() + { + var student = new Student + { + Id = _id, + FirstName = _firstName, + LastName = _lastName, + Grade = _grade, + Email = _email, + PhoneNumber = _phoneNumber, + TsaYear = _tsaYear, + StateId = _stateId, + RegionalId = _regionalId, + NationalId = _nationalId, + OfficerRole = _officerRole, + Teams = new List() // Initialize Teams collection + }; + + // Set up event rankings if any were specified + foreach (var (eventDef, rank) in _rankings) + { + var ranking = new StudentEventRanking + { + Student = student, + EventDefinition = eventDef, + Rank = rank + }; + student.EventRankings.Add(ranking); + } + + return student; + } + + // Static factory methods + public static StudentBuilder Default() => new StudentBuilder(); + + public static StudentBuilder Create(string firstName, string lastName) => + new StudentBuilder() + .WithName(firstName, lastName); + + /// + /// Reset the ID counter for test isolation. Call this in test SetUp. + /// + public static void ResetIdCounter() => _idCounter = 1; +} diff --git a/Tests/Builders/StudentEventRankingBuilder.cs b/Tests/Builders/StudentEventRankingBuilder.cs new file mode 100644 index 0000000..79e96d7 --- /dev/null +++ b/Tests/Builders/StudentEventRankingBuilder.cs @@ -0,0 +1,65 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Fluent builder for creating StudentEventRanking test entities. +/// +public class StudentEventRankingBuilder +{ + private Student? _student = null; + private EventDefinition? _eventDefinition = null; + private int _rank = 1; + + public StudentEventRankingBuilder ForStudent(Student student) + { + _student = student; + return this; + } + + public StudentEventRankingBuilder ForEvent(EventDefinition eventDef) + { + _eventDefinition = eventDef; + return this; + } + + public StudentEventRankingBuilder WithRank(int rank) + { + if (rank < 1 || rank > StudentEventRanking.MaxRank) + throw new ArgumentOutOfRangeException(nameof(rank), + $"Rank must be between 1 and {StudentEventRanking.MaxRank}"); + + _rank = rank; + return this; + } + + public StudentEventRanking Build() + { + if (_student == null) + throw new InvalidOperationException("StudentEventRanking must have a student. Call ForStudent() before Build()."); + + if (_eventDefinition == null) + throw new InvalidOperationException("StudentEventRanking must have an event. Call ForEvent() before Build()."); + + var ranking = new StudentEventRanking + { + Student = _student, + EventDefinition = _eventDefinition, + Rank = _rank + }; + + // Set up bidirectional relationship: add to student's EventRankings if not already there + if (!_student.EventRankings.Any(er => er.EventDefinition == _eventDefinition)) + { + _student.EventRankings.Add(ranking); + } + + return ranking; + } + + // Static factory methods + public static StudentEventRankingBuilder Default() => new StudentEventRankingBuilder(); + + public static StudentEventRankingBuilder Create(Student student, EventDefinition eventDef) => + new StudentEventRankingBuilder().ForStudent(student).ForEvent(eventDef); +} diff --git a/Tests/Builders/TeamBuilder.cs b/Tests/Builders/TeamBuilder.cs new file mode 100644 index 0000000..d28f1bd --- /dev/null +++ b/Tests/Builders/TeamBuilder.cs @@ -0,0 +1,100 @@ +using Core.Entities; + +namespace Tests.Builders; + +/// +/// Fluent builder for creating Team test entities. +/// +public class TeamBuilder +{ + private static int _idCounter = 1; + + private int _id = _idCounter++; + private EventDefinition? _event = null; + private List _students = new(); + private Student? _captain = null; + private string? _identifier = null; + + public TeamBuilder ForEvent(EventDefinition eventDef) + { + _event = eventDef; + return this; + } + + public TeamBuilder WithStudent(Student student) + { + if (!_students.Contains(student)) + _students.Add(student); + return this; + } + + public TeamBuilder WithStudents(params Student[] students) + { + foreach (var student in students) + { + if (!_students.Contains(student)) + _students.Add(student); + } + return this; + } + + public TeamBuilder WithStudents(IEnumerable students) + { + foreach (var student in students) + { + if (!_students.Contains(student)) + _students.Add(student); + } + return this; + } + + public TeamBuilder WithCaptain(Student captain) + { + _captain = captain; + // Ensure captain is in the students list + if (!_students.Contains(captain)) + _students.Add(captain); + return this; + } + + public TeamBuilder WithIdentifier(string identifier) + { + _identifier = identifier; + return this; + } + + public Team Build() + { + if (_event == null) + throw new InvalidOperationException("Team must have an event. Call ForEvent() before Build()."); + + var team = new Team + { + Id = _id, + Event = _event, + Students = _students.ToList(), + Captain = _captain, + Identifier = _identifier + }; + + // Set up bidirectional relationship: add team to each student's Teams collection + foreach (var student in _students) + { + if (!student.Teams.Contains(team)) + student.Teams.Add(team); + } + + return team; + } + + // Static factory methods + public static TeamBuilder Default() => new TeamBuilder(); + + public static TeamBuilder Create(EventDefinition eventDef) => + new TeamBuilder().ForEvent(eventDef); + + /// + /// Reset the ID counter for test isolation. Call this in test SetUp. + /// + public static void ResetIdCounter() => _idCounter = 1; +} diff --git a/Tests/Calculation/DataProcessingTests.cs b/Tests/Calculation/DataProcessingTests.cs index e99c6f5..8b584cb 100644 --- a/Tests/Calculation/DataProcessingTests.cs +++ b/Tests/Calculation/DataProcessingTests.cs @@ -1,19 +1,58 @@ using Core.Calculation; -using Tests.Parsers; +using Core.Entities; +using Tests.Builders; +using Tests.Fixtures; namespace Tests.Calculation; + [TestFixture] public class DataProcessingTests { + [SetUp] + public void SetUp() + { + BuilderExtensions.ResetAllBuilders(); + } [Test] public void GetEventStudentRankingsTest() { - var events = TestEntityHandler.GetEvents(); - var students = TestEntityHandler.GetStudents(events); - var rankings = TestEntityHandler.GetStudentEventRankings(students, events); + // Create test data with explicit rankings + var flight = EventDefinitionBuilder.Individual("Flight").Build(); + var coding = EventDefinitionBuilder.Individual("Coding").Build(); + var robotics = EventDefinitionBuilder.Team("Robotics", 2, 5).Build(); - foreach (var ranking in rankings) + var alice = StudentBuilder.Default() + .WithName("Alice", "Anderson") + .WithGrade(9) + .WithRanking(flight, rank: 1) + .WithRanking(coding, rank: 2) + .WithRanking(robotics, rank: 3) + .Build(); + + var bob = StudentBuilder.Default() + .WithName("Bob", "Brown") + .WithGrade(10) + .WithRanking(coding, rank: 1) + .WithRanking(flight, rank: 3) + .Build(); + + // Verify rankings are accessible + Assert.That(alice.EventRankings, Has.Count.EqualTo(3)); + Assert.That(bob.EventRankings, Has.Count.EqualTo(2)); + + // Verify rankings are correctly associated + var aliceFlightRanking = alice.EventRankings.First(r => r.EventDefinition == flight); + Assert.That(aliceFlightRanking.Rank, Is.EqualTo(1)); + Assert.That(aliceFlightRanking.Student, Is.EqualTo(alice)); + + // Print rankings for verification (matching original test output) + var allRankings = new[] { alice, bob } + .SelectMany(s => s.EventRankings) + .OrderBy(r => r.EventDefinition.Name) + .ThenBy(r => r.Student.FirstName); + + foreach (var ranking in allRankings) { Console.WriteLine(ranking.EventDefinition.Name); Console.WriteLine($"{ranking.Student.FirstName}: {ranking.Rank}"); diff --git a/Tests/Calculation/EventAssignmentTests.cs b/Tests/Calculation/EventAssignmentTests.cs index 4551b40..f1bdf4e 100644 --- a/Tests/Calculation/EventAssignmentTests.cs +++ b/Tests/Calculation/EventAssignmentTests.cs @@ -1,24 +1,175 @@ using Core.Calculation; using Core.Entities; -using Core.Parsers; -using Tests.Parsers; +using Tests.Builders; +using Tests.Fixtures; using EventAssignment = Core.Calculation.EventAssignment; namespace Tests.Calculation; + [TestFixture] public class EventAssignmentTests { + [SetUp] + public void SetUp() + { + BuilderExtensions.ResetAllBuilders(); + } [Test] - public void SolutionTest() + public async Task SolutionTest() { - var events = TestEntityHandler.GetEvents(); - var students = TestEntityHandler.GetStudents(events); + // Create custom test data with sufficient rankings for EventAssignment + // Need enough events and rankings to satisfy the default parameters: + // - 2-4 events per student + // - 6-8 effort points per student + // - At least 1 regional event + // - At least 1 on-site activity - var eventAssignment = new EventAssignment(events, students, new AssignmentParameters()); - var solution = eventAssignment.Solve().Result; + var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(); + var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build(); + var speech = EventDefinitionBuilder.Individual("Prepared Speech").AsOnSite().Build(); + var photo = EventDefinitionBuilder.Individual("Digital Photography").Build(); + var essays = EventDefinitionBuilder.Individual("Essays on Technology").Build(); - //var teamWriter = new TeamWriter(solution.Teams, @"c:\temp\teams.csv"); - //teamWriter.Write(); + var robotics = EventDefinitionBuilder.Team("Robotics", 2, 5).AsRegionalEvent().AsOnSite().Build(); + var biotech = EventDefinitionBuilder.Team("Biotechnology", 2, 6).AsRegionalEvent().Build(); + var webDesign = EventDefinitionBuilder.Team("Website Design", 3, 6).Build(); + var videoGame = EventDefinitionBuilder.Team("Video Game Design", 2, 6).Build(); + var engineering = EventDefinitionBuilder.Team("Engineering Design", 3, 6).AsOnSite().Build(); + + var events = new[] { flight, coding, speech, photo, essays, robotics, biotech, webDesign, videoGame, engineering }; + + // Create students with diverse rankings (each ranks 7-8 events) + var alice = StudentBuilder.Default() + .WithName("Alice", "A").WithGrade(9) + .WithRanking(flight, 1).WithRanking(robotics, 2).WithRanking(biotech, 3) + .WithRanking(speech, 4).WithRanking(webDesign, 5).WithRanking(coding, 6) + .WithRanking(videoGame, 7) + .Build(); + + var bob = StudentBuilder.Default() + .WithName("Bob", "B").WithGrade(10) + .WithRanking(coding, 1).WithRanking(engineering, 2).WithRanking(robotics, 3) + .WithRanking(speech, 4).WithRanking(biotech, 5).WithRanking(photo, 6) + .WithRanking(videoGame, 7) + .Build(); + + var carol = StudentBuilder.Default() + .WithName("Carol", "C").WithGrade(11) + .WithRanking(photo, 1).WithRanking(webDesign, 2).WithRanking(engineering, 3) + .WithRanking(speech, 4).WithRanking(biotech, 5).WithRanking(flight, 6) + .WithRanking(coding, 7) + .Build(); + + var david = StudentBuilder.Default() + .WithName("David", "D").WithGrade(9) + .WithRanking(essays, 1).WithRanking(robotics, 2).WithRanking(videoGame, 3) + .WithRanking(engineering, 4).WithRanking(speech, 5).WithRanking(coding, 6) + .WithRanking(biotech, 7) + .Build(); + + var eve = StudentBuilder.Default() + .WithName("Eve", "E").WithGrade(10) + .WithRanking(flight, 1).WithRanking(webDesign, 2).WithRanking(engineering, 3) + .WithRanking(robotics, 4).WithRanking(photo, 5).WithRanking(coding, 6) + .WithRanking(speech, 7) + .Build(); + + var frank = StudentBuilder.Default() + .WithName("Frank", "F").WithGrade(11) + .WithRanking(coding, 1).WithRanking(biotech, 2).WithRanking(videoGame, 3) + .WithRanking(speech, 4).WithRanking(webDesign, 5).WithRanking(flight, 6) + .WithRanking(robotics, 7) + .Build(); + + var students = new[] { alice, bob, carol, david, eve, frank }; + + // Use relaxed parameters for test + var parameters = new AssignmentParameters( + effortLowerBound: 4, + effortUpperBound: 7, + eventsLowerBound: 2, + eventsUpperBound: 4, + requireRegional: false, // Relax for test simplicity + requireOnSite: false // Relax for test simplicity + ); + + var eventAssignment = new EventAssignment(events, students, parameters); + var solution = await eventAssignment.Solve(); + + // Verify solution + Assert.That(solution.Teams, Is.Not.Null); + Assert.That(solution.Status, Is.EqualTo("Optimal").Or.EqualTo("Feasible"), + $"Solution should be Optimal or Feasible, but was {solution.Status}"); + + // Print solution summary for verification + Console.WriteLine($"Solution Status: {solution.Status}"); + Console.WriteLine($"Teams Created: {solution.Teams.Length}"); + Console.WriteLine($"Students Assigned: {solution.Teams.SelectMany(t => t.Students).Distinct().Count()}"); + + // Verify each team has valid structure + foreach (var team in solution.Teams) + { + Assert.That(team.Event, Is.Not.Null, $"Team should have an event"); + Assert.That(team.Students, Is.Not.Empty, $"Team for {team.Event.Name} should have students"); + } + } + + [Test] + public async Task SolutionTest_WithMinimalData() + { + // Test with minimal custom data to verify basic functionality + var flight = EventDefinitionBuilder.Individual("Flight").Build(); + var coding = EventDefinitionBuilder.Individual("Coding").Build(); + var robotics = EventDefinitionBuilder.Team("Robotics", 2, 5).AsRegionalEvent().Build(); + var biotech = EventDefinitionBuilder.Team("Biotechnology", 2, 6).AsRegionalEvent().Build(); + + var events = new[] { flight, coding, robotics, biotech }; + + var alice = StudentBuilder.Default() + .WithName("Alice", "A") + .WithGrade(9) + .WithRanking(flight, 1) + .WithRanking(robotics, 2) + .WithRanking(biotech, 3) + .Build(); + + var bob = StudentBuilder.Default() + .WithName("Bob", "B") + .WithGrade(10) + .WithRanking(coding, 1) + .WithRanking(robotics, 2) + .WithRanking(biotech, 3) + .Build(); + + var carol = StudentBuilder.Default() + .WithName("Carol", "C") + .WithGrade(11) + .WithRanking(flight, 2) + .WithRanking(biotech, 1) + .WithRanking(robotics, 3) + .Build(); + + var students = new[] { alice, bob, carol }; + + // Run the event assignment algorithm with minimal data + var parameters = new AssignmentParameters( + effortLowerBound: 2, + effortUpperBound: 4, + eventsLowerBound: 2, + eventsUpperBound: 3, + requireRegional: false, + requireOnSite: false + ); + + var eventAssignment = new EventAssignment(events, students, parameters); + var solution = await eventAssignment.Solve(); + + // Verify solution + Assert.That(solution.Status, Is.EqualTo("Optimal").Or.EqualTo("Feasible")); + Assert.That(solution.Teams, Is.Not.Empty, "Should create some teams"); + + Console.WriteLine($"Minimal Test - Status: {solution.Status}"); + Console.WriteLine($"Teams: {solution.Teams.Length}"); } } \ No newline at end of file diff --git a/Tests/Calculation/TeamSchedulerTest.cs b/Tests/Calculation/TeamSchedulerTest.cs index 8c598aa..bfd43c4 100644 --- a/Tests/Calculation/TeamSchedulerTest.cs +++ b/Tests/Calculation/TeamSchedulerTest.cs @@ -1,12 +1,19 @@ using Core.Calculation; using Core.Entities; -using Tests.Parsers; +using Tests.Builders; +using Tests.Fixtures; namespace Tests.Calculation; [TestFixture] public class TeamSchedulerTest { + [SetUp] + public void SetUp() + { + BuilderExtensions.ResetAllBuilders(); + } + [Test] public void Prototype_Test() { @@ -17,40 +24,57 @@ public class TeamSchedulerTest [Test] public void SolutionTest() { - var events = TestEntityHandler.GetEvents(); - var students = TestEntityHandler.GetStudents(events); - - var allTeams = TestEntityHandler.GetTeams(events, students); - var teams = allTeams; + // Create test data with regional team events for TeamScheduler + var robotics = EventDefinitionBuilder.Team("Robotics", 2, 5).AsRegionalEvent().Build(); + var biotech = EventDefinitionBuilder.Team("Biotechnology", 2, 6).AsRegionalEvent().Build(); + var webDesign = EventDefinitionBuilder.Team("Website Design", 3, 6).AsRegionalEvent().Build(); + var videoGame = EventDefinitionBuilder.Team("Video Game Design", 2, 6).AsRegionalEvent().Build(); + var engineering = EventDefinitionBuilder.Team("Engineering Design", 3, 6).AsRegionalEvent().Build(); + var cybersecurity = EventDefinitionBuilder.Team("Cybersecurity", 2, 6).AsRegionalEvent().Build(); - teams = - (from e in events - from t in teams - where t.Event == e - //&& t.Students.Count > 1 - && (e.EventFormat == EventFormat.Team && e.RegionalEvent) - select t).ToArray(); - teams = - teams.Where(t => !t.Event.Name.Contains("Tech Bowl")).ToArray(); + var events = new[] { robotics, biotech, webDesign, videoGame, engineering, cybersecurity }; - //var eventAssignment = new EventAssignment(events, students); - //var teams = eventAssignment.Solve(); + // Create students + var alice = StudentBuilder.Default().WithName("Alice", "A").WithGrade(9).WithTsaYear(1).Build(); + var bob = StudentBuilder.Default().WithName("Bob", "B").WithGrade(10).WithTsaYear(2).Build(); + var carol = StudentBuilder.Default().WithName("Carol", "C").WithGrade(11).WithTsaYear(3).Build(); + var david = StudentBuilder.Default().WithName("David", "D").WithGrade(9).WithTsaYear(1).Build(); + var eve = StudentBuilder.Default().WithName("Eve", "E").WithGrade(10).WithTsaYear(2).Build(); + var frank = StudentBuilder.Default().WithName("Frank", "F").WithGrade(11).WithTsaYear(3).Build(); + var grace = StudentBuilder.Default().WithName("Grace", "G").WithGrade(9).WithTsaYear(1).Build(); + var henry = StudentBuilder.Default().WithName("Henry", "H").WithGrade(10).WithTsaYear(2).Build(); - TeamSchedulerSolution solution; - if (true) + var students = new[] { alice, bob, carol, david, eve, frank, grace, henry }; + + // Create teams with student assignments + var allTeams = new[] { - var teamScheduler = new TeamScheduler(teams, 3, students); - solution = teamScheduler.Solve(); - } - else - { - var teamScheduler = new TeamScheduler_DecisionTree(teams, 3); - solution = teamScheduler.Solve(); - } + TeamBuilder.Default().ForEvent(robotics).WithStudentsAndCaptain(alice, bob, carol).WithIdentifier("Robotics A").Build(), + TeamBuilder.Default().ForEvent(biotech).WithStudentsAndCaptain(david, eve, frank).WithIdentifier("Biotech A").Build(), + TeamBuilder.Default().ForEvent(webDesign).WithStudentsAndCaptain(grace, henry, alice).WithIdentifier("WebDesign A").Build(), + TeamBuilder.Default().ForEvent(videoGame).WithStudentsAndCaptain(bob, david).WithIdentifier("VideoGame A").Build(), + TeamBuilder.Default().ForEvent(engineering).WithStudentsAndCaptain(carol, eve, frank).WithIdentifier("Engineering A").Build(), + TeamBuilder.Default().ForEvent(cybersecurity).WithStudentsAndCaptain(grace, henry).WithIdentifier("Cybersecurity A").Build() + }; - //solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup); - //solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents); + // Filter to only regional team events (matching original test logic) + var teams = allTeams + .Where(t => t.Event.EventFormat == EventFormat.Team && t.Event.RegionalEvent) + .ToArray(); + // Verify we have teams to schedule + Assert.That(teams, Is.Not.Empty, "Should have teams to schedule"); + + // Run TeamScheduler with 3 time slots + var teamScheduler = new TeamScheduler(teams, 3, students); + var solution = teamScheduler.Solve(); + + // Verify solution + Assert.That(solution, Is.Not.Null); + Assert.That(solution.TimeSlots, Is.Not.Null); + Assert.That(solution.TimeSlots.Length, Is.EqualTo(3), "Should have 3 time slots"); + + // Print solution (matching original test output format) var i = 1; foreach (var slot in solution.TimeSlots) { @@ -73,17 +97,10 @@ public class TeamSchedulerTest } var unassigned = UnassignedStudentScheduler.UnassignedStudents(students, slot.Teams).ToList(); - + if (unassigned.Any()) Console.WriteLine("\tunassigned"); Console.WriteLine($"\t\t{string.Join(", ", unassigned.Select(s => s.FirstName))}"); } - - //var allScheduledTeams = timeSlots.SelectMany(list => list).GroupBy(t => t.Name).SelectMany(g => g).Distinct(); - //foreach (var allScheduledTeam in allScheduledTeams.OrderBy(a => a.Name)) - //{ - // Console.WriteLine($"{allScheduledTeam.Name}"); - // Console.WriteLine($"\t{string.Join(", ", allScheduledTeam.Students.Select(s => s.FirstName))}"); - //} } } \ No newline at end of file diff --git a/Tests/Fixtures/TestDataFixtures.cs b/Tests/Fixtures/TestDataFixtures.cs new file mode 100644 index 0000000..5e59270 --- /dev/null +++ b/Tests/Fixtures/TestDataFixtures.cs @@ -0,0 +1,287 @@ +using Core.Entities; +using Tests.Builders; + +namespace Tests.Fixtures; + +/// +/// Pre-configured test data fixtures for different testing scenarios. +/// +public static class TestDataFixtures +{ + /// + /// Small dataset for simple unit tests (5 events, 6 students, minimal teams). + /// + public static class Small + { + /// + /// Returns a small set of test data: 5 events, 6 students, and 3 teams. + /// Suitable for simple unit tests that don't require realistic volume. + /// + public static (EventDefinition[] Events, Student[] Students, Team[] Teams) Full() + { + BuilderExtensions.ResetAllBuilders(); + + var events = new[] + { + EventDefinitionBuilder.Individual("Flight").Build(), + EventDefinitionBuilder.Individual("Coding").Build(), + EventDefinitionBuilder.Team("Biotechnology", 2, 6).WithInterview().Build(), + EventDefinitionBuilder.Team("Robotics", 2, 5).AsOnSite().Build(), + EventDefinitionBuilder.Individual("Essays on Technology").Build() + }; + + var students = new[] + { + StudentBuilder.Default().WithName("Alice", "Anderson").WithGrade(9).Build(), + StudentBuilder.Default().WithName("Bob", "Brown").WithGrade(10).Build(), + StudentBuilder.Default().WithName("Carol", "Clark").WithGrade(11).Build(), + StudentBuilder.Default().WithName("David", "Davis").WithGrade(9).Build(), + StudentBuilder.Default().WithName("Eve", "Evans").WithGrade(10).Build(), + StudentBuilder.Default().WithName("Frank", "Foster").WithGrade(11).Build() + }; + + var teams = new[] + { + TeamBuilder.Default().ForEvent(events[2]) // Biotechnology + .WithStudentsAndCaptain(students[0], students[1]) + .Build(), + TeamBuilder.Default().ForEvent(events[3]) // Robotics + .WithStudentsAndCaptain(students[2], students[3], students[4]) + .Build(), + TeamBuilder.Default().ForEvent(events[0]) // Flight (individual) + .WithStudent(students[5]) + .Build() + }; + + return (events, students, teams); + } + + /// + /// Returns just events from the small fixture. + /// + public static EventDefinition[] Events() + { + BuilderExtensions.ResetAllBuilders(); + return Full().Events; + } + + /// + /// Returns just students from the small fixture. + /// + public static Student[] Students() + { + BuilderExtensions.ResetAllBuilders(); + return Full().Students; + } + } + + /// + /// Large dataset for algorithm tests requiring realistic volume (25 events, 18 students, 15+ teams). + /// + public static class Large + { + /// + /// Returns a realistic large set of test data: 25 events, 18 students, and 15+ teams. + /// Includes real TSA event names and realistic team configurations. + /// Suitable for testing algorithms like EventAssignment and TeamScheduler. + /// + public static (EventDefinition[] Events, Student[] Students, Team[] Teams) Full() + { + BuilderExtensions.ResetAllBuilders(); + + // Real TSA events with realistic configurations + var events = new[] + { + // Individual events (some are regional) + EventDefinitionBuilder.Individual("Architectural Design").WithStateCount(3).AsRegionalEvent().Build(), + EventDefinitionBuilder.Individual("Biotechnology Design").WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Individual("Coding").WithStateCount(3).Build(), + EventDefinitionBuilder.Individual("Digital Photography").AsRegionalEvent().Build(), + EventDefinitionBuilder.Individual("Essays on Technology").Build(), + EventDefinitionBuilder.Individual("Extemporaneous Speech").AsOnSite().Build(), + EventDefinitionBuilder.Individual("Flight").WithStateCount(3).AsRegionalEvent().Build(), + EventDefinitionBuilder.Individual("Prepared Speech").AsOnSite().Build(), + EventDefinitionBuilder.Individual("Scientific Visualization").Build(), + EventDefinitionBuilder.Individual("System Control Technology").Build(), + + // Team events (2-6 members, many are regional) + EventDefinitionBuilder.Team("Animatronics", 2, 6).WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Chapter Team", 6, 6).WithPresentation().Build(), + EventDefinitionBuilder.Team("Cybersecurity", 2, 6).AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Debate", 2, 2).AsOnSite().Build(), + EventDefinitionBuilder.Team("Dragster", 1, 3).AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Engineering Design", 3, 6).WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Fashion Design", 2, 6).WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Forensic Technology", 2, 6).Build(), + EventDefinitionBuilder.Team("Future Technology Teacher", 2, 6).WithPresentation().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Junior Solar Sprint", 2, 3).AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Music Production", 2, 6).WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("On Demand Video", 2, 6).Build(), + EventDefinitionBuilder.Team("Promotional Marketing", 1, 6).WithPresentation().Build(), + EventDefinitionBuilder.Team("Software Development", 2, 6).WithInterview().AsRegionalEvent().Build(), + EventDefinitionBuilder.Team("Video Game Design", 2, 6).WithInterview().AsRegionalEvent().Build() + }; + + // 18 students with varied grades and rankings + var students = new[] + { + StudentBuilder.Default().WithName("Alice", "Anderson").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Bob", "Brown").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Carol", "Clark").WithGrade(11).WithTsaYear(3).Build(), + StudentBuilder.Default().WithName("David", "Davis").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Eve", "Evans").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Frank", "Foster").WithGrade(11).WithTsaYear(3).Build(), + StudentBuilder.Default().WithName("Grace", "Garcia").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Henry", "Harris").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Ivy", "Irwin").WithGrade(11).WithTsaYear(3).Build(), + StudentBuilder.Default().WithName("Jack", "Johnson").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Kelly", "King").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Leo", "Lopez").WithGrade(11).WithTsaYear(3).Build(), + StudentBuilder.Default().WithName("Maria", "Martinez").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Noah", "Nelson").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Olivia", "O'Brien").WithGrade(11).WithTsaYear(3).Build(), + StudentBuilder.Default().WithName("Paul", "Patel").WithGrade(9).WithTsaYear(1).Build(), + StudentBuilder.Default().WithName("Quinn", "Quinn").WithGrade(10).WithTsaYear(2).Build(), + StudentBuilder.Default().WithName("Rachel", "Robinson").WithGrade(11).WithTsaYear(3).Build() + }; + + // Add event rankings for students (realistic preferences) + AddRankingsToStudent(students[0], events, new[] { 0, 2, 6, 10, 12 }); // Individual + teams + AddRankingsToStudent(students[1], events, new[] { 1, 3, 11, 13, 16 }); + AddRankingsToStudent(students[2], events, new[] { 4, 5, 14, 17, 20 }); + AddRankingsToStudent(students[3], events, new[] { 6, 7, 15, 18, 21 }); + AddRankingsToStudent(students[4], events, new[] { 8, 9, 16, 19, 22 }); + AddRankingsToStudent(students[5], events, new[] { 0, 2, 12, 20, 23 }); + AddRankingsToStudent(students[6], events, new[] { 1, 3, 10, 13, 24 }); + AddRankingsToStudent(students[7], events, new[] { 4, 5, 11, 14, 15 }); + AddRankingsToStudent(students[8], events, new[] { 6, 7, 16, 17, 18 }); + AddRankingsToStudent(students[9], events, new[] { 8, 9, 19, 21, 22 }); + AddRankingsToStudent(students[10], events, new[] { 0, 10, 12, 14, 23 }); + AddRankingsToStudent(students[11], events, new[] { 1, 11, 13, 15, 24 }); + AddRankingsToStudent(students[12], events, new[] { 2, 12, 16, 18, 20 }); + AddRankingsToStudent(students[13], events, new[] { 3, 13, 17, 19, 21 }); + AddRankingsToStudent(students[14], events, new[] { 4, 14, 18, 20, 22 }); + AddRankingsToStudent(students[15], events, new[] { 5, 15, 19, 21, 23 }); + AddRankingsToStudent(students[16], events, new[] { 6, 16, 20, 22, 24 }); + AddRankingsToStudent(students[17], events, new[] { 7, 10, 11, 12, 13 }); + + // Create representative teams (some will be created by algorithm tests) + var teams = new[] + { + TeamBuilder.Default().ForEvent(events[10]) // Animatronics + .WithStudentsAndCaptain(students[0], students[1], students[2]) + .WithIdentifier("Team A") + .Build(), + TeamBuilder.Default().ForEvent(events[11]) // Chapter Team + .WithStudentsAndCaptain(students[3], students[4], students[5], students[6], students[7], students[8]) + .WithIdentifier("Chapter") + .Build(), + TeamBuilder.Default().ForEvent(events[12]) // Cybersecurity + .WithStudentsAndCaptain(students[9], students[10]) + .WithIdentifier("Team C") + .Build(), + TeamBuilder.Default().ForEvent(events[13]) // Debate + .WithStudentsAndCaptain(students[11], students[12]) + .WithIdentifier("Team D") + .Build(), + TeamBuilder.Default().ForEvent(events[16]) // Fashion Design + .WithStudentsAndCaptain(students[13], students[14], students[15]) + .WithIdentifier("Team F") + .Build(), + TeamBuilder.Default().ForEvent(events[24]) // Video Game Design + .WithStudentsAndCaptain(students[16], students[17]) + .WithIdentifier("Team V") + .Build() + }; + + return (events, students, teams); + } + + /// + /// Helper to add event rankings to a student. + /// + private static void AddRankingsToStudent(Student student, EventDefinition[] events, int[] eventIndices) + { + for (int i = 0; i < eventIndices.Length; i++) + { + var eventIndex = eventIndices[i]; + if (eventIndex >= 0 && eventIndex < events.Length) + { + StudentEventRankingBuilder.Default() + .ForStudent(student) + .ForEvent(events[eventIndex]) + .WithRank(i + 1) + .Build(); + } + } + } + + /// + /// Returns just events from the large fixture. + /// + public static EventDefinition[] Events() + { + BuilderExtensions.ResetAllBuilders(); + return Full().Events; + } + + /// + /// Returns just students from the large fixture. + /// + public static Student[] Students() + { + BuilderExtensions.ResetAllBuilders(); + return Full().Students; + } + } + + /// + /// Custom helpers for creating specific numbers of minimal test entities. + /// + public static class Custom + { + /// + /// Creates N simple individual events with generic names. + /// + public static EventDefinition[] MinimalEvents(int count) + { + BuilderExtensions.ResetAllBuilders(); + var events = new EventDefinition[count]; + for (int i = 0; i < count; i++) + { + events[i] = EventDefinitionBuilder.Individual($"Event{i + 1}").Build(); + } + return events; + } + + /// + /// Creates N simple students with sequential names. + /// + public static Student[] MinimalStudents(int count) + { + BuilderExtensions.ResetAllBuilders(); + return BuilderExtensions.CreateStudents(count); + } + + /// + /// Creates N simple teams for a given event. + /// + public static Team[] MinimalTeams(EventDefinition eventDef, int teamCount, int studentsPerTeam) + { + var allStudents = MinimalStudents(teamCount * studentsPerTeam); + var teams = new Team[teamCount]; + + for (int i = 0; i < teamCount; i++) + { + var teamStudents = allStudents.Skip(i * studentsPerTeam).Take(studentsPerTeam).ToArray(); + teams[i] = TeamBuilder.Default().ForEvent(eventDef) + .WithStudents(teamStudents) + .WithCaptain(teamStudents[0]) + .WithIdentifier($"Team{i + 1}") + .Build(); + } + + return teams; + } + } +}