Files
h-school/tests/HSchool.Schedule.Tests/TimetablePlannerTests.cs
T
Leonid Pershin 2011d12b1d
ci / server (push) Failing after 3m33s
ci / client (push) Successful in 13s
Add timetable planning and related tests to school simulation
- Introduced a new project for timetable planning, dependent on HSchool.Content.
- Updated documentation to reflect the addition of timetable planning and its dependencies.
- Added tests for timetable planning to ensure deterministic behavior with the same staff and map.
- Revised architecture documentation to include the new HSchool.Schedule component and its interactions.
2026-08-19 00:55:02 +03:00

156 lines
6.3 KiB
C#

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}")));
}