Compare commits

..
Author SHA1 Message Date
5c09b7c5a8 feat: Add GitRepoRegistration cmdlets (#5)
- adds GitRepoRegistration cmdlets
- adds basic tests for GitRepoRegistration implementations
- adds initial build project and scripts

Refs: #5, #6
2026-09-06 09:33:35 +10:00
56 changed files with 371 additions and 192 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8
*.verified.bin binary

View file

@ -1,6 +1,7 @@
using Cake.Common.IO;
using Cake.Common.IO;
using Cake.Common.IO.Paths;
using Cake.Core;
using Cake.Core.IO;
using Cake.Frosting;
namespace Build;
@ -10,7 +11,7 @@ public class BuildContext : FrostingContext
/// <summary>
/// Base source directory
/// </summary>
public ConvertableDirectoryPath BaseSourceLocation { get; set; }
public DirectoryPath BaseSourceLocation { get; set; }
/// <summary>
/// Powershell module project folder
@ -32,19 +33,29 @@ public class BuildContext : FrostingContext
/// </summary>
public ConvertableFilePath CreateModuleManifestScript { get; set; }
/// <summary>
/// Suffix to tag the build with, defaults to pre-release.
/// <para>
/// Immediately follows the version number and before the commit hash.
/// </para>
/// </summary>
public string BuildSuffix { get; set; }
/// <summary>
/// Disable the commit hash from being added
/// </summary>
public bool DisableCommitHash { get; set; }
public BuildContext(ICakeContext context)
: base(context)
{
BaseSourceLocation = context.Directory("../src");
BaseSourceLocation = context.Directory("../src").Path.MakeAbsolute(context.Environment);
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");
BuildSuffix = context.Configuration.GetValue(nameof(BuildSuffix));
BuildSuffix = context.Configuration.GetValue(nameof(BuildSuffix)) ?? "pre-release";
DisableCommitHash = context.Configuration.GetBoolValue(nameof(DisableCommitHash));
}
}

48
build/Helpers.cs Normal file
View file

@ -0,0 +1,48 @@
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
namespace Build;
public class Helpers
{
/// <summary>
/// Returns a formatted version of the assembly that contains PowerShell cmdlets
/// </summary>
/// <param name="powershelModuleOutputLocation"></param>
/// <returns></returns>
public static string GetPowershellModuleVersion(string powershelModuleOutputLocation)
{
// When the project name changes, this dll will also need to be updated
var moduleVersionInfo = FileVersionInfo.GetVersionInfo(Path.Combine(powershelModuleOutputLocation, "PowershellModule.dll"));
// It's (almost) impossible to not have a product version tag here. There is a reason why the implementation
// returns a string? but I can't find it and I don't really care too much. If we have null return an empty string
// and append no version to the archive bundle.
if (moduleVersionInfo.ProductVersion == null)
{
return string.Empty;
}
var versionRegex = new Regex(@"(\d+\.\d+\.\d+)(?:\-?([\w\-]+)\+?(\w+)?)?");
var match = versionRegex.Match(moduleVersionInfo.ProductVersion);
if (match.Success)
{
// If we have 4 groups, we've got a version number, suffix, and commit hash, so we return the first 2 as is
// and the commit has capped to 8 characters
if (match.Groups.Count == 4)
{
return $"{match.Groups[1]}-{match.Groups[2]}+{match.Groups[3].Value.Substring(0, 8)}";
}
if (match.Groups.Count == 3)
{
return $"{match.Groups[1]}-{match.Groups[2]}";
}
return $"{match.Groups[1]}";
}
return string.Empty;
}
}

View file

@ -8,8 +8,6 @@ public static class Program
{
return new CakeHost()
.UseContext<BuildContext>()
// Uncomment this if you don't want to set the suffix via the run profile program arguments
//.UseCakeSetting(nameof(BuildContext.BuildSuffix), "<your suffix here>")
.Run(args);
}
}

