tests(calendar): Add calendar render tests (#3)

- updates calendar to support overriding the current day when rendering
- supports current culture when rendering
- renames Calendar to CalendarGenerator
This commit is contained in:
Scott 2026-08-05 06:15:53 +00:00
commit 3547e32b6e
29 changed files with 542 additions and 19 deletions

View file

@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="System.IO.Packaging" Version="10.0.10" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.10" />
<PackageVersion Include="Microsoft.PowerShell.SDK" Version="7.6.4" />
@ -10,5 +11,7 @@
<PackageVersion Include="System.Management.Automation" Version="7.6.4" />
<PackageVersion Include="Cake.Frosting" Version="6.2.0" />
<PackageVersion Include="Cake.Powershell" Version="4.0.0" />
<PackageVersion Include="Verify.XunitV3" Version="31.27.0" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>

View file

@ -12,6 +12,9 @@
<Folder Name="/solutionItems/">
<File Path="Directory.Packages.props" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ModuleTests/ModuleTests.csproj" />
</Folder>
<Project Path="src/ModuleCore/ModuleCore.csproj" />
<Project Path="src/PowershellModule/PowershellModule.csproj" />
</Solution>

View file

@ -1,10 +1,9 @@
using System.Globalization;
using System.IO.MemoryMappedFiles;
using System.Text;
namespace ModuleCore.Calendar;
public sealed class Calendar
public sealed class CalendarGenerator
{
public const string DefaultMarkedOffSymbol = "x";
public const DayOfWeek DefaultStartOfWeek = DayOfWeek.Monday;
@ -30,7 +29,7 @@ public sealed class Calendar
private readonly string _rolloverDaySymbol = "";
public Calendar(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null)
public CalendarGenerator(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null)
{
_markedOffSymbol = markedDaySymbol ?? DefaultMarkedOffSymbol;
_startOfWeek = startOfWeek ?? DefaultStartOfWeek;
@ -38,10 +37,7 @@ public sealed class Calendar
// Generate localised day of week lookup table
_daysOfWeekLookup = new List<string>(7);
_dayWidth = _markedOffSymbol.Length;
// TODO: make this passed in from configuration, and if its invalid default it to
// CultureInfo.CurrentCulture
// TODO: test if its possible to determine if a culture string is invalid and what happens
_cultureInfo = new CultureInfo("en-AU");
_cultureInfo = CultureInfo.CurrentCulture;
for (var i = 0; i < 7; i++)
{
var localisedDayAbbreviation = _cultureInfo.DateTimeFormat.GetAbbreviatedDayName((DayOfWeek)i);
@ -68,13 +64,19 @@ public sealed class Calendar
_dayWidth += 2;
}
public string RenderCalendar(DateTime month)
/// <summary>
/// Renders the configured calendar for the given datetime
/// </summary>
/// <param name="month"></param>
/// <param name="todayOverride">Defaults to <see cref="DateTime.Now"/>, used to display elapsed days</param>
/// <returns></returns>
public string Render(DateTime month, DateTime? todayOverride = null)
{
var sb = new StringBuilder();
// Generate the header for the month/days of week
GenerateHeader(sb, month);
var calendarForMonth = GenerateCalendarForMonth(month, _markedOffSymbol, _startOfWeek, _rolloverDaySymbol);
var calendarForMonth = GenerateCalendarForMonth(month, _markedOffSymbol, _startOfWeek, _rolloverDaySymbol, todayOverride ?? DateTime.Now);
for (var i = 0; i < calendarForMonth.WeeksInMonth; i++)
{
@ -168,9 +170,10 @@ public sealed class Calendar
}
}
private MonthCalendar GenerateCalendarForMonth(DateTime month, string markedOffSymbol, DayOfWeek calendarStartOfWeek, string rolloverDaySymbol)
private MonthCalendar GenerateCalendarForMonth(DateTime month, string markedOffSymbol, DayOfWeek calendarStartOfWeek, string rolloverDaySymbol, DateTime today)
{
// TODO: use startOfWeek to offset the start of the calendar later
// TODO: I think I meant the calendar name header?
// Reset the month to the start
var startOfMonth = new DateTime(month.Year, month.Month, 1);
@ -218,7 +221,6 @@ public sealed class Calendar
cal.Add(rolloverDaySymbol);
}
var today = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
// If we're in the same year and month, any days already elapsed should be marked off,
// otherwise we display all days.
// If the DateTime we're rendering is in the past we render all days as marked off

View file

@ -8,10 +8,10 @@ class Program
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
var a = new Calendar();
var lastMonth = a.RenderCalendar(DateTime.Now.AddMonths(-1));
var now = a.RenderCalendar(DateTime.Now);
var august = a.RenderCalendar(DateTime.Now.AddMonths(1));
var a = new CalendarGenerator();
var lastMonth = a.Render(DateTime.Now.AddMonths(-1));
var now = a.Render(DateTime.Now);
var august = a.Render(DateTime.Now.AddMonths(1));
Console.Write(lastMonth);
Console.Write(now);

View file

@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Management.Automation;
using ModuleCore.Calendar;
namespace PowershellModule.Calendar
{
@ -42,7 +43,7 @@ namespace PowershellModule.Calendar
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
// Guaranteed to be initialised in BeginProcessing. If it's not, something is cooked in some other way
private ModuleCore.Calendar.Calendar _calendar = null!;
private CalendarGenerator _calendar = null!;
private const string Noun = "Calendar";
public const string FullName = $"{VerbsCommon.Get}-{Noun}";
@ -80,7 +81,7 @@ namespace PowershellModule.Calendar
{
if (dayOfWeek is null)
{
return ModuleCore.Calendar.Calendar.DefaultStartOfWeek;
return CalendarGenerator.DefaultStartOfWeek;
}
return dayOfWeek.ToLowerInvariant() switch
@ -92,13 +93,13 @@ namespace PowershellModule.Calendar
"thursday" => DayOfWeek.Thursday,
"friday" => DayOfWeek.Friday,
"saturday" => DayOfWeek.Saturday,
_ => ModuleCore.Calendar.Calendar.DefaultStartOfWeek
_ => CalendarGenerator.DefaultStartOfWeek
};
}
protected override void ProcessRecord()
{
var calendar = _calendar.RenderCalendar(_dateToRender);
var calendar = _calendar.Render(_dateToRender);
// From _very_ basic testing, string split allocates the same as making a span and iterating over it.
// I'm assuming this is because a lot of the span code is what string.Split() does internally and the rest

View file

@ -0,0 +1,48 @@
using ModuleCore.Calendar;
using ModuleTests.Calendar.TestData;
using System.Globalization;
namespace ModuleTests.Calendar;
public class BasicRenderTest
{
private static readonly VerifySettings Settings;
static BasicRenderTest()
{
Settings = new VerifySettings();
var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(BasicRenderTest));
Settings.UseDirectory(testBaseDirectory);
Settings.DisableDiff();
}
[Theory]
[ClassData(typeof(CalendarTestDates))]
public Task BasicCalendarRender(DateTime month)
{
Settings.UseFileName($"{month:yyyy-MM-dd}");
var calendar = new CalendarGenerator();
// Render the calendar with the start of the month as the today date so no elapsed marks are shown
var renderedMonth = calendar.Render(month, new DateTime(month.Year, month.Month, 1));
return Verify(renderedMonth, Settings);
}
[Theory]
[ClassData(typeof(CultureCodeTestData))]
public Task CultureInfoCalendarRender(string cultureCode)
{
Settings.UseFileName($"{cultureCode}");
Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureCode);
var month = new DateTime(2026, 8, 1);
var calendar = new CalendarGenerator();
// Render the calendar with the start of the month as the today date so no elapsed marks are shown
var renderedMonth = calendar.Render(month, new DateTime(month.Year, month.Month, 1));
return Verify(renderedMonth, Settings);
}
}

View file

@ -0,0 +1,86 @@
using System.Text;
using ModuleCore.Calendar;
namespace ModuleTests.Calendar;
public class MarkedDayRenderTests
{
private static readonly VerifySettings Settings;
static MarkedDayRenderTests()
{
Settings = new VerifySettings();
var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(MarkedDayRenderTests));
Settings.UseDirectory(testBaseDirectory);
Settings.DisableDiff();
}
[Fact]
public Task DefaultMarkedDayRender()
{
Settings.UseFileName(nameof(DefaultMarkedDayRender));
var date = new DateTime(2026, 8, 1);
var calendar = new CalendarGenerator();
var renderedMonth = calendar.Render(date, date.AddDays(15));
return Verify(renderedMonth, Settings);
}
[Fact]
public Task SimpleMarkedDayRender()
{
Settings.UseFileName(nameof(SimpleMarkedDayRender));
var date = new DateTime(2026, 8, 1);
var calendar = new CalendarGenerator("|");
var renderedMonth = calendar.Render(date, date.AddDays(15));
return Verify(renderedMonth, Settings);
}
[Fact]
public Task LongMarkedDayRender()
{
Settings.UseFileName(nameof(LongMarkedDayRender));
var date = new DateTime(2026, 8, 1);
var calendar = new CalendarGenerator("passed");
var renderedMonth = calendar.Render(date, date.AddDays(15));
return Verify(renderedMonth, Settings);
}
[Fact]
public Task EmojiMarkedDayRender()
{
Settings.UseFileName(nameof(EmojiMarkedDayRender));
var date = new DateTime(2026, 8, 1);
var calendar = new CalendarGenerator("🤫");
var sb = new StringBuilder();
sb.AppendLine("Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.")
.AppendLine(calendar.Render(date, date.AddDays(15)));
return Verify(sb, Settings);
}
[Fact]
public Task DoubleWideMarkedDayRender()
{
Settings.UseFileName(nameof(DoubleWideMarkedDayRender));
var date = new DateTime(2026, 8, 1);
// Using an asian-character to have a test covering the unfortunate reality of character rendering of
// characters that are visually 1 character in width, but render as double width or some other cursed form.
// Written language was a mistake but trying to render it on computers alongside a different one
// is arguably worse.
var calendar = new CalendarGenerator("火");
var sb = new StringBuilder();
sb.AppendLine("Double-wide characters will display incorrectly but still tested until rendering can be improved")
.AppendLine(calendar.Render(date, date.AddDays(15)));
return Verify(sb, Settings);
}
}

View file

@ -0,0 +1,35 @@
using System.Text;
using ModuleCore.Calendar;
namespace ModuleTests.Calendar;
public class StartDayOfWeekTests
{
private static readonly VerifySettings Settings;
static StartDayOfWeekTests()
{
Settings = new VerifySettings();
var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(StartDayOfWeekTests));
Settings.UseDirectory(testBaseDirectory);
Settings.DisableDiff();
}
[Fact]
public Task StartOfDayRender()
{
Settings.UseFileName(nameof(StartOfDayRender));
var date = new DateTime(2026, 8, 1);
var sb = new StringBuilder();
foreach (var dayOfWeek in Enum.GetValues<DayOfWeek>())
{
var calendar = new CalendarGenerator(startOfWeek: dayOfWeek);
sb.AppendLine($"Start Day: {dayOfWeek}");
sb.AppendLine(calendar.Render(date, date));
}
return Verify(sb, Settings);
}
}

