using HSchool.Content; namespace HSchool.Schedule.Tests; public class TimetablePlannerTests { private readonly DefCatalog _catalog = Fixtures.Catalog(); [Fact] public void OneGym_SpreadsPhysicalEducationAcrossSlots() { var classes = Enumerable.Range(1, 11).Select(year => Fixtures.Class(year - 1, year, pupils: 16)).ToArray(); var map = Fixtures.ClassroomsAndGym(11); var teachers = new[] { Fixtures.Teacher("pe", "PhysicalEducation") }; var table = TimetablePlanner.Build(_catalog, map, classes, teachers); var pe = table.Lessons.Where(lesson => lesson.Subject == "PhysicalEducation").ToArray(); Assert.Equal(11 * 3, pe.Length); Assert.All(pe, lesson => Assert.Equal("gym-hall", lesson.RoomId)); Assert.Equal(pe.Length, pe.Select(lesson => (lesson.Day, lesson.Period)).Distinct().Count()); Assert.All(pe, lesson => Assert.Equal("pe", lesson.TeacherId)); } [Fact] public void TeacherWithTwoSubjects_IsNeverInTwoRoomsAtOnce() { var classes = new[] { Fixtures.Class(0, year: 5, pupils: 16, letter: "A"), Fixtures.Class(1, year: 5, pupils: 16, letter: "B"), }; var map = Fixtures.ClassroomsAndGym(2); var teachers = new[] { Fixtures.Teacher("t1", "Mathematics", "PhysicalEducation") }; var table = TimetablePlanner.Build(_catalog, map, classes, teachers); Assert.Contains(table.Lessons, lesson => lesson.Subject == "Mathematics"); Assert.Contains(table.Lessons, lesson => lesson.Subject == "PhysicalEducation"); Assert.Equal( table.Lessons.Count, table.Lessons.Select(lesson => (lesson.TeacherId, lesson.Day, lesson.Period)).Distinct().Count()); Assert.Equal( table.Lessons.Count, table.Lessons.Select(lesson => (lesson.ClassId, lesson.Day, lesson.Period)).Distinct().Count()); Assert.Equal( table.Lessons.Count, table.Lessons.Select(lesson => (lesson.RoomId, lesson.Day, lesson.Period)).Distinct().Count()); } [Fact] public void ClassOfSixteen_DoesNotEnterALabOfTwelve() { var classes = new[] { Fixtures.Class(0, year: 5, pupils: 16) }; var map = Fixtures.ClassroomAndLab(desks: 16, computers: 12); var teachers = new[] { Fixtures.Teacher("inf", "Informatics") }; var table = TimetablePlanner.Build(_catalog, map, classes, teachers); Assert.DoesNotContain(table.Lessons, lesson => lesson.Subject == "Informatics"); Assert.Contains(table.Uncovered, row => row.ClassId == "y5A" && row.Subject == "Informatics" && row.Hours == 1); } [Fact] public void SubjectWithoutATeacher_IsUncoveredAndLeavesAGap() { var classes = new[] { Fixtures.Class(0, year: 5, pupils: 16) }; var map = Fixtures.ClassroomsAndGym(1); var teachers = new[] { Fixtures.Teacher("pe", "PhysicalEducation") }; var table = TimetablePlanner.Build(_catalog, map, classes, teachers); Assert.DoesNotContain(table.Lessons, lesson => lesson.Subject == "Mathematics"); Assert.Contains(table.Uncovered, row => row.ClassId == "y5A" && row.Subject == "Mathematics" && row.Hours == 5); Assert.Equal(3, table.Lessons.Count(lesson => lesson.Subject == "PhysicalEducation")); } [Fact] public void LockedLesson_SurvivesHiringAnotherTeacher() { var classes = new[] { Fixtures.Class(0, year: 5, pupils: 16) }; var map = Fixtures.ClassroomsAndGym(1); var first = TimetablePlanner.Build( _catalog, map, classes, [Fixtures.Teacher("t1", "Mathematics")]); var pinned = first.Lessons.First(lesson => lesson.Subject == "Mathematics"); var locked = pinned with { Locked = true }; var rebuilt = TimetablePlanner.Build( _catalog, map, classes, [Fixtures.Teacher("t1", "Mathematics"), Fixtures.Teacher("t2", "Mathematics")], [locked]); Assert.Contains( rebuilt.Lessons, lesson => lesson.ClassId == locked.ClassId && lesson.Subject == locked.Subject && lesson.TeacherId == locked.TeacherId && lesson.RoomId == locked.RoomId && lesson.Day == locked.Day && lesson.Period == locked.Period && lesson.Locked); } [Fact] public void SameInputs_YieldTheSameTable() { var classes = Enumerable.Range(1, 11).Select(year => Fixtures.Class(year - 1, year, pupils: 16)).ToArray(); var map = Fixtures.ClassroomsAndGym(11); var teachers = new[] { Fixtures.Teacher("pe", "PhysicalEducation"), Fixtures.Teacher("math", "Mathematics"), }; var a = TimetablePlanner.Build(_catalog, map, classes, teachers); var b = TimetablePlanner.Build(_catalog, map, classes, teachers); Assert.Equal(Snapshot(a), Snapshot(b)); } [Fact] public void Assembly_DoesNotReferenceArchAspNetOrSockets() { var names = typeof(TimetablePlanner).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!); Assert.DoesNotContain(names, name => name.StartsWith("Arch", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Sources_DoNotUseWallClock() { var root = Path.Combine(Fixtures.RepoRoot(), "src", "HSchool.Schedule"); foreach (var path in Directory.EnumerateFiles(root, "*.cs")) { var text = File.ReadAllText(path); Assert.DoesNotContain("DateTime.Now", text, StringComparison.Ordinal); Assert.DoesNotContain("DateTime.UtcNow", text, StringComparison.Ordinal); } } private static string Snapshot(Timetable table) => string.Join( '\n', table.Lessons.Select(lesson => $"{lesson.Day}/{lesson.Period} {lesson.ClassId} {lesson.Subject} {lesson.TeacherId} {lesson.RoomId} {(lesson.Locked ? "L" : "")}") .Concat(table.Uncovered.Select(row => $"U {row.ClassId} {row.Subject} {row.Hours}"))); }