Merge branch 'phase/47-romance-pack'
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 19s

# Conflicts:
#	docs/phases/README.md
#	src/HSchool.Content/PeopleDefs.cs
#	src/HSchool.People/Seed.cs
This commit is contained in:
Leonid Pershin
2026-08-20 11:00:12 +03:00
44 changed files with 1841 additions and 30 deletions
@@ -0,0 +1,236 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class RomancePackTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task Catalog_WithoutRomance_HasNoRomanticTopics()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.LoginAsync(client);
var vanilla = await client.GetFromJsonAsync<CatalogResponse>("/api/catalog?lang=ru", TestContext.Current.CancellationToken);
var example = await client.GetFromJsonAsync<CatalogResponse>(
"/api/catalog?lang=ru&mods=example",
TestContext.Current.CancellationToken);
Assert.NotNull(vanilla);
Assert.NotNull(example);
Assert.DoesNotContain(vanilla.Topics, topic => topic.DefName is "TopicCrush" or "TopicCouple" or "TopicTease");
Assert.DoesNotContain(vanilla.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
Assert.DoesNotContain(example.Topics, topic => topic.DefName is "TopicCrush" or "TopicCouple" or "TopicTease");
Assert.DoesNotContain(example.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
}
[Fact]
public async Task Catalog_WithRomance_AddsPackTopics()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.LoginAsync(client);
var catalog = await client.GetFromJsonAsync<CatalogResponse>(
"/api/catalog?lang=ru&mods=romance",
TestContext.Current.CancellationToken);
Assert.NotNull(catalog);
Assert.Contains(catalog.Topics, topic => topic.DefName == "TopicCrush");
Assert.Contains(catalog.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
}
[Fact]
public async Task Create_WithPack_WritesOrientation_WithoutPack_OmitsField()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var vanilla = await SchoolApiTests.CreateAsync(client, "Без романтики", Start, seed: 47);
var vanillaJson = await ReadPeopleJsonAsync(client, vanilla.Id);
Assert.DoesNotContain("\"orientation\"", vanillaJson, StringComparison.Ordinal);
var packed = await CreateWithModsAsync(client, "С романтикой", ["romance"]);
var packedJson = await ReadPeopleJsonAsync(client, packed.Id);
Assert.Contains("\"orientation\"", packedJson, StringComparison.Ordinal);
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{packed.Id}/people?role=student&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{packed.Id}/people/{Uri.EscapeDataString(page.People[0].Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.False(string.IsNullOrWhiteSpace(card.Orientation?.DefName));
}
[Fact]
public async Task SixteenYearOlds_DoNotPair_WhenOpinionsAreMax()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Шестнадцать", ["romance"]);
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=10&pageSize=40",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
Assert.True(page.People.Count >= 2, "need two tenth-years");
var first = page.People[0];
var second = page.People.First(person => person.Id != first.Id);
await PatchPeopleAsync(client, school.Id, people =>
{
Link(people, first.Id, second.Id, 100);
Link(people, second.Id, first.Id, 100);
});
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(first.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.Null(card.Connections?.Pair);
Assert.DoesNotContain("\"partnerId\"", await ReadPeopleJsonAsync(client, school.Id), StringComparison.Ordinal);
}
[Fact]
public async Task Teacher_DoesNotGainCrushOnStudent()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Учитель", ["romance"]);
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
$"/api/schools/{school.Id}/staffing",
TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
var applicant = staffing.Applicants.First(row => !row.IsParent);
using var hire = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/hire",
new { personId = applicant.Id, position = "Teacher" },
TestContext.Current.CancellationToken);
hire.EnsureSuccessStatusCode();
var pupils = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(pupils);
var pupilId = pupils.People[0].Id;
await PatchPeopleAsync(client, school.Id, people => Link(people, applicant.Id, pupilId, 100));
var json = await ReadPeopleJsonAsync(client, school.Id);
Assert.DoesNotContain($"\"partnerId\": \"{pupilId}\"", json, StringComparison.Ordinal);
var teacher = JsonNode.Parse(json)!["people"]!.AsArray()
.First(node => node!["id"]?.GetValue<string>() == applicant.Id);
var crushes = teacher!["bonds"]?["crushes"]?.AsArray() ?? [];
Assert.DoesNotContain(crushes, node => node?.GetValue<string>() == pupilId);
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(applicant.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.DoesNotContain(card.Connections?.Crushes ?? [], row => row.Id == pupilId);
}
[Fact]
public async Task FirstYear_DoesNotCrushEleventh()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Параллели", ["romance"]);
var first = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=1&pageSize=5",
TestContext.Current.CancellationToken);
var eleventh = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=11&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(first);
Assert.NotNull(eleventh);
Assert.NotEmpty(first.People);
Assert.NotEmpty(eleventh.People);
var youngId = first.People[0].Id;
var oldId = eleventh.People[0].Id;
await PatchPeopleAsync(client, school.Id, people => Link(people, youngId, oldId, 100));
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(youngId)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.DoesNotContain(card.Connections?.Crushes ?? [], row => row.Id == oldId);
}
private static async Task<SchoolApiTests.SchoolResponse> CreateWithModsAsync(
HttpClient client,
string name,
string[] modIds)
{
using var response = await client.PostAsJsonAsync(
"/api/schools",
new { name, startDate = Start, modIds, seed = 47 },
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<SchoolApiTests.SchoolResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
return created;
}
private static async Task<string> ReadPeopleJsonAsync(HttpClient client, int schoolId)
{
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
return await File.ReadAllTextAsync(
Path.Combine(directory, $"{schoolId}.people.json"),
TestContext.Current.CancellationToken);
}
private static async Task PatchPeopleAsync(HttpClient client, int schoolId, Action<JsonArray> patch)
{
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
var path = Path.Combine(directory, $"{schoolId}.people.json");
var doc = JsonNode.Parse(await File.ReadAllTextAsync(path, TestContext.Current.CancellationToken))!;
var people = doc["people"]!.AsArray();
patch(people);
await File.WriteAllTextAsync(path, doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), TestContext.Current.CancellationToken);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
}
private static void Link(JsonArray people, string fromId, string toId, int opinion)
{
var from = people.First(node => node!["id"]?.GetValue<string>() == fromId)!.AsObject();
from["orientation"] = "Bisexual";
var opinions = from["opinions"] as JsonObject ?? new JsonObject();
opinions[toId] = opinion;
from["opinions"] = opinions;
var bonds = from["bonds"] as JsonObject ?? new JsonObject();
bonds["crushes"] = new JsonArray();
from["bonds"] = bonds;
}
private sealed record CatalogResponse(IReadOnlyList<TopicInfo> Topics);
private sealed record TopicInfo(string DefName, string Label, IReadOnlyList<string> Tags);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id);
private sealed record Card(string Id, DefLabel? Orientation, PersonConnections? Connections);
private sealed record DefLabel(string DefName, string Label);
private sealed record PersonConnections(
IReadOnlyList<PersonLink>? Crushes,
PersonLink? Pair);
private sealed record PersonLink(string Id);
private sealed record StaffingResponse(IReadOnlyList<ApplicantRow> Applicants);
private sealed record ApplicantRow(string Id, bool IsParent);
}
@@ -29,6 +29,10 @@
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\src\HSchool.Server\mods\romance\**\*">
<Link>romance\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
@@ -0,0 +1,83 @@
namespace HSchool.Content.Tests;
public class RomancePackTests
{
private const string RomancePackId = "romance";
private readonly CatalogLoader _loader = new();
[Fact]
public void VanillaCatalog_HasNoOrientationsOrRomanceTopics()
{
var vanilla = LoadCore();
Assert.Empty(vanilla.Orientations);
Assert.Null(vanilla.AffinityRules);
Assert.DoesNotContain(vanilla.Topics.Keys, id => id.StartsWith("TopicCrush", StringComparison.Ordinal));
Assert.DoesNotContain(vanilla.Topics.Values, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
Assert.False(vanilla.Traits.ContainsKey("CrushProne"));
}
[Fact]
public void Pack_LoadsOrientationsTraitsTopicsAndAffinityRules()
{
var catalog = LoadCoreAndRomance();
Assert.True(catalog.Orientations.ContainsKey("Heterosexual"));
Assert.True(catalog.Orientations.ContainsKey("Bisexual"));
Assert.True(catalog.Orientations.ContainsKey("Homosexual"));
Assert.NotNull(catalog.AffinityRules);
Assert.Equal(18, catalog.AffinityRules!.PairMinAge);
Assert.True(catalog.Traits.ContainsKey("CrushProne"));
Assert.True(catalog.Traits.ContainsKey("Jealous"));
Assert.Equal(-15, catalog.Traits["CrushProne"].AffinityThresholdOffset);
Assert.True(catalog.Topics.ContainsKey("TopicCrush"));
Assert.True(catalog.Topics.ContainsKey("TopicCouple"));
Assert.True(catalog.Topics.ContainsKey("TopicTease"));
Assert.Contains("romance", catalog.Topics["TopicAppearance"].Tags);
Assert.Equal("Романтика", catalog.Text("ru", "romance"));
Assert.Equal("Romance", catalog.Text("en", "romance"));
}
[Fact]
public void ExampleWithoutRomance_KeepsVanillaTopics()
{
var vanilla = LoadCore();
var example = LoadCoreAndExample();
Assert.Equal(
vanilla.Topics.Keys.OrderBy(id => id, StringComparer.Ordinal),
example.Topics.Keys.OrderBy(id => id, StringComparer.Ordinal));
Assert.Empty(example.Orientations);
Assert.DoesNotContain(example.Topics.Values, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
}
private DefCatalog LoadCore() =>
_loader.Load(
[CatalogLoader.CorePackId],
PackDocuments.FromDirectory(CatalogLoader.CorePackId, CoreRoot()));
private DefCatalog LoadCoreAndRomance() =>
_loader.Load(
[CatalogLoader.CorePackId, RomancePackId],
[
.. PackDocuments.FromDirectory(CatalogLoader.CorePackId, CoreRoot()),
.. PackDocuments.FromDirectory(RomancePackId, RomanceRoot()),
]);
private DefCatalog LoadCoreAndExample() =>
_loader.Load(
[CatalogLoader.CorePackId, "example"],
[
.. PackDocuments.FromDirectory(CatalogLoader.CorePackId, CoreRoot()),
.. PackDocuments.FromDirectory("example", ExampleRoot()),
]);
private static string CoreRoot() => Path.Combine(AppContext.BaseDirectory, "vanilla");
private static string RomanceRoot()
{
var root = Path.Combine(AppContext.BaseDirectory, "romance");
Assert.True(Directory.Exists(root), $"Romance pack was not copied to {root}.");
return root;
}
private static string ExampleRoot() => Path.Combine(AppContext.BaseDirectory, "example");
}
+195
View File
@@ -0,0 +1,195 @@
namespace HSchool.People.Tests;
public class AffinityTests
{
private static readonly DateTime AsOf = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public void SixteenYearOlds_NeverPair_AtAnyOpinion()
{
var catalog = Fixtures.RomanceCatalog();
var boy = Pupil("a", year: 10, age: 16, female: false, "Bisexual");
var girl = Pupil("b", year: 10, age: 16, female: true, "Bisexual");
SetOpinion(boy, girl.Id, 100);
SetOpinion(girl, boy.Id, 100);
var roster = RosterOf(boy, girl, classYear: 10);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Contains("b", boy.Bonds!.Crushes);
Assert.Contains("a", girl.Bonds!.Crushes);
Assert.Null(boy.Bonds.PartnerId);
Assert.Null(girl.Bonds.PartnerId);
Assert.False(Affinity.CanFormPair(catalog, roster, boy, girl, AsOf));
}
[Fact]
public void FirstYear_DoesNotCrushEleventh()
{
var catalog = Fixtures.RomanceCatalog();
var young = Pupil("c1", year: 1, age: 7, female: true, "Bisexual", classId: "y1");
var old = Pupil("c11", year: 11, age: 17, female: false, "Bisexual", classId: "y11");
SetOpinion(young, old.Id, 100);
var roster = new Roster(
[young, old],
[],
[
new SchoolClass("y1", 1, "А", "r1", 16, [young.Id]),
new SchoolClass("y11", 11, "А", "r11", 16, [old.Id]),
]);
Affinity.Refresh(catalog, roster, AsOf);
Assert.DoesNotContain("c11", young.Bonds!.Crushes);
Assert.False(Affinity.CanHaveSympathy(catalog, roster, young, old, AsOf));
}
[Fact]
public void Staff_NeverGetsCrushOnStudent()
{
var catalog = Fixtures.RomanceCatalog();
var teacher = Staff("t", female: false, "Bisexual");
var pupil = Pupil("p", year: 8, age: 14, female: true, "Bisexual");
SetOpinion(teacher, pupil.Id, 100);
SetOpinion(pupil, teacher.Id, 100);
var roster = RosterOf(teacher, pupil, classYear: 8);
Affinity.Refresh(catalog, roster, AsOf);
Assert.DoesNotContain(pupil.Id, teacher.Bonds!.Crushes);
Assert.Contains(teacher.Id, pupil.Bonds!.Crushes);
Assert.Null(teacher.Bonds.PartnerId);
Assert.Null(pupil.Bonds.PartnerId);
Assert.False(Affinity.CanHaveSympathy(catalog, roster, teacher, pupil, AsOf));
Assert.True(Affinity.CanHaveSympathy(catalog, roster, pupil, teacher, AsOf));
}
[Fact]
public void StaffPair_IsAllowedWhenBothAdults()
{
var catalog = Fixtures.RomanceCatalog();
var left = Staff("s1", female: false, "Bisexual");
var right = Staff("s2", female: true, "Bisexual");
SetOpinion(left, right.Id, 80);
SetOpinion(right, left.Id, 80);
var roster = new Roster([left, right], [], []);
var events = Affinity.Refresh(catalog, roster, AsOf);
Assert.Equal(right.Id, left.Bonds!.PartnerId);
Assert.Equal(left.Id, right.Bonds!.PartnerId);
Assert.Contains(events, row => row.Type == Affinity.Pair);
}
[Fact]
public void PairBreak_BlocksNewPairUntilNextDay()
{
var catalog = Fixtures.RomanceCatalog();
var left = Staff("s1", female: false, "Bisexual");
var right = Staff("s2", female: true, "Bisexual");
var third = Staff("s3", female: true, "Bisexual");
SetOpinion(left, right.Id, 80);
SetOpinion(right, left.Id, 80);
var roster = new Roster([left, right, third], [], []);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Equal(right.Id, left.Bonds!.PartnerId);
SetOpinion(left, right.Id, 0);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Null(left.Bonds.PartnerId);
Assert.NotNull(left.Bonds.PairEndedOn);
SetOpinion(left, third.Id, 80);
SetOpinion(third, left.Id, 80);
Affinity.Refresh(catalog, roster, AsOf);
Assert.Null(left.Bonds.PartnerId);
Affinity.Refresh(catalog, roster, AsOf.AddDays(1));
Assert.Equal(third.Id, left.Bonds.PartnerId);
}
[Fact]
public void AdjacentYearStudents_CanHaveSympathy()
{
var catalog = Fixtures.RomanceCatalog();
var younger = Pupil("y", year: 9, age: 15, female: true, "Bisexual", classId: "c9");
var older = Pupil("o", year: 10, age: 18, female: false, "Bisexual", classId: "c10");
SetOpinion(younger, older.Id, 80);
var roster = new Roster(
[younger, older],
[],
[
new SchoolClass("c9", 9, "А", "r9", 16, [younger.Id]),
new SchoolClass("c10", 10, "А", "r10", 16, [older.Id]),
]);
Assert.True(Affinity.CanHaveSympathy(catalog, roster, younger, older, AsOf));
Affinity.Refresh(catalog, roster, AsOf);
Assert.Contains(older.Id, younger.Bonds!.Crushes);
}
private static void SetOpinion(Person person, string targetId, int value)
{
if (value == 0)
{
OpinionStore.Remove(person, targetId);
return;
}
OpinionStore.Set(person, targetId, value);
}
private static Roster RosterOf(Person first, Person second, int classYear)
{
var pupils = new[] { first, second }.Where(person => person.IsStudent).Select(person => person.Id).ToArray();
return new Roster(
[first, second],
[],
[new SchoolClass("c", classYear, "А", "room", 16, pupils)]);
}
private static Person Pupil(string id, int year, int age, bool female, string orientation, string? classId = null)
{
_ = year;
var birth = AsOf.AddYears(-age);
return AdultLike(id, female, orientation, isStudent: true, isStaff: false, birth, classId ?? "c");
}
private static Person Staff(string id, bool female, string orientation) =>
AdultLike(id, female, orientation, isStudent: false, isStaff: true, AsOf.AddYears(-30), classId: null);
private static Person AdultLike(
string id,
bool female,
string orientation,
bool isStudent,
bool isStaff,
DateTime birth,
string? classId)
{
var cases = new CaseTable
{
Nom = id,
Gen = id,
Dat = id,
Acc = id,
Ins = id,
Pre = id,
};
return new Person
{
Id = id,
FamilyId = "f",
Female = female,
BirthDate = DateTime.SpecifyKind(birth, DateTimeKind.Utc),
Name = new PersonName(id, id, id, cases, cases, cases),
IsStudent = isStudent,
IsStaff = isStaff,
IsParent = false,
ClassId = classId,
Numbers = new Dictionary<string, int>(StringComparer.Ordinal),
Choices = new Dictionary<string, string>(StringComparer.Ordinal),
Skills = new Dictionary<string, int>(StringComparer.Ordinal),
Traits = [],
Needs = new Dictionary<string, float>(StringComparer.Ordinal),
Opinions = new Dictionary<string, int>(StringComparer.Ordinal),
Orientation = orientation,
Bonds = new PersonBonds(),
};
}
}
+13
View File
@@ -35,6 +35,19 @@ internal static class Fixtures
PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
}
public static DefCatalog RomanceCatalog()
{
var vanilla = Path.Combine(AppContext.BaseDirectory, "vanilla");
var romance = Path.Combine(AppContext.BaseDirectory, "romance");
Assert.True(Directory.Exists(romance), $"Romance pack was not copied to {romance}.");
return new CatalogLoader().Load(
[CatalogLoader.CorePackId, "romance"],
[
.. PackDocuments.FromDirectory(CatalogLoader.CorePackId, vanilla),
.. PackDocuments.FromDirectory("romance", romance),
]);
}
public static MapLayout Classrooms(int count, int desks = 16)
{
var rooms = new RoomNode[count];
@@ -30,6 +30,10 @@
<Link>example\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\src\HSchool.Server\mods\romance\**\*">
<Link>romance\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="golden\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -0,0 +1,37 @@
namespace HSchool.People.Tests;
public class OrientationGeneratorTests
{
[Fact]
public void VanillaGenerate_DoesNotWriteOrientation()
{
var roster = Fixtures.Generate(Fixtures.Classrooms(2));
Assert.All(roster.People, person => Assert.Null(person.Orientation));
Assert.All(roster.People, person => Assert.Null(person.Bonds));
}
[Fact]
public void RomanceGenerate_WritesOrientationToEveryone()
{
var catalog = Fixtures.RomanceCatalog();
var roster = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
Assert.NotEmpty(roster.People);
Assert.All(roster.People, person =>
{
Assert.False(string.IsNullOrWhiteSpace(person.Orientation));
Assert.True(catalog.Orientations.ContainsKey(person.Orientation!));
Assert.NotNull(person.Bonds);
});
}
[Fact]
public void SameSeed_YieldsTheSameOrientations()
{
var catalog = Fixtures.RomanceCatalog();
var a = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
var b = RosterGenerator.Generate(catalog, Fixtures.Classrooms(2), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "RussianLanguage");
Assert.Equal(
a.People.Select(person => (person.Id, person.Orientation)),
b.People.Select(person => (person.Id, person.Orientation)));
}
}