diff --git a/.gitignore b/.gitignore index f34ab01..2fe225e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ riderModule.iml /_ReSharper.Caches/ # Don't care about IDE specific things /.idea -*.DotSettings.user \ No newline at end of file +*.DotSettings.user +# Don't care about files created from the build script +output/ \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..1e8aa77 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,17 @@ + + + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PowershellModule.slnx b/PowershellModule.slnx new file mode 100644 index 0000000..04cf69b --- /dev/null +++ b/PowershellModule.slnx @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..a707719 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,2 @@ +dotnet run --project build/Build.csproj -- $args +exit $LASTEXITCODE; \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..dfd6b85 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +dotnet run --project ./build/Build.csproj -- "$@" diff --git a/build/Build.csproj b/build/Build.csproj new file mode 100644 index 0000000..0d0955f --- /dev/null +++ b/build/Build.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + $(MSBuildProjectDirectory) + + + + + + + + \ No newline at end of file diff --git a/build/Program.cs b/build/Program.cs new file mode 100644 index 0000000..ffb3738 --- /dev/null +++ b/build/Program.cs @@ -0,0 +1,55 @@ +using Cake.Common; +using Cake.Common.IO; +using Cake.Common.IO.Paths; +using Cake.Core; +using Cake.Frosting; +using Cake.Powershell; + +public static class Program +{ + public static int Main(string[] args) + { + return new CakeHost() + .UseContext() + .Run(args); + } +} + +public class BuildContext : FrostingContext +{ + /// + /// Base source directory + /// + public ConvertableDirectoryPath BaseSourceLocation { get; set; } + + /// + /// Powershell module project folder + /// + public ConvertableDirectoryPath PowershellModuleProjectDirectory { get; set; } + + /// + /// Powershell module csproj location + /// + public ConvertableFilePath PowershellModuleCsproj { get; set; } + + /// + /// Output directory for built DLLs and powershell module manifest files + /// + public ConvertableDirectoryPath PowershellModuleOutputDir { get; set; } + + /// + /// Path to powershell script that creates the module manifest files + /// + public ConvertableFilePath CreateModuleManifestScript { get; set; } + + public BuildContext(ICakeContext context) + : base(context) + { + BaseSourceLocation = context.Directory("../src"); + PowershellModuleProjectDirectory = BaseSourceLocation + context.Directory("PowershellModule"); + PowershellModuleCsproj = PowershellModuleProjectDirectory + context.File("PowershellModule.csproj"); + PowershellModuleOutputDir = context.Directory("../") + context.Directory("output") + context.Directory("PowershellModule"); + var buildScriptDirectory = context.Directory("./Scripts"); + CreateModuleManifestScript = buildScriptDirectory + context.File("CreateModuleManifest.ps1"); + } +} \ No newline at end of file diff --git a/build/Scripts/CreateModuleManifest.ps1 b/build/Scripts/CreateModuleManifest.ps1 new file mode 100644 index 0000000..4e3d920 --- /dev/null +++ b/build/Scripts/CreateModuleManifest.ps1 @@ -0,0 +1,24 @@ +param ( + [string]$path, + [string]$guid, + [string]$author, + [string[]]$nestedModules, + [string]$rootModule, + [string[]]$cmdletsToExport, + [string]$manifestFileLocation +) + +$manifestSplat = @{ + Path = "$path" + GUID = "$guid" + Author = "$author" + NestedModules = @($nestedModules) + RootModule = "$rootModule" + CmdletsToExport = @($cmdletsToExport) + FunctionsToExport = @() + VariablesToExport = @() + AliasesToExport = @() +} + +New-ModuleManifest @manifestSplat +New-Item "$manifestFileLocation" -ItemType File \ No newline at end of file diff --git a/build/Tasks/BuildTask.cs b/build/Tasks/BuildTask.cs new file mode 100644 index 0000000..c731bcb --- /dev/null +++ b/build/Tasks/BuildTask.cs @@ -0,0 +1,36 @@ +using Cake.Common.Tools.DotNet; +using Cake.Common.Tools.DotNet.Build; +using Cake.Common.Tools.DotNet.MSBuild; +using Cake.Core.Diagnostics; +using Cake.Frosting; + +namespace Build.Tasks; + +[TaskName("Build")] +[IsDependentOn(typeof(CleanTask))] +[IsDependeeOf(typeof(CopyOutputTask))] +public class BuildTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.Log.Information($"Building: {context.PowershellModuleCsproj}"); + + var buildSettings = new DotNetBuildSettings() + { + MSBuildSettings = new DotNetMSBuildSettings() + { + VersionSuffix = "cake" + }, + OutputDirectory = context.PowershellModuleOutputDir, + DiagnosticOutput = true + }; + + // /p:DebugSymbols=false + buildSettings.MSBuildSettings.Properties.Add("DebugSymbols", ["false"]); + // /p:DebugType=None + buildSettings.MSBuildSettings.Properties.Add("DebugType", ["None"]); + + context.DotNetBuild(context.PowershellModuleCsproj, buildSettings); + base.Run(context); + } +} \ No newline at end of file diff --git a/build/Tasks/CleanTask.cs b/build/Tasks/CleanTask.cs new file mode 100644 index 0000000..36ab859 --- /dev/null +++ b/build/Tasks/CleanTask.cs @@ -0,0 +1,23 @@ +using Cake.Common; +using Cake.Common.Tools.DotNet; +using Cake.Core.Diagnostics; +using Cake.Frosting; + +namespace Build.Tasks; + +[TaskName("Clean")] +public class CleanTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.DotNetClean(context.PowershellModuleProjectDirectory); + + var outDir = context.FileSystem.GetDirectory(context.PowershellModuleOutputDir); + if (outDir.Exists) + { + outDir.Delete(true); + } + + base.Run(context); + } +} \ No newline at end of file diff --git a/build/Tasks/CopyOutputTask.cs b/build/Tasks/CopyOutputTask.cs new file mode 100644 index 0000000..175f82a --- /dev/null +++ b/build/Tasks/CopyOutputTask.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Cake.Core.Diagnostics; +using Cake.Core.IO; +using Cake.Frosting; +using Cake.Powershell; + +namespace Build.Tasks; + +[TaskName("CopyOutput")] +public class CopyOutputTask : FrostingTask +{ + public override void Run(BuildContext context) + { + var powershellModuleName = "PowershellModule"; + var scriptParams = new + { + Path = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psd1", + Guid = Guid.Parse("5cdf4635-edb0-428c-8d9b-92d0bcd47443"), + Author = "Me", + NestedModules = new[] { $"{powershellModuleName}.dll" }, + RootModule = $"{powershellModuleName}.psm1", + CmdletsToExport = new[] { "Get-Calendar" }, + ManifestFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psm1" + }; + + var psSettings = new PowershellSettings() + { + Arguments = new ProcessArgumentBuilder(), + }; + + // This feels a bit ugly/redundant seeing as I've defined the scriptParams above, but I'm leaving it as is + // until I want to come back and clean this up properly + psSettings.Arguments.Append("path", ToPowershellSafeString(scriptParams.Path)); + psSettings.Arguments.Append("guid", ToPowershellSafeString(scriptParams.Guid.ToString())); + psSettings.Arguments.Append("author", ToPowershellSafeString(scriptParams.Author)); + psSettings.Arguments.Append("nestedModules", $"@({string.Join(",", scriptParams.NestedModules.Select(ToPowershellSafeString))})"); + psSettings.Arguments.Append("rootModule", ToPowershellSafeString(scriptParams.RootModule)); + psSettings.Arguments.Append("cmdletsToExport", $"@({string.Join(",", scriptParams.CmdletsToExport.Select(ToPowershellSafeString))})"); + psSettings.Arguments.Append("manifestFileLocation", ToPowershellSafeString(scriptParams.ManifestFileLocation)); + + context.StartPowershellFile(context.CreateModuleManifestScript, psSettings); + context.Log.Information($"Module output to: {context.PowershellModuleOutputDir.Path.MakeAbsolute(context.Environment)}"); + + base.Run(context); + } + + private static string ToPowershellSafeString(string unescapedString) => $"'{unescapedString}'"; +} \ No newline at end of file diff --git a/build/Tasks/DefaultTask.cs b/build/Tasks/DefaultTask.cs new file mode 100644 index 0000000..200768c --- /dev/null +++ b/build/Tasks/DefaultTask.cs @@ -0,0 +1,16 @@ +using Cake.Core; +using Cake.Core.Diagnostics; +using Cake.Frosting; + +namespace Build.Tasks; + +// Consider this the "entry" point for builds, task order is defined by a chain of IsDependentOn +[TaskName("Default")] +[IsDependentOn(typeof(CopyOutputTask))] +public class DefaultTask : FrostingTask +{ + public override void Run(ICakeContext context) + { + base.Run(context); + } +} \ No newline at end of file diff --git a/src/ModuleCore/Calendar/CalendarGenerator.cs b/src/ModuleCore/Calendar/CalendarGenerator.cs new file mode 100644 index 0000000..8494fc9 --- /dev/null +++ b/src/ModuleCore/Calendar/CalendarGenerator.cs @@ -0,0 +1,261 @@ +using System.Globalization; +using System.Text; + +namespace ModuleCore.Calendar; + +public sealed class CalendarGenerator +{ + public const string DefaultMarkedOffSymbol = "x"; + public const DayOfWeek DefaultStartOfWeek = DayOfWeek.Monday; + + private readonly string _markedOffSymbol; + private readonly DayOfWeek _startOfWeek; + + /// + /// Holds configured day abbreviations, based on . + /// + /// Using english as the culture: Sunday = Sun, Monday = Mon and so on. This lookup also starts with + /// Sunday as the first day to match the enum starting with Sunday. + /// + /// + private readonly List _daysOfWeekLookup; + + /// + /// Used to work out how to pad columns for each day, based on the longest abbreviated day + /// + private readonly int _dayWidth; + + private readonly CultureInfo _cultureInfo; + + private readonly string _rolloverDaySymbol = ""; + + public CalendarGenerator(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null) + { + _markedOffSymbol = markedDaySymbol ?? DefaultMarkedOffSymbol; + _startOfWeek = startOfWeek ?? DefaultStartOfWeek; + + // Generate localised day of week lookup table + _daysOfWeekLookup = new List(7); + _dayWidth = _markedOffSymbol.Length; + _cultureInfo = CultureInfo.CurrentCulture; + for (var i = 0; i < 7; i++) + { + var localisedDayAbbreviation = _cultureInfo.DateTimeFormat.GetAbbreviatedDayName((DayOfWeek)i); + + // For unicode languages (such as Japanese and Chinese), the length of a day will be reported as 1, + // but will display in a way that makes them look short or offset. + // eg: + // ┌───────────────────────────┐ + // │ 7月 │ + // ├───┬───┬───┬───┬───┬───┬───┤ + // │ 月 │ 火 │ 水 │ 木 │ 金 │ 土 │ 日 │ + // ├───┴───┴───┴───┴───┴───┴───┤ + // There's not really an easy fix I know of (yet) to resolve this (and I'm not really too bothered by it yet) + // so unfortunately it has to stay as a known bug as is currently + if (localisedDayAbbreviation.Length > _dayWidth) + { + _dayWidth = localisedDayAbbreviation.Length; + } + + _daysOfWeekLookup.Add(localisedDayAbbreviation); + } + + // Increase the width by 2 to include a space on either side + _dayWidth += 2; + } + + /// + /// Renders the configured calendar for the given datetime + /// + /// + /// Defaults to , used to display elapsed days + /// + 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, todayOverride ?? DateTime.Now); + + for (var i = 0; i < calendarForMonth.WeeksInMonth; i++) + { + sb.Append("│"); + for (var j = 0; j < 7; j++) + { + var index = j + i * 7; + // We align days to the right of their box, 1 off from the border. + // Sure I could center them, but honestly this is better to read. + sb.Append(calendarForMonth.Calendar[index].PadLeft(_dayWidth - 1)); + sb.Append(" │"); + } + + sb.AppendLine(); + } + + // Cap the calendar off + GenerateSpacer(sb, "└", "─", "┴", "┘", true); + + return sb.ToString(); + } + + private void GenerateHeader(StringBuilder sb, DateTime month) + { + // Top border for the calendar, with padder and seperator the same as there's no columns immediately + // under it + GenerateSpacer(sb, "┌", "─", "─", "┐"); + + var monthString = month.ToString("MMMM yyyy", _cultureInfo); + // total width of the calendar is width of the localised day calculated in the constructor, plus 7 extra + // to account for separators for each day. + // It shouldn't need to be said that the 7 here stands for days in a week + var calendarWidth = _dayWidth * 7 + 7; + // Calculate the offsets for + var offsets = ( + left: Math.Floor(calendarWidth / 2.0 + monthString.Length / 2.0), + right: Math.Ceiling(calendarWidth / 2.0 - monthString.Length / 2.0) + ); + + sb.Append("│") + .Append(monthString.PadLeft((int)offsets.left)) + .AppendLine("│".PadLeft((int)offsets.right)); + + // Top border for the week + GenerateSpacer(sb, "├", "─", "┬", "┤"); + + // Work out the week display based on start of week + var d = 0; + do + { + // Index into the day of week, wrapping if needed from the configured start of the week. + // The DayOfWeek enum starts with Sunday, so by default the start of the week is 0, + // But most calendars visually start with Monday as the start of the week which is our default start + // of week. + // We also pad the string to the width of the day (minus 2 for spaces either side of the day), if + // the marked day symbol is a long string. + // Why did I decide to support arbitrary length marked days? Why not? + sb.Append($"│ {_daysOfWeekLookup[(d + (int)_startOfWeek) % 7].PadLeft(_dayWidth - 2)} "); + d++; + } while (d < 7); + + // End the week block + sb.AppendLine("│"); + // bottom border + GenerateSpacer(sb, "├", "─", "┼", "┤"); + } + + private void GenerateSpacer(StringBuilder sb, string leftCap, string padder, string seperator, string rightCap, bool noNewLine = false) + { + sb.Append(leftCap); + for (var i = 0; i < 7; i++) + { + for (var j = 0; j < _dayWidth; j++) + { + sb.Append(padder); + } + + if (i < 6) + { + sb.Append(seperator); + } + } + + if (noNewLine) + { + sb.Append(rightCap); + } + else + { + sb.AppendLine(rightCap); + } + } + + 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); + // If the visual start of the month is before the actual start day of the month, we need to visually shift the + // calendar "back" a week, and pad appropriately. + // Eg, if the start of the month is Sunday, but our visual start of the week is a Monday, then we need to + // make a new week to display Monday as the start, and then pad blank days to Sunday. + // This could probably be calculated better to avoid annoying to read logic but I'm drunk at this moment so the + // brain ain't there. + // For what its worth, I wasn't drunk for the rest of the code in this file, as hard as that is to believe! + var daysToPadToStartMonth = startOfMonth.DayOfWeek >= calendarStartOfWeek + ? startOfMonth.DayOfWeek - calendarStartOfWeek + : 7 - (calendarStartOfWeek - startOfMonth.DayOfWeek); + + var daysInMonth = DateTime.DaysInMonth(month.Year, month.Month); + // TODO: a lot of this could probably be simplified by simply taking the start day of the week, and then + // based on where it would be in the first week, work out if the month is 4, 5 or 6 weeks from that. + // all this needs is a pre-calculated table and that's it. + // Weeks have 7 days so there's only a fixed permutation of weeks based on the day of the week a month + // starts on, and months have anywhere from 28 to 31 days + // Increase the days by the previous months + var paddedDaysInMonth = daysInMonth + daysToPadToStartMonth; + // How many days to add to the end to make the absolute number of days (including any + // days that would visually roll over on a calendar) + // eg, if there are 2 "roll over" days from the previous month and this month has 31 days, + // Work out how many days we have left from 7, then get the _final_ number of days to make it a multiple + // _of_ 7. + // So this results in (33%7) == 5 - 7 == 2 + 33 == 35 % 7 == 0 which means we have a "complete" month + // including next month roll over. + // Visually we won't be showing those roll over days but it makes things easier if I decide to later. + // If the days in the month plus padded days is already a multiple of 7, we don't need to add any more days. + var daysToPadToEndMonth = paddedDaysInMonth % 7 != 0 + ? 7 - paddedDaysInMonth % 7 + : 0; + paddedDaysInMonth += daysToPadToEndMonth; + // Round to the nearest week after padding for the previous months days. + // Naively allowing a loss of precision via int division because the paddedDaysInMonth should always + // be a multiple of 7. + // "should always" is going to be found not as true later + var weeksInMonth = paddedDaysInMonth / 7; + + var cal = new List(); + for (var i = 0; i < daysToPadToStartMonth; i++) + { + cal.Add(rolloverDaySymbol); + } + + // 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 + // TODO: maybe don't do that and display the calendar differently + var hideElapsedDays = today.Year == month.Year && today.Month == month.Month; + + for (var i = 1; i < daysInMonth + 1; i++) + { + // if we're marking off elapsed days and we're not today or ahead, 'mark' it off + if (hideElapsedDays && i < today.Day) + { + cal.Add(markedOffSymbol); + } + else + { + cal.Add($"{i}"); + } + } + + // pad the rollover days at the end of the calendar + for (var i = 0; i < daysToPadToEndMonth; i++) + { + cal.Add(rolloverDaySymbol); + } + + return new MonthCalendar + { + Calendar = cal, + WeeksInMonth = weeksInMonth + }; + } +} + +public class MonthCalendar +{ + public List Calendar { get; set; } = []; + public int WeeksInMonth { get; set; } +} \ No newline at end of file diff --git a/src/ModuleCore/Directory.Build.props b/src/ModuleCore/Directory.Build.props new file mode 100644 index 0000000..afa6e9c --- /dev/null +++ b/src/ModuleCore/Directory.Build.props @@ -0,0 +1,6 @@ + + + 0.0.0 + dev + + \ No newline at end of file diff --git a/src/ModuleCore/ModuleCore.csproj b/src/ModuleCore/ModuleCore.csproj new file mode 100644 index 0000000..4ef1a45 --- /dev/null +++ b/src/ModuleCore/ModuleCore.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + latestmajor + + + diff --git a/src/ModuleHarness/ModuleHarness.csproj b/src/ModuleHarness/ModuleHarness.csproj new file mode 100644 index 0000000..9fe2c2b --- /dev/null +++ b/src/ModuleHarness/ModuleHarness.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + latestmajor + + + + + + + diff --git a/src/ModuleHarness/Program.cs b/src/ModuleHarness/Program.cs new file mode 100644 index 0000000..d314f9d --- /dev/null +++ b/src/ModuleHarness/Program.cs @@ -0,0 +1,20 @@ +using System.Text; +using ModuleCore.Calendar; + +namespace ModuleHarness; + +class Program +{ + static void Main(string[] args) + { + Console.OutputEncoding = Encoding.Unicode; + 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); + Console.Write(august); + } +} \ No newline at end of file diff --git a/src/PowershellHarness/CustomHost.cs b/src/PowershellHarness/CustomHost.cs new file mode 100644 index 0000000..96ef22b --- /dev/null +++ b/src/PowershellHarness/CustomHost.cs @@ -0,0 +1,216 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Security; +using System.Text; + +namespace PowershellHarness; + +// A whole bunch of empty implementations just so cmdlets have access to anything within Host.Ui (and probably any cmdlets +// that prompt for information later) +// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSHost.cs#L13 +public class CustomHost : PSHost +{ + public override string Name => "Custom Host"; + public override Version Version { get; } = new Version(0, 0, 0, 0); + public override Guid InstanceId { get; } = Guid.NewGuid(); + + private CustomUiHost _ui = new CustomUiHost(); + public override CustomUiHost UI => _ui; + + public override CultureInfo CurrentCulture { get; } = CultureInfo.CurrentCulture; + public override CultureInfo CurrentUICulture { get; } = CultureInfo.CurrentUICulture; + + public CustomHost(int width, int height = 100) + { + _ui.RawUI.WindowSize = new() { Width = width, Height = height }; + } + + public override void EnterNestedPrompt() + { + throw new NotImplementedException("EnterNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override void ExitNestedPrompt() + { + throw new NotImplementedException("ExitNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override void NotifyBeginApplication() + { + } + + public override void NotifyEndApplication() + { + } + + public override void SetShouldExit(int exitCode) + { + } +} + +// https://github.com/leechristensen/OffensivePowerShellTasking/blob/d1b498d874948f41e6dc053204c511fa6fc11c9c/OffensivePowerShellTasking/CustomPSHostUserInterface.cs#L8 +public class CustomUiHost : PSHostUserInterface +{ + // The only stuff that really matters to expose a host for any cmdlets + private readonly CustomRawUiHost _rawUI = new CustomRawUiHost(); + public override CustomRawUiHost RawUI => _rawUI; + + // Replace StringBuilder with whatever your preferred output method is (e.g. a socket or a named pipe) + public StringBuilder output { get; set; } + + public CustomUiHost() + { + output = new StringBuilder(); + } + + public CustomUiHost(ref StringBuilder sb) + { + output = sb; + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + //output.Append("!").Append(value); + } + + public override void WriteLine() + { + //output.Append("!").Append("\n"); + } + + public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + //output.Append("!").Append(value + "\n"); + } + + public override void Write(string value) + { + //output.Append("!").Append(value); + } + + public override void WriteDebugLine(string message) + { + //output.Append("!").AppendLine("DEBUG: " + message); + } + + public override void WriteErrorLine(string value) + { + //output.Append("!").AppendLine("ERROR: " + value); + } + + public override void WriteLine(string value) + { + //output.Append("!").AppendLine(value); + } + + public override void WriteVerboseLine(string message) + { + //output.Append("!").AppendLine("VERBOSE: " + message); + } + + public override void WriteWarningLine(string message) + { + //output.Append("!").AppendLine("WARNING: " + message); + } + + public override void WriteProgress(long sourceId, ProgressRecord record) + { + } + + public string Output => output.ToString(); + + public override Dictionary Prompt(string caption, string message, System.Collections.ObjectModel.Collection descriptions) + { + throw new NotImplementedException("Prompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection choices, int defaultChoice) + { + throw new NotImplementedException("PromptForChoice is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + throw new NotImplementedException("PromptForCredential1 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + throw new NotImplementedException("PromptForCredential2 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override string ReadLine() + { + throw new NotImplementedException("ReadLine is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override SecureString ReadLineAsSecureString() + { + throw new NotImplementedException("ReadLineAsSecureString is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } +} + +// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSRHostRawUserInterface.cs#L8 +public class CustomRawUiHost : PSHostRawUserInterface +{ + public override ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black; + + public override Size BufferSize { get; set; } = new Size { Width = 100, Height = 1000 }; + + public override Coordinates CursorPosition { get; set; } = new Coordinates { X = 0, Y = 0 }; + + public override int CursorSize { get; set; } = 1; + + public override void FlushInputBuffer() + { + throw new NotImplementedException("FlushInputBuffer is not implemented."); + } + + public override ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White; + + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new NotImplementedException("GetBufferContents is not implemented."); + } + + public override bool KeyAvailable + { + get { throw new NotImplementedException("KeyAvailable is not implemented."); } + } + + public override Size MaxPhysicalWindowSize { get; } = new Size + { + Width = int.MaxValue, + Height = int.MaxValue + }; + + public override Size MaxWindowSize { get; } = new Size { Width = 100, Height = 100 }; + + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new NotImplementedException("ReadKey is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input."); + } + + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + throw new NotImplementedException("ScrollBufferContents is not implemented"); + } + + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + throw new NotImplementedException("SetBufferContents is not implemented."); + } + + public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) + { + throw new NotImplementedException("SetBufferContents is not implemented"); + } + + public override Coordinates WindowPosition { get; set; } = new Coordinates { X = 0, Y = 0 }; + + public override Size WindowSize { get; set; } = new Size { Width = 120, Height = 100 }; + + public override string WindowTitle { get; set; } = ""; +} \ No newline at end of file diff --git a/src/PowershellHarness/PowershellHarness.csproj b/src/PowershellHarness/PowershellHarness.csproj new file mode 100644 index 0000000..80cdb76 --- /dev/null +++ b/src/PowershellHarness/PowershellHarness.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/src/PowershellHarness/Program.cs b/src/PowershellHarness/Program.cs new file mode 100644 index 0000000..077a9b7 --- /dev/null +++ b/src/PowershellHarness/Program.cs @@ -0,0 +1,145 @@ +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using PowershellModule.Calendar; + +namespace PowershellHarness; + +// https://github.com/FuseCP/FuseCP/blob/278a19dc06949600f25a1b4ed74d0419a8fa3fc2/FuseCP/Sources/FuseCP.Providers.HostedSolution.SfB2015/SfBBase.cs#L224 +// some useful code in here tbh +class Program +{ + static void Main(string[] args) + { + // I've gotta work out how to get the rider terminal to act closer to powershell because .PadLeft in powershell + // will correctly output lines when output with WriteOutput, but the Jetbrains debug worker (or whatever + // it is that rider spawns), has none of that and operates in its own world. + // Which is fine if you never want new lines. + // Ideally you'd just launch pwsh.exe and attach to that, but _that_ has its own issues as well. + // The new line issue might be related to how I've set up the host seeing as I just did a bunch of copy paste shit + // from something I found on github to get it across the line so I could debug it. + // I'll eventually revisit those hosts and rewrite them properly once I start adding more commands, and if I care. + // Threads & variables has all the visuals I need after all. +#if DEBUG + if (!System.Diagnostics.Debugger.IsAttached) + { + Console.BackgroundColor = ConsoleColor.Yellow; + Console.WriteLine(CenterText("Running without a debugger attached may result in lines not being output correctly")); + Console.ResetColor(); + } +#endif + Console.WriteLine(CenterText(" PowerShell Debug Harness ")); + Console.WriteLine($"Reported console window size: {Console.WindowWidth}x{Console.WindowHeight}"); + Console.WriteLine($"Reported console buffer size: {Console.BufferWidth}x{Console.BufferHeight}"); + Console.WriteLine(CenterText(" WIDTH ", '-')); + + var host = new CustomHost(Console.WindowWidth); + var runspace = InitialisePowershellHost(host); + + // InvokeCommand(runspace, GetCalendarCommand.FullName); + + foreach (var day in Enum.GetValues()) + { + InvokeCommand(runspace, GetCalendarCommand.FullName, [ + CreateCommand(nameof(GetCalendarCommand.MarkedDaySymbol), "faker"), + CreateCommand(nameof(GetCalendarCommand.StartOfWeek), day.ToString()), + CreateCommand(nameof(GetCalendarCommand.Date), "22/6/26"), + // CreateCommand(nameof(GetCalendarCommand.AlignRight)) + ]); + } + + // InvokeCommand(runspace, GetCalendarCommand.FullName, [ + // new CommandParameter(nameof(GetCalendarCommand.MarkedDaySymbol), "🤫"), + // new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), DayOfWeek.Saturday) + // ]); + // InvokeCommand(runspace, GetCalendarCommand.FullName, [new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), "Sunday")]); + } + + private static CommandParameter CreateCommand(string name, string? argument = null) + { + if (argument is not null) + { + return new CommandParameter(name, argument); + } + + return new CommandParameter(name); + } + + static string CenterText(string text, char paddingChar = ' ') + { + return text.PadLeft((Console.WindowWidth + text.Length) / 2, paddingChar).PadRight(Console.WindowWidth, paddingChar); + } + + /// + /// Initialises a PowerShell session and returns an open Runspace + /// + /// + /// + static Runspace InitialisePowershellHost(CustomHost host) + { + // Create the initial session state for the host. Yes, that 2 on the end does indicate that the underlying + // implementation for this is C++ + // Welcome to the lands of fuck all documentation and figuring shit out from an increasingly shit internet + // where finding non-slop answers gets harder by the day as people close sources of information to prevent + // scraping. Fuck every single person involved modern AI. + // It wasn't much better finding documentation about the System.Management.* namespace before that, but it's + // definitely a lot worse because of it + var initialSessionState = InitialSessionState.CreateDefault2(); + + + // No idea what the helpFileName should be. As is common with _a lot_ of the System.Management.* namespace, + // fuck all is actually documented with comments, or documented at all! + // From https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-a-constrained-runspace?view=powershell-7.6 + // (which yes, this section _is_ under a legacy category!), it's perfectly fine to leave it as null. + // We also don't really care as we're just doing this so we can test commands without having to deal with Rider + // and it's quirks around runnning a powershell terminal and attaching to it. Yeah it technically works, but + // it's way too common to end up with Rider refusing to build correctly or ensure the right dll is used for the module + var getCalendarCommand = new SessionStateCmdletEntry(GetCalendarCommand.FullName, typeof(GetCalendarCommand), null); + initialSessionState.Commands.Add(getCalendarCommand); + + // Create a runspace from the state, open and return it + var runspace = RunspaceFactory.CreateRunspace(host, initialSessionState); + + // A runspace is technically disposable, but we're not reimplementing a full host and we won't be doing anything + // that would require a fresh runspace multiple times over (yet), so we can just treat the lifetime of the + // disposable as application lifetime + runspace.Open(); + + return runspace; + } + + static void InvokeCommand(Runspace runspace, string command, IEnumerable? parameters = null) + { + // Not too sure on the difference of runspace vs PowerShell here. Doesn't seem to make a difference either way + // and this is just a debug harness so it doesn't really matter for now + using var pipeline = runspace.CreatePipeline(); + // using var powershell = PowerShell.Create(runspace); + + var cmd = new Command(command); + if (parameters is not null) + { + foreach (var commandParameter in parameters) + { + cmd.Parameters.Add(commandParameter); + } + } + + pipeline.Commands.Add(cmd); + // powershell.Commands.AddCommand(cmd); + + try + { + var results = pipeline.Invoke(); + // var results = powershell.Invoke(); + foreach (var result in results) + { + Console.Write(result); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unable to execute command {command}"); + Console.Error.WriteLine(ex.GetBaseException().Message); + } + } +} \ No newline at end of file diff --git a/src/PowershellModule/Calendar/GetCalendarCommand.cs b/src/PowershellModule/Calendar/GetCalendarCommand.cs new file mode 100644 index 0000000..f70d6e0 --- /dev/null +++ b/src/PowershellModule/Calendar/GetCalendarCommand.cs @@ -0,0 +1,174 @@ +using System; +using System.Globalization; +using System.Management.Automation; +using ModuleCore.Calendar; + +namespace PowershellModule.Calendar +{ + [Cmdlet(VerbsCommon.Get, Noun)] + public class GetCalendarCommand : PSCmdlet + { +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + [Parameter( + Mandatory = false, + Position = 0)] + public string Date { get; set; } + + [Parameter( + Mandatory = false, + Position = 1)] + public string MarkedDaySymbol { get; set; } + + [Parameter( + Mandatory = false, + Position = 2)] + [ValidateSet("Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday")] + // pass a type in to get dynamic types + // NOTE: the generator gets called for _each_ character input, so ensure the values are idempotent for every + // keydown (and fast), or cached + // [ValidateSet(typeof(StartDayOfWeekGenerator))] + public string StartOfWeek { get; set; } + + [Parameter( + Mandatory = false, + Position = 3)] + public SwitchParameter AlignRight { get; set; } + +#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 CalendarGenerator _calendar = null!; + private const string Noun = "Calendar"; + public const string FullName = $"{VerbsCommon.Get}-{Noun}"; + + private DateTime _dateToRender = DateTime.Now; + private int _leftPadding; + + protected override void BeginProcessing() + { + // This is the first point in script execution where we'll have parameters populated + _calendar = new( + MarkedDaySymbol, + DayOfWeekFromString(StartOfWeek) + ); + + if (!string.IsNullOrWhiteSpace(Date)) + { + _dateToRender = DateTime.ParseExact(Date, CultureInfo.CurrentCulture.DateTimeFormat.GetAllDateTimePatterns(), null); + } + + if (AlignRight) + { + _leftPadding = Host.UI.RawUI.WindowSize.Width; + } + } + + /// + /// Returns a from a string. + /// + /// Defaults to the calendars default start of week on null or invalid values. + /// + /// + /// + /// + private static DayOfWeek DayOfWeekFromString(string? dayOfWeek) + { + if (dayOfWeek is null) + { + return CalendarGenerator.DefaultStartOfWeek; + } + + return dayOfWeek.ToLowerInvariant() switch + { + "sunday" => DayOfWeek.Sunday, + "monday" => DayOfWeek.Monday, + "tuesday" => DayOfWeek.Tuesday, + "wednesday" => DayOfWeek.Wednesday, + "thursday" => DayOfWeek.Thursday, + "friday" => DayOfWeek.Friday, + "saturday" => DayOfWeek.Saturday, + _ => CalendarGenerator.DefaultStartOfWeek + }; + } + + protected override void ProcessRecord() + { + 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 + // is trivially optimised out. + // Not too fussed currently but it's most likely because Pad methods also allocate a string, so they end up + // equivalent at the end + // OutputCalendarStringSplit(calendar); + OutputCalendar(calendar); + + base.ProcessRecord(); + } + + private void OutputCalendarStringSplit(string calendar) + { + var splitCalendarLines = calendar.Split(Environment.NewLine); + foreach (var splitLines in splitCalendarLines) + { + // Have to output a newline character here + WriteObject(splitLines.PadLeft(_leftPadding)); + } + } + + private void OutputCalendar(string calendar) + { + // Work on a span - we avoid using string.Split() as this allocates on certain runtimes + var calendarSpan = calendar.AsSpan(); + // Get the end of line for the calendar. We use Environment.NewLine as the calendar is built up using + // StringBuilder which uses Environment.NewLine for new lines so this should be consistent across operating systems + var firstNewlineIndex = calendarSpan.IndexOf(Environment.NewLine); + // Calculate how many lines the calendar is from how many new lines we have. + // Sure we could count the number of \r\n are present, but the last line might not end with + // a new line. + // Conveniently enough, int division works out in our favor here + var lines = calendarSpan.Length / firstNewlineIndex; + + // We need to account for the size of the newline so we have an accurate length when outputting each line + // of the calendar below + var newLineLength = firstNewlineIndex + Environment.NewLine.Length; + + for (int i = 0; i < lines; i++) + { + // The start of each calendar line offset starts at a position including the length of the newline, + // so in effect the start of the line is the length of the previous line output, plus however many + // characters the newline was that weren't output as we're taking control there + var startOfLine = i * newLineLength; + // End of the line is exclusive of any newline characters + var endOfLine = startOfLine + firstNewlineIndex; + var calendarLine = calendarSpan[startOfLine .. endOfLine]; + + WriteObject(calendarLine.ToString().PadLeft(_leftPadding)); + } + } + } + + // Was here to test dynamic day names but most people using the console will be using english command names + // so there's at least some assumption that they understand days of the week in english. + // It sucks but it remains at least consistent with the argument being in english as well. + // public class StartDayOfWeekGenerator : IValidateSetValuesGenerator + // { + // private string[] _cached { get; set; } = new string[7]; + // + // public string[] GetValidValues() + // { + // if (_cached is null) + // { + // // _cached = Enumerable.Range(0, r.Next(3, 63)).Select(x => x.ToString()).ToArray(); + // } + // + // return _cached; + // } + // } +} \ No newline at end of file diff --git a/src/PowershellModule/Directory.Build.props b/src/PowershellModule/Directory.Build.props new file mode 100644 index 0000000..afa6e9c --- /dev/null +++ b/src/PowershellModule/Directory.Build.props @@ -0,0 +1,6 @@ + + + 0.0.0 + dev + + \ No newline at end of file diff --git a/src/PowershellModule/PowershellModule.csproj b/src/PowershellModule/PowershellModule.csproj new file mode 100644 index 0000000..c7f5801 --- /dev/null +++ b/src/PowershellModule/PowershellModule.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + PowershellModule + latestmajor + enable + + + + + All + + + + + + + + diff --git a/src/PowershellModule/manifest.json b/src/PowershellModule/manifest.json new file mode 100644 index 0000000..3039b76 --- /dev/null +++ b/src/PowershellModule/manifest.json @@ -0,0 +1,5 @@ +{ + "_TODO":[ + "add manifest details here maybe idk" + ] +} \ No newline at end of file diff --git a/src/PowershellModule/meta/PowershellModule.psd1 b/src/PowershellModule/meta/PowershellModule.psd1 new file mode 100644 index 0000000..e69de29 diff --git a/src/PowershellModule/meta/PowershellModule.psm1 b/src/PowershellModule/meta/PowershellModule.psm1 new file mode 100644 index 0000000..e69de29 diff --git a/tests/ModuleTests/Calendar/BasicRenderTest.cs b/tests/ModuleTests/Calendar/BasicRenderTest.cs new file mode 100644 index 0000000..3bc791f --- /dev/null +++ b/tests/ModuleTests/Calendar/BasicRenderTest.cs @@ -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); + } +} \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs b/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs new file mode 100644 index 0000000..24af2ce --- /dev/null +++ b/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs @@ -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); + } +} \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs b/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs new file mode 100644 index 0000000..c6680c7 --- /dev/null +++ b/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs @@ -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()) + { + var calendar = new CalendarGenerator(startOfWeek: dayOfWeek); + sb.AppendLine($"Start Day: {dayOfWeek}"); + sb.AppendLine(calendar.Render(date, date)); + } + + return Verify(sb, Settings); + } +} \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs b/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs new file mode 100644 index 0000000..189b4a4 --- /dev/null +++ b/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs @@ -0,0 +1,14 @@ +namespace ModuleTests.Calendar.TestData; + +public class CalendarTestDates : TestDataEnumerator +{ + public CalendarTestDates() + { + Data = + [ + new DateTime(2026, 06, 01), + new DateTime(2026, 07, 01), + new DateTime(2026, 08, 01), + ]; + } +} \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs b/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs new file mode 100644 index 0000000..9bdf1f0 --- /dev/null +++ b/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs @@ -0,0 +1,19 @@ +namespace ModuleTests.Calendar.TestData; + +public class CultureCodeTestData : TestDataEnumerator +{ + 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" + ]; + } +} \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt new file mode 100644 index 0000000..de75dd0 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt @@ -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 │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt new file mode 100644 index 0000000..a74744c --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt @@ -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 │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt new file mode 100644 index 0000000..9598b49 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt new file mode 100644 index 0000000..797c58a --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt @@ -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 │ │ │ │ │ │ │ +└────┴────┴────┴────┴────┴────┴────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt new file mode 100644 index 0000000..58618a1 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt new file mode 100644 index 0000000..9598b49 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt new file mode 100644 index 0000000..9395157 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt @@ -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 │ │ │ │ │ │ │ +└──────┴──────┴──────┴──────┴──────┴──────┴──────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt new file mode 100644 index 0000000..101ef77 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt @@ -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 │ │ │ │ │ │ │ +└──────┴──────┴──────┴──────┴──────┴──────┴──────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt new file mode 100644 index 0000000..1aac604 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt @@ -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 │ │ │ │ │ │ │ +└────┴────┴────┴────┴────┴────┴────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt new file mode 100644 index 0000000..5568a20 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt @@ -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 │ │ │ │ │ │ │ +└───────┴───────┴───────┴───────┴───────┴───────┴───────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt new file mode 100644 index 0000000..4181cc1 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt new file mode 100644 index 0000000..cc27f4f --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt new file mode 100644 index 0000000..a02a725 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt new file mode 100644 index 0000000..a8ebbf6 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt @@ -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 │ │ │ │ │ │ │ +└────────┴────────┴────────┴────────┴────────┴────────┴────────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt new file mode 100644 index 0000000..f546aac --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt @@ -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 │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ \ No newline at end of file diff --git a/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt new file mode 100644 index 0000000..8393453 --- /dev/null +++ b/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt @@ -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 │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/tests/ModuleTests/ModuleTests.csproj b/tests/ModuleTests/ModuleTests.csproj new file mode 100644 index 0000000..7343cfd --- /dev/null +++ b/tests/ModuleTests/ModuleTests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + Exe + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/ModuleTests/TestConstants.cs b/tests/ModuleTests/TestConstants.cs new file mode 100644 index 0000000..e32b3cd --- /dev/null +++ b/tests/ModuleTests/TestConstants.cs @@ -0,0 +1,6 @@ +namespace ModuleTests; + +public class TestConstants +{ + public const string SnapshotFolderName = "Snapshots"; +} \ No newline at end of file diff --git a/tests/ModuleTests/TestDataEnumerator.cs b/tests/ModuleTests/TestDataEnumerator.cs new file mode 100644 index 0000000..92937f5 --- /dev/null +++ b/tests/ModuleTests/TestDataEnumerator.cs @@ -0,0 +1,15 @@ +using System.Collections; + +namespace ModuleTests; + +public abstract class TestDataEnumerator : IEnumerable> +{ + protected IEnumerable Data { get; init; } = []; + + public IEnumerator> GetEnumerator() + { + return Data.Select(data => new TheoryDataRow(data)).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file