Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3547e32b6e | |||
| ecb0a20cbb | |||
| 274d279732 |
50 changed files with 1670 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -5,4 +5,6 @@ riderModule.iml
|
||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
# Don't care about IDE specific things
|
# Don't care about IDE specific things
|
||||||
/.idea
|
/.idea
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
||||||
|
# Don't care about files created from the build script
|
||||||
|
output/
|
||||||
17
Directory.Packages.props
Normal file
17
Directory.Packages.props
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<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" />
|
||||||
|
<PackageVersion Include="PowerShellStandard.Library" Version="5.1.1" />
|
||||||
|
<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>
|
||||||
20
PowershellModule.slnx
Normal file
20
PowershellModule.slnx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Solution>
|
||||||
|
<Folder Name="/build/">
|
||||||
|
<Project Path="build/Build.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/harnesses/">
|
||||||
|
<Project Path="src/ModuleHarness/ModuleHarness.csproj" />
|
||||||
|
<Project Path="src/PowershellHarness/PowershellHarness.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/scripts/">
|
||||||
|
<File Path="build.ps1" />
|
||||||
|
</Folder>
|
||||||
|
<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>
|
||||||
2
build.ps1
Normal file
2
build.ps1
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
dotnet run --project build/Build.csproj -- $args
|
||||||
|
exit $LASTEXITCODE;
|
||||||
1
build.sh
Normal file
1
build.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
dotnet run --project ./build/Build.csproj -- "$@"
|
||||||
13
build/Build.csproj
Normal file
13
build/Build.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Cake.Frosting"/>
|
||||||
|
<PackageReference Include="Cake.Powershell"/>
|
||||||
|
<PackageReference Include="System.IO.Packaging" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
55
build/Program.cs
Normal file
55
build/Program.cs
Normal file
|
|
@ -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<BuildContext>()
|
||||||
|
.Run(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BuildContext : FrostingContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base source directory
|
||||||
|
/// </summary>
|
||||||
|
public ConvertableDirectoryPath BaseSourceLocation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Powershell module project folder
|
||||||
|
/// </summary>
|
||||||
|
public ConvertableDirectoryPath PowershellModuleProjectDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Powershell module csproj location
|
||||||
|
/// </summary>
|
||||||
|
public ConvertableFilePath PowershellModuleCsproj { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output directory for built DLLs and powershell module manifest files
|
||||||
|
/// </summary>
|
||||||
|
public ConvertableDirectoryPath PowershellModuleOutputDir { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to powershell script that creates the module manifest files
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
build/Scripts/CreateModuleManifest.ps1
Normal file
24
build/Scripts/CreateModuleManifest.ps1
Normal file
|
|
@ -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
|
||||||
36
build/Tasks/BuildTask.cs
Normal file
36
build/Tasks/BuildTask.cs
Normal file
|
|
@ -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<BuildContext>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
build/Tasks/CleanTask.cs
Normal file
23
build/Tasks/CleanTask.cs
Normal file
|
|
@ -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<BuildContext>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
build/Tasks/CopyOutputTask.cs
Normal file
49
build/Tasks/CopyOutputTask.cs
Normal file
|
|
@ -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<BuildContext>
|
||||||
|
{
|
||||||
|
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}'";
|
||||||
|
}
|
||||||
16
build/Tasks/DefaultTask.cs
Normal file
16
build/Tasks/DefaultTask.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/ModuleCore/Calendar/CalendarGenerator.cs
Normal file
261
src/ModuleCore/Calendar/CalendarGenerator.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds configured day abbreviations, based on <see cref="CultureInfo"/>.
|
||||||
|
/// <para>
|
||||||
|
/// 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 <see cref="DayOfWeek"/> enum starting with Sunday.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<string> _daysOfWeekLookup;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to work out how to pad columns for each day, based on the longest abbreviated day
|
||||||
|
/// </summary>
|
||||||
|
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<string>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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, 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<string>();
|
||||||
|
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<string> Calendar { get; set; } = [];
|
||||||
|
public int WeeksInMonth { get; set; }
|
||||||
|
}
|
||||||
6
src/ModuleCore/Directory.Build.props
Normal file
6
src/ModuleCore/Directory.Build.props
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionPrefix>0.0.0</VersionPrefix>
|
||||||
|
<VersionSuffix>dev</VersionSuffix>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
10
src/ModuleCore/ModuleCore.csproj
Normal file
10
src/ModuleCore/ModuleCore.csproj
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latestmajor</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
15
src/ModuleHarness/ModuleHarness.csproj
Normal file
15
src/ModuleHarness/ModuleHarness.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latestmajor</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ModuleCore\ModuleCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
20
src/ModuleHarness/Program.cs
Normal file
20
src/ModuleHarness/Program.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/PowershellHarness/CustomHost.cs
Normal file
216
src/PowershellHarness/CustomHost.cs
Normal file
|
|
@ -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<string, PSObject> Prompt(string caption, string message, System.Collections.ObjectModel.Collection<FieldDescription> 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<ChoiceDescription> 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; } = "";
|
||||||
|
}
|
||||||
19
src/PowershellHarness/PowershellHarness.csproj
Normal file
19
src/PowershellHarness/PowershellHarness.csproj
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.PowerShell.SDK" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PowershellModule\PowershellModule.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
145
src/PowershellHarness/Program.cs
Normal file
145
src/PowershellHarness/Program.cs
Normal file
|
|
@ -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<DayOfWeek>())
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a PowerShell session and returns an open Runspace
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<CommandParameter>? 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/PowershellModule/Calendar/GetCalendarCommand.cs
Normal file
174
src/PowershellModule/Calendar/GetCalendarCommand.cs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="DayOfWeek"/> from a string.
|
||||||
|
/// <para>
|
||||||
|
/// Defaults to the calendars default start of week on null or invalid values.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dayOfWeek"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
6
src/PowershellModule/Directory.Build.props
Normal file
6
src/PowershellModule/Directory.Build.props
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionPrefix>0.0.0</VersionPrefix>
|
||||||
|
<VersionSuffix>dev</VersionSuffix>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
20
src/PowershellModule/PowershellModule.csproj
Normal file
20
src/PowershellModule/PowershellModule.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<AssemblyName>PowershellModule</AssemblyName>
|
||||||
|
<LangVersion>latestmajor</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="PowerShellStandard.Library" >
|
||||||
|
<PrivateAssets>All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Management.Automation" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ModuleCore\ModuleCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
5
src/PowershellModule/manifest.json
Normal file
5
src/PowershellModule/manifest.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"_TODO":[
|
||||||
|
"add manifest details here maybe idk"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
src/PowershellModule/meta/PowershellModule.psd1
Normal file
0
src/PowershellModule/meta/PowershellModule.psd1
Normal file
0
src/PowershellModule/meta/PowershellModule.psm1
Normal file
0
src/PowershellModule/meta/PowershellModule.psm1
Normal file
48
tests/ModuleTests/Calendar/BasicRenderTest.cs
Normal file
48
tests/ModuleTests/Calendar/BasicRenderTest.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
tests/ModuleTests/Calendar/MarkedDayRenderTests.cs
Normal file
86
tests/ModuleTests/Calendar/MarkedDayRenderTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/ModuleTests/Calendar/StartDayOfWeekTests.cs
Normal file
35
tests/ModuleTests/Calendar/StartDayOfWeekTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs
Normal file
14
tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs
Normal 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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
19
tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs
Normal file
19
tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs
Normal 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"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└────┴────┴────┴────┴────┴────┴────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└────┴────┴────┴────┴────┴────┴────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└───────┴───────┴───────┴───────┴───────┴───────┴───────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└────────┴────────┴────────┴────────┴────────┴────────┴────────┘
|
||||||
|
|
@ -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 │ │ │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
|
@ -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 │ │ │ │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
25
tests/ModuleTests/ModuleTests.csproj
Normal file
25
tests/ModuleTests/ModuleTests.csproj
Normal 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>
|
||||||
6
tests/ModuleTests/TestConstants.cs
Normal file
6
tests/ModuleTests/TestConstants.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace ModuleTests;
|
||||||
|
|
||||||
|
public class TestConstants
|
||||||
|
{
|
||||||
|
public const string SnapshotFolderName = "Snapshots";
|
||||||
|
}
|
||||||
15
tests/ModuleTests/TestDataEnumerator.cs
Normal file
15
tests/ModuleTests/TestDataEnumerator.cs
Normal 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();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue