Implement applicant pool functionality in school simulation, allowing for the management of job seekers who are not yet part of the school roster. Update the catalog to include staffing definitions and enhance the API to support applicant data retrieval. Revise the school architecture to handle applicant refresh logic and ensure proper integration with existing roster management. Update tests to validate the new applicant functionalities and ensure robustness in handling staffing scenarios.
This commit is contained in:
@@ -64,6 +64,23 @@ public class PeopleApiTests(AppHostFixture fixture)
|
||||
Assert.Equal("invalid-query", await ProblemCodeAsync(huge));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_HasPupilsAndParentsButNoStaff()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Пустой штат", Start);
|
||||
|
||||
var students = await GetPeopleAsync(client, school.Id, "role=student&pageSize=10");
|
||||
var parents = await GetPeopleAsync(client, school.Id, "role=parent&pageSize=10");
|
||||
var staff = await GetPeopleAsync(client, school.Id, "role=staff&pageSize=10");
|
||||
|
||||
Assert.NotEmpty(students.People);
|
||||
Assert.NotEmpty(parents.People);
|
||||
Assert.Equal(0, staff.Total);
|
||||
Assert.Empty(staff.People);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_UnknownSchool_IsNotFound()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,8 @@ public class PeopleDefTests
|
||||
Assert.Equal("Slavic", catalog.Label("en", catalog.NameSets["Slavic"]));
|
||||
Assert.Equal("Усидчивый", catalog.Label("ru", catalog.Traits["Diligent"]));
|
||||
Assert.True(catalog.Subjects.ContainsKey("PrimarySchool"));
|
||||
Assert.NotNull(catalog.StaffingRules);
|
||||
Assert.Equal(12, catalog.StaffingRules.PoolSize);
|
||||
Assert.Equal("Начальные классы", catalog.Label("ru", catalog.Subjects["PrimarySchool"]));
|
||||
Assert.Equal("Primary", catalog.Label("en", catalog.Subjects["PrimarySchool"]));
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ public class VanillaCoreTests
|
||||
Assert.Equal(1, catalog.Subjects["PrimarySchool"].Grades.Min);
|
||||
Assert.Equal(4, catalog.Subjects["PrimarySchool"].Grades.Max);
|
||||
Assert.True(catalog.Subjects.ContainsKey("PhysicalEducation"));
|
||||
Assert.NotNull(catalog.StaffingRules);
|
||||
Assert.Equal(12, catalog.StaffingRules.PoolSize);
|
||||
Assert.Equal(2, map.Buildings.Count);
|
||||
var homerooms = map.Rooms.Where(room => room.Def == "Classroom").ToList();
|
||||
Assert.Equal(11, homerooms.Count);
|
||||
@@ -79,6 +81,7 @@ public class VanillaCoreTests
|
||||
keys.AddRange(Names(catalog.Needs.Values));
|
||||
keys.AddRange(Names(catalog.NameSets.Values));
|
||||
keys.AddRange(Names(catalog.Subjects.Values));
|
||||
keys.AddRange(Names(catalog.Staffing.Values));
|
||||
|
||||
// Derived in code, so no def carries them.
|
||||
keys.Add(BodyBuilds.Attribute);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
namespace HSchool.People.Tests;
|
||||
|
||||
public class ApplicantPoolTests
|
||||
{
|
||||
[Fact]
|
||||
public void SameSeedAndWeek_YieldTheSamePool()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
var a = Create(roster);
|
||||
var b = Create(roster);
|
||||
|
||||
Assert.Equal(Snapshot(a), Snapshot(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Advance_KeepsSomePeopleWithTheSameSkillsAndAsk()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
var first = Create(roster);
|
||||
var next = first.Advance(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf.AddDays(7));
|
||||
|
||||
Assert.NotEqual(first.Week, next.Week);
|
||||
var stayed = first.Applicants
|
||||
.Join(
|
||||
next.Applicants,
|
||||
applicant => applicant.Person.Id,
|
||||
applicant => applicant.Person.Id,
|
||||
(before, after) => (before, after),
|
||||
StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(stayed);
|
||||
foreach (var (before, after) in stayed)
|
||||
{
|
||||
Assert.Equal(before.HourlyWageAsk, after.HourlyWageAsk);
|
||||
Assert.Equal(Skills(before.Person), Skills(after.Person));
|
||||
Assert.Equal(before.Person.Name.Full, after.Person.Name.Full);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiftyWeeks_StayAtPoolSize()
|
||||
{
|
||||
var catalog = Fixtures.Catalog();
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
var size = catalog.StaffingRules!.PoolSize;
|
||||
var asOf = Fixtures.AsOf;
|
||||
var pool = ApplicantPool.Create(catalog, roster, Fixtures.SchoolSeed, "Slavic", asOf);
|
||||
|
||||
for (var week = 0; week < 50; week++)
|
||||
{
|
||||
asOf = asOf.AddDays(7);
|
||||
pool = pool.Advance(catalog, roster, Fixtures.SchoolSeed, "Slavic", asOf);
|
||||
Assert.Equal(size, pool.Applicants.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrongerSkills_AskForMore()
|
||||
{
|
||||
var catalog = Fixtures.Catalog();
|
||||
var person = Fixtures.Generate(Fixtures.Classrooms(1)).People.First(candidate => candidate.IsParent);
|
||||
var weak = person with { Skills = person.Skills.ToDictionary(pair => pair.Key, _ => 15) };
|
||||
var strong = person with { Skills = person.Skills.ToDictionary(pair => pair.Key, _ => 90) };
|
||||
|
||||
Assert.True(ApplicantPool.HourlyAsk(catalog, strong) > ApplicantPool.HourlyAsk(catalog, weak));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedApplicants_AreNotOnTheRoster_ParentsKeepTheirId()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
ApplicantPool? pool = null;
|
||||
for (var seed = 1; seed <= 20 && pool is null; seed++)
|
||||
{
|
||||
var candidate = Fixtures.Generate(Fixtures.VanillaMap(), seed);
|
||||
var created = ApplicantPool.Create(Fixtures.Catalog(), candidate, seed, "Slavic", Fixtures.AsOf);
|
||||
if (created.Applicants.Any(applicant => candidate.People.Any(person => person.Id == applicant.Person.Id)))
|
||||
{
|
||||
roster = candidate;
|
||||
pool = created;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.NotNull(pool);
|
||||
var rosterIds = roster.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var applicant in pool.Applicants)
|
||||
{
|
||||
if (rosterIds.Contains(applicant.Person.Id))
|
||||
{
|
||||
Assert.Contains(roster.People, person => person.Id == applicant.Person.Id && person.IsParent && !person.IsStaff);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.StartsWith("a", applicant.Person.Id, StringComparison.Ordinal);
|
||||
Assert.False(applicant.Person.IsStaff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeopleJson_RoundTripsThePool()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.Classrooms(1));
|
||||
var pool = Create(roster);
|
||||
var json = RosterJson.Serialize(RosterDocument.From(Fixtures.SchoolSeed, roster, pool));
|
||||
var loaded = RosterJson.Parse(json);
|
||||
|
||||
Assert.Equal(Snapshot(pool), Snapshot(loaded.Applicants!));
|
||||
Assert.DoesNotContain(loaded.People, person => pool.Applicants.Any(applicant =>
|
||||
applicant.Person.Id.StartsWith('a') && applicant.Person.Id == person.Id));
|
||||
}
|
||||
|
||||
private static ApplicantPool Create(Roster roster) =>
|
||||
ApplicantPool.Create(Fixtures.Catalog(), roster, Fixtures.SchoolSeed, "Slavic", Fixtures.AsOf);
|
||||
|
||||
private static string Snapshot(ApplicantPool pool) =>
|
||||
string.Join('\n', pool.Applicants.Select(applicant =>
|
||||
$"{pool.Week}|{pool.NextIndex}|{applicant.Person.Id}|{applicant.HourlyWageAsk:0.00}|{applicant.Person.Name.Full}|{Skills(applicant.Person)}"));
|
||||
|
||||
private static string Skills(Person person) =>
|
||||
string.Join(',', person.Skills.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
|
||||
}
|
||||
@@ -45,16 +45,15 @@ public class ReviewFixTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaffTopUp_LeavesNobodyWithoutARole()
|
||||
public void RosterWithoutStaff_LeavesNobodyWithoutARole()
|
||||
{
|
||||
// More posts than the pupils' parents can fill, and an odd deficit: the old top-up added
|
||||
// two adults per opening and the spare one ended up neither staff, parent nor pupil.
|
||||
var roster = Fixtures.Generate(Fixtures.PostHeavyMap());
|
||||
|
||||
Assert.DoesNotContain(roster.People, person => person.IsStaff);
|
||||
Assert.All(
|
||||
roster.People,
|
||||
person => Assert.True(
|
||||
person.IsStudent || person.IsStaff || person.IsParent,
|
||||
person.IsStudent || person.IsParent,
|
||||
$"{person.Id} ({person.Name.Full}) has no role at all"));
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class RosterBrowserTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentFilter_IncludesStaffWhoAreAlsoParents()
|
||||
public void ParentFilter_ReturnsOnlyParents()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
var page = RosterBrowser.Apply(
|
||||
@@ -61,7 +61,7 @@ public class RosterBrowserTests
|
||||
Fixtures.AsOf,
|
||||
Query(role: PersonRoles.Parent, pageSize: RosterBrowser.MaxPageSize));
|
||||
|
||||
Assert.Contains(page.People, person => person.IsStaff && person.IsParent);
|
||||
Assert.NotEmpty(page.People);
|
||||
Assert.All(page.People, person => Assert.True(person.IsParent));
|
||||
}
|
||||
|
||||
|
||||
@@ -95,17 +95,19 @@ public class RosterGeneratorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneClassroomAndEleven_BothFillSeatsAndJobs()
|
||||
public void OneClassroomAndEleven_BothFillSeatsAndLeaveJobsEmpty()
|
||||
{
|
||||
AssertFilled(Fixtures.Generate(Fixtures.Classrooms(1)), classrooms: 1);
|
||||
AssertFilled(Fixtures.Generate(Fixtures.Classrooms(11)), classrooms: 11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SomeStaffAreAlsoParents()
|
||||
public void NewSchool_HasNoStaff()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.VanillaMap());
|
||||
Assert.Contains(roster.People, person => person.IsStaff && person.IsParent);
|
||||
Assert.DoesNotContain(roster.People, person => person.IsStaff);
|
||||
Assert.Equal(11 * 16, roster.People.Count(person => person.IsStudent));
|
||||
Assert.Contains(roster.People, person => person.IsParent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -173,11 +175,8 @@ public class RosterGeneratorTests
|
||||
});
|
||||
|
||||
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));
|
||||
Assert.Equal(0, roster.People.Count(person => person.IsStaff));
|
||||
Assert.True(RosterFit.Matches(roster, demand));
|
||||
}
|
||||
|
||||
private static string Snapshot(Roster roster) =>
|
||||
|
||||
@@ -9,21 +9,23 @@ public class PeopleInSchoolTests
|
||||
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void InstallPeople_FillsHomeroomsAndJobs()
|
||||
public void InstallPeople_FillsHomeroomsAndLeavesJobsEmpty()
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
var demand = SchoolDemand.From(catalog, map);
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Start);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Start);
|
||||
|
||||
using var school = School.Create(1, "Полная", Start, catalog, map);
|
||||
school.InstallPeople(roster, seed: 1);
|
||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
||||
school.Tick(1d / 20d, 5d);
|
||||
|
||||
Assert.Same(roster, school.Roster);
|
||||
Assert.Equal(11, roster.Classes.Count);
|
||||
Assert.Equal(demand.Seats.Count, roster.People.Count(person => person.IsStudent));
|
||||
Assert.Equal(demand.Staff.Count, roster.People.Count(person => person.IsStaff));
|
||||
Assert.Equal(0, roster.People.Count(person => person.IsStaff));
|
||||
Assert.True(RosterFit.Matches(roster, demand));
|
||||
Assert.Equal(catalog.StaffingRules!.PoolSize, school.Applicants!.Applicants.Count);
|
||||
|
||||
var peopleQuery = new QueryDescription().WithAll<PersonIdentity, PersonNeeds, PersonRoles>();
|
||||
var classesQuery = new QueryDescription().WithAll<ClassIdentity>();
|
||||
@@ -31,6 +33,28 @@ public class PeopleInSchoolTests
|
||||
Assert.Equal(11, school.World.CountEntities(in classesQuery));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tick_AcrossAWeekBoundary_RefreshesTheApplicantPool()
|
||||
{
|
||||
var start = new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||
var (catalog, map) = Vanilla();
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", start);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", start);
|
||||
|
||||
using var school = School.Create(1, "Неделя", start, catalog, map);
|
||||
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
||||
|
||||
var monday = new DateTime(2012, 4, 2, 6, 0, 0, DateTimeKind.Utc);
|
||||
var changed = school.Tick((monday - start).TotalMinutes / 5d, 5d);
|
||||
|
||||
Assert.True(changed);
|
||||
Assert.NotNull(school.Applicants);
|
||||
Assert.NotEqual(pool.Week, school.Applicants.Week);
|
||||
Assert.Equal(pool.Applicants.Count, school.Applicants.Applicants.Count);
|
||||
var peopleQuery = new QueryDescription().WithAll<PersonIdentity>();
|
||||
Assert.Equal(roster.People.Count, school.World.CountEntities(in peopleQuery));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreNeeds_DoNotMoveOnTick()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user