View file

@ -1,4 +1,4 @@
param (
param (
[string]$powershellModuleFileLocation,
[string]$guid,
[string]$author,

View file

@ -1,4 +1,4 @@
using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet.Build;
using Cake.Common.Tools.DotNet.MSBuild;
using Cake.Core.Diagnostics;

View file

@ -1,4 +1,4 @@
using Cake.Common;
using Cake.Common;
using Cake.Common.Tools.DotNet;
using Cake.Core.Diagnostics;
using Cake.Frosting;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Cake.Core.Diagnostics;
@ -15,8 +15,8 @@ public class CopyOutputTask : FrostingTask<BuildContext>
public override void Run(BuildContext context)
{
var powershellModuleName = "PowershellModule";
// TODO: probably don't create full file locations when I can pass the output dir in and have the script
// make the path
// TODO: [#10] Refactor file paths used in build project to be more explict and easier to understand
// Probably don't create full file locations when I can pass the output dir in and have the script make the path
var scriptParams = new
{
PowershellModuleFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psd1",

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
@ -79,7 +80,7 @@ public class CreateBundleArchiveTask : FrostingTask<BuildContext>
context.Log.Information($"Bundle files copied");
GenerateModuleImportScript(moduleFolderLocation, context);
var moduleVersionForFilename = GetPowershellModuleVersion(powershelModuleOutputLocation);
var moduleVersionForFilename = Helpers.GetPowershellModuleVersion(powershelModuleOutputLocation);
CreateBundleZip(bundleRootLocation, moduleFolderLocation, moduleVersionForFilename, context);
}
@ -132,38 +133,4 @@ public class CreateBundleArchiveTask : FrostingTask<BuildContext>
ZipFile.CreateFromDirectory(moduleFolderLocation, bundleZipFileLocaiton, CompressionLevel.Fastest, true);
context.Log.Information($"Archive created at '{bundleZipFileLocaiton}'");
}
private static string GetPowershellModuleVersion(string powershelModuleOutputLocation)
{
var moduleVersionInfo = FileVersionInfo.GetVersionInfo(Path.Combine(powershelModuleOutputLocation, "PowershellModule.dll"));
// It's (almost) impossible to not have a product version tag here. There is a reason why the implementation
// returns a string? but I can't find it and I don't really care too much. If we have null return an empty string
// and append no version to the archive bundle.
if (moduleVersionInfo.ProductVersion == null)
{
return string.Empty;
}
var versionRegex = new Regex(@"(\d+\.\d+\.\d+)(?:\-?([\w\-]+)\+?(\w+)?)?");
var match = versionRegex.Match(moduleVersionInfo.ProductVersion);
if (match.Success)
{
// If we have 4 groups, we've got a version number, suffix, and commit hash, so we return the first 2 as is
// and the commit has capped to 8 characters
if (match.Groups.Count == 4)
{
return $"{match.Groups[1]}-{match.Groups[2]}+{match.Groups[3].Value.Substring(0, 8)}";
}
if (match.Groups.Count == 3)
{
return $"{match.Groups[1]}-{match.Groups[2]}";
}
return $"{match.Groups[1]}";
}
return string.Empty;
}
}

View file

@ -1,4 +1,4 @@
using Cake.Core;
using Cake.Core;
using Cake.Core.Diagnostics;
using Cake.Frosting;
@ -7,6 +7,7 @@ namespace Build.Tasks;
// Consider this the "entry" point for builds, task order is defined by a chain of IsDependentOn
[TaskName("Default")]
[IsDependentOn(typeof(BuildTask))]
[IsDependentOn(typeof(TagCommitTask))]
[IsDependentOn(typeof(CopyOutputTask))]
[IsDependentOn(typeof(CreateBundleArchiveTask))]
public class DefaultTask : FrostingTask

View file

@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
using Cake.Core.Diagnostics;
using Cake.Frosting;
namespace Build.Tasks;
[TaskName("TagCurrentCommitWithVersion")]
[IsDependentOn(typeof(BuildTask))]
public class TagCommitTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
// This task only really exists to create pre-release builds for pull requests.
// It'll tag the current commit with the same version that'll be used for the bundle archive filename.
// TODO: update build.ps1 in the solution root so it doesn't do this tagging task
TagCommit(context);
base.Run(context);
}
private static void TagCommit(BuildContext context)
{
var moduleVersionForFilename = Helpers.GetPowershellModuleVersion(context.PowershellModuleOutputDir.Path.MakeAbsolute(context.Environment).FullPath);
var ps = new ProcessStartInfo("git",
["-C", context.BaseSourceLocation.FullPath, "tag", moduleVersionForFilename])
{
RedirectStandardOutput = true,
RedirectStandardError = true,
};
// If the user doesn't have git on their path, this will throw an exception that I don't have to do anything
// special with, it'll be unhandled and powershell will handle it
var gitProcess = Process.Start(ps);
// This probably shouldn't be possible?
if (gitProcess is null)
{
throw new Exception("git failed to start")
{
Source = "git-process",
};
}
gitProcess.WaitForExit();
if (!gitProcess.StandardError.EndOfStream)
{
throw new Exception($"Unable to tag: {gitProcess.StandardError.ReadToEnd().Trim()}. Either remove the existing tag, or commit your changes before running a build");
}
context.Log.Information($"Tagged commit with {moduleVersionForFilename}");
}
}