View file

@ -0,0 +1,14 @@
namespace ModuleTests.Calendar.TestData;
public class CalendarTestDates : TestDataEnumerator<DateTime>
{
public CalendarTestDates()
{
Data =
[
new DateTime(2026, 06, 01),
new DateTime(2026, 07, 01),
new DateTime(2026, 08, 01),
];
}
}

View file

@ -0,0 +1,19 @@
namespace ModuleTests.Calendar.TestData;
public class CultureCodeTestData : TestDataEnumerator<string>
{
public CultureCodeTestData()
{
Data =
[
"da-DK",
"en-AU",
"cs-CZ",
"es-PR",
"fr-LU",
"nl-NL",
// Will render unaligned due to the character encoding, here to catch if I fix this later
"te-IN"
];
}
}

View file

@ -0,0 +1,11 @@
┌─────────────────────────────────────────┐
│ June 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
│ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │
│ 15 │ 16 │ 17 │ 18 │ 19 │ 20 │ 21 │
│ 22 │ 23 │ 24 │ 25 │ 26 │ 27 │ 28 │
│ 29 │ 30 │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,11 @@
┌─────────────────────────────────────────┐
│ July 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ 1 │ 2 │ 3 │ 4 │ 5 │
│ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │
│ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ 19 │
│ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ 26 │
│ 27 │ 28 │ 29 │ 30 │ 31 │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,12 @@
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,12 @@
┌──────────────────────────────────┐
│ srpen 2026 │
├────┬────┬────┬────┬────┬────┬────┤
│ po │ út │ st │ čt │ pá │ so │ ne │
├────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┘

