Files
h-school/tests/HSchool.Simulation.Tests/PersonDayLogTests.cs
T
Leonid PershinandCursor 3485ce8ac4 Close phase 36 apparel-changed log tail.
Add simulation and paged day-log tests; mark the log task complete in the phase doc.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 05:44:09 +03:00

248 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Arch.Core;
using HSchool.Ai;
using HSchool.Content;
using HSchool.People;
namespace HSchool.Simulation.Tests;
public class PersonDayLogTests
{
private static readonly DateTime TuesdayFive = new(2012, 4, 3, 5, 0, 0, DateTimeKind.Utc);
private static readonly DateTime TuesdayNoon = new(2012, 4, 3, 12, 0, 0, DateTimeKind.Utc);
private static readonly DateTime TuesdayEvening = new(2012, 4, 3, 22, 0, 0, DateTimeKind.Utc);
[Fact]
public void TryStartAndFinish_WriteStartAndEndRows()
{
using var school = OpenCafeteria(TuesdayNoon);
Spawn(school, "a", hunger: 0.3f);
Assert.True(school.TryStartAction("a", "EatLunch"));
Assert.Contains(
school.DayLog,
row => row.PersonId == "a"
&& row.Type == PersonLogTypes.ActionStarted
&& row.ThingDef == "EatLunch");
TickMinutes(school, 16);
Assert.Contains(
school.DayLog,
row => row.PersonId == "a"
&& row.Type == PersonLogTypes.ActionEnded
&& row.ThingDef == "EatLunch");
Assert.Equal(
"начал: Обед",
school.DayLog.First(row => row.Type == PersonLogTypes.ActionStarted).Caption(school.Catalog!, "ru"));
}
[Fact]
public void AfterSix_YesterdaysRowsAreGone()
{
using var school = OpenCafeteria(TuesdayFive);
Spawn(school, "a", hunger: 0.3f);
Assert.True(school.TryStartAction("a", "EatLunch"));
Assert.NotEmpty(school.DayLog);
TickMinutes(school, 61);
Assert.True(school.Clock.Time >= new DateTime(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc));
Assert.Empty(school.DayLog);
}
[Fact]
public void SkipEmpty_ClearsYesterdaysRows()
{
using var school = OpenEmpty(TuesdayEvening);
school.AppendDayLog(new PersonLogEvent("f0.c0", TuesdayEvening, PersonLogTypes.ActionStarted, "EatLunch"));
Assert.NotEmpty(school.DayLog);
var result = school.TrySkipEmpty();
Assert.True(result.Succeeded);
Assert.Empty(school.DayLog);
}
[Fact]
public void Pages_DoNotOverlap()
{
var time = TuesdayNoon;
var log = new List<PersonLogEvent>();
for (var i = 0; i < 25; i++)
{
log.Add(new PersonLogEvent("p", time.AddMinutes(i), PersonLogTypes.ActionStarted, "EatLunch"));
}
var first = PersonLogBrowser.Apply(log, "p", new PersonLogQuery(null, Descending: false, Page: 1, PageSize: 10), catalog: null, "ru");
var second = PersonLogBrowser.Apply(log, "p", new PersonLogQuery(null, Descending: false, Page: 2, PageSize: 10), catalog: null, "ru");
Assert.Equal(25, first.Total);
Assert.Equal(10, first.Entries.Count);
Assert.Equal(10, second.Entries.Count);
Assert.Empty(first.Entries.Select(row => row.Time).Intersect(second.Entries.Select(row => row.Time)));
Assert.Equal(time, first.Entries[0].Time);
Assert.Equal(time.AddMinutes(10), second.Entries[0].Time);
}
[Fact]
public void Search_FiltersByCaption()
{
var log = new[]
{
new PersonLogEvent("p", TuesdayNoon, PersonLogTypes.ActionStarted, "EatLunch"),
new PersonLogEvent("p", TuesdayNoon.AddMinutes(1), PersonLogTypes.ActionStarted, "UseToilet"),
};
var page = PersonLogBrowser.Apply(log, "p", new PersonLogQuery("toilet", Descending: false, Page: 1, PageSize: 20), catalog: null, "en");
Assert.Equal(1, page.Total);
Assert.Equal("action-started", page.Entries[0].Type);
Assert.Equal("UseToilet", page.Entries[0].ThingDef);
}
[Fact]
public void ApparelReplaced_FlowsThroughPagedLog()
{
using var school = OpenEmpty(TuesdayNoon);
var person = school.Roster!.People.First();
school.AppendDayLog(new PersonLogEvent(person.Id, TuesdayNoon, PersonLogTypes.ApparelReplaced, "Shirt"));
var page = PersonLogBrowser.Apply(
school.DayLog,
person.Id,
new PersonLogQuery(null, Descending: true, Page: 1, PageSize: 20),
school.Catalog,
"ru");
Assert.Single(page.Entries);
Assert.Equal(PersonLogTypes.ApparelReplaced, page.Entries[0].Type);
Assert.StartsWith("получил новую ", page.Entries[0].Label, StringComparison.Ordinal);
}
private static void TickMinutes(School school, int minutes)
{
for (var i = 0; i < minutes; i++)
{
school.Tick(0.2d, 5d);
}
}
private static School OpenCafeteria(DateTime start)
{
var catalog = new CatalogLoader().Load(
[CatalogLoader.CorePackId],
[
new ContentDocument("core", "defs/actions/sit.jsonc", """{ "defName": "Sit", "abstract": true }"""),
new ContentDocument(
"core",
"defs/actions/eat.jsonc",
"""
{
"defName": "EatLunch",
"room": "Cafeteria",
"thing": "Chair",
"minutes": 15,
"need": "Hunger",
"needGain": 0.5,
"roles": ["student"]
}
"""),
new ContentDocument(
"core",
"defs/needs/hunger.jsonc",
"""{ "defName": "Hunger", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 }"""),
new ContentDocument("core", "defs/things/chair.jsonc", """{ "defName": "Chair", "actions": ["Sit"] }"""),
new ContentDocument("core", "defs/territories/yard.jsonc", """{ "defName": "Yard", "travelMinutes": 1 }"""),
new ContentDocument("core", "defs/buildings/main.jsonc", """{ "defName": "Main" }"""),
new ContentDocument("core", "defs/floors/floor.jsonc", """{ "defName": "Floor" }"""),
new ContentDocument(
"core",
"defs/rooms/cafeteria.jsonc",
"""
{
"defName": "Cafeteria",
"slots": [{ "key": "seats", "thing": "Chair" }],
"travelMinutes": 1
}
"""),
new ContentDocument("core", "localizations/ru.jsonc", """{ "EatLunch": "Обед", "ActionStarted": "начал: {0}", "ActionEnded": "закончил: {0}" }"""),
]);
var map = new MapLayout
{
Territory = new TerritoryNode { Id = "yard", Def = "Yard" },
Buildings = [new BuildingNode { Id = "main", Def = "Main" }],
Floors = [new FloorNode { Id = "floor-1", Def = "Floor", Building = "main" }],
Rooms =
[
new RoomNode
{
Id = "cafe",
Def = "Cafeteria",
Building = "main",
Floor = "floor-1",
Slots = [new SlotFill { Key = "seats", Thing = "Chair", Count = 8 }],
},
],
Links = [new MapLink { A = "yard", B = "cafe" }],
};
MapValidator.Validate(map, catalog);
return School.Create(1, "Столовая", start, catalog, map);
}
private static void Spawn(School school, string id, float hunger)
{
school.World.Create(
new PersonIdentity(id, "f", false, TuesdayNoon, DummyName()),
new PersonRoles(true, false, false, null, null, null),
new PersonNeeds(new Dictionary<string, float>(StringComparer.Ordinal) { ["Hunger"] = hunger }),
new Presence("cafe", 0f, "cafe", false, []),
PersonActivity.Idle);
}
private static School OpenEmpty(DateTime start)
{
var (catalog, map) = Vanilla();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start);
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start);
var school = School.Create(1, "Пустая", start, catalog, map);
school.InstallPeople(roster, seed: 1, "Russia", pool);
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
return school;
}
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
{
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
var documents = new List<ContentDocument>();
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
{
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
}
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
Assert.NotNull(map);
return (catalog, map);
}
private static PersonName DummyName()
{
var cases = new CaseTable
{
Nom = "А",
Gen = "А",
Dat = "А",
Acc = "А",
Ins = "А",
Pre = "А",
};
return new PersonName("А", "А", "А", cases, cases, cases);
}
}