View file

@ -1,4 +1,4 @@
# Git Repo Registration
# Git Repo Registration
- all commands support `-debug`
- at its simplest level these commands allow you to register a git repo against a simple name, and provide the ability to quickly pushd to a location

View file

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text;
namespace ModuleCore.Calendar;

View file

@ -1,4 +1,4 @@
using SQLite;
using SQLite;
namespace ModuleCore.Database;
@ -26,12 +26,22 @@ public class DatabaseManager
}
}
/// <summary>
/// Runs the given action within a new database connection
/// </summary>
/// <param name="dbAction"></param>
public void InConnection(Action<SQLiteConnection> dbAction)
{
using var conn = new SQLiteConnection(_databaseLocation.FullName);
dbAction(conn);
}
/// <summary>
/// Runs the given func in a new database connection, returning the result
/// </summary>
/// <param name="dbAction"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T InConnection<T>(Func<SQLiteConnection, T> dbAction)
{
using var conn = new SQLiteConnection(_databaseLocation.FullName);
@ -51,6 +61,18 @@ public class DatabaseManager
return exists ?? false;
}
/// <summary>
/// Deletes the current database file. This will cause any future instance methods to fail on database action if
/// a new instance is not created.
/// <para>
/// This method should be avoided unless calling from a test.
/// </para>
/// </summary>
internal void DeleteDatabase()
{
_databaseLocation.Delete();
}
/// <summary>
/// Removes double dots from the filename and removes the file extension
/// </summary>

View file

@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<VersionPrefix>0.0.1</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>

View file

