namespace HSchool.People.Tests; public class RosterGeneratorTests { [Fact] public void SameSeedMapAndNameSet_YieldTheSameRoster() { var map = Fixtures.Classrooms(4); var a = Fixtures.Generate(map); var b = Fixtures.Generate(map); Assert.Equal(Snapshot(a), Snapshot(b)); } /// /// Each family draws from its own seeded stream, so a bigger map does not reshuffle the /// families already planned: the first twelve keep their surname, their size and every given /// name. What a bigger map does move is which classroom a child sits in — seats are dealt /// across the whole school so siblings are not automatically classmates — and with the /// classroom comes the year, hence the birth year. That is the one thing not asserted here. /// [Fact] public void ThirteenthFamily_DoesNotChangeHowTheFirstTwelveAreDrawn() { var twelve = FamilyIdentities(Fixtures.Generate(Fixtures.Classrooms(4)), take: 12); var thirteen = FamilyIdentities(Fixtures.Generate(Fixtures.Classrooms(5)), take: 12); Assert.Equal(12, twelve.Count); Assert.Equal(twelve, thirteen); } [Fact] public void FamilyNames_ShareSurnameAndPatronymicFromTheFather() { var roster = Fixtures.Generate(Fixtures.Classrooms(4)); var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal); foreach (var family in roster.Families) { if (family.ChildIds.Count == 0) { continue; } // One parent may be absent, so the father's name comes off the family, not off a person. var parents = family.ParentIds.Select(id => people[id]).ToArray(); Assert.NotEmpty(parents); Assert.Equal(parents.Length, parents.DistinctBy(parent => parent.Female).Count()); Assert.NotEmpty(family.FatherGiven); foreach (var parent in parents) { Assert.Equal(parent.Name.SurnameCases.Nom, parent.Name.Surname); } foreach (var childId in family.ChildIds) { var child = people[childId]; Assert.Equal( NameGrammar.Patronymic(family.FatherGiven, child.Female, NameGrammar.SlavicPatronymic), child.Name.Patronymic); var sameSexParent = parents.FirstOrDefault(parent => parent.Female == child.Female); if (sameSexParent is not null) { Assert.Equal(sameSexParent.Name.Surname, child.Name.Surname); } Assert.Equal(child.Name.SurnameCases.Nom, child.Name.Surname); } } } [Fact] public void ObesePupil_CannotHaveHighAgility() { var agility = Fixtures.Catalog().Skills["Agility"]; var clamped = PersonSampler.ApplyBodyLimits( 80, agility, new Dictionary { [BodyBuilds.Attribute] = BodyBuilds.Obese }); Assert.Equal(25, clamped); var roster = Fixtures.Generate(Fixtures.Classrooms(11)); var obesePupils = roster.People.Where(person => person.IsStudent && person.Choices.TryGetValue(BodyBuilds.Attribute, out var build) && build == BodyBuilds.Obese); Assert.All( obesePupils, person => Assert.True( person.Skills["Agility"] <= 25, $"{person.Name.Surname} {person.Name.Given} is obese with agility {person.Skills["Agility"]}.")); } [Fact] public void OneClassroomAndEleven_BothFillSeatsAndJobs() { AssertFilled(Fixtures.Generate(Fixtures.Classrooms(1)), classrooms: 1); AssertFilled(Fixtures.Generate(Fixtures.Classrooms(11)), classrooms: 11); } [Fact] public void SomeStaffAreAlsoParents() { var roster = Fixtures.Generate(Fixtures.Classrooms(4)); Assert.Contains(roster.People, person => person.IsStaff && person.IsParent); } [Fact] public void VanillaMap_HasElevenHomeroomsNotTheComputerLab() { var catalog = Fixtures.Catalog(); var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); var map = CatalogLoader.LastDefaultMap( [CatalogLoader.CorePackId], PackDocuments.FromDirectory(CatalogLoader.CorePackId, root)); Assert.NotNull(map); var demand = SchoolDemand.From(catalog, map); Assert.Equal(11, demand.Classes.Count); Assert.Equal(11 * 16, demand.Seats.Count); Assert.DoesNotContain(demand.Classes, schoolClass => schoolClass.RoomId == "computer-lab"); Assert.Contains(demand.Staff, opening => opening.RoomId == "computer-lab" && opening.Position == "Teacher"); } [Fact] public void RosterJson_RoundTripsAGeneratedRoster() { var roster = Fixtures.Generate(Fixtures.Classrooms(1)); var json = RosterJson.Serialize(RosterDocument.From(7, roster)); var loaded = RosterJson.Parse(json).ToRoster(); Assert.Equal(roster.People.Count, loaded.People.Count); Assert.Equal(roster.People[0].Name.Given, loaded.People[0].Name.Given); Assert.Equal(roster.People[0].Name.SurnameCases.Gen, loaded.People[0].Name.SurnameCases.Gen); Assert.True(RosterFit.Matches(loaded, SchoolDemand.From(Fixtures.Catalog(), Fixtures.Classrooms(1)))); Assert.False(RosterFit.Matches(loaded, SchoolDemand.From(Fixtures.Catalog(), Fixtures.Classrooms(2)))); } [Fact] public void Assembly_DoesNotReferenceArchAspNetOrSockets() { var names = typeof(RosterGenerator).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.People"); 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 void AssertFilled(Roster roster, int classrooms) { Assert.Equal(classrooms, roster.Classes.Count); Assert.Equal(classrooms * 16, roster.People.Count(person => person.IsStudent)); Assert.All(roster.Classes, schoolClass => { Assert.Equal(schoolClass.Capacity, schoolClass.PupilIds.Count); Assert.InRange(schoolClass.Year, 1, 11); }); var demand = SchoolDemand.From(Fixtures.Catalog(), Fixtures.Classrooms(classrooms)); Assert.Equal(demand.Staff.Count, roster.People.Count(person => person.IsStaff)); Assert.All(demand.Staff, opening => Assert.Contains( roster.People, person => person.IsStaff && person.Position == opening.Position && person.WorkplaceRoomId == opening.RoomId)); } private static string Snapshot(Roster roster) => string.Join('\n', roster.People.Select(person => $"{person.Id}|{person.FamilyId}|{person.Female}|{person.BirthDate:O}|{person.Name.Given}|{person.Name.Surname}|{person.Name.Patronymic}|{person.IsStudent}|{person.IsStaff}|{person.IsParent}|{person.ClassId}|{person.Position}|{person.Choices[BodyBuilds.Attribute]}|{Skills(person)}|{string.Join(',', person.Traits)}")); private static string Skills(Person person) => string.Join(',', person.Skills.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}")); /// Who a family is, without where its children ended up sitting. private static List FamilyIdentities(Roster roster, int take) { var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal); return roster.Families .OrderBy(family => family.Id, StringComparer.Ordinal) .Take(take) .Select(family => { var members = family.ParentIds.Concat(family.ChildIds).Select(id => people[id]); return string.Join(';', members.Select(person => $"{person.Id}:{person.Name.Surname} {person.Name.Given} {person.Name.Patronymic}:{person.Female}")); }) .ToList(); } private static List FamilySnapshots(Roster roster, int take) { var people = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal); return roster.Families .OrderBy(family => family.Id, StringComparer.Ordinal) .Take(take) .Select(family => { var members = family.ParentIds.Concat(family.ChildIds).Select(id => people[id]); return string.Join(';', members.Select(person => $"{person.Id}:{person.Name.Surname} {person.Name.Given} {person.Name.Patronymic}:{person.BirthDate:O}:{person.Female}:{Skills(person)}")); }) .ToList(); } }