View file

@ -0,0 +1,12 @@
┌─────────────────────────────────────────┐
│ august 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ man │ tir │ ons │ tor │ fre │ lør │ søn │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,12 @@
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,12 @@
┌────────────────────────────────────────────────┐
│ agosto 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mié. │ jue. │ vie. │ sáb. │ dom. │
├──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘

View file

@ -0,0 +1,12 @@
┌────────────────────────────────────────────────┐
│ août 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mer. │ jeu. │ ven. │ sam. │ dim. │
├──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘

View file

@ -0,0 +1,12 @@
┌──────────────────────────────────┐
│ augustus 2026 │
├────┬────┬────┬────┬────┬────┬────┤
│ ma │ di │ wo │ do │ vr │ za │ zo │
├────┼────┼────┼────┼────┼────┼────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┘

View file

@ -0,0 +1,12 @@
┌───────────────────────────────────────────────────────┐
│ ఆగస్టు 2026 │
├───────┬───────┬───────┬───────┬───────┬───────┬───────┤
│ సోమ │ మంగళ │ బుధ │ గురు │ శుక్ర │ శని │ ఆది │
├───────┼───────┼───────┼───────┼───────┼───────┼───────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┘

View file

@ -0,0 +1,12 @@
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ x │ x │
│ x │ x │ x │ x │ x │ x │ x │
│ x │ x │ x │ x │ x │ x │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,13 @@
Double-wide characters will display incorrectly but still tested until rendering can be improved
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 火 │ 火 │
│ 火 │ 火 │ 火 │ 火 │ 火 │ 火 │ 火 │
│ 火 │ 火 │ 火 │ 火 │ 火 │ 火 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,13 @@
Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 🤫 │ 🤫 │
│ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 🤫 │
│ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 🤫 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,12 @@
┌──────────────────────────────────────────────────────────────┐
│ August 2026 │
├────────┬────────┬────────┬────────┬────────┬────────┬────────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├────────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ │ │ │ │ │ passed │ passed │
│ passed │ passed │ passed │ passed │ passed │ passed │ passed │
│ passed │ passed │ passed │ passed │ passed │ passed │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┘