@ -1,4 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Diagnostics;
using ModuleCore.Database;
using ModuleCore.Git.Models;
@ -6,28 +6,44 @@ using SQLite;
namespace ModuleCore.Git;
// TODO: better name for this
public class GitManager
/// <summary>
/// Manages git repo registration, including creating any registration persistence via a backing <see cref="DatabaseManager"/>
/// </summary>
public class GitRepoRegistrationManager
{
private static readonly Lazy<GitManager> GitManagerInstance = new(() => new GitManager());
private static readonly Lazy<GitRepoRegistrationManager> GitManagerInstance = new(() => new GitRepoRegistrationManager());
private static Action<string>? _debugWriterDelegate;
private readonly DatabaseManager _db;
private readonly ConcurrentDictionary<string, InternalGitRegistration> _registrations;
private GitManager()
private GitRepoRegistrationManager(string? databaseName = null)
{
_registrations = new ConcurrentDictionary<string, InternalGitRegistration>();
_db = new DatabaseManager("git.db");
// Regular usage of this constructor will never pass a database name in. Currently only tests should be hitting
// a code path that has a different database name
_db = new DatabaseManager(databaseName ?? "git.db");
InitialiseRegistrations();
}
public static GitManager Instance => GitManagerInstance.Value;
/// <summary>
/// Returns the current <see cref="GitRepoRegistrationManager"/> instance. If no instance has been created, returns a new instance
/// and then the same instance every call after.
/// </summary>
public static GitRepoRegistrationManager Instance => GitManagerInstance.Value;
/// <summary>
/// Always returns a new clean instance of GitManager
/// </summary>
internal static GitManager InternalFreshInstance => new();
internal static GitRepoRegistrationManager InternalFreshInstance(string databaseName) => new(databaseName);
/// <summary>
/// Deletes the underlying database file.
/// <para>
/// Avoid calling this outside of tests.
/// </para>
/// </summary>
internal void DeleteDatabase() => _db.DeleteDatabase();
/// <summary>
/// Creates up any database tables and loads all previously saved git registrations.
@ -118,6 +134,11 @@ public class GitManager
});
}
/// <summary>
/// Unregisters a git repo registration by name. If no registration exists an exception will be thrown.
/// </summary>
/// <param name="registrationName"></param>
/// <exception cref="Exception"></exception>
public void UnregisterRepo(string registrationName)
{
_db.InConnection(conn =>
@ -185,7 +206,13 @@ public class GitManager
.ToList();
}
public string GetRepo(string? registeredName)
/// <summary>
/// Returns the file location for a git repo registration by name.
/// </summary>
/// <param name="registeredName"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public string GetDirectoryForRegisteredRepo(string? registeredName)
{
if (string.IsNullOrEmpty(registeredName))
{
@ -312,7 +339,7 @@ public class GitManager
/// </summary>
public string CurrentBranch => GetCurrentBranch();
// TODO: not fully decided on if I want this feature or not, but keeping it in for now
// TODO: [#13] Create GitManager to centralise calls to git process
private string GetCurrentBranch()
{
var now = DateTime.Now;
@ -364,19 +391,3 @@ public class GitManager
}
}
}
/// <summary>
/// The directory details of the directory returned from git rev-parse --show-toplevel
/// </summary>
public class ParsedGitFolderDetails
{
/// <summary>
/// The full path to the top level folder containing a git repository
/// </summary>
public string Directory { get; init; } = null!;
/// <summary>
/// The last folder name of the directory
/// </summary>
public string Folder { get; init; } = null!;
}

View file

@ -1,8 +1,21 @@
namespace ModuleCore.Git.Models;
namespace ModuleCore.Git.Models;
/// <summary>
/// Details for a registered git repo
/// </summary>
public class GitRegistration
{
/// <summary>
/// Display name for the registration. Can either be the name of the repo retrieved from git, or a user supplied
/// alias
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Location on disk for the top level of the registered git repo. Not guaranteed to exist on disk
/// </summary>
public required string Location { get; set; }
/// <summary>
/// Current branch for the git repo
/// </summary>
public required string CurrentBranch { get; set; }
}

View file

@ -0,0 +1,17 @@
namespace ModuleCore.Git.Models;
/// <summary>
/// The directory details of the directory returned from git rev-parse --show-toplevel
/// </summary>
public class ParsedGitFolderDetails
{
/// <summary>
/// The full path to the top level folder containing a git repository
/// </summary>
public string Directory { get; init; } = null!;
/// <summary>
/// The last folder name of the directory
/// </summary>
public string Folder { get; init; } = null!;
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@ -14,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="sqlite-net-pcl"/>
</ItemGroup>
</Project>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;
using System.Management.Automation;
using ModuleCore.Calendar;

View file

@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<VersionPrefix>0.0.1</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>

View file

@ -1,4 +1,4 @@
using System.Management.Automation;
using System.Management.Automation;
using ModuleCore.Git;
using ModuleCore.Git.Models;
@ -13,7 +13,7 @@ public class GetGitRepoRegistrationCommand : PSCmdlet
{
protected override void BeginProcessing()
{
var repos = GitManager.Instance.ListRepos();
var repos = GitRepoRegistrationManager.Instance.ListRepos();
WriteObject(repos);

View file

@ -1,4 +1,4 @@
namespace PowershellModule.Git.Commands;
namespace PowershellModule.Git.Commands;
public class GitCommands
{

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Management.Automation;
using ModuleCore.Git;
@ -17,19 +17,19 @@ public sealed class NewGitRepoRegistrationCommand : PSCmdlet
{
try
{
GitManager.SetDebugWriter(WriteDebug);
GitRepoRegistrationManager.SetDebugWriter(WriteDebug);
// Test that we're in a git repo first. If we aren't (or git isn't available), this method will throw
// so we don't need to handle for null (yet).
var repoFolder = GitManager.IsGitRepo(SessionState.Path.CurrentLocation.Path);
var repoFolder = GitRepoRegistrationManager.IsGitRepo(SessionState.Path.CurrentLocation.Path);
if (string.IsNullOrWhiteSpace(Name))
{
WriteDebug("No name given for registration, defaulting to git folder root.");
}
GitManager.Instance.RegisterRepo(repoFolder.Directory, Name ?? repoFolder.Folder);
GitManager.ClearDebugWriter();
GitRepoRegistrationManager.Instance.RegisterRepo(repoFolder.Directory, Name ?? repoFolder.Folder);
GitRepoRegistrationManager.ClearDebugWriter();
base.BeginProcessing();
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Management.Automation;
using ModuleCore.Git;
@ -17,21 +17,21 @@ public class RemoveGitRepoRegistrationCommand : PSCmdlet
{
try
{
GitManager.SetDebugWriter(WriteDebug);
GitRepoRegistrationManager.SetDebugWriter(WriteDebug);
// If we aren't given a value for the Name argument, default behaviour is to attempt to remove a registration
// by the current git repo folder name for the current location.
// If we have a name, don't bother testing for a git repo, just attempt to remove the registration by name
// regardless of where we're being called from
var registrationNameToRemove = string.IsNullOrEmpty(Name)
? GitManager.IsGitRepo(SessionState.Path.CurrentLocation.Path).Folder
? GitRepoRegistrationManager.IsGitRepo(SessionState.Path.CurrentLocation.Path).Folder
: Name;
GitManager.Instance.UnregisterRepo(registrationNameToRemove);
GitRepoRegistrationManager.Instance.UnregisterRepo(registrationNameToRemove);
// Removing a registration works similar to registering a new one - we either remove by exact name, or by
// the folder if no name is given (so a user can remove a registration from a git repo they're currently in)
GitManager.ClearDebugWriter();
GitRepoRegistrationManager.ClearDebugWriter();
base.BeginProcessing();
}

View file

@ -1,4 +1,4 @@
using System.Management.Automation;
using System.Management.Automation;
using ModuleCore.Git;
namespace PowershellModule.Git.Commands;
@ -21,7 +21,7 @@ public class ShowGitRepoRegistrationCommand : PSCmdlet
protected override void BeginProcessing()
{
var location = GitManager.Instance.GetRepo(Name);
var location = GitRepoRegistrationManager.Instance.GetDirectoryForRegisteredRepo(Name);
// By default instead of doing the same as cd, we instead do pushd so a user can popd straight back to where
// they came from.

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Management.Automation;
using System.Management.Automation.Provider;

View file

@ -1,4 +1,4 @@
using System.Management.Automation;
using System.Management.Automation;
namespace PowershellModule.Git;

View file

@ -1,4 +1,4 @@
<#
<#
.SYNOPSIS
Removes all non-core files from build output
#>

View file

@ -9,17 +9,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" >
<PackageReference Include="PowerShellStandard.Library">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Management.Automation" />
<PackageReference Include="System.Management.Automation"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ModuleCore\ModuleCore.csproj" />
<ProjectReference Include="..\ModuleCore\ModuleCore.csproj"/>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="pwsh.exe -file &quot;$(ProjectDir)PostBuild.ps1&quot; $(TargetDir)" />
<Exec Command="pwsh.exe -file &quot;$(ProjectDir)PostBuild.ps1&quot; $(TargetDir)"/>
</Target>
</Project>

View file

@ -1,4 +1,4 @@
using ModuleCore.Calendar;
using ModuleCore.Calendar;
using ModuleTests.Calendar.TestData;
using System.Globalization;

View file

@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using ModuleCore.Calendar;
namespace ModuleTests.Calendar;

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ June 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ July 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────┐
┌──────────────────────────────────┐
│ srpen 2026 │
├────┬────┬────┬────┬────┬────┬────┤
│ po │ út │ st │ čt │ pá │ so │ ne │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ august 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ man │ tir │ ons │ tor │ fre │ lør │ søn │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌────────────────────────────────────────────────┐
┌────────────────────────────────────────────────┐
│ agosto 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mié. │ jue. │ vie. │ sáb. │ dom. │

View file

@ -1,4 +1,4 @@
┌────────────────────────────────────────────────┐
┌────────────────────────────────────────────────┐
│ août 2026 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┤
│ lun. │ mar. │ mer. │ jeu. │ ven. │ sam. │ dim. │

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────┐
┌──────────────────────────────────┐
│ augustus 2026 │
├────┬────┬────┬────┬────┬────┬────┤
│ ma │ di │ wo │ do │ vr │ za │ zo │

View file

@ -1,4 +1,4 @@
┌───────────────────────────────────────────────────────┐
┌───────────────────────────────────────────────────────┐
│ ఆగస్టు 2026 │
├───────┬───────┬───────┬───────┬───────┬───────┬───────┤
│ సోమ │ మంగళ │ బుధ │ గురు │ శుక్ర │ శని │ ఆది │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
Double-wide characters will display incorrectly but still tested until rendering can be improved
Double-wide characters will display incorrectly but still tested until rendering can be improved
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.
Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
┌──────────────────────────────────────────────────────────────┐
┌──────────────────────────────────────────────────────────────┐
│ August 2026 │
├────────┬────────┬────────┬────────┬────────┬────────┬────────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │

View file

@ -1,4 +1,4 @@
Start Day: Sunday
Start Day: Sunday
┌─────────────────────────────────────────┐
│ August 2026 │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

View file

@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using ModuleCore.Calendar;
namespace ModuleTests.Calendar;

View file

@ -1,4 +1,4 @@
namespace ModuleTests.Calendar.TestData;
namespace ModuleTests.Calendar.TestData;
public class CalendarTestDates : TestDataEnumerator<DateTime>
{

View file

@ -1,4 +1,4 @@
namespace ModuleTests.Calendar.TestData;
namespace ModuleTests.Calendar.TestData;
public class CultureCodeTestData : TestDataEnumerator<string>
{

View file

@ -1,10 +1,10 @@
using System.Text;
using ModuleCore.Calendar;
using System.Text;
using ModuleCore.Git;
using ModuleTests.Git.TestData;
namespace ModuleTests.Git;
// TODO: [#12] GitRepoRegistration tests should reset database at start of each test
public class AddRegistrationTests
{
private static readonly VerifySettings Settings;
@ -23,9 +23,10 @@ public class AddRegistrationTests
public Task BasicRepoRegistration((int testId, string path) testData)
{
Settings.UseFileName($"{nameof(BasicRepoRegistration)}_{testData.testId}");
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(BasicRepoRegistration));
var gitManager = GitManager.InternalFreshInstance;
try
{
var repoRegistration = gitManager.RegisterRepo("Test:/some/test/repo", testData.path);
var sb = new StringBuilder();
sb.AppendLine($"Attempted to register: {testData.path}")
@ -33,60 +34,82 @@ public class AddRegistrationTests
return Verify(sb, Settings);
}
finally
{
gitManager.DeleteDatabase();
}
}
[Fact]
public void RepoRegistrationWithEmptyName()
{
Settings.UseFileName(nameof(RepoRegistrationWithEmptyName));
var gitManager = GitManager.InternalFreshInstance;
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithEmptyName));
var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
var emptyName = gitManager.RegisterRepo(testRepoAbsolutePath, "");
Assert.Equal("repo", emptyName);
}
finally
{
gitManager.DeleteDatabase();
}
}
[Fact]
public void RepoRegistrationWithNullName()
{
Settings.UseFileName(nameof(RepoRegistrationWithNullName));
var gitManager = GitManager.InternalFreshInstance;
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithNullName));
var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
// Name is technically not-nullable, but string is a reference type so null can be passed in so we should test
// it regardless
var nullName = gitManager.RegisterRepo(testRepoAbsolutePath, null!);
Assert.Equal("repo", nullName);
}
finally
{
gitManager.DeleteDatabase();
}
}
[Fact]
public void RepoRegistrationWithWhitespaceName()
{
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance;
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var testRepoAbsolutePath = "Test:/some/test/repo";
try
{
var whitespaceName = gitManager.RegisterRepo(testRepoAbsolutePath, " ");
Assert.Equal("repo", whitespaceName);
}
finally
{
gitManager.DeleteDatabase();
}
}
[Fact]
public void DuplicateRepoRegistrationShouldFail()
{
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance;
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var testRepoAbsolutePath = "Test:/some/test/repo";
string[] paths = ["test", "nested", "path"];
var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths));
try
{
var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator);
// TODO: make nested registrations fail in both directions and test
var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]));
Assert.Equal(Path.Combine(paths), firstRegistration);
@ -94,19 +117,24 @@ public class AddRegistrationTests
Assert.Throws<Exception>(() => gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1])));
}
finally
{
gitManager.DeleteDatabase();
}
}
[Fact]
public void DuplicateRepoRegistrationDifferentSlashShouldNotFail()
{
Settings.UseFileName(nameof(RepoRegistrationWithWhitespaceName));
var gitManager = GitManager.InternalFreshInstance;
var gitManager = GitRepoRegistrationManager.InternalFreshInstance(nameof(RepoRegistrationWithWhitespaceName));
var testRepoAbsolutePath = "Test:/some/test/repo";
string[] paths = ["test", "nested", "path"];
var names = (NormalSeparator: string.Join(Path.DirectorySeparatorChar, paths), AltSeparator: string.Join(Path.AltDirectorySeparatorChar, paths));
try
{
var firstRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.NormalSeparator);
// TODO: make nested registrations fail in both directions and test
var secondRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, Path.Combine(paths[..1]));
var differentPathSeparatorRegistration = gitManager.RegisterRepo(testRepoAbsolutePath, names.AltSeparator);
@ -114,4 +142,9 @@ public class AddRegistrationTests
Assert.Equal(Path.Combine(paths[..1]), secondRegistration);
Assert.Equal(names.AltSeparator, differentPathSeparatorRegistration);
}
finally
{
gitManager.DeleteDatabase();
}
}
}

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
namespace ModuleTests.Git.TestData;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>

View file

@ -1,4 +1,4 @@
namespace ModuleTests;
namespace ModuleTests;
public class TestConstants
{

View file

@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
namespace ModuleTests;