Merge branch 'phase/56-architecture-tests'

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	docs/phases/10-craft/README.md
This commit is contained in:
Leonid Pershin
2026-08-20 12:54:53 +03:00
11 changed files with 195 additions and 87 deletions
+2
View File
@@ -141,6 +141,8 @@ only resolved a status line. Solution-wide is CI.
same route and commute; the suite does not boot a host, and it does not reference Arch. same route and commute; the suite does not boot a host, and it does not reference Arch.
- Catalog, inheritance, patches and map validation belong in `tests/HSchool.Content.Tests`. - Catalog, inheritance, patches and map validation belong in `tests/HSchool.Content.Tests`.
Feed the loader documents, not disk paths. Feed the loader documents, not disk paths.
- Layer bans (Arch, ASP.NET, sockets, wall-clock) belong in `tests/HSchool.Architecture.Tests`.
They do not boot a host.
- Protocol changes need a round-trip test **and** a byte-layout assertion on both sides. - Protocol changes need a round-trip test **and** a byte-layout assertion on both sides.
- Server wiring, endpoints and the WebSocket belong in `tests/HSchool.AppHost.Tests`. That suite - Server wiring, endpoints and the WebSocket belong in `tests/HSchool.AppHost.Tests`. That suite
shares one AppHost across all tests (`AppHostFixture`) — keep it that way, booting per test costs shares one AppHost across all tests (`AppHostFixture`) — keep it that way, booting per test costs
+6 -3
View File
@@ -5,7 +5,7 @@ in-game calendars, a TypeScript client that renders the menus, and .NET Aspire t
for local runs and integration tests. for local runs and integration tests.
What exists today is the shell around a game: a main menu of schools, a creation form, and a What exists today is the shell around a game: a main menu of schools, a creation form, and a
school screen with a running game clock. The school itself is still empty. school screen with a running game clock. People already walk, dress, take lessons and talk.
## Stack ## Stack
@@ -65,7 +65,8 @@ Do **not** add port 5180 or the Aspire dashboard to `tailscale serve`.
ticking live. Deleting one asks for confirmation first. The footer switches the UI between ticking live. Deleting one asks for confirmation first. The footer switches the UI between
Russian and English; the choice is remembered in the browser. Russian and English; the choice is remembered in the browser.
- **Create a school** — type a name or roll a random one, pick a start date (3 April 2012, 06:00 - **Create a school** — type a name or roll a random one, pick a start date (3 April 2012, 06:00
by default). At six schools the create button is disabled and says why. by default). When the player has filled their school slots the create button is disabled and
says why.
- **Inside a school** — the date, time and weekday of the game calendar, play/pause and the - **Inside a school** — the date, time and weekday of the game calendar, play/pause and the
×½ ×1 ×2 ×5 ×10 speed buttons, plus a way back to the menu. ×½ ×1 ×2 ×5 ×10 speed buttons, plus a way back to the menu.
@@ -94,6 +95,8 @@ which matches the server's launch profile.
dotnet test dotnet test
``` ```
- `tests/HSchool.Architecture.Tests` — layer bans (Arch, ASP.NET, sockets, wall-clock) for
Protocol through Simulation. No host.
- `tests/HSchool.Protocol.Tests` — wire-format round-trips and byte layouts. - `tests/HSchool.Protocol.Tests` — wire-format round-trips and byte layouts.
- `tests/HSchool.Content.Tests` — JSONC catalog, inheritance, patches, map connectivity. No host. - `tests/HSchool.Content.Tests` — JSONC catalog, inheritance, patches, map connectivity. No host.
- `tests/HSchool.Simulation.Tests` — the game clock, school load/create, and the school registry, no host involved. - `tests/HSchool.Simulation.Tests` — the game clock, school load/create, and the school registry, no host involved.
@@ -133,7 +136,7 @@ Simulation tunables live under the `Simulation` section of
| Key | Default | Meaning | | Key | Default | Meaning |
| --- | --- | --- | | --- | --- | --- |
| `TickRate` | 20 | fixed simulation steps per second | | `TickRate` | 20 | fixed simulation steps per second |
| `MaxSchools` | 6 | how many schools may exist at once | | `MaxSchools` | 2 | how many schools each player may own |
| `GameMinutesPerRealSecond` | 1 | game minutes per real second at ×1 | | `GameMinutesPerRealSecond` | 1 | game minutes per real second at ×1 |
| `DefaultStartDate` | `2012-03-31T06:00:00` | prefilled start of a new school | | `DefaultStartDate` | `2012-03-31T06:00:00` | prefilled start of a new school |
| `SavesDirectory` | `saves` | per-school JSON files | | `SavesDirectory` | `saves` | per-school JSON files |
+12 -12
View File
@@ -11,24 +11,24 @@
## Задачи ## Задачи
- [ ] Проект `tests/HSchool.Architecture.Tests` в solution, `IsTestProject`, без AppHost и без - [x] Проект `tests/HSchool.Architecture.Tests` в solution, `IsTestProject`, без AppHost и без
нового NuGet. Ссылки на Protocol, Content, People, Schedule, Ai, Simulation нового NuGet. Ссылки на Protocol, Content, People, Schedule, Ai, Simulation
- [ ] Тесты ссылок: каждая библиотека из таблицы в [`craft.md`](../../design/10-craft/craft.md) — этап A - [x] Тесты ссылок: каждая библиотека из таблицы в [`craft.md`](../../design/10-craft/craft.md) — этап A
- [ ] Тесты исходников: в `src/HSchool.{Protocol,Content,People,Schedule,Ai,Simulation}` нет - [x] Тесты исходников: в `src/HSchool.{Protocol,Content,People,Schedule,Ai,Simulation}` нет
`DateTime.Now` / `DateTime.UtcNow` `DateTime.Now` / `DateTime.UtcNow`
- [ ] Удалить копипасты `Assembly_DoesNotReference*` и `Sources_DoNotUseWallClock` из People, - [x] Удалить копипасты `Assembly_DoesNotReference*` и `Sources_DoNotUseWallClock` из People,
Ai, Schedule, Simulation (остальные факты тех файлов не трогать) Ai, Schedule, Simulation (остальные факты тех файлов не трогать)
- [ ] Testing policy в `AGENTS.md`: слои — `HSchool.Architecture.Tests`, не хост - [x] Testing policy в `AGENTS.md`: слои — `HSchool.Architecture.Tests`, не хост
- [ ] README: школа не «ещё пустая»; потолок create — слоты игрока, не «шесть» - [x] README: школа не «ещё пустая»; потолок create — слоты игрока, не «шесть»
## Тесты, без которых фаза не закрыта ## Тесты, без которых фаза не закрыта
- [ ] Protocol не ссылается на Arch, ASP.NET, Sockets, Content, Simulation - [x] Protocol не ссылается на Arch, ASP.NET, Sockets, Content, Simulation
- [ ] Content не ссылается на Arch, ASP.NET, Sockets, Protocol, Simulation - [x] Content не ссылается на Arch, ASP.NET, Sockets, Protocol, Simulation
- [ ] People, Schedule, Ai не ссылаются на Arch, ASP.NET, Sockets - [x] People, Schedule, Ai не ссылаются на Arch, ASP.NET, Sockets
- [ ] Simulation не ссылается на ASP.NET и Sockets (Arch — можно) - [x] Simulation не ссылается на ASP.NET и Sockets (Arch — можно)
- [ ] Исходники шести библиотек выше не содержат `DateTime.Now` / `DateTime.UtcNow` - [x] Исходники шести библиотек выше не содержат `DateTime.Now` / `DateTime.UtcNow`
- [ ] В People/Ai/Schedule/Simulation.Tests больше нет этих двух фактов - [x] В People/Ai/Schedule/Simulation.Tests больше нет этих двух фактов
## Критерий готовности ## Критерий готовности
+1 -1
View File
@@ -15,7 +15,7 @@
| Фаза | Статус | Зачем | | Фаза | Статус | Зачем |
| --- | --- | --- | | --- | --- | --- |
| [56. Слои одной пачкой](56-architecture-tests.md) | 🔄 | Один проект вместо четырёх копипаст | | [56. Слои одной пачкой](56-architecture-tests.md) | | Один проект вместо четырёх копипаст |
| [57. tsc в npm test](57-client-typecheck.md) | 🔄 | Дубликат ключа красный без `npm run build` | | [57. tsc в npm test](57-client-typecheck.md) | 🔄 | Дубликат ключа красный без `npm run build` |
**Этап B — горячие точки.** Поведение то же. **Этап B — горячие точки.** Поведение то же.
+15
View File
@@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSchool.Ai.Tests", "tests\H
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSchool.Server.Tests", "tests\HSchool.Server.Tests\HSchool.Server.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSchool.Server.Tests", "tests\HSchool.Server.Tests\HSchool.Server.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HSchool.Architecture.Tests", "tests\HSchool.Architecture.Tests\HSchool.Architecture.Tests.csproj", "{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -255,6 +257,18 @@ Global
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|x64.ActiveCfg = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|x64.Build.0 = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|x86.ActiveCfg = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Debug|x86.Build.0 = Debug|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|Any CPU.Build.0 = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|x64.ActiveCfg = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|x64.Build.0 = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|x86.ActiveCfg = Release|Any CPU
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -277,6 +291,7 @@ Global
{C21FB23F-F131-4651-8236-4F3E076B40BF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {C21FB23F-F131-4651-8236-4F3E076B40BF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C355307C-D7D3-48C7-9615-CB5FBCF83D49} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {C355307C-D7D3-48C7-9615-CB5FBCF83D49} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{75B7B736-9775-4F3E-B39B-29A3FAF1CCBB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DD14EF4D-167E-4AC7-953A-AF606CC34829} SolutionGuid = {DD14EF4D-167E-4AC7-953A-AF606CC34829}
-21
View File
@@ -188,27 +188,6 @@ public class WalkingTests
} }
} }
[Fact]
public void Assembly_DoesNotReferenceArchAspNetOrSockets()
{
var names = typeof(WalkGraph).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.Ai");
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 (DayPlan Clear, DayPlan Weather, int Extra) PlansWithExtra(Func<DefCatalog, int> extraOf) private static (DayPlan Clear, DayPlan Weather, int Extra) PlansWithExtra(Func<DefCatalog, int> extraOf)
{ {
var (catalog, map) = Fixtures.Vanilla(); var (catalog, map) = Fixtures.Vanilla();
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>HSchool.Architecture.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\HSchool.Ai\HSchool.Ai.csproj" />
<ProjectReference Include="..\..\src\HSchool.Content\HSchool.Content.csproj" />
<ProjectReference Include="..\..\src\HSchool.People\HSchool.People.csproj" />
<ProjectReference Include="..\..\src\HSchool.Protocol\HSchool.Protocol.csproj" />
<ProjectReference Include="..\..\src\HSchool.Schedule\HSchool.Schedule.csproj" />
<ProjectReference Include="..\..\src\HSchool.Simulation\HSchool.Simulation.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,131 @@
using HSchool.Ai;
using HSchool.Content;
using HSchool.People;
using HSchool.Protocol;
using HSchool.Schedule;
using HSchool.Simulation;
namespace HSchool.Architecture.Tests;
public class LayerBoundaryTests
{
private static readonly string[] LibraryFolders =
[
"HSchool.Protocol",
"HSchool.Content",
"HSchool.People",
"HSchool.Schedule",
"HSchool.Ai",
"HSchool.Simulation"
];
private static readonly string[] OldTestProjects =
[
"HSchool.People.Tests",
"HSchool.Ai.Tests",
"HSchool.Schedule.Tests",
"HSchool.Simulation.Tests"
];
[Fact]
public void Protocol_DoesNotReferenceArchAspNetSocketsContentOrSimulation()
{
var names = References(typeof(ProtocolCodec));
AssertNoArchAspNetOrSockets(names);
Assert.DoesNotContain("HSchool.Content", names, StringComparer.Ordinal);
Assert.DoesNotContain("HSchool.People", names, StringComparer.Ordinal);
Assert.DoesNotContain("HSchool.Schedule", names, StringComparer.Ordinal);
Assert.DoesNotContain("HSchool.Ai", names, StringComparer.Ordinal);
Assert.DoesNotContain("HSchool.Simulation", names, StringComparer.Ordinal);
}
[Fact]
public void Content_DoesNotReferenceArchAspNetSocketsProtocolOrSimulation()
{
var names = References(typeof(MapLayout));
AssertNoArchAspNetOrSockets(names);
Assert.DoesNotContain("HSchool.Protocol", names, StringComparer.Ordinal);
Assert.DoesNotContain("HSchool.Simulation", names, StringComparer.Ordinal);
}
[Theory]
[InlineData(typeof(RosterGenerator))]
[InlineData(typeof(TimetablePlanner))]
[InlineData(typeof(WalkGraph))]
public void PeopleScheduleAi_DoNotReferenceArchAspNetOrSockets(Type marker)
{
AssertNoArchAspNetOrSockets(References(marker));
}
[Fact]
public void Simulation_DoesNotReferenceAspNetOrSockets()
{
var names = References(typeof(School));
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void SixLibrarySources_DoNotUseWallClock()
{
foreach (var folder in LibraryFolders)
{
var root = Path.Combine(RepoRoot(), "src", folder);
Assert.True(Directory.Exists(root), root);
foreach (var path in SourceFiles(root))
{
var text = File.ReadAllText(path);
Assert.DoesNotContain("DateTime.Now", text, StringComparison.Ordinal);
Assert.DoesNotContain("DateTime.UtcNow", text, StringComparison.Ordinal);
// Unused `using System.Net.Http` never becomes a referenced assembly.
Assert.DoesNotContain("System.Net.Http", text, StringComparison.Ordinal);
}
}
}
[Fact]
public void PeopleAiScheduleSimulationTests_NoLongerCarryLayerBans()
{
foreach (var project in OldTestProjects)
{
var root = Path.Combine(RepoRoot(), "tests", project);
Assert.True(Directory.Exists(root), root);
foreach (var path in SourceFiles(root))
{
var text = File.ReadAllText(path);
Assert.DoesNotContain("Assembly_DoesNotReference", text, StringComparison.Ordinal);
Assert.DoesNotContain("Sources_DoNotUseWallClock", text, StringComparison.Ordinal);
}
}
}
private static IEnumerable<string> SourceFiles(string root) =>
Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories)
.Where(path => path.Split(Path.DirectorySeparatorChar).All(part => part is not ("bin" or "obj")));
private static string[] References(Type marker) =>
[.. marker.Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!)];
private static void AssertNoArchAspNetOrSockets(IEnumerable<string> names)
{
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));
}
private static string RepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "h-school.sln")))
{
dir = dir.Parent;
}
if (dir is null)
{
throw new InvalidOperationException("Could not find h-school.sln from the test output directory.");
}
return dir.FullName;
}
}
@@ -143,27 +143,6 @@ public class RosterGeneratorTests
Assert.False(RosterFit.Matches(loaded, SchoolDemand.From(Fixtures.Catalog(), Fixtures.Classrooms(2)))); 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) private static void AssertFilled(Roster roster, int classrooms)
{ {
Assert.Equal(classrooms, roster.Classes.Count); Assert.Equal(classrooms, roster.Classes.Count);
@@ -125,27 +125,6 @@ public class TimetablePlannerTests
Assert.Equal(Snapshot(a), Snapshot(b)); Assert.Equal(Snapshot(a), Snapshot(b));
} }
[Fact]
public void Assembly_DoesNotReferenceArchAspNetOrSockets()
{
var names = typeof(TimetablePlanner).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.Schedule");
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);
}
}
/// <summary> /// <summary>
/// The four bans alone allowed a legal but unusable week. Taking the first free slot day by /// The four bans alone allowed a legal but unusable week. Taking the first free slot day by
/// day gave a full school 77 lessons on Monday and 16 on Friday, with three foreign-language /// day gave a full school 77 lessons on Monday and 16 on Friday, with three foreign-language
@@ -274,14 +274,6 @@ public class PresenceTests
"stale day plans after an assign that did not change headcount left the campus empty"); "stale day plans after an assign that did not change headcount left the campus empty");
} }
[Fact]
public void Simulation_DoesNotReferenceSockets()
{
var names = typeof(School).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
}
private static School UnstaffedDuringLessonThree() private static School UnstaffedDuringLessonThree()
{ {
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc); var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);