View file

@ -0,0 +1,12 @@
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ | │ | │
│ | │ | │ | │ | │ | │ | │ | │
│ | │ | │ | │ | │ | │ | │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,86 @@
Start Day: Sunday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ │ 1 │
│ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │
│ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │
│ 16 │ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │
│ 23 │ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │
│ 30 │ 31 │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Monday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ │ 1 │ 2 │
│ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
│ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │
│ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│ 31 │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Tuesday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │ Mon │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ │ 1 │ 2 │ 3 │
│ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │
│ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │
│ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │
│ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Wednesday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Wed │ Thu │ Fri │ Sat │ Sun │ Mon │ Tue │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ │ 1 │ 2 │ 3 │ 4 │
│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │
│ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │
│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │
│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Thursday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Thu │ Fri │ Sat │ Sun │ Mon │ Tue │ Wed │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ │ 1 │ 2 │ 3 │ 4 │ 5 │
│ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │
│ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ 19 │
│ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ 26 │
│ 27 │ 28 │ 29 │ 30 │ 31 │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Friday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Fri │ Sat │ Sun │ Mon │ Tue │ Wed │ Thu │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
│ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │
│ 14 │ 15 │ 16 │ 17 │ 18 │ 19 │ 20 │
│ 21 │ 22 │ 23 │ 24 │ 25 │ 26 │ 27 │
│ 28 │ 29 │ 30 │ 31 │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
Start Day: Saturday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Sat │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
│ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │
│ 15 │ 16 │ 17 │ 18 │ 19 │ 20 │ 21 │
│ 22 │ 23 │ 24 │ 25 │ 26 │ 27 │ 28 │
│ 29 │ 30 │ 31 │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Verify.XunitV3" />
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ModuleCore\ModuleCore.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,6 @@
namespace ModuleTests;
public class TestConstants
{
public const string SnapshotFolderName = "Snapshots";
}

View file

@ -0,0 +1,15 @@
using System.Collections;
namespace ModuleTests;
public abstract class TestDataEnumerator<T> : IEnumerable<TheoryDataRow<T>>
{
protected IEnumerable<T> Data { get; init; } = [];
public IEnumerator<TheoryDataRow<T>> GetEnumerator()
{
return Data.Select(data => new TheoryDataRow<T>(data)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}