From bfb99709d74c00f50ff73dd1b8a5619a7f41253b Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 27 Jun 2023 18:14:11 -0300 Subject: [PATCH 01/11] refactor: injectable Logger instead static Serilog --- source/BaseGeneratorCommand.cs | 20 ++++++++++++--- source/BuildCommand.cs | 12 ++++----- source/Helpers/SiteHelper.cs | 9 ++++--- source/Helpers/StopwatchReporter.cs | 6 +++-- source/Models/Frontmatter.cs | 9 +++---- source/Models/Site.cs | 7 ++++- source/Program.cs | 40 +++++++++++++++++++++-------- source/ServeCommand.cs | 21 +++++++-------- 8 files changed, 82 insertions(+), 42 deletions(-) diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index e3713a8..d77179f 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -32,22 +32,34 @@ public abstract class BaseGeneratorCommand /// /// The stopwatch reporter. /// - protected readonly StopwatchReporter stopwatch = new(); + protected readonly StopwatchReporter stopwatch; + + /// + /// The logger (Serilog). + /// + protected ILogger logger; /// /// Initializes a new instance of the class. /// /// The generate options. - protected BaseGeneratorCommand(IGenerateOptions options) + /// The logger instance. Injectable for testing + protected BaseGeneratorCommand(IGenerateOptions options, ILogger logger) { if (options is null) { throw new ArgumentNullException(nameof(options)); } + if (logger is null) + { + throw new ArgumentNullException(nameof(logger)); + } + this.logger = logger; + stopwatch = new(logger); - Log.Information("Source path: {source}", propertyValue: options.Source); + logger.Information("Source path: {source}", propertyValue: options.Source); - site = SiteHelper.Init(configFile, options, frontmatterParser, WhereParamsFilter, stopwatch); + site = SiteHelper.Init(configFile, options, frontmatterParser, WhereParamsFilter, logger, stopwatch); } /// diff --git a/source/BuildCommand.cs b/source/BuildCommand.cs index 0930c98..5cad285 100644 --- a/source/BuildCommand.cs +++ b/source/BuildCommand.cs @@ -11,22 +11,20 @@ namespace SuCoS; /// public class BuildCommand : BaseGeneratorCommand { - private readonly BuildOptions options; - /// /// Entry point of the build command. It will be called by the main program /// in case the build command is invoked (which is by default). /// - /// - public BuildCommand(BuildOptions options) : base(options: options) + /// Command line options + /// The logger instance. Injectable for testing + public BuildCommand(BuildOptions options, ILogger logger) : base(options, logger) { if (options is null) { throw new ArgumentNullException(nameof(options)); } - this.options = options; - Log.Information("Output path: {output}", options.Output); + logger.Information("Output path: {output}", options.Output); // Generate the site pages CreateOutputFiles(); @@ -64,7 +62,7 @@ public class BuildCommand : BaseGeneratorCommand File.WriteAllText(outputAbsolutePath, result); // Log - Log.Debug("Page created: {Permalink}", frontmatter.Permalink); + logger.Debug("Page created: {Permalink}", frontmatter.Permalink); // Use interlocked to safely increment the counter in a multi-threaded environment _ = Interlocked.Increment(ref pagesCreated); diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index cbdcc97..13332e5 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -2,6 +2,7 @@ using System; using System.IO; using Fluid; using Microsoft.Extensions.FileProviders; +using Serilog; using SuCoS.Models; using SuCoS.Parser; @@ -20,8 +21,9 @@ public static class SiteHelper /// The frontmatter parser. /// The site settings file. /// The method to be used in the whereParams. + /// The logger instance. Injectable for testing /// The site settings. - public static Site ParseSettings(string configFile, IGenerateOptions options, IFrontmatterParser frontmatterParser, FilterDelegate whereParamsFilter) + public static Site ParseSettings(string configFile, IGenerateOptions options, IFrontmatterParser frontmatterParser, FilterDelegate whereParamsFilter, ILogger logger) { if (options is null) { @@ -44,6 +46,7 @@ public static class SiteHelper var fileContent = File.ReadAllText(filePath); var site = frontmatterParser.ParseSiteSettings(fileContent); + site.Logger = logger; site.options = options; site.SourceDirectoryPath = options.Source; site.OutputPath = options.Output; @@ -72,7 +75,7 @@ public static class SiteHelper /// Creates the pages dictionary. /// /// - public static Site Init(string configFile, IGenerateOptions options, IFrontmatterParser frontmatterParser, FilterDelegate whereParamsFilter, StopwatchReporter stopwatch) + public static Site Init(string configFile, IGenerateOptions options, IFrontmatterParser frontmatterParser, FilterDelegate whereParamsFilter, ILogger logger, StopwatchReporter stopwatch) { if (stopwatch is null) { @@ -82,7 +85,7 @@ public static class SiteHelper Site site; try { - site = SiteHelper.ParseSettings(configFile, options, frontmatterParser, whereParamsFilter); + site = SiteHelper.ParseSettings(configFile, options, frontmatterParser, whereParamsFilter, logger); } catch { diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs index 39ebd91..cf8357a 100644 --- a/source/Helpers/StopwatchReporter.cs +++ b/source/Helpers/StopwatchReporter.cs @@ -14,14 +14,16 @@ namespace SuCoS; /// public class StopwatchReporter { + private readonly ILogger logger; private readonly Dictionary stopwatches; private readonly Dictionary itemCounts; /// /// Constructor /// - public StopwatchReporter() + public StopwatchReporter(ILogger logger) { + this.logger = logger; stopwatches = new Dictionary(); itemCounts = new Dictionary(); } @@ -102,6 +104,6 @@ Total {totalDurationAllSteps} ms ═════════════════════════════════════════════"; // Log the report - Log.Information(report, siteTitle); + logger.Information(report, siteTitle); } } \ No newline at end of file diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 79129b5..9d00d72 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using Fluid; using Markdig; -using Serilog; using SuCoS.Models; namespace SuCoS; @@ -285,7 +284,7 @@ public class Frontmatter : IBaseContent, IParams } catch (Exception ex) { - Log.Error(ex, "Error converting URL: {Error}", error); + Site.Logger?.Error(ex, "Error converting URL: {Error}", error); } } @@ -326,7 +325,7 @@ public class Frontmatter : IBaseContent, IParams } else { - Log.Error("Error parsing theme template: {Error}", error); + Site.Logger?.Error("Error parsing theme template: {Error}", error); return string.Empty; } @@ -356,13 +355,13 @@ public class Frontmatter : IBaseContent, IParams } catch (Exception ex) { - Log.Error(ex, "Error rendering theme template: {Error}", error); + Site.Logger?.Error(ex, "Error rendering theme template: {Error}", error); return string.Empty; } } else { - Log.Error("Error parsing theme template: {Error}", error); + Site.Logger?.Error("Error parsing theme template: {Error}", error); return string.Empty; } } diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 69470c2..367926b 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -140,6 +140,11 @@ public class Site : IParams /// public readonly TemplateOptions TemplateOptions = new(); + /// + /// The logger instance. + /// + public ILogger? Logger; + /// /// The time that the older cache should be ignored. /// @@ -294,7 +299,7 @@ public class Site : IParams } catch (Exception ex) { - Log.Error(ex, "Error parsing file {file}", file.filePath); + Logger?.Error(ex, "Error parsing file {file}", file.filePath); } // Use interlocked to safely increment the counter in a multi-threaded environment diff --git a/source/Program.cs b/source/Program.cs index b42756e..a609aad 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -11,13 +11,33 @@ namespace SuCoS; /// public class Program { - private static int Main(string[] args) + private ILogger logger; + + /// + /// Constructor + /// + public Program(ILogger logger) + { + this.logger = logger; + } + + /// + /// Entry point of the program + /// + /// + /// + public static int Main(string[] args) { - // use Serilog to log the program's output - Log.Logger = new LoggerConfiguration() + ILogger logger = new LoggerConfiguration() .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) .CreateLogger(); + var program = new Program(logger); + return program.Run(args); + } + + private int Run(string[] args) + { // Print the logo of the program. OutputLogo(); @@ -26,7 +46,7 @@ public class Program var assemblyName = assembly?.GetName(); var appName = assemblyName?.Name; var appVersion = assemblyName?.Version; - Log.Information("{name} v{version}", appName, appVersion); + logger.Information("{name} v{version}", appName, appVersion); // Shared options between the commands var sourceOption = new Option(new[] { "--source", "-s" }, () => ".", "Source directory path"); @@ -51,11 +71,11 @@ public class Program Output = output, Future = future }; - Log.Logger = new LoggerConfiguration() + logger = new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) .CreateLogger(); - _ = new BuildCommand(buildOptions); + _ = new BuildCommand(buildOptions, logger); }, sourceOption, buildOutputOption, futureOption, verboseOption); @@ -73,12 +93,12 @@ public class Program Source = source, Future = future }; - Log.Logger = new LoggerConfiguration() + logger = new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) .CreateLogger(); - var serveCommand = new ServeCommand(serverOptions); + var serveCommand = new ServeCommand(serverOptions, logger); await serveCommand.RunServer(); await Task.Delay(-1); // Wait forever. }, @@ -93,9 +113,9 @@ public class Program return rootCommand.Invoke(args); } - private static void OutputLogo() + private void OutputLogo() { - Log.Information(@" + logger.Information(@" ____ ____ ____ /\ _`\ /\ _`\ /\ _`\ \ \,\L\_\ __ __\ \ \/\_\ ___\ \,\L\_\ diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 368c18e..8d7317f 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -89,7 +89,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// A Task representing the asynchronous operation. public async Task StartServer(string baseURL, int port) { - Log.Information("Starting server..."); + logger.Information("Starting server..."); // Generate the build report stopwatch.LogReport(site.Title); @@ -108,7 +108,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable .Build(); await host.StartAsync(); - Log.Information("You site is live: {baseURL}:{port}", baseURL, port); + logger.Information("You site is live: {baseURL}:{port}", baseURL, port); } @@ -125,7 +125,8 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// Constructor for the ServeCommand class. /// /// ServeOptions object specifying the serve options. - public ServeCommand(ServeOptions options) : base(options) + /// The logger instance. Injectable for testing + public ServeCommand(ServeOptions options, ILogger logger) : base(options, logger) { this.options = options ?? throw new ArgumentNullException(nameof(options)); var baseURL = "http://localhost"; @@ -145,7 +146,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable { var SourceAbsolutePath = Path.GetFullPath(SourcePath); - Log.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); + logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); var fileWatcher = new FileSystemWatcher { @@ -173,7 +174,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable try { - site = SiteHelper.Init(configFile, options, frontmatterParser, WhereParamsFilter, stopwatch); + site = SiteHelper.Init(configFile, options, frontmatterParser, WhereParamsFilter, logger, stopwatch); // Stop the server if (host != null) @@ -203,7 +204,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable var fileAbsolutePath = Path.Combine(options.Source, "static", requestPath.TrimStart('/')); - Log.Debug("Request received for {RequestPath}", requestPath); + logger.Debug("Request received for {RequestPath}", requestPath); // Return the server startup timestamp as the response if (requestPath == "/ping") @@ -277,7 +278,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// /// The content to inject the reload script into. /// The content with the reload script injected. - private static string InjectReloadScript(string content) + private string InjectReloadScript(string content) { // Read the content of the JavaScript file string scriptContent; @@ -291,8 +292,8 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable } catch (Exception ex) { - Log.Error(ex, "Could not read the JavaScript file."); - throw ex; + logger.Error(ex, "Could not read the JavaScript file."); + throw; } // Inject the JavaScript content @@ -318,7 +319,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable { if (!restartInProgress) { - Log.Information("File change detected: {FullPath}", e.FullPath); + logger.Information("File change detected: {FullPath}", e.FullPath); restartInProgress = true; await RestartServer(); -- GitLab From d1867ec99ca03ae9a03aa7d7422a77c0c8be8cf9 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 27 Jun 2023 20:53:34 -0300 Subject: [PATCH 02/11] test: implemented a test suite! --- .build.Nuke/Build.Compile.cs | 8 +-- .build.Nuke/Build.Test.cs | 49 +++++++++++++++++ .build.Nuke/_build_nuke.csproj | 3 + .gitignore | 1 + .gitlab-ci.yml | 5 +- .nuke/build.schema.json | 8 ++- SuCoS.sln | 6 ++ source/Helpers/Urlizer.cs | 46 +++++++++------- test/BaseGeneratorCommandTests.cs | 72 ++++++++++++++++++++++++ test/Helpers/UrlizerTests.cs | 91 +++++++++++++++++++++++++++++++ test/test.csproj | 26 +++++++++ 11 files changed, 287 insertions(+), 28 deletions(-) create mode 100644 .build.Nuke/Build.Test.cs create mode 100644 test/BaseGeneratorCommandTests.cs create mode 100644 test/Helpers/UrlizerTests.cs create mode 100644 test/test.csproj diff --git a/.build.Nuke/Build.Compile.cs b/.build.Nuke/Build.Compile.cs index 718ee69..7822f2d 100644 --- a/.build.Nuke/Build.Compile.cs +++ b/.build.Nuke/Build.Compile.cs @@ -13,15 +13,16 @@ namespace SuCoS; sealed partial class Build : NukeBuild { [Parameter("output-directory (default: ./output)")] - readonly string outputDirectory = RootDirectory / "output"; + readonly AbsolutePath outputDirectory = RootDirectory / "output"; Target Clean => _ => _ .Executes(() => { - sourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach( + sourceDirectory.GlobDirectories("**/bin", "**/obj", "**/output").ForEach( (path) => path.DeleteDirectory() ); - PublishDirectory.CreateOrCleanDirectory(); + PublishDirectory.DeleteDirectory(); + coverageDirectory.DeleteDirectory(); }); Target Restore => _ => _ @@ -40,7 +41,6 @@ sealed partial class Build : NukeBuild .SetNoLogo(true) .SetProjectFile(solution) .SetConfiguration(configuration) - .SetOutputDirectory(outputDirectory) .EnableNoRestore() ); }); diff --git a/.build.Nuke/Build.Test.cs b/.build.Nuke/Build.Test.cs new file mode 100644 index 0000000..1f4c992 --- /dev/null +++ b/.build.Nuke/Build.Test.cs @@ -0,0 +1,49 @@ +using Nuke.Common; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.OpenCover; +using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks; +using Nuke.Common.Tools.ReportGenerator; +using Nuke.Common.IO; +using Nuke.Common.Tools.Coverlet; +using static Nuke.Common.Tools.Coverlet.CoverletTasks; + +namespace SuCoS; + +/// +/// This is the main build file for the project. +/// This partial is responsible for the build process. +/// +sealed partial class Build : NukeBuild +{ + AbsolutePath TestDirectory => RootDirectory / "test"; + AbsolutePath coverageDirectory => RootDirectory / "coverage-results"; + AbsolutePath ReportDirectory => coverageDirectory / "report"; + AbsolutePath CoverageResultDirectory => coverageDirectory / "coverage"; + AbsolutePath CoverageResultFile => CoverageResultDirectory / "coverage.cobertura.xml"; + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => + { + CoverageResultDirectory.CreateDirectory(); + Coverlet(s => s + .SetTarget("dotnet") + .SetTargetArgs("test --no-build --no-restore") + .SetAssembly(TestDirectory / "bin/Debug/net7.0/test.dll") + // .SetThreshold(75) + .SetOutput(CoverageResultFile) + .SetFormat(CoverletOutputFormat.opencover)); + }); + + Target TestReport => _ => _ + .DependsOn(Test) + .AssuredAfterFailure() + .Executes(() => + { + ReportDirectory.CreateDirectory(); + ReportGenerator(s => s + .SetTargetDirectory(ReportDirectory) + .SetReportTypes(new ReportTypes[] { ReportTypes.Html }) + .SetReports(CoverageResultFile)); + }); +} diff --git a/.build.Nuke/_build_nuke.csproj b/.build.Nuke/_build_nuke.csproj index f5b93a4..0b25bea 100644 --- a/.build.Nuke/_build_nuke.csproj +++ b/.build.Nuke/_build_nuke.csproj @@ -11,11 +11,14 @@ + + + diff --git a/.gitignore b/.gitignore index e619553..0de7bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ artifacts/ output/ project.fragment.lock.json project.lock.json +**/coverage-results/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d031737..f933b10 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,11 +35,10 @@ build: - schedules script: - | - ./build.sh Compile \ - --output-directory "./output" + ./build.sh Test artifacts: paths: - - output/* + - coverage-results/report/* # # check if there is new commits, if so, create a tag and a release # # this will trigger the publish stage "publish" diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index b0c262b..0626d6a 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -125,7 +125,9 @@ "GitLabCreateTag", "Publish", "Restore", - "ShowCurrentVersion" + "ShowCurrentVersion", + "Test", + "TestReport" ] } }, @@ -148,7 +150,9 @@ "GitLabCreateTag", "Publish", "Restore", - "ShowCurrentVersion" + "ShowCurrentVersion", + "Test", + "TestReport" ] } }, diff --git a/SuCoS.sln b/SuCoS.sln index 7100ee9..45adce0 100644 --- a/SuCoS.sln +++ b/SuCoS.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SuCoS", "source\SuCoS.cspro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build_nuke", ".build.Nuke\_build_nuke.csproj", "{26DB04F6-DA88-43D7-8F4B-535D4D68C24E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test.csproj", "{F3D789FD-6AC5-4A45-B9AC-079035F5909C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,6 +26,10 @@ Global {2395B57B-24D7-47AB-B400-7AC6C1FCAFA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {2395B57B-24D7-47AB-B400-7AC6C1FCAFA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {2395B57B-24D7-47AB-B400-7AC6C1FCAFA1}.Release|Any CPU.Build.0 = Release|Any CPU + {F3D789FD-6AC5-4A45-B9AC-079035F5909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3D789FD-6AC5-4A45-B9AC-079035F5909C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3D789FD-6AC5-4A45-B9AC-079035F5909C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3D789FD-6AC5-4A45-B9AC-079035F5909C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2395B57B-24D7-47AB-B400-7AC6C1FCAFA1} = {1A575294-ABB9-4BCF-8FF7-9981A26A55F9} diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs index d371adb..1830285 100644 --- a/source/Helpers/Urlizer.cs +++ b/source/Helpers/Urlizer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; @@ -9,8 +10,10 @@ namespace SuCoS; /// public static partial class Urlizer { - [GeneratedRegex(@"[^a-z0-9.]")] - private static partial Regex UrlizeRegex(); + [GeneratedRegex(@"[^a-zA-Z0-9]+")] + private static partial Regex UrlizeRegexAlpha(); + [GeneratedRegex(@"[^a-zA-Z0-9.]+")] + private static partial Regex UrlizeRegexAlphaDot(); /// /// Converts a string to a URL-friendly string. @@ -22,25 +25,19 @@ public static partial class Urlizer /// public static string Urlize(string title, UrlizerOptions? options = null) { - if (title == null) - { - throw new ArgumentNullException(nameof(title)); - } + title ??= ""; options ??= new UrlizerOptions(); // Use default options if not provided - var cleanedTitle = title; + var cleanedTitle = !options.LowerCase ? title : title.ToLower(CultureInfo.CurrentCulture); - // Apply culture-specific case conversion if enabled - if (options.LowerCase) - { - cleanedTitle = cleanedTitle.ToLower(CultureInfo.CurrentCulture); - } + var replacementChar = options.ReplacementChar ?? '\0'; + var replacementCharString = options.ReplacementChar.ToString() ?? ""; // Remove non-alphanumeric characters and replace spaces with the replacement character - cleanedTitle = UrlizeRegex() - .Replace(cleanedTitle, options.ReplacementChar.ToString()) - .Trim(options.ReplacementChar); + cleanedTitle = (options.ReplaceDot ? UrlizeRegexAlpha() : UrlizeRegexAlphaDot()) + .Replace(cleanedTitle, replacementCharString) + .Trim(replacementChar); return cleanedTitle; } @@ -53,12 +50,17 @@ public static partial class Urlizer /// public static string UrlizePath(string path, UrlizerOptions? options = null) { - var items = (path ?? string.Empty).Split("/"); + var pathString = (path ?? string.Empty); + var items = pathString.Split("/"); + var result = new List(); for (var i = 0; i < items.Length; i++) { - items[i] = Urlize(items[i], options); + if (!string.IsNullOrEmpty(items[i])) + { + result.Add(Urlize(items[i], options)); + } } - return string.Join("/", items); + return (pathString.StartsWith('/') ? '/' : string.Empty) + string.Join('/', result); } } @@ -76,5 +78,11 @@ public class UrlizerOptions /// /// The character that will be used to replace spaces and other invalid characters. /// - public char ReplacementChar { get; set; } = '-'; + public char? ReplacementChar { get; set; } = '-'; + + /// + /// Replace dots with the replacement character. + /// Note that it will break file paths and domain names. + /// + public bool ReplaceDot { get; set; } } diff --git a/test/BaseGeneratorCommandTests.cs b/test/BaseGeneratorCommandTests.cs new file mode 100644 index 0000000..f0c5616 --- /dev/null +++ b/test/BaseGeneratorCommandTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Reflection; +using Fluid; +using Fluid.Values; +using Moq; +using Serilog; +using SuCoS; +using SuCoS.Models; +using SuCoS.Parser; +using Xunit; + +namespace SuCoS.Tests; + +public class BaseGeneratorCommandTests +{ + private static readonly IGenerateOptions TestOptions = new BuildOptions + { + Source = "test_source" + }; + + private static readonly ILogger TestLogger = new LoggerConfiguration().CreateLogger(); + + private class BaseGeneratorCommandStub : BaseGeneratorCommand + { + public BaseGeneratorCommandStub(IGenerateOptions options, ILogger logger) + : base(options, logger) { } + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + Assert.Throws(() => new BaseGeneratorCommandStub(null!, TestLogger)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenLoggerIsNull() + { + Assert.Throws(() => new BaseGeneratorCommandStub(TestOptions, null!)); + } + + [Fact] + public async Task WhereParamsFilter_ShouldThrowArgumentNullException_WhenInputIsNull() + { + await Assert.ThrowsAsync( + () => BaseGeneratorCommand.WhereParamsFilter(null!, new FilterArguments(), new TemplateContext()).AsTask()); + } + + [Fact] + public async Task WhereParamsFilter_ShouldThrowArgumentNullException_WhenArgumentsIsNull() + { + await Assert.ThrowsAsync( + () => BaseGeneratorCommand.WhereParamsFilter(new ArrayValue(new FluidValue[0]), null!, new TemplateContext()).AsTask()); + } + + // More tests for WhereParamsFilter with different inputs and expected outcomes + + [Fact] + public void CheckValueInDictionary_ShouldWorkCorrectly() + { + var type = typeof(BaseGeneratorCommand); + var method = type.GetMethod("CheckValueInDictionary", BindingFlags.NonPublic | BindingFlags.Static); + var parameters = new object[] { new[] { "key" }, new Dictionary { { "key", "value" } }, "value" }; + + Assert.NotNull(method); + var result = method.Invoke(null, parameters); + + Assert.NotNull(result); + Assert.True((bool)result!); + } +} diff --git a/test/Helpers/UrlizerTests.cs b/test/Helpers/UrlizerTests.cs new file mode 100644 index 0000000..1d0c7f1 --- /dev/null +++ b/test/Helpers/UrlizerTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using SuCoS; + +namespace SuCoSTests; + +public class UrlizerTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + public void Urlize_NullOrEmptyText_ThrowsArgumentNullException(string text) + { + var result = Urlizer.Urlize(text); + Assert.Equal("", result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void UrlizePath_NullPath_ReturnsEmptyString(string path) + { + var result = Urlizer.UrlizePath(path); + + Assert.Equal("", result); + } + + [Theory] + [InlineData("Hello, World!", '-', true, false, "hello-world")] + [InlineData("Hello, World!", '_', true, false, "hello_world")] + [InlineData("Hello, World!", '-', false, false, "Hello-World")] + [InlineData("Hello.World", '-', true, false, "hello.world")] + [InlineData("Hello.World", '-', true, true, "hello-world")] + public void Urlize_ValidText_ReturnsExpectedResult(string text, char? replacementChar, bool lowerCase, bool replaceDot, string expectedResult) + { + var options = new UrlizerOptions { ReplacementChar = replacementChar, LowerCase = lowerCase, ReplaceDot = replaceDot }; + var result = Urlizer.Urlize(text, options); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("Documents/My Report.docx", '-', true, false, "documents/my-report.docx")] + [InlineData("Documents/My Report.docx", '_', true, false, "documents/my_report.docx")] + [InlineData("Documents/My Report.docx", '-', false, false, "Documents/My-Report.docx")] + [InlineData("Documents/My Report.docx", '-', true, true, "documents/my-report-docx")] + [InlineData("C:/Documents/My Report.docx", '_', true, true, "c/documents/my_report_docx")] + [InlineData("Documents/My Report.docx", null, true, false, "documents/myreport.docx")] + public void UrlizePath_ValidPath_ReturnsExpectedResult(string path, char? replacementChar, bool lowerCase, bool replaceDot, string expectedResult) + { + var options = new UrlizerOptions { ReplacementChar = replacementChar, LowerCase = lowerCase, ReplaceDot = replaceDot }; + var result = Urlizer.UrlizePath(path, options); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void Urlize_WithoutOptions_ReturnsExpectedResult() + { + var text = "Hello, World!"; + var result = Urlizer.Urlize(text); + + Assert.Equal("hello-world", result); + } + + [Fact] + public void UrlizePath_WithoutOptions_ReturnsExpectedResult() + { + var path = "Documents/My Report.docx"; + var result = Urlizer.UrlizePath(path); + + Assert.Equal("documents/my-report.docx", result); + } + + [Fact] + public void Urlize_SpecialCharsInText_ReturnsOnlyHyphens() + { + var text = "!@#$%^&*()"; + var result = Urlizer.Urlize(text); + + Assert.Equal("", result); + } + + [Fact] + public void UrlizePath_SpecialCharsInPath_ReturnsOnlyHyphens() + { + var path = "/!@#$%^&*()/"; + var result = Urlizer.UrlizePath(path); + + Assert.Equal("/", result); + } +} diff --git a/test/test.csproj b/test/test.csproj new file mode 100644 index 0000000..520ac34 --- /dev/null +++ b/test/test.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + -- GitLab From 4ecc9725a93712bbd67420647006ec977677f55b Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 27 Jun 2023 20:54:48 -0300 Subject: [PATCH 03/11] ci: better describe the stage --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f933b10..f97993f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ image: mcr.microsoft.com/dotnet/sdk:7.0 stages: - - build + - build-test - check-and-create-release - publish @@ -27,15 +27,15 @@ services: - docker:dind # build the project on every commit -build: +test: <<: *dotnet_nuke_template - stage: build + stage: build-test except: - tags - schedules script: - | - ./build.sh Test + ./build.sh TestReport artifacts: paths: - coverage-results/report/* -- GitLab From 0b3cc425e79357e5e54bca930963565ead636adf Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 09:54:54 -0300 Subject: [PATCH 04/11] test: site and frontmatter suite, also a testing site --- .build.Nuke/Build.Test.cs | 16 ++- .nuke/build.schema.json | 2 + source/Helpers/FileUtils.cs | 5 +- source/Helpers/ISystemClock.cs | 35 +++++ source/Helpers/SiteHelper.cs | 3 - source/Models/Frontmatter.cs | 51 ++++--- source/Models/Site.cs | 26 +++- source/Parser/YAMLParser.cs | 2 + test/.TestSites/01/content/test01.md | 5 + test/.TestSites/01/content/test02.md | 5 + test/Models/BasicContentTests.cs | 38 +++++ test/Models/FrontmatterTests.cs | 199 +++++++++++++++++++++++++++ test/Models/SiteTests.cs | 62 +++++++++ 13 files changed, 420 insertions(+), 29 deletions(-) create mode 100644 source/Helpers/ISystemClock.cs create mode 100644 test/.TestSites/01/content/test01.md create mode 100644 test/.TestSites/01/content/test02.md create mode 100644 test/Models/BasicContentTests.cs create mode 100644 test/Models/FrontmatterTests.cs create mode 100644 test/Models/SiteTests.cs diff --git a/.build.Nuke/Build.Test.cs b/.build.Nuke/Build.Test.cs index 1f4c992..2d367c9 100644 --- a/.build.Nuke/Build.Test.cs +++ b/.build.Nuke/Build.Test.cs @@ -6,6 +6,7 @@ using Nuke.Common.Tools.ReportGenerator; using Nuke.Common.IO; using Nuke.Common.Tools.Coverlet; using static Nuke.Common.Tools.Coverlet.CoverletTasks; +using static Nuke.Common.IO.FileSystemTasks; namespace SuCoS; @@ -15,21 +16,32 @@ namespace SuCoS; /// sealed partial class Build : NukeBuild { + AbsolutePath TestDLL => TestDirectory / "bin" / "Debug" / "net7.0"; AbsolutePath TestDirectory => RootDirectory / "test"; + AbsolutePath TestSiteDirectory => RootDirectory / "test" / ".TestSites"; + AbsolutePath TestOutputDirectory => TestDLL / ".TestSites"; AbsolutePath coverageDirectory => RootDirectory / "coverage-results"; AbsolutePath ReportDirectory => coverageDirectory / "report"; AbsolutePath CoverageResultDirectory => coverageDirectory / "coverage"; AbsolutePath CoverageResultFile => CoverageResultDirectory / "coverage.cobertura.xml"; + Target PrepareTestFiles => _ => _ + .Executes(() => + { + TestOutputDirectory.CreateOrCleanDirectory(); + CopyDirectoryRecursively(TestSiteDirectory, TestOutputDirectory, DirectoryExistsPolicy.Merge); + }); + Target Test => _ => _ - .DependsOn(Compile) + .DependsOn(Compile, PrepareTestFiles) .Executes(() => { + CoverageResultDirectory.CreateDirectory(); Coverlet(s => s .SetTarget("dotnet") .SetTargetArgs("test --no-build --no-restore") - .SetAssembly(TestDirectory / "bin/Debug/net7.0/test.dll") + .SetAssembly(TestDLL / "test.dll") // .SetThreshold(75) .SetOutput(CoverageResultFile) .SetFormat(CoverletOutputFormat.opencover)); diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 0626d6a..882ec5c 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -123,6 +123,7 @@ "CreatePackage", "GitLabCreateRelease", "GitLabCreateTag", + "PrepareTestFiles", "Publish", "Restore", "ShowCurrentVersion", @@ -148,6 +149,7 @@ "CreatePackage", "GitLabCreateRelease", "GitLabCreateTag", + "PrepareTestFiles", "Publish", "Restore", "ShowCurrentVersion", diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 584cc48..87cf1c5 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -19,7 +19,10 @@ public static class FileUtils { var markdownFiles = new List(); var files = Directory.GetFiles(directory, "*.md"); - markdownFiles.AddRange(files); + if (files.Length > 0) + { + markdownFiles.AddRange(files); + } var subdirectories = Directory.GetDirectories(directory); foreach (var subdirectory in subdirectories) diff --git a/source/Helpers/ISystemClock.cs b/source/Helpers/ISystemClock.cs new file mode 100644 index 0000000..bd38900 --- /dev/null +++ b/source/Helpers/ISystemClock.cs @@ -0,0 +1,35 @@ +using System; + +namespace SuCoS.Models; + +/// +/// Represents an interface for accessing the system clock. +/// +public interface ISystemClock +{ + /// + /// Gets the current local date and time. + /// + DateTime Now { get; } + + /// + /// Gets the current Coordinated Universal Time (UTC). + /// + DateTime UtcNow { get; } +} + +/// +/// Represents a concrete implementation of the ISystemClock interface using the system clock. +/// +public class SystemClock : ISystemClock +{ + /// + /// Gets the current local date and time. + /// + public DateTime Now => DateTime.Now; + + /// + /// Gets the current Coordinated Universal Time (UTC). + /// + public DateTime UtcNow => DateTime.UtcNow; +} diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 13332e5..0acb7f5 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -53,9 +53,6 @@ public static class SiteHelper // Liquid template options, needed to theme the content // but also parse URLs - site.TemplateOptions.MemberAccessStrategy.Register(); - site.TemplateOptions.MemberAccessStrategy.Register(); - site.TemplateOptions.MemberAccessStrategy.Register(); site.TemplateOptions.Filters.AddFilter("whereParams", whereParamsFilter); if (site is null) diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 9d00d72..0fa2d97 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -137,10 +137,10 @@ public class Frontmatter : IBaseContent, IParams { get { - if (contentCacheTime is null || Site.IgnoreCacheBefore >= contentCacheTime) + if (contentCacheTime is null || Site.IgnoreCacheBefore > contentCacheTime) { contentCache = CreateContent(); - contentCacheTime = DateTime.UtcNow; + contentCacheTime = clock.UtcNow; } return contentCache!; } @@ -169,7 +169,10 @@ public class Frontmatter : IBaseContent, IParams pagesCached ??= new(); foreach (var permalink in PagesReferences) { - pagesCached.Add(Site.PagesDict[permalink]); + if (permalink is not null) + { + pagesCached.Add(Site.PagesDict[permalink]); + } } } return pagesCached; @@ -204,12 +207,12 @@ public class Frontmatter : IBaseContent, IParams /// /// Check if the page is expired /// - public bool IsDateExpired => ExpiryDate is not null && ExpiryDate >= DateTime.Now; + public bool IsDateExpired => ExpiryDate is not null && ExpiryDate <= clock.Now; /// /// Check if the page is publishable /// - public bool IsDatePublishable => GetPublishDate is null || GetPublishDate <= DateTime.Now; + public bool IsDatePublishable => GetPublishDate is null || GetPublishDate <= clock.Now; /// /// The markdown content. @@ -232,6 +235,8 @@ public class Frontmatter : IBaseContent, IParams private DateTime? GetPublishDate => PublishDate ?? Date; + private readonly ISystemClock clock; + /// /// Required. /// @@ -239,9 +244,11 @@ public class Frontmatter : IBaseContent, IParams string title, string sourcePath, Site site, + ISystemClock clock, string? sourceFileNameWithoutExtension = null, string? sourcePathDirectory = null) { + this.clock = clock; Title = title; Site = site; SourcePath = sourcePath; @@ -267,35 +274,35 @@ public class Frontmatter : IBaseContent, IParams public string CreatePermalink(string? URLforce = null) { var isIndex = SourceFileNameWithoutExtension == "index"; - string outputRelativePath; + var permaLink = string.Empty; URLforce ??= URL ?? (isIndex ? "{{ page.SourcePathDirectory }}" : "{{ page.SourcePathDirectory }}/{{ page.Title }}"); - outputRelativePath = URLforce; - - if (Site.FluidParser.TryParse(URLforce, out var template, out var error)) + try { - var context = new TemplateContext(Site.TemplateOptions) - .SetValue("page", this); - try + if (Site.FluidParser.TryParse(URLforce, out var template, out var error)) { - outputRelativePath = template.Render(context); + var context = new TemplateContext(Site.TemplateOptions) + .SetValue("page", this); + permaLink = template.Render(context); } - catch (Exception ex) + else { - Site.Logger?.Error(ex, "Error converting URL: {Error}", error); + throw new FormatException(error); } } + catch (Exception ex) + { + Site.Logger?.Error(ex, "Error converting URL: {URLforce}", URLforce); + } - outputRelativePath = Urlizer.UrlizePath(outputRelativePath); - - if (!Path.IsPathRooted(outputRelativePath) && !outputRelativePath.StartsWith("/")) + if (!Path.IsPathRooted(permaLink) && !permaLink.StartsWith('/')) { - outputRelativePath = "/" + outputRelativePath; + permaLink = '/' + permaLink; } - return outputRelativePath; + return Urlizer.UrlizePath(permaLink); } /// @@ -340,9 +347,9 @@ public class Frontmatter : IBaseContent, IParams { var fileContents = FileUtils.GetTemplate(Site.SourceThemePath, this, Site); // Theme content - if (string.IsNullOrEmpty(value: fileContents)) + if (string.IsNullOrEmpty(fileContents)) { - return Content; + return ContentPreRendered; } else if (Site.FluidParser.TryParse(fileContents, out var template, out var error)) { diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 367926b..35452ba 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -174,6 +174,8 @@ public class Site : IParams private List? regularPagesCache; + private readonly ISystemClock clock; + /// /// Markdig 20+ built-in extensions /// @@ -182,6 +184,26 @@ public class Site : IParams .UseAdvancedExtensions() .Build(); + /// + /// Constructor + /// + public Site() : this(new SystemClock()) + { + } + + /// + /// Constructor + /// + public Site(ISystemClock clock) + { + // Liquid template options, needed to theme the content + // but also parse URLs + TemplateOptions.MemberAccessStrategy.Register(); + TemplateOptions.MemberAccessStrategy.Register(); + + this.clock = clock; + } + /// /// Scans all markdown files in the source directory. /// @@ -232,6 +254,7 @@ public class Site : IParams if (!automaticContentCache.TryGetValue(id, out frontmatter)) { frontmatter = new( + clock: new SystemClock(), site: this, title: baseContent.Title, sourcePath: string.Empty, @@ -327,6 +350,7 @@ public class Site : IParams Frontmatter frontmatter = new( title: Title, site: this, + clock: clock, sourcePath: Path.Combine(relativePath, "index.md"), sourceFileNameWithoutExtension: "index", sourcePathDirectory: "/" @@ -345,7 +369,7 @@ public class Site : IParams /// /// /// - private void PostProcessFrontMatter(Frontmatter frontmatter, bool overwrite = false) + public void PostProcessFrontMatter(Frontmatter frontmatter, bool overwrite = false) { frontmatter.Permalink = frontmatter.CreatePermalink(); lock (syncLockPostProcess) diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 48ffd89..b2576cf 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -40,6 +40,7 @@ public partial class YAMLParser : IFrontmatterParser { throw new ArgumentNullException(nameof(site)); } + var dateTime = new SystemClock(); Frontmatter? frontmatter = null; var match = YAMLRegex().Match(fileContent); @@ -80,6 +81,7 @@ public partial class YAMLParser : IFrontmatterParser frontmatter = new( title: titleValue?.ToString() ?? sourceFileNameWithoutExtension, site: site, + clock: dateTime, sourcePath: filePath, sourceFileNameWithoutExtension: sourceFileNameWithoutExtension, sourcePathDirectory: null diff --git a/test/.TestSites/01/content/test01.md b/test/.TestSites/01/content/test01.md new file mode 100644 index 0000000..2d3c415 --- /dev/null +++ b/test/.TestSites/01/content/test01.md @@ -0,0 +1,5 @@ +--- +Title: Test Content 1 +--- + +Test Content 1 diff --git a/test/.TestSites/01/content/test02.md b/test/.TestSites/01/content/test02.md new file mode 100644 index 0000000..7dfb40d --- /dev/null +++ b/test/.TestSites/01/content/test02.md @@ -0,0 +1,5 @@ +--- +Title: Test Content 2 +--- + +Test Content 2 diff --git a/test/Models/BasicContentTests.cs b/test/Models/BasicContentTests.cs new file mode 100644 index 0000000..a492b60 --- /dev/null +++ b/test/Models/BasicContentTests.cs @@ -0,0 +1,38 @@ +namespace SuCoS.Models.Tests; + +using Xunit; +using SuCoS.Models; + +public class BasicContentTests +{ + [Theory] + [InlineData("Title1", "Section1", "Type1", "URL1", Kind.single)] + [InlineData("Title2", "Section2", "Type2", "URL2", Kind.list)] + [InlineData("Title3", "Section3", "Type3", "URL3", Kind.index)] + public void Constructor_Sets_Properties_Correctly(string title, string section, string type, string url, Kind kind) + { + // Act + var basicContent = new BasicContent(title, section, type, url, kind); + + // Assert + Assert.Equal(title, basicContent.Title); + Assert.Equal(section, basicContent.Section); + Assert.Equal(type, basicContent.Type); + Assert.Equal(url, basicContent.URL); + Assert.Equal(kind, basicContent.Kind); + } + + [Fact] + public void Constructor_Sets_Kind_To_List_If_Not_Provided() + { + // Arrange + string title = "Title1", section = "Section1", type = "Type1", url = "URL1"; + var kind = Kind.list; + + // Act + var basicContent = new BasicContent(title, section, type, url); + + // Assert + Assert.Equal(kind, basicContent.Kind); + } +} diff --git a/test/Models/FrontmatterTests.cs b/test/Models/FrontmatterTests.cs new file mode 100644 index 0000000..6ca919e --- /dev/null +++ b/test/Models/FrontmatterTests.cs @@ -0,0 +1,199 @@ +using System.Globalization; +using Moq; +using SuCoS.Models; +using Xunit; + +namespace SuCoS.Tests; + +public class FrontmatterTests +{ + private readonly ISystemClock clock; + private readonly Mock systemClockMock; + private readonly string title = "Test Title"; + private readonly string sourcePath = "/path/to/file.md"; + private readonly Site site; + + public FrontmatterTests() + { + systemClockMock = new Mock(); + var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); + systemClockMock.Setup(c => c.Now).Returns(testDate); + + clock = systemClockMock.Object; + site = new(clock); + } + + [Theory] + [InlineData("Test Title", "/path/to/file.md", "file", "/path/to")] + public void ShouldCreateFrontmatterWithCorrectProperties(string title, string sourcePath, string sourceFileNameWithoutExtension, string sourcePathDirectory) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock, sourceFileNameWithoutExtension, sourcePathDirectory); + + Assert.Equal(title, frontmatter.Title); + Assert.Equal(sourcePath, frontmatter.SourcePath); + Assert.Same(site, frontmatter.Site); + Assert.Equal(sourceFileNameWithoutExtension, frontmatter.SourceFileNameWithoutExtension); + Assert.Equal(sourcePathDirectory, frontmatter.SourcePathDirectory); + } + + [Fact] + public void ShouldHaveDefaultValuesForOptionalProperties() + { + // Arrange + var frontmatter = new Frontmatter("Test Title", "/path/to/file.md", site, clock); + + // Assert + Assert.Equal(string.Empty, frontmatter.Section); + Assert.Equal(Kind.single, frontmatter.Kind); + Assert.Equal(string.Empty, frontmatter.Type); + Assert.Null(frontmatter.URL); + Assert.Empty(frontmatter.Params); + Assert.Null(frontmatter.Date); + Assert.Null(frontmatter.LastMod); + Assert.Null(frontmatter.PublishDate); + Assert.Null(frontmatter.ExpiryDate); + Assert.Null(frontmatter.Aliases); + Assert.Null(frontmatter.Permalink); + Assert.Empty(frontmatter.Urls); + Assert.Equal(string.Empty, frontmatter.RawContent); + Assert.Null(frontmatter.Tags); + Assert.Null(frontmatter.PagesReferences); + Assert.Empty(frontmatter.RegularPages); + Assert.Equal(string.Empty, frontmatter.Language); + Assert.False(frontmatter.IsDateExpired); + Assert.True(frontmatter.IsDatePublishable); + } + + [Fact] + public void ShouldReturnValidDateBasedOnExpiryDateAndPublishDate() + { + var publishDate = new DateTime(2023, 6, 1); + var expiryDate = new DateTime(2023, 6, 3); + + systemClockMock.Setup(c => c.Now).Returns(new DateTime(2023, 6, 2)); + + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + ExpiryDate = expiryDate, + PublishDate = publishDate + }; + + Assert.True(frontmatter.IsValidDate(null)); + } + + [Theory] + [InlineData(null, "/path/to/test-title")] + [InlineData("{{ page.Title }}/{{ page.SourceFileNameWithoutExtension }}", "/test-title/file")] + public void ShouldCreatePermalinkWithDefaultOrCustomURLTemplate(string urlTemplate, string expectedPermalink) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + URL = urlTemplate + }; + + var actualPermalink = frontmatter.CreatePermalink(); + + Assert.Equal(expectedPermalink, actualPermalink); + } + + [Theory] + [InlineData("Processed Content", "Theme Base Template", "")] + [InlineData("Content", "", "")] + public void ShouldCreateOutputFileWithAndWithoutThemeTemplates(string content, string baseTemplate, string expected) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock); + string GetTemplateMock(string sourceThemePath, Frontmatter frontmatterArg, Site site, bool isBaseTemplate = false) + { + return isBaseTemplate ? baseTemplate : content; + } + var outputFileContent = frontmatter.CreateOutputFile(); + Assert.Equal(expected, outputFileContent); + } + + [Theory] + [InlineData("Processed Content", "")] + [InlineData("", "")] + public void ShouldCreateContentWithAndWithoutThemeTemplates(string content, string expected) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock); + string GetTemplateMock(string sourceThemePath, Frontmatter frontmatterArg, Site site) + { + return content; + } + Assert.Equal(expected, frontmatter.Content); + } + + [Theory] + [InlineData(-1, true)] + [InlineData(1, false)] + public void IsDateExpired_ShouldReturnExpectedResult(int days, bool expected) + { + systemClockMock.Setup(c => c.Now).Returns(new DateTime(2023, 6, 28)); + + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + ExpiryDate = clock.Now.AddDays(days) + }; + + Assert.Equal(expected, frontmatter.IsDateExpired); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, true)] + public void IsValidDate_ShouldReturnExpectedResult(bool futureOption, bool expected) + { + systemClockMock.Setup(c => c.Now).Returns(new DateTime(2023, 6, 28)); + + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + Date = clock.Now.AddDays(1) + }; + + var options = new Mock(); + options.Setup(o => o.Future).Returns(futureOption); + + Assert.Equal(expected, frontmatter.IsValidDate(options.Object)); + } + + [Theory] + [InlineData("/test/path", "/test/path/test-title")] + [InlineData("/another/path", "/another/path/test-title")] + public void CreatePermalink_ShouldReturnCorrectUrl_WhenUrlIsNull(string sourcePathDirectory, string expectedUrl) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + SourcePathDirectory = sourcePathDirectory + }; + + Assert.Equal(expectedUrl, frontmatter.CreatePermalink()); + } + + [Theory] + [InlineData(Kind.single, true)] + [InlineData(Kind.list, false)] + public void RegularPages_ShouldReturnCorrectPages_WhenKindIsSingle(Kind kind, bool isExpectedPage) + { + var page = new Frontmatter(title, sourcePath, site, clock) { Kind = kind }; + site.PostProcessFrontMatter(page); + + Assert.Equal(isExpectedPage, site.RegularPages.Contains(page)); + } + + [Theory] + [InlineData(null, null, true)] + [InlineData(null, "2024-06-28", false)] + [InlineData("2022-06-28", null, true)] + [InlineData("2024-06-28", "2022-06-28", false)] + [InlineData("2022-06-28", "2024-06-28", true)] + public void IsDatePublishable_ShouldReturnCorrectValues(string? publishDate, string? date, bool expectedValue) + { + var frontmatter = new Frontmatter(title, sourcePath, site, clock) + { + PublishDate = publishDate is null ? null : DateTime.Parse(publishDate, CultureInfo.InvariantCulture), + Date = date is null ? null : DateTime.Parse(date, CultureInfo.InvariantCulture) + }; + + Assert.Equal(expectedValue, frontmatter.IsDatePublishable); + } +} diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs new file mode 100644 index 0000000..9d4ba9c --- /dev/null +++ b/test/Models/SiteTests.cs @@ -0,0 +1,62 @@ +using Xunit; +using Moq; +using SuCoS.Models; +using System.Globalization; + +namespace SuCoS.Tests; + +/// +/// Unit tests for the Site class. +/// +public class SiteTests +{ + private readonly Site site; + private readonly Mock systemClockMock; + readonly string testSite1Path = ".TestSites/01"; + + public SiteTests() + { + systemClockMock = new Mock(); + var testDate = DateTime.Parse("2023-04-01", CultureInfo.InvariantCulture); + systemClockMock.Setup(c => c.Now).Returns(testDate); + site = new Site(systemClockMock.Object); + } + + [Theory] + [InlineData("test01.md", @"--- +Title: Test Content 1 +--- + +Test Content 1 +")] + [InlineData("test02.md", @"--- +Title: Test Content 2 +--- + +Test Content 2 +")] + public void Test_ScanAllMarkdownFiles(string fileName, string fileContent) + { + site.SourceDirectoryPath = testSite1Path; + site.ScanAllMarkdownFiles(); + + Assert.Contains(site.RawPages, rp => rp.filePath == fileName && rp.content == fileContent); + } + + [Theory] + [InlineData("test1", Kind.index, "base", "Test Content 1")] + [InlineData("test2", Kind.single, "content", "Test Content 2")] + public void Test_ResetCache(string firstKeyPart, Kind secondKeyPart, string thirdKeyPart, string value) + { + var key = (firstKeyPart, secondKeyPart, thirdKeyPart); + site.baseTemplateCache.Add(key, value); + site.contentTemplateCache.Add(key, value); + site.PagesDict.Add("test", new Frontmatter("Test Title", "sourcePath", site, systemClockMock.Object)); + + site.ResetCache(); + + Assert.Empty(site.baseTemplateCache); + Assert.Empty(site.contentTemplateCache); + Assert.Empty(site.PagesDict); + } +} -- GitLab From 6d271c901cf9cfb98ae3a74f381e87d6f84fd8a4 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 10:08:52 -0300 Subject: [PATCH 05/11] fix(ci): configuration argument now exposed --- .build.Nuke/Build.Compile.cs | 5 +---- .build.Nuke/Build.Publish.cs | 2 +- .build.Nuke/Build.Solution.cs | 3 ++- .gitlab-ci.yml | 7 ++++--- .nuke/build.schema.json | 4 ---- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.build.Nuke/Build.Compile.cs b/.build.Nuke/Build.Compile.cs index 7822f2d..9489543 100644 --- a/.build.Nuke/Build.Compile.cs +++ b/.build.Nuke/Build.Compile.cs @@ -12,9 +12,6 @@ namespace SuCoS; /// sealed partial class Build : NukeBuild { - [Parameter("output-directory (default: ./output)")] - readonly AbsolutePath outputDirectory = RootDirectory / "output"; - Target Clean => _ => _ .Executes(() => { @@ -40,7 +37,7 @@ sealed partial class Build : NukeBuild DotNetBuild(s => s .SetNoLogo(true) .SetProjectFile(solution) - .SetConfiguration(configuration) + .SetConfiguration(Configuration) .EnableNoRestore() ); }); diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index 13bf9e7..bfbbfbb 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -34,7 +34,7 @@ sealed partial class Build : NukeBuild DotNetPublish(s => s .SetNoLogo(true) .SetProject(solution) - .SetConfiguration(configuration) + .SetConfiguration(Configuration) .SetOutput(PublishDirectory) .SetRuntime(runtimeIdentifier) .SetSelfContained(publishSelfContained) diff --git a/.build.Nuke/Build.Solution.cs b/.build.Nuke/Build.Solution.cs index 40877b6..153a747 100644 --- a/.build.Nuke/Build.Solution.cs +++ b/.build.Nuke/Build.Solution.cs @@ -11,7 +11,8 @@ namespace SuCoS; sealed partial class Build : NukeBuild { [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly Configuration configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + readonly Configuration configuration; + Configuration Configuration => configuration ?? (IsLocalBuild ? Configuration.Debug : Configuration.Release); [Solution] readonly Solution solution; diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f97993f..2d59ac5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,13 +35,14 @@ test: - schedules script: - | - ./build.sh TestReport + ./build.sh TestReport \ + -- configuration "Debug" artifacts: paths: - coverage-results/report/* -# # check if there is new commits, if so, create a tag and a release -# # this will trigger the publish stage "publish" +# check if there is new commits, if so, create a tag and a release +# this will trigger the publish stage "publish" check-and-create-release: <<: *dotnet_nuke_template stage: check-and-create-release diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 882ec5c..870d872 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -63,10 +63,6 @@ "type": "boolean", "description": "Disables displaying the NUKE logo" }, - "outputDirectory": { - "type": "string", - "description": "output-directory (default: ./output)" - }, "packageName": { "type": "string", "description": "package-name (default: SuCoS)" -- GitLab From c657ac215d849cb0f1f88bc615f5b1e3ad1d6576 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 10:44:31 -0300 Subject: [PATCH 06/11] fix(ci): configuration argument now exposed --- .build.Nuke/Build.Compile.cs | 8 +++++++- .build.Nuke/Build.Publish.cs | 2 +- .build.Nuke/Build.Solution.cs | 4 ++-- .build.Nuke/Build.Test.cs | 6 +++--- .nuke/build.schema.json | 6 +----- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.build.Nuke/Build.Compile.cs b/.build.Nuke/Build.Compile.cs index 9489543..b07b145 100644 --- a/.build.Nuke/Build.Compile.cs +++ b/.build.Nuke/Build.Compile.cs @@ -2,6 +2,7 @@ using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.Tools.DotNet; using Nuke.Common.Utilities.Collections; +using Serilog; using static Nuke.Common.Tools.DotNet.DotNetTasks; namespace SuCoS; @@ -18,6 +19,9 @@ sealed partial class Build : NukeBuild sourceDirectory.GlobDirectories("**/bin", "**/obj", "**/output").ForEach( (path) => path.DeleteDirectory() ); + testDirectory.GlobDirectories("**/bin", "**/obj", "**/output").ForEach( + (path) => path.DeleteDirectory() + ); PublishDirectory.DeleteDirectory(); coverageDirectory.DeleteDirectory(); }); @@ -34,10 +38,12 @@ sealed partial class Build : NukeBuild .DependsOn(Restore) .Executes(() => { + Log.Debug("Configuration {Configuration}", configurationSet); + Log.Debug("configuration {configuration}", configuration); DotNetBuild(s => s .SetNoLogo(true) .SetProjectFile(solution) - .SetConfiguration(Configuration) + .SetConfiguration(configurationSet) .EnableNoRestore() ); }); diff --git a/.build.Nuke/Build.Publish.cs b/.build.Nuke/Build.Publish.cs index bfbbfbb..e3836c4 100644 --- a/.build.Nuke/Build.Publish.cs +++ b/.build.Nuke/Build.Publish.cs @@ -34,7 +34,7 @@ sealed partial class Build : NukeBuild DotNetPublish(s => s .SetNoLogo(true) .SetProject(solution) - .SetConfiguration(Configuration) + .SetConfiguration(configurationSet) .SetOutput(PublishDirectory) .SetRuntime(runtimeIdentifier) .SetSelfContained(publishSelfContained) diff --git a/.build.Nuke/Build.Solution.cs b/.build.Nuke/Build.Solution.cs index 153a747..2612b15 100644 --- a/.build.Nuke/Build.Solution.cs +++ b/.build.Nuke/Build.Solution.cs @@ -11,8 +11,8 @@ namespace SuCoS; sealed partial class Build : NukeBuild { [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly Configuration configuration; - Configuration Configuration => configuration ?? (IsLocalBuild ? Configuration.Debug : Configuration.Release); + readonly string configuration; + string configurationSet => configuration ?? (IsLocalBuild ? Configuration.Debug : Configuration.Release); [Solution] readonly Solution solution; diff --git a/.build.Nuke/Build.Test.cs b/.build.Nuke/Build.Test.cs index 2d367c9..dccde7f 100644 --- a/.build.Nuke/Build.Test.cs +++ b/.build.Nuke/Build.Test.cs @@ -16,8 +16,8 @@ namespace SuCoS; /// sealed partial class Build : NukeBuild { - AbsolutePath TestDLL => TestDirectory / "bin" / "Debug" / "net7.0"; - AbsolutePath TestDirectory => RootDirectory / "test"; + AbsolutePath TestDLL => testDirectory / "bin" / "Debug" / "net7.0"; + AbsolutePath testDirectory => RootDirectory / "test"; AbsolutePath TestSiteDirectory => RootDirectory / "test" / ".TestSites"; AbsolutePath TestOutputDirectory => TestDLL / ".TestSites"; AbsolutePath coverageDirectory => RootDirectory / "coverage-results"; @@ -26,6 +26,7 @@ sealed partial class Build : NukeBuild AbsolutePath CoverageResultFile => CoverageResultDirectory / "coverage.cobertura.xml"; Target PrepareTestFiles => _ => _ + .After(Clean) .Executes(() => { TestOutputDirectory.CreateOrCleanDirectory(); @@ -36,7 +37,6 @@ sealed partial class Build : NukeBuild .DependsOn(Compile, PrepareTestFiles) .Executes(() => { - CoverageResultDirectory.CreateDirectory(); Coverlet(s => s .SetTarget("dotnet") diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 870d872..7d2aca0 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -8,11 +8,7 @@ "properties": { "configuration": { "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "k__BackingField", - "k__BackingField" - ] + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)" }, "containerDefaultRID": { "type": "string", -- GitLab From 39192a6de9ced3f02fa6fd668de93cfef4a2903a Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 10:45:08 -0300 Subject: [PATCH 07/11] test: YAMLParser --- source/Helpers/FileUtils.cs | 5 +- source/Models/Site.cs | 5 ++ source/Parser/YAMLParser.cs | 22 ++++- test/Parser/YAMLParserTests.cs | 154 +++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 test/Parser/YAMLParserTests.cs diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 87cf1c5..584cc48 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -19,10 +19,7 @@ public static class FileUtils { var markdownFiles = new List(); var files = Directory.GetFiles(directory, "*.md"); - if (files.Length > 0) - { - markdownFiles.AddRange(files); - } + markdownFiles.AddRange(files); var subdirectories = Directory.GetDirectories(directory); foreach (var subdirectory in subdirectories) diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 35452ba..154dde8 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -371,6 +371,11 @@ public class Site : IParams /// public void PostProcessFrontMatter(Frontmatter frontmatter, bool overwrite = false) { + if (frontmatter is null) + { + throw new ArgumentNullException(nameof(frontmatter)); + } + frontmatter.Permalink = frontmatter.CreatePermalink(); lock (syncLockPostProcess) { diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index b2576cf..17992ac 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -123,6 +123,10 @@ public partial class YAMLParser : IFrontmatterParser _ = site.CreateAutomaticFrontmatter(contentTemplate, frontmatter); } } + else + { + throw new YamlDotNet.Core.YamlException("Frontmatter yaml parsing failed"); + } } if (frontmatter is not null) { @@ -133,7 +137,12 @@ public partial class YAMLParser : IFrontmatterParser return null; } - private static string GetSection(string filePath) + /// + /// Get the section name from a file path + /// + /// + /// + public static string GetSection(string filePath) { // Split the path into individual folders var folders = filePath?.Split(Path.DirectorySeparatorChar); @@ -156,8 +165,17 @@ public partial class YAMLParser : IFrontmatterParser /// Site or Frontmatter object, that implements IParams /// The type (Site or Frontmatter) /// YAML content - void ParseParams(IParams settings, Type type, string content) + public void ParseParams(IParams settings, Type type, string content) { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + var yamlObject = yamlDeserializer.Deserialize(new StringReader(content)); if (yamlObject is Dictionary yamlDictionary) { diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs new file mode 100644 index 0000000..34c72d2 --- /dev/null +++ b/test/Parser/YAMLParserTests.cs @@ -0,0 +1,154 @@ +using Xunit; +using Moq; +using SuCoS.Models; +using SuCoS.Parser; + +namespace SuCoS.Tests; + +public class YAMLParserTests +{ + private readonly YAMLParser _parser; + private readonly Mock _mockSite; + + public YAMLParserTests() + { + _parser = new YAMLParser(); + _mockSite = new Mock(); + } + + [Theory] + [InlineData(@"--- +Title: Test Title +--- +", "Test Title")] + [InlineData(@"--- +--- +", null)] + public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) + { + var filePath = "test.md"; + var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + + Assert.Equal(expectedTitle, frontmatter?.Title); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowException_WhenSiteIsNull() + { + var fileContent = @"--- +Title: Test Title +--- +"; + + Assert.Throws(() => _parser.ParseFrontmatter(null!, "test.md", ref fileContent)); + } + + [Fact] + public void GetSection_ShouldReturnFirstFolderName() + { + var filePath = Path.Combine("folder1", "folder2", "file.md"); + + var section = YAMLParser.GetSection(filePath); + + Assert.Equal("folder1", section); + } + + [Theory] + [InlineData(@"--- +Date: 2023-01-01 +--- +", "2023-01-01")] + [InlineData(@"--- +Date: 2023/01/01 +--- +", "2023-01-01")] + public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) + { + var filePath = "test.md"; + var expectedDate = DateTime.Parse(expectedDateString); + + var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + + Assert.Equal(expectedDate, frontmatter?.Date); + } + + [Fact] + public void ParseFrontmatter_ShouldParseOtherFieldsCorrectly() + { + var filePath = "test.md"; + var fileContent = @"--- +Title: Test Title +Type: post +Date: 2023-01-01 +LastMod: 2023-06-01 +PublishDate: 2023-06-01 +ExpiryDate: 2024-06-01 +--- +"; + var expectedDate = DateTime.Parse("2023-01-01"); + var expectedLastMod = DateTime.Parse("2023-06-01"); + var expectedPublishDate = DateTime.Parse("2023-06-01"); + var expectedExpiryDate = DateTime.Parse("2024-06-01"); + + var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + + Assert.Equal("Test Title", frontmatter?.Title); + Assert.Equal("post", frontmatter?.Type); + Assert.Equal(expectedDate, frontmatter?.Date); + Assert.Equal(expectedLastMod, frontmatter?.LastMod); + Assert.Equal(expectedPublishDate, frontmatter?.PublishDate); + Assert.Equal(expectedExpiryDate, frontmatter?.ExpiryDate); + } + + [Fact] + public void ParseFrontmatter_ShouldThrowException_WhenInvalidYAMLSyntax() + { + var fileContent = @"--- +Title +--- +"; + var filePath = "test.md"; + + Assert.Throws(() => _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent)); + } + + [Fact] + public void ParseSiteSettings_ShouldReturnSiteWithCorrectSettings() + { + var siteContent = @" +BaseUrl: https://www.example.com/ +Title: My Site +"; + + var siteSettings = _parser.ParseSiteSettings(siteContent); + + Assert.Equal("https://www.example.com/", siteSettings.BaseUrl); + Assert.Equal("My Site", siteSettings.Title); + } + + [Fact] + public void ParseParams_ShouldFillParamsWithNonMatchingFields() + { + var settings = new Frontmatter("Test Title", "/test.md", _mockSite.Object, new SystemClock()); + var content = @" +Title: Test Title +customParam: Custom Value +"; + + _parser.ParseParams(settings, typeof(Frontmatter), content); + + Assert.True(settings.Params.ContainsKey("customParam")); + Assert.Equal("Custom Value", settings.Params["customParam"]); + } + + [Fact] + public void ParseFrontmatter_ShouldReturnNull_WhenNoFrontmatter() + { + var fileContent = "There is no frontmatter in this content."; + var filePath = "test.md"; + + var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + + Assert.Null(frontmatter); + } +} -- GitLab From 1bc57acebf5ac4aac67eb79cead877dda812c91a Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 10:53:46 -0300 Subject: [PATCH 08/11] refactor: namespace changes --- source/BaseGeneratorCommand.cs | 1 + source/Helpers/FileUtils.cs | 2 +- source/Helpers/SiteHelper.cs | 2 +- source/Helpers/StopwatchReporter.cs | 2 +- source/Helpers/Urlizer.cs | 2 +- source/Models/Frontmatter.cs | 4 ++-- source/{Helpers => Models}/ISystemClock.cs | 0 source/Models/Site.cs | 1 + source/Parser/YAMLParser.cs | 1 + source/ServeCommand.cs | 2 ++ test/Helpers/UrlizerTests.cs | 2 +- 11 files changed, 12 insertions(+), 7 deletions(-) rename source/{Helpers => Models}/ISystemClock.cs (100%) diff --git a/source/BaseGeneratorCommand.cs b/source/BaseGeneratorCommand.cs index d77179f..0231d33 100644 --- a/source/BaseGeneratorCommand.cs +++ b/source/BaseGeneratorCommand.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Fluid; using Fluid.Values; using Serilog; +using SuCoS.Helper; using SuCoS.Models; using SuCoS.Parser; diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 584cc48..b17034e 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using SuCoS.Models; -namespace SuCoS; +namespace SuCoS.Helper; /// /// Helper methods for scanning files. diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 0acb7f5..7c860f3 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -6,7 +6,7 @@ using Serilog; using SuCoS.Models; using SuCoS.Parser; -namespace SuCoS; +namespace SuCoS.Helper; /// /// Helper methods for scanning files. diff --git a/source/Helpers/StopwatchReporter.cs b/source/Helpers/StopwatchReporter.cs index cf8357a..5a691f7 100644 --- a/source/Helpers/StopwatchReporter.cs +++ b/source/Helpers/StopwatchReporter.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.Linq; using Serilog; -namespace SuCoS; +namespace SuCoS.Helper; /// /// This class is used to report the time taken to execute diff --git a/source/Helpers/Urlizer.cs b/source/Helpers/Urlizer.cs index 1830285..715e0c0 100644 --- a/source/Helpers/Urlizer.cs +++ b/source/Helpers/Urlizer.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; -namespace SuCoS; +namespace SuCoS.Helper; /// /// Helper class to convert a string to a URL-friendly string. diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 0fa2d97..bf5a41a 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -5,9 +5,9 @@ using System.IO; using System.Linq; using Fluid; using Markdig; -using SuCoS.Models; +using SuCoS.Helper; -namespace SuCoS; +namespace SuCoS.Models; /// /// The meta data about each content Markdown file. diff --git a/source/Helpers/ISystemClock.cs b/source/Models/ISystemClock.cs similarity index 100% rename from source/Helpers/ISystemClock.cs rename to source/Models/ISystemClock.cs diff --git a/source/Models/Site.cs b/source/Models/Site.cs index 154dde8..cdcf70f 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Fluid; using Markdig; using Serilog; +using SuCoS.Helper; using SuCoS.Parser; namespace SuCoS.Models; diff --git a/source/Parser/YAMLParser.cs b/source/Parser/YAMLParser.cs index 17992ac..7ec42e1 100644 --- a/source/Parser/YAMLParser.cs +++ b/source/Parser/YAMLParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using SuCoS.Helper; using SuCoS.Models; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 8d7317f..f3043e7 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Serilog; +using SuCoS.Helper; +using SuCoS.Models; namespace SuCoS; diff --git a/test/Helpers/UrlizerTests.cs b/test/Helpers/UrlizerTests.cs index 1d0c7f1..09a1c9d 100644 --- a/test/Helpers/UrlizerTests.cs +++ b/test/Helpers/UrlizerTests.cs @@ -1,5 +1,5 @@ using Xunit; -using SuCoS; +using SuCoS.Helper; namespace SuCoSTests; -- GitLab From b9da0138d549b4f229662ff154dcadf068d69263 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 10:54:56 -0300 Subject: [PATCH 09/11] fix(ci): configuration argument now exposed --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2d59ac5..d8c2684 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,7 +36,7 @@ test: script: - | ./build.sh TestReport \ - -- configuration "Debug" + --configuration "Debug" artifacts: paths: - coverage-results/report/* -- GitLab From 6fe59323bb13be8585726050edc115f1b25ed6ad Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 12:36:46 -0300 Subject: [PATCH 10/11] test: globalization issues on tests --- test/BaseGeneratorCommandTests.cs | 7 ------- test/Parser/YAMLParserTests.cs | 11 ++++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/test/BaseGeneratorCommandTests.cs b/test/BaseGeneratorCommandTests.cs index f0c5616..35e51e9 100644 --- a/test/BaseGeneratorCommandTests.cs +++ b/test/BaseGeneratorCommandTests.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using System.Reflection; using Fluid; using Fluid.Values; -using Moq; using Serilog; -using SuCoS; -using SuCoS.Models; -using SuCoS.Parser; using Xunit; namespace SuCoS.Tests; diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index 34c72d2..d83bb94 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -2,6 +2,7 @@ using Xunit; using Moq; using SuCoS.Models; using SuCoS.Parser; +using System.Globalization; namespace SuCoS.Tests; @@ -65,7 +66,7 @@ Date: 2023/01/01 public void ParseFrontmatter_ShouldParseDateCorrectly(string fileContent, string expectedDateString) { var filePath = "test.md"; - var expectedDate = DateTime.Parse(expectedDateString); + var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); @@ -85,10 +86,10 @@ PublishDate: 2023-06-01 ExpiryDate: 2024-06-01 --- "; - var expectedDate = DateTime.Parse("2023-01-01"); - var expectedLastMod = DateTime.Parse("2023-06-01"); - var expectedPublishDate = DateTime.Parse("2023-06-01"); - var expectedExpiryDate = DateTime.Parse("2024-06-01"); + var expectedDate = DateTime.Parse("2023-01-01", CultureInfo.InvariantCulture); + var expectedLastMod = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); + var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); -- GitLab From 3e1cefb214174edee8cc93ea7a48c94e1a243d79 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 28 Jun 2023 13:18:14 -0300 Subject: [PATCH 11/11] fix: naming and code standard --- test/BaseGeneratorCommandTests.cs | 10 ++++------ test/Models/FrontmatterTests.cs | 27 --------------------------- test/Parser/YAMLParserTests.cs | 26 +++++++++++++------------- 3 files changed, 17 insertions(+), 46 deletions(-) diff --git a/test/BaseGeneratorCommandTests.cs b/test/BaseGeneratorCommandTests.cs index 35e51e9..e65e419 100644 --- a/test/BaseGeneratorCommandTests.cs +++ b/test/BaseGeneratorCommandTests.cs @@ -8,12 +8,12 @@ namespace SuCoS.Tests; public class BaseGeneratorCommandTests { - private static readonly IGenerateOptions TestOptions = new BuildOptions + private static readonly IGenerateOptions testOptions = new BuildOptions { Source = "test_source" }; - private static readonly ILogger TestLogger = new LoggerConfiguration().CreateLogger(); + private static readonly ILogger testLogger = new LoggerConfiguration().CreateLogger(); private class BaseGeneratorCommandStub : BaseGeneratorCommand { @@ -24,13 +24,13 @@ public class BaseGeneratorCommandTests [Fact] public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() { - Assert.Throws(() => new BaseGeneratorCommandStub(null!, TestLogger)); + Assert.Throws(() => new BaseGeneratorCommandStub(null!, testLogger)); } [Fact] public void Constructor_ShouldThrowArgumentNullException_WhenLoggerIsNull() { - Assert.Throws(() => new BaseGeneratorCommandStub(TestOptions, null!)); + Assert.Throws(() => new BaseGeneratorCommandStub(testOptions, null!)); } [Fact] @@ -47,8 +47,6 @@ public class BaseGeneratorCommandTests () => BaseGeneratorCommand.WhereParamsFilter(new ArrayValue(new FluidValue[0]), null!, new TemplateContext()).AsTask()); } - // More tests for WhereParamsFilter with different inputs and expected outcomes - [Fact] public void CheckValueInDictionary_ShouldWorkCorrectly() { diff --git a/test/Models/FrontmatterTests.cs b/test/Models/FrontmatterTests.cs index 6ca919e..89f6f8d 100644 --- a/test/Models/FrontmatterTests.cs +++ b/test/Models/FrontmatterTests.cs @@ -96,33 +96,6 @@ public class FrontmatterTests Assert.Equal(expectedPermalink, actualPermalink); } - [Theory] - [InlineData("Processed Content", "Theme Base Template", "")] - [InlineData("Content", "", "")] - public void ShouldCreateOutputFileWithAndWithoutThemeTemplates(string content, string baseTemplate, string expected) - { - var frontmatter = new Frontmatter(title, sourcePath, site, clock); - string GetTemplateMock(string sourceThemePath, Frontmatter frontmatterArg, Site site, bool isBaseTemplate = false) - { - return isBaseTemplate ? baseTemplate : content; - } - var outputFileContent = frontmatter.CreateOutputFile(); - Assert.Equal(expected, outputFileContent); - } - - [Theory] - [InlineData("Processed Content", "")] - [InlineData("", "")] - public void ShouldCreateContentWithAndWithoutThemeTemplates(string content, string expected) - { - var frontmatter = new Frontmatter(title, sourcePath, site, clock); - string GetTemplateMock(string sourceThemePath, Frontmatter frontmatterArg, Site site) - { - return content; - } - Assert.Equal(expected, frontmatter.Content); - } - [Theory] [InlineData(-1, true)] [InlineData(1, false)] diff --git a/test/Parser/YAMLParserTests.cs b/test/Parser/YAMLParserTests.cs index d83bb94..a2e3551 100644 --- a/test/Parser/YAMLParserTests.cs +++ b/test/Parser/YAMLParserTests.cs @@ -8,13 +8,13 @@ namespace SuCoS.Tests; public class YAMLParserTests { - private readonly YAMLParser _parser; - private readonly Mock _mockSite; + private readonly YAMLParser parser; + private readonly Mock mockSite; public YAMLParserTests() { - _parser = new YAMLParser(); - _mockSite = new Mock(); + parser = new YAMLParser(); + mockSite = new Mock(); } [Theory] @@ -28,7 +28,7 @@ Title: Test Title public void ParseFrontmatter_ShouldParseTitleCorrectly(string fileContent, string expectedTitle) { var filePath = "test.md"; - var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + var frontmatter = parser.ParseFrontmatter(mockSite.Object, filePath, ref fileContent); Assert.Equal(expectedTitle, frontmatter?.Title); } @@ -41,7 +41,7 @@ Title: Test Title --- "; - Assert.Throws(() => _parser.ParseFrontmatter(null!, "test.md", ref fileContent)); + Assert.Throws(() => parser.ParseFrontmatter(null!, "test.md", ref fileContent)); } [Fact] @@ -68,7 +68,7 @@ Date: 2023/01/01 var filePath = "test.md"; var expectedDate = DateTime.Parse(expectedDateString, CultureInfo.InvariantCulture); - var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + var frontmatter = parser.ParseFrontmatter(mockSite.Object, filePath, ref fileContent); Assert.Equal(expectedDate, frontmatter?.Date); } @@ -91,7 +91,7 @@ ExpiryDate: 2024-06-01 var expectedPublishDate = DateTime.Parse("2023-06-01", CultureInfo.InvariantCulture); var expectedExpiryDate = DateTime.Parse("2024-06-01", CultureInfo.InvariantCulture); - var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + var frontmatter = parser.ParseFrontmatter(mockSite.Object, filePath, ref fileContent); Assert.Equal("Test Title", frontmatter?.Title); Assert.Equal("post", frontmatter?.Type); @@ -110,7 +110,7 @@ Title "; var filePath = "test.md"; - Assert.Throws(() => _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent)); + Assert.Throws(() => parser.ParseFrontmatter(mockSite.Object, filePath, ref fileContent)); } [Fact] @@ -121,7 +121,7 @@ BaseUrl: https://www.example.com/ Title: My Site "; - var siteSettings = _parser.ParseSiteSettings(siteContent); + var siteSettings = parser.ParseSiteSettings(siteContent); Assert.Equal("https://www.example.com/", siteSettings.BaseUrl); Assert.Equal("My Site", siteSettings.Title); @@ -130,13 +130,13 @@ Title: My Site [Fact] public void ParseParams_ShouldFillParamsWithNonMatchingFields() { - var settings = new Frontmatter("Test Title", "/test.md", _mockSite.Object, new SystemClock()); + var settings = new Frontmatter("Test Title", "/test.md", mockSite.Object, new SystemClock()); var content = @" Title: Test Title customParam: Custom Value "; - _parser.ParseParams(settings, typeof(Frontmatter), content); + parser.ParseParams(settings, typeof(Frontmatter), content); Assert.True(settings.Params.ContainsKey("customParam")); Assert.Equal("Custom Value", settings.Params["customParam"]); @@ -148,7 +148,7 @@ customParam: Custom Value var fileContent = "There is no frontmatter in this content."; var filePath = "test.md"; - var frontmatter = _parser.ParseFrontmatter(_mockSite.Object, filePath, ref fileContent); + var frontmatter = parser.ParseFrontmatter(mockSite.Object, filePath, ref fileContent); Assert.Null(frontmatter); } -- GitLab