Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ implementation demonstrating best practices for DEMA Consulting .NET CLI tools.

## Tech Stack

- C# 12, .NET 8.0/9.0/10.0, dotnet CLI, NuGet
- C# (latest), .NET 8.0/9.0/10.0, dotnet CLI, NuGet

## Key Files

Expand Down
3 changes: 3 additions & 0 deletions docs/guide/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Save validation results to a file:
templatetool --validate --results results.trx
```

The results file format is determined by the file extension: `.trx` for TRX (MSTest) format,
or `.xml` for JUnit format.

## Silent Mode

Suppress console output:
Expand Down
55 changes: 43 additions & 12 deletions requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sections:
sections:
- title: Command-Line Interface
requirements:
- id: TMPL-REQ-001
- id: TMPL-CMD-001
title: The tool shall implement a Context class for command-line argument handling.
justification: |
Provides a standardized approach to command-line argument parsing and output
Expand All @@ -46,7 +46,7 @@ sections:
- Context_Create_ResultsFlag_SetsResultsFile
- Context_Create_LogFlag_OpensLogFile

- id: TMPL-REQ-002
- id: TMPL-CMD-002
title: The tool shall support -v and --version flags to display version information.
justification: |
Users need to quickly identify the version of the tool they are using for
Expand All @@ -55,9 +55,10 @@ sections:
- Context_Create_VersionFlag_SetsVersionTrue
- Context_Create_ShortVersionFlag_SetsVersionTrue
- Program_Run_WithVersionFlag_DisplaysVersionOnly
- Program_Version_ReturnsNonEmptyString
- IntegrationTest_VersionFlag_OutputsVersion

- id: TMPL-REQ-003
- id: TMPL-CMD-003
title: The tool shall support -?, -h, and --help flags to display usage information.
justification: |
Users need access to command-line usage documentation without requiring
Expand All @@ -69,7 +70,7 @@ sections:
- Program_Run_WithHelpFlag_DisplaysUsageInformation
- IntegrationTest_HelpFlag_OutputsUsageInformation

- id: TMPL-REQ-004
- id: TMPL-CMD-004
title: The tool shall support --silent flag to suppress console output.
justification: |
Enables automated scripts and CI/CD pipelines to run the tool without
Expand All @@ -79,7 +80,7 @@ sections:
- Context_WriteLine_Silent_DoesNotWriteToConsole
- IntegrationTest_SilentFlag_SuppressesOutput

- id: TMPL-REQ-005
- id: TMPL-CMD-005
title: The tool shall support --validate flag to run self-validation tests.
justification: |
Provides a built-in mechanism to verify the tool is functioning correctly
Expand All @@ -89,25 +90,55 @@ sections:
- Program_Run_WithValidateFlag_RunsValidation
- IntegrationTest_ValidateFlag_RunsValidation

- id: TMPL-REQ-006
- id: TMPL-CMD-006
title: The tool shall support --results flag to write validation results in TRX or JUnit format.
justification: |
Enables integration with CI/CD systems that expect standard test result formats.
tests:
- Context_Create_ResultsFlag_SetsResultsFile
- IntegrationTest_ValidateWithResults_GeneratesTrxFile
- IntegrationTest_ValidateWithResults_GeneratesJUnitFile

- id: TMPL-REQ-007
- id: TMPL-CMD-007
title: The tool shall support --log flag to write output to a log file.
justification: |
Provides persistent logging for debugging and audit trails.
tests:
- Context_Create_LogFlag_OpensLogFile
- IntegrationTest_LogFlag_WritesOutputToFile

- id: TMPL-CMD-008
title: The tool shall write error messages to stderr.
justification: |
Error messages must be written to stderr so they remain visible to the user
without polluting stdout, which consumers may pipe or redirect for data capture.
tests:
- Context_WriteError_NotSilent_WritesToConsole
- IntegrationTest_UnknownArgument_ReturnsError

- id: TMPL-CMD-009
title: The tool shall reject unknown or malformed command-line arguments with a descriptive error.
justification: |
Providing clear feedback for invalid arguments helps users quickly correct
mistakes and prevents silent misconfiguration.
tests:
- Context_Create_UnknownArgument_ThrowsArgumentException
- Context_Create_LogFlag_WithoutValue_ThrowsArgumentException
- Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException
- IntegrationTest_UnknownArgument_ReturnsError

- id: TMPL-CMD-010
title: The tool shall return a non-zero exit code on failure.
justification: |
Callers (scripts, CI/CD pipelines) must be able to detect failure conditions
programmatically via the process exit code.
tests:
- Context_WriteError_SetsErrorExitCode
- IntegrationTest_UnknownArgument_ReturnsError

- title: Platform Support
requirements:
- id: TMPL-REQ-008
- id: TMPL-PLT-001
title: The tool shall build and run on Windows platforms.
justification: |
DEMA Consulting tools must support Windows as a major development platform.
Expand All @@ -116,7 +147,7 @@ sections:
- "windows@TemplateTool_VersionDisplay"
- "windows@TemplateTool_HelpDisplay"

- id: TMPL-REQ-009
- id: TMPL-PLT-002
title: The tool shall build and run on Linux platforms.
justification: |
DEMA Consulting tools must support Linux for CI/CD and containerized environments.
Expand All @@ -125,23 +156,23 @@ sections:
- "ubuntu@TemplateTool_VersionDisplay"
- "ubuntu@TemplateTool_HelpDisplay"

- id: TMPL-REQ-010
- id: TMPL-PLT-003
title: The tool shall support .NET 8 runtime.
justification: |
.NET 8 is an LTS release providing long-term stability for enterprise users.
tests:
- "dotnet8.x@TemplateTool_VersionDisplay"
- "dotnet8.x@TemplateTool_HelpDisplay"

- id: TMPL-REQ-011
- id: TMPL-PLT-004
title: The tool shall support .NET 9 runtime.
justification: |
.NET 9 support enables users to leverage the latest .NET features.
tests:
- "dotnet9.x@TemplateTool_VersionDisplay"
- "dotnet9.x@TemplateTool_HelpDisplay"

- id: TMPL-REQ-012
- id: TMPL-PLT-005
title: The tool shall support .NET 10 runtime.
justification: |
.NET 10 support ensures the tool remains compatible with the latest .NET ecosystem.
Expand Down
6 changes: 4 additions & 2 deletions src/DemaConsulting.TemplateDotNetTool/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ private void OpenLogFile(string logFile)
{
try
{
_logWriter = new StreamWriter(logFile, append: false);
// Open with AutoFlush enabled so log entries are immediately written to disk
// even if the application terminates unexpectedly before Dispose is called
_logWriter = new StreamWriter(logFile, append: false) { AutoFlush = true };
}
// Generic catch is justified here to wrap any file system exception with context.
// Expected exceptions include IOException, UnauthorizedAccessException, ArgumentException,
Expand Down Expand Up @@ -267,7 +269,7 @@ public void WriteError(string message)
{
var previousColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.Error.WriteLine(message);
Console.ForegroundColor = previousColor;
}

Expand Down
6 changes: 3 additions & 3 deletions src/DemaConsulting.TemplateDotNetTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ private static int Main(string[] args)
catch (ArgumentException ex)
{
// Print expected argument exceptions and return error code
Console.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
catch (InvalidOperationException ex)
{
// Print expected operation exceptions and return error code
Console.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
catch (Exception ex)
{
// Print unexpected exceptions and re-throw to generate event logs
Console.WriteLine($"Unexpected error: {ex.Message}");
Console.Error.WriteLine($"Unexpected error: {ex.Message}");
throw;
}
}
Expand Down
134 changes: 134 additions & 0 deletions test/DemaConsulting.TemplateDotNetTool.Tests/ContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,28 @@ public void Context_Create_UnknownArgument_ThrowsArgumentException()
Assert.Contains("Unsupported argument", exception.Message);
}

/// <summary>
/// Test creating a context with --log flag but no value throws exception.
/// </summary>
[TestMethod]
public void Context_Create_LogFlag_WithoutValue_ThrowsArgumentException()
{
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => Context.Create(["--log"]));
Assert.Contains("--log", exception.Message);
}

/// <summary>
/// Test creating a context with --results flag but no value throws exception.
/// </summary>
[TestMethod]
public void Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException()
{
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => Context.Create(["--results"]));
Assert.Contains("--results", exception.Message);
}

/// <summary>
/// Test WriteLine writes to console output when not silent.
/// </summary>
Expand Down Expand Up @@ -256,4 +278,116 @@ public void Context_WriteLine_Silent_DoesNotWriteToConsole()
Console.SetOut(originalOut);
}
}

/// <summary>
/// Test WriteError does not write to console when silent.
/// </summary>
[TestMethod]
public void Context_WriteError_Silent_DoesNotWriteToConsole()
{
// Arrange
var originalError = Console.Error;
try
{
using var errWriter = new StringWriter();
Console.SetError(errWriter);
using var context = Context.Create(["--silent"]);

// Act
context.WriteError("Test error message");

// Assert - error output should be suppressed in silent mode
var output = errWriter.ToString();
Assert.DoesNotContain("Test error message", output);
}
finally
{
Console.SetError(originalError);
}
}

/// <summary>
/// Test WriteError sets exit code to 1.
/// </summary>
[TestMethod]
public void Context_WriteError_SetsErrorExitCode()
{
// Arrange
var originalError = Console.Error;
try
{
using var errWriter = new StringWriter();
Console.SetError(errWriter);
using var context = Context.Create([]);

// Act
context.WriteError("Test error message");

// Assert
Assert.AreEqual(1, context.ExitCode);
}
finally
{
Console.SetError(originalError);
}
}

/// <summary>
/// Test WriteError writes message to console when not silent.
/// </summary>
[TestMethod]
public void Context_WriteError_NotSilent_WritesToConsole()
{
// Arrange
var originalError = Console.Error;
try
{
using var errWriter = new StringWriter();
Console.SetError(errWriter);
using var context = Context.Create([]);

// Act
context.WriteError("Test error message");

// Assert
var output = errWriter.ToString();
Assert.Contains("Test error message", output);
}
finally
{
Console.SetError(originalError);
}
}

/// <summary>
/// Test WriteError writes message to log file when logging is enabled.
/// </summary>
[TestMethod]
public void Context_WriteError_WritesToLogFile()
{
// Arrange
var logFile = Path.GetTempFileName();
try
{
// Act - use silent to avoid console output; verify the error still goes to the log
using (var context = Context.Create(["--silent", "--log", logFile]))
{
context.WriteError("Test error in log");
Assert.AreEqual(1, context.ExitCode);
}

// Assert - log file should contain the error message
Assert.IsTrue(File.Exists(logFile));
var logContent = File.ReadAllText(logFile);
Assert.Contains("Test error in log", logContent);
}
finally
{
if (File.Exists(logFile))
{
File.Delete(logFile);
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<!-- Build Configuration -->
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down
Loading