Enhance school creation functionality by introducing support for name sets in the API and UI. Update the catalog to include skills, traits, body attributes, needs, and name sets, improving character generation capabilities. Revise localization strings for better user guidance and update tests to validate the new name set functionality and ensure robustness in school creation processes.
ci / server (push) Failing after 3m45s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 18:57:01 +03:00
parent 38afbcad36
commit e6182e0e45
56 changed files with 3706 additions and 9 deletions
@@ -0,0 +1,153 @@
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));
}
[Fact]
public void ThirteenthFamily_DoesNotChangeTheFirstTwelve()
{
var twelve = FamilySnapshots(Fixtures.Generate(Fixtures.Classrooms(4)), take: 12);
var thirteen = FamilySnapshots(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;
}
var father = people[family.ParentIds[0]];
var mother = people[family.ParentIds[1]];
Assert.False(father.Female);
Assert.True(mother.Female);
foreach (var childId in family.ChildIds)
{
var child = people[childId];
Assert.Equal(
NameGrammar.Patronymic(father.Name.Given, child.Female, NameGrammar.SlavicPatronymic),
child.Name.Patronymic);
Assert.Equal(child.Female ? mother.Name.Surname : father.Name.Surname, child.Name.Surname);
Assert.Equal(father.Name.SurnameCases.Nom, father.Name.Surname);
Assert.Equal(mother.Name.SurnameCases.Nom, mother.Name.Surname);
}
}
}
[Fact]
public void ObesePupil_CannotHaveHighAgility()
{
var agility = Fixtures.Catalog().Skills["Agility"];
var clamped = PersonSampler.ApplyBodyLimits(
80,
agility,
new Dictionary<string, string> { [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 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}"));
private static List<string> 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();
}
}