Files
UA-.NETStandard/tests/Opc.Ua.SourceGeneration.Core.Tests/Templating/ParsedTemplateStringTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

3181 lines
117 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
namespace Opc.Ua.SourceGeneration.Templating.Tests
{
/// <summary>
/// Tests for the ParsedTemplateString.ToString method
/// </summary>
[TestFixture]
[Category("Templating")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public class ParsedTemplateStringTests
{
/// <summary>
/// Tests ToString with null format provider.
/// Verifies that null format provider is handled correctly (uses current culture).
/// </summary>
[Test]
public void ToString_NullFormatProvider_UsesCurrentCulture()
{
// Arrange
var parsed = new ParsedTemplateString(6, 1);
parsed.AddLiteral("Hello ");
parsed.AddFormatted("World", typeof(string));
// Act
string result = parsed.ToString(null!);
// Assert
Assert.That(result, Is.EqualTo("Hello World"));
}
/// <summary>
/// Tests ToString with an empty ParsedTemplateString.
/// Verifies that an empty string is returned when there are no operations.
/// </summary>
[Test]
public void ToString_EmptyParsedTemplateString_ReturnsEmptyString()
{
// Arrange
var parsed = new ParsedTemplateString(0, 0);
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests ToString with only literal content and no formatted values.
/// Verifies that literals are returned as-is without any formatting.
/// </summary>
[Test]
public void ToString_OnlyLiterals_ReturnsLiteralText()
{
// Arrange
var parsed = new ParsedTemplateString(11, 0);
parsed.AddLiteral("Hello World");
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("Hello World"));
}
/// <summary>
/// Tests ToString with multiple formatted values.
/// Verifies that multiple placeholders are correctly replaced with arguments.
/// </summary>
[Test]
public void ToString_MultipleFormattedValues_FormatsAllCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(10, 3);
parsed.AddFormatted("First", typeof(string));
parsed.AddLiteral(" and ");
parsed.AddFormatted("Second", typeof(string));
parsed.AddLiteral(" and ");
parsed.AddFormatted("Third", typeof(string));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("First and Second and Third"));
}
/// <summary>
/// Tests ToString with culture-specific number formatting.
/// Verifies that numeric values are formatted according to the provided culture.
/// </summary>
[Test]
public void ToString_WithCultureSpecificNumberFormatting_FormatsAccordingToCulture()
{
// Arrange - US culture uses period as decimal separator
var parsed = new ParsedTemplateString(7, 1);
parsed.AddLiteral("Value: ");
parsed.AddFormatted("1234.56", typeof(double));
var usCulture = CultureInfo.GetCultureInfo("en-US");
// Act
string result = parsed.ToString(usCulture);
// Assert
Assert.That(result, Is.EqualTo("Value: 1234.56"));
}
/// <summary>
/// Tests ToString with numeric value formatting using invariant culture.
/// Verifies that numeric arguments are formatted correctly with InvariantCulture.
/// </summary>
[Test]
public void ToString_WithNumericValue_FormatsCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(9, 1);
parsed.AddLiteral("Number: ");
parsed.AddFormatted("42", typeof(int));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("Number: 42"));
}
/// <summary>
/// Tests ToString with whitespace and line breaks.
/// Verifies that whitespace and line breaks are preserved in the output.
/// </summary>
[Test]
public void ToString_WithWhitespaceAndLineBreaks_PreservesFormatting()
{
// Arrange
var parsed = new ParsedTemplateString(20, 1);
parsed.AddLiteral("Line1\n");
parsed.AddFormatted("Value", typeof(string));
parsed.AddLiteral("\nLine3");
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Does.Contain("Line1"));
Assert.That(result, Does.Contain("Value"));
Assert.That(result, Does.Contain("Line3"));
}
/// <summary>
/// Tests ToString with only formatted values and no literals.
/// Verifies that formatted values alone are correctly formatted.
/// </summary>
[Test]
public void ToString_OnlyFormattedValues_FormatsCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 2);
parsed.AddFormatted("First", typeof(string));
parsed.AddFormatted("Second", typeof(string));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("FirstSecond"));
}
/// <summary>
/// Tests ToString with special characters in literals.
/// Verifies that special characters are preserved correctly.
/// </summary>
[Test]
public void ToString_WithSpecialCharactersInLiterals_PreservesCharacters()
{
// Arrange
var parsed = new ParsedTemplateString(20, 1);
parsed.AddLiteral("Special: @#$%^& ");
parsed.AddFormatted("Value", typeof(string));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("Special: @#$%^& Value"));
}
/// <summary>
/// Tests ToString with empty string as formatted value.
/// Verifies that empty formatted values are handled correctly.
/// </summary>
[Test]
public void ToString_WithEmptyFormattedValue_HandlesCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(6, 1);
parsed.AddLiteral("Value:");
parsed.AddFormatted(string.Empty, typeof(string));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("Value:"));
}
/// <summary>
/// Tests ToString with mixed string and non-string types.
/// Verifies that different value types are handled correctly.
/// </summary>
[Test]
public void ToString_WithMixedTypes_FormatsAllCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(20, 3);
parsed.AddLiteral("String: ");
parsed.AddFormatted("text", typeof(string));
parsed.AddLiteral(", Number: ");
parsed.AddFormatted("123", typeof(int));
parsed.AddLiteral(", Bool: ");
parsed.AddFormatted("True", typeof(bool));
// Act
string result = parsed.ToString(CultureInfo.InvariantCulture);
// Assert
Assert.That(result, Is.EqualTo("String: text, Number: 123, Bool: True"));
}
/// <summary>
/// Tests that AddLiteral with an empty string does not add any operations.
/// </summary>
[Test]
public void AddLiteral_EmptyString_DoesNotAddOperation()
{
// Arrange
var parsed = new ParsedTemplateString(0, 0);
// Act
parsed.AddLiteral(string.Empty);
// Assert
Assert.That(parsed.Operations, Is.Empty);
}
/// <summary>
/// Tests that AddLiteral with whitespace-only string creates a WhiteSpace operation.
/// </summary>
[TestCase(" ")]
[TestCase(" ")]
[TestCase(" ")]
[TestCase(" ")]
public void AddLiteral_WhitespaceOnlyString_CreatesWhiteSpaceOperation(string whitespace)
{
// Arrange
var parsed = new ParsedTemplateString(10, 0);
// Act
parsed.AddLiteral(whitespace);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.WhiteSpace));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(whitespace));
Assert.That(parsed.Operations[0].Offset, Is.Zero);
Assert.That(parsed.Operations[0].LineNumber, Is.Zero);
}
/// <summary>
/// Tests that AddLiteral with regular text creates a Literal operation.
/// </summary>
[TestCase("Hello")]
[TestCase("World")]
[TestCase("Test123")]
[TestCase("a")]
public void AddLiteral_RegularText_CreatesLiteralOperation(string text)
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(text));
Assert.That(parsed.Operations[0].Offset, Is.Zero);
Assert.That(parsed.Operations[0].LineNumber, Is.Zero);
}
/// <summary>
/// Tests that AddLiteral with Unix line ending creates correct operations.
/// </summary>
[Test]
public void AddLiteral_UnixLineEnding_CreatesLineBreakOperation()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\nLine2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[1].Item, Is.EqualTo(Environment.NewLine));
Assert.That(parsed.Operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[2].Item, Is.EqualTo("Line2"));
}
/// <summary>
/// Tests that AddLiteral with Windows line ending strips \r and creates LineBreak operation.
/// </summary>
[Test]
public void AddLiteral_WindowsLineEnding_StripsCarriageReturnAndCreatesLineBreak()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\r\nLine2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[1].Item, Is.EqualTo(Environment.NewLine));
Assert.That(parsed.Operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[2].Item, Is.EqualTo("Line2"));
}
/// <summary>
/// Tests that AddLiteral with multiline text splits into correct operations.
/// </summary>
[Test]
public void AddLiteral_MultilineText_SplitsIntoCorrectOperations()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string multilineText = "Line1\nLine2\nLine3";
// Act
parsed.AddLiteral(multilineText);
// Assert
var operations = parsed.Operations.ToList();
var literalOps = operations.Where(op => op.Type == ParsedTemplateString.OpType.Literal).ToList();
var lineBreakOps = operations.Where(op => op.Type == ParsedTemplateString.OpType.LineBreak).ToList();
Assert.That(literalOps, Has.Count.EqualTo(3));
Assert.That(lineBreakOps, Has.Count.EqualTo(2));
Assert.That(literalOps[0].Item, Is.EqualTo("Line1"));
Assert.That(literalOps[1].Item, Is.EqualTo("Line2"));
Assert.That(literalOps[2].Item, Is.EqualTo("Line3"));
}
/// <summary>
/// Tests that AddLiteral with consecutive line breaks creates correct operations.
/// </summary>
[Test]
public void AddLiteral_ConsecutiveLineBreaks_CreatesMultipleLineBreakOperations()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\n\nLine2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(4));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[3].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[3].Item, Is.EqualTo("Line2"));
}
/// <summary>
/// Tests that AddLiteral with line break at start creates LineBreak operation first.
/// </summary>
[Test]
public void AddLiteral_LineBreakAtStart_CreatesLineBreakFirst()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "\nLine1";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(2));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[1].Item, Is.EqualTo("Line1"));
}
/// <summary>
/// Tests that AddLiteral with line break at end creates LineBreak operation last.
/// </summary>
[Test]
public void AddLiteral_LineBreakAtEnd_CreatesLineBreakLast()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\n";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(2));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
}
/// <summary>
/// Tests that AddLiteral with only line breaks creates only LineBreak operations.
/// </summary>
[Test]
public void AddLiteral_OnlyLineBreaks_CreatesOnlyLineBreakOperations()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "\n\n\n";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations.All(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.True);
}
/// <summary>
/// Tests that AddLiteral tracks offset correctly across operations.
/// </summary>
[Test]
public void AddLiteral_MultipleOperations_TracksOffsetCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "Hello World Test";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Offset, Is.Zero);
// Add another literal
parsed.AddLiteral(" More");
Assert.That(parsed.Operations, Has.Count.EqualTo(2));
Assert.That(parsed.Operations[1].Offset, Is.EqualTo(16));
}
/// <summary>
/// Tests that AddLiteral resets offset at line breaks.
/// </summary>
[Test]
public void AddLiteral_LineBreak_ResetsOffset()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "Hello\nWorld";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations[0].Offset, Is.Zero); // "Hello"
Assert.That(parsed.Operations[1].Offset, Is.EqualTo(5)); // LineBreak at offset 5
Assert.That(parsed.Operations[2].Offset, Is.Zero); // "World" starts at 0 on new line
}
/// <summary>
/// Tests that AddLiteral tracks line numbers correctly across line breaks.
/// </summary>
[Test]
public void AddLiteral_MultipleLineBreaks_TracksLineNumberCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "Line0\nLine1\nLine2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations[0].LineNumber, Is.Zero); // "Line0"
Assert.That(parsed.Operations[1].LineNumber, Is.Zero); // First LineBreak
Assert.That(parsed.Operations[2].LineNumber, Is.EqualTo(1)); // "Line1"
Assert.That(parsed.Operations[3].LineNumber, Is.EqualTo(1)); // Second LineBreak
Assert.That(parsed.Operations[4].LineNumber, Is.EqualTo(2)); // "Line2"
}
/// <summary>
/// Tests that AddLiteral with mixed Windows and Unix line endings handles both correctly.
/// </summary>
[Test]
public void AddLiteral_MixedLineEndings_HandlesCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "Line1\r\nLine2\nLine3";
// Act
parsed.AddLiteral(text);
// Assert
var operations = parsed.Operations.ToList();
var literalOps = operations.Where(op => op.Type == ParsedTemplateString.OpType.Literal).ToList();
var lineBreakOps = operations.Where(op => op.Type == ParsedTemplateString.OpType.LineBreak).ToList();
Assert.That(literalOps, Has.Count.EqualTo(3));
Assert.That(lineBreakOps, Has.Count.EqualTo(2));
Assert.That(literalOps[0].Item, Is.EqualTo("Line1"));
Assert.That(literalOps[1].Item, Is.EqualTo("Line2"));
Assert.That(literalOps[2].Item, Is.EqualTo("Line3"));
}
/// <summary>
/// Tests that AddLiteral with carriage return not followed by newline treats it as regular character.
/// </summary>
[Test]
public void AddLiteral_CarriageReturnNotFollowedByNewline_TreatsAsRegularCharacter()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\rLine2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1\rLine2"));
}
/// <summary>
/// Tests that AddLiteral with text containing spaces but not only spaces creates Literal operation.
/// </summary>
[Test]
public void AddLiteral_TextWithSpaces_CreatesLiteralOperation()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Hello World";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(text));
}
/// <summary>
/// Tests that AddLiteral with whitespace between text on different lines classifies correctly.
/// </summary>
[Test]
public void AddLiteral_WhitespaceBetweenLines_ClassifiesCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "Text1\n \nText2";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(5));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Text1"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.WhiteSpace));
Assert.That(parsed.Operations[2].Item, Is.EqualTo(" "));
Assert.That(parsed.Operations[3].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(parsed.Operations[4].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[4].Item, Is.EqualTo("Text2"));
}
/// <summary>
/// Tests that AddLiteral with very long string handles correctly.
/// </summary>
[Test]
public void AddLiteral_VeryLongString_HandlesCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(10000, 0);
string longText = new('A', 5000);
// Act
parsed.AddLiteral(longText);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(longText));
Assert.That(parsed.Operations[0].Item, Has.Length.EqualTo(5000));
}
/// <summary>
/// Tests that AddLiteral with special characters creates Literal operation.
/// </summary>
[TestCase("Hello\tWorld")]
[TestCase("Test@#$%")]
[TestCase("Unicode™©®")]
[TestCase("Emoji😀")]
public void AddLiteral_SpecialCharacters_CreatesLiteralOperation(string text)
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Is.Not.Empty);
var literalOps = parsed.Operations.Where(op => op.Type == ParsedTemplateString.OpType.Literal).ToList();
Assert.That(literalOps, Has.Count.GreaterThanOrEqualTo(1));
}
/// <summary>
/// Tests that AddLiteral with single character creates Literal operation.
/// </summary>
[TestCase("A")]
[TestCase("1")]
[TestCase("@")]
public void AddLiteral_SingleCharacter_CreatesLiteralOperation(string text)
{
// Arrange
var parsed = new ParsedTemplateString(10, 0);
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(text));
}
/// <summary>
/// Tests that AddLiteral with single space creates WhiteSpace operation.
/// </summary>
[Test]
public void AddLiteral_SingleSpace_CreatesWhiteSpaceOperation()
{
// Arrange
var parsed = new ParsedTemplateString(10, 0);
// Act
parsed.AddLiteral(" ");
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.WhiteSpace));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(" "));
}
/// <summary>
/// Tests that multiple calls to AddLiteral accumulate operations correctly.
/// </summary>
[Test]
public void AddLiteral_MultipleCalls_AccumulatesOperationsCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
// Act
parsed.AddLiteral("First");
parsed.AddLiteral(" ");
parsed.AddLiteral("Second");
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("First"));
Assert.That(parsed.Operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.WhiteSpace));
Assert.That(parsed.Operations[1].Item, Is.EqualTo(" "));
Assert.That(parsed.Operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[2].Item, Is.EqualTo("Second"));
}
/// <summary>
/// Tests that AddLiteral with carriage return at end not followed by newline includes it.
/// </summary>
[Test]
public void AddLiteral_CarriageReturnAtEnd_IncludesInLiteral()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
const string text = "Line1\r";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("Line1\r"));
}
/// <summary>
/// Tests that AddLiteral with only carriage return creates Literal operation.
/// </summary>
[Test]
public void AddLiteral_OnlyCarriageReturn_CreatesLiteralOperation()
{
// Arrange
var parsed = new ParsedTemplateString(10, 0);
const string text = "\r";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("\r"));
}
/// <summary>
/// Tests that AddLiteral with tab character creates Literal operation.
/// </summary>
[Test]
public void AddLiteral_TabCharacter_CreatesLiteralOperation()
{
// Arrange
var parsed = new ParsedTemplateString(10, 0);
const string text = "\t";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(parsed.Operations[0].Item, Is.EqualTo("\t"));
}
/// <summary>
/// Tests that AddLiteral preserves offset across multiple calls with no line breaks.
/// </summary>
[Test]
public void AddLiteral_MultipleCallsNoLineBreaks_PreservesOffsetContinuity()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
// Act
parsed.AddLiteral("Hello"); // Offset 0, length 5
parsed.AddLiteral("World"); // Offset 5, length 5
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(2));
Assert.That(parsed.Operations[0].Offset, Is.Zero);
Assert.That(parsed.Operations[1].Offset, Is.EqualTo(5));
}
/// <summary>
/// Tests that AddLiteral with line break in the middle correctly splits and tracks offsets.
/// </summary>
[Test]
public void AddLiteral_LineBreakInMiddle_SplitsAndTracksOffsetsCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(50, 0);
const string text = "ABC\nDEF";
// Act
parsed.AddLiteral(text);
// Assert
Assert.That(parsed.Operations, Has.Count.EqualTo(3));
Assert.That(parsed.Operations[0].Offset, Is.Zero); // "ABC" at offset 0
Assert.That(parsed.Operations[1].Offset, Is.EqualTo(3)); // LineBreak at offset 3
Assert.That(parsed.Operations[2].Offset, Is.Zero); // "DEF" at offset 0 (new line)
}
/// <summary>
/// Tests that Format returns an empty string when no operations are added.
/// </summary>
[Test]
public void Format_NoOperations_ReturnsEmptyString()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests that Format returns the correct concatenation of a single literal operation.
/// </summary>
[Test]
public void Format_SingleLiteral_ReturnsLiteralValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(1, 0);
parsedTemplate.AddLiteral("Hello");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Hello"));
}
/// <summary>
/// Tests that Format returns the correct concatenation of multiple literal operations.
/// </summary>
[Test]
public void Format_MultipleLiterals_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(2, 0);
parsedTemplate.AddLiteral("Hello");
parsedTemplate.AddLiteral("World");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("HelloWorld"));
}
/// <summary>
/// Tests that Format returns the correct concatenation when operations include formatted values.
/// </summary>
[Test]
public void Format_WithFormattedValue_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(2, 1);
parsedTemplate.AddLiteral("Value: ");
parsedTemplate.AddFormatted("42", typeof(int));
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Value: 42"));
}
/// <summary>
/// Tests that Format returns the correct concatenation when operations include string tokens.
/// </summary>
[Test]
public void Format_WithStringToken_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(2, 1);
parsedTemplate.AddLiteral("Name: ");
parsedTemplate.AddFormatted("John", typeof(string));
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Name: John"));
}
/// <summary>
/// Tests that Format returns the correct concatenation when operations include line breaks.
/// </summary>
[Test]
public void Format_WithLineBreaks_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(3, 0);
parsedTemplate.AddLiteral("Line1\nLine2\nLine3");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Contains("Line1", StringComparison.Ordinal), Is.True);
Assert.That(result.Contains("Line2", StringComparison.Ordinal), Is.True);
Assert.That(result.Contains("Line3", StringComparison.Ordinal), Is.True);
}
/// <summary>
/// Tests that Format returns the correct concatenation when operations include whitespace.
/// </summary>
[Test]
public void Format_WithWhitespace_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(3, 0);
parsedTemplate.AddLiteral(" ");
parsedTemplate.AddLiteral("Text");
parsedTemplate.AddLiteral(" ");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo(" Text "));
}
/// <summary>
/// Tests that Format returns the correct concatenation with empty string literals.
/// </summary>
[Test]
public void Format_WithEmptyLiterals_ReturnsExpectedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(3, 0);
parsedTemplate.AddLiteral(string.Empty);
parsedTemplate.AddLiteral("Middle");
parsedTemplate.AddLiteral(string.Empty);
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Middle"));
}
/// <summary>
/// Tests that Format returns the correct concatenation when operations are mixed (literals, values, whitespace).
/// </summary>
[Test]
public void Format_MixedOperations_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(5, 2);
parsedTemplate.AddLiteral("Start ");
parsedTemplate.AddFormatted("Value1", typeof(string));
parsedTemplate.AddLiteral(" Middle ");
parsedTemplate.AddFormatted("Value2", typeof(int));
parsedTemplate.AddLiteral(" End");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Start Value1 Middle Value2 End"));
}
/// <summary>
/// Tests that Format handles special characters correctly.
/// </summary>
[Test]
public void Format_WithSpecialCharacters_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(3, 0);
parsedTemplate.AddLiteral("Special: \t\r");
parsedTemplate.AddLiteral("chars!");
parsedTemplate.AddLiteral("@#$%");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Contains("Special:", StringComparison.Ordinal), Is.True);
Assert.That(result.Contains("chars!", StringComparison.Ordinal), Is.True);
Assert.That(result.Contains("@#$%", StringComparison.Ordinal), Is.True);
}
/// <summary>
/// Tests that Format handles long strings correctly.
/// </summary>
[Test]
public void Format_WithLongString_ReturnsConcatenatedValue()
{
// Arrange
string longString = new('A', 10000);
var parsedTemplate = new ParsedTemplateString(1, 0);
parsedTemplate.AddLiteral(longString);
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo(longString));
Assert.That(result, Has.Length.EqualTo(10000));
}
/// <summary>
/// Tests that Format returns the correct concatenation using FromString factory method.
/// </summary>
[Test]
public void Format_FromString_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString("Simple text");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Simple text"));
}
/// <summary>
/// Tests that Format returns empty string when FromString is called with empty string.
/// </summary>
[Test]
public void Format_FromEmptyString_ReturnsEmptyString()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString(string.Empty);
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests that Format handles unicode characters correctly.
/// </summary>
[Test]
public void Format_WithUnicodeCharacters_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(1, 0);
parsedTemplate.AddLiteral("Unicode: 你好世界 🌍");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("Unicode: 你好世界 🌍"));
}
/// <summary>
/// Tests that Format returns the correct concatenation with multiple formatted values.
/// </summary>
[Test]
public void Format_MultipleFormattedValues_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 5);
parsedTemplate.AddFormatted("1", typeof(int));
parsedTemplate.AddFormatted("2", typeof(int));
parsedTemplate.AddFormatted("3", typeof(int));
parsedTemplate.AddFormatted("4", typeof(int));
parsedTemplate.AddFormatted("5", typeof(int));
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.EqualTo("12345"));
}
/// <summary>
/// Tests that Format handles Windows line endings correctly.
/// </summary>
[Test]
public void Format_WithWindowsLineEndings_ReturnsConcatenatedValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(1, 0);
parsedTemplate.AddLiteral("Line1\r\nLine2");
// Act
string result = parsedTemplate.Format;
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Contains("Line1", StringComparison.Ordinal), Is.True);
Assert.That(result.Contains("Line2", StringComparison.Ordinal), Is.True);
}
/// <summary>
/// Tests that Format returns consistent results when called multiple times.
/// </summary>
[Test]
public void Format_CalledMultipleTimes_ReturnsSameValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(2, 1);
parsedTemplate.AddLiteral("Test ");
parsedTemplate.AddFormatted("Value", typeof(string));
// Act
string result1 = parsedTemplate.Format;
string result2 = parsedTemplate.Format;
// Assert
Assert.That(result1, Is.EqualTo(result2));
Assert.That(result1, Is.EqualTo("Test Value"));
}
/// <summary>
/// Tests that ArgumentCount returns 0 for an empty ParsedTemplateString with no operations.
/// </summary>
[Test]
public void ArgumentCount_EmptyParsedTemplateString_ReturnsZero()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that ArgumentCount returns 0 for a ParsedTemplateString created from a plain string literal.
/// </summary>
[Test]
public void ArgumentCount_FromStringWithLiteralsOnly_ReturnsZero()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString("Hello World");
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that ArgumentCount returns 0 for a ParsedTemplateString created from an empty string.
/// </summary>
[Test]
public void ArgumentCount_FromEmptyString_ReturnsZero()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString(string.Empty);
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that ArgumentCount returns 0 for a ParsedTemplateString with only whitespace.
/// </summary>
[Test]
public void ArgumentCount_OnlyWhitespace_ReturnsZero()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString(" \t ");
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that ArgumentCount returns 0 for a ParsedTemplateString with line breaks.
/// </summary>
[Test]
public void ArgumentCount_WithLineBreaks_ReturnsZero()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString("Line1\nLine2\nLine3");
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that ArgumentCount returns 1 for a ParsedTemplateString with a single formatted non-string value.
/// </summary>
[Test]
public void ArgumentCount_SingleFormattedValue_ReturnsOne()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 1);
parsedTemplate.AddFormatted("42", typeof(int));
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(1));
}
/// <summary>
/// Tests that ArgumentCount returns 1 for a ParsedTemplateString with a single formatted string token.
/// </summary>
[Test]
public void ArgumentCount_SingleFormattedToken_ReturnsOne()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 1);
parsedTemplate.AddFormatted("token", typeof(string));
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(1));
}
/// <summary>
/// Tests that ArgumentCount returns the correct count for multiple formatted items.
/// </summary>
[TestCase(2)]
[TestCase(5)]
[TestCase(10)]
public void ArgumentCount_MultipleFormattedItems_ReturnsCorrectCount(int count)
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, count);
for (int i = 0; i < count; i++)
{
parsedTemplate.AddFormatted(i.ToString(CultureInfo.InvariantCulture), typeof(int));
}
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(count));
}
/// <summary>
/// Tests that ArgumentCount correctly counts only formatted items when mixed with literals.
/// </summary>
[Test]
public void ArgumentCount_MixedLiteralsAndFormattedItems_ReturnsFormattedItemsCount()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 3);
parsedTemplate.AddLiteral("Hello ");
parsedTemplate.AddFormatted("World", typeof(string));
parsedTemplate.AddLiteral(" and ");
parsedTemplate.AddFormatted("42", typeof(int));
parsedTemplate.AddLiteral(" items");
parsedTemplate.AddFormatted("end", typeof(string));
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(3));
}
/// <summary>
/// Tests that ArgumentCount returns consistent results across multiple calls.
/// </summary>
[Test]
public void ArgumentCount_MultipleAccesses_ReturnsConsistentValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(5, 2);
parsedTemplate.AddLiteral("Test ");
parsedTemplate.AddFormatted("value", typeof(string));
parsedTemplate.AddLiteral(" and ");
parsedTemplate.AddFormatted("123", typeof(int));
// Act
int firstAccess = parsedTemplate.ArgumentCount;
int secondAccess = parsedTemplate.ArgumentCount;
int thirdAccess = parsedTemplate.ArgumentCount;
// Assert
Assert.That(firstAccess, Is.EqualTo(2));
Assert.That(secondAccess, Is.EqualTo(2));
Assert.That(thirdAccess, Is.EqualTo(2));
Assert.That(firstAccess, Is.EqualTo(secondAccess));
Assert.That(secondAccess, Is.EqualTo(thirdAccess));
}
/// <summary>
/// Tests that ArgumentCount handles a large number of formatted items correctly.
/// </summary>
[Test]
public void ArgumentCount_LargeNumberOfFormattedItems_ReturnsCorrectCount()
{
// Arrange
const int largeCount = 1000;
var parsedTemplate = new ParsedTemplateString(0, largeCount);
for (int i = 0; i < largeCount; i++)
{
parsedTemplate.AddFormatted(i.ToString(CultureInfo.InvariantCulture), typeof(int));
}
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(largeCount));
}
/// <summary>
/// Tests that ArgumentCount returns correct value when mixing different formatted types.
/// </summary>
[Test]
public void ArgumentCount_MixedFormattedTypes_ReturnsCorrectCount()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 5);
parsedTemplate.AddFormatted("text", typeof(string));
parsedTemplate.AddFormatted("42", typeof(int));
parsedTemplate.AddFormatted("3.14", typeof(double));
parsedTemplate.AddFormatted("True", typeof(bool));
parsedTemplate.AddFormatted("X", typeof(char));
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.EqualTo(5));
}
/// <summary>
/// Tests that ArgumentCount returns 0 for a ParsedTemplateString with only whitespace and line breaks.
/// </summary>
[Test]
public void ArgumentCount_OnlyWhitespaceAndLineBreaks_ReturnsZero()
{
// Arrange
var parsedTemplate = ParsedTemplateString.FromString(" \n \n ");
// Act
int argumentCount = parsedTemplate.ArgumentCount;
// Assert
Assert.That(argumentCount, Is.Zero);
}
/// <summary>
/// Tests that the constructor correctly initializes properties with typical positive values.
/// Input: literalLength = 10, formattedCount = 5
/// Expected: Properties are assigned correctly and Operations list is initialized.
/// </summary>
[TestCase(10, 5)]
[TestCase(1, 1)]
[TestCase(100, 50)]
[TestCase(1000, 2000)]
public void Constructor_WithPositiveValues_InitializesPropertiesCorrectly(int literalLength, int formattedCount)
{
// Arrange & Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.EqualTo(literalLength));
Assert.That(parsed.FormattedCount, Is.EqualTo(formattedCount));
Assert.That(parsed.Operations, Is.Not.Null);
Assert.That(parsed.Operations, Is.Empty);
}
/// <summary>
/// Tests that the constructor handles zero values for both parameters.
/// Input: literalLength = 0, formattedCount = 0
/// Expected: Properties are set to zero and Operations list is initialized as empty.
/// </summary>
[Test]
public void Constructor_WithZeroValues_InitializesPropertiesCorrectly()
{
// Arrange
const int literalLength = 0;
const int formattedCount = 0;
// Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.Zero);
Assert.That(parsed.FormattedCount, Is.Zero);
Assert.That(parsed.Operations, Is.Not.Null);
Assert.That(parsed.Operations, Is.Empty);
}
/// <summary>
/// Tests that the constructor handles one parameter being zero.
/// Input: literalLength = 0, formattedCount = 5
/// Expected: Properties are assigned correctly.
/// </summary>
[TestCase(0, 5)]
[TestCase(10, 0)]
public void Constructor_WithOneZeroValue_InitializesPropertiesCorrectly(int literalLength, int formattedCount)
{
// Arrange & Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.EqualTo(literalLength));
Assert.That(parsed.FormattedCount, Is.EqualTo(formattedCount));
Assert.That(parsed.Operations, Is.Not.Null);
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException when literalLength is negative.
/// Input: literalLength = -1, formattedCount = 5
/// Expected: ArgumentOutOfRangeException is thrown from List constructor.
/// </summary>
[Test]
public void Constructor_WithNegativeLiteralLength_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = -1;
const int formattedCount = 5;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException when formattedCount is negative.
/// Input: literalLength = 10, formattedCount = -1
/// Expected: ArgumentOutOfRangeException is thrown from List constructor.
/// </summary>
[Test]
public void Constructor_WithNegativeFormattedCount_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = 10;
const int formattedCount = -1;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException when both parameters are negative.
/// Input: literalLength = -5, formattedCount = -10
/// Expected: ArgumentOutOfRangeException is thrown from List constructor.
/// </summary>
[Test]
public void Constructor_WithBothNegativeValues_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = -5;
const int formattedCount = -10;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException when literalLength is int.MinValue.
/// Input: literalLength = int.MinValue, formattedCount = 0
/// Expected: ArgumentOutOfRangeException is thrown from List constructor.
/// </summary>
[Test]
public void Constructor_WithMinValueLiteralLength_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = int.MinValue;
const int formattedCount = 0;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException when formattedCount is int.MinValue.
/// Input: literalLength = 0, formattedCount = int.MinValue
/// Expected: ArgumentOutOfRangeException is thrown from List constructor.
/// </summary>
[Test]
public void Constructor_WithMinValueFormattedCount_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = 0;
const int formattedCount = int.MinValue;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws ArgumentOutOfRangeException 16k exceeded
/// </summary>
[Test]
public void Constructor_WithOverflowingSum_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = ParsedTemplateString.MaxLiteralLength + 1;
const int formattedCount = 1;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor throws OverflowException when both parameters are int.MaxValue.
/// Input: literalLength = int.MaxValue, formattedCount = int.MaxValue
/// Expected: OverflowException is thrown due to integer overflow.
/// </summary>
[Test]
public void Constructor_WithBothMaxValues_ThrowsArgumentOutOfRangeException()
{
// Arrange
const int literalLength = int.MaxValue;
const int formattedCount = int.MaxValue;
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ParsedTemplateString(literalLength, formattedCount));
}
/// <summary>
/// Tests that the constructor correctly handles int.MaxValue for literalLength when formattedCount is zero.
/// Input: literalLength = int.MaxValue, formattedCount = 0
/// Expected: Properties are assigned correctly and no exception is thrown.
/// </summary>
[Test]
public void Constructor_WithMaxValueLiteralLengthAndZeroFormattedCount_InitializesCorrectly()
{
// Arrange
const int literalLength = ParsedTemplateString.MaxLiteralLength;
const int formattedCount = 0;
// Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.EqualTo(ParsedTemplateString.MaxLiteralLength));
Assert.That(parsed.FormattedCount, Is.Zero);
Assert.That(parsed.Operations, Is.Not.Null);
}
/// <summary>
/// Tests that the constructor correctly handles max count for formatted count.
/// </summary>
[Test]
public void Constructor_WithZeroLiteralLengthAndMaxValueFormattedCount_InitializesCorrectly()
{
// Arrange
const int literalLength = 0;
const int formattedCount = ParsedTemplateString.MaxFormattedCount;
// Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.Zero);
Assert.That(parsed.FormattedCount, Is.EqualTo(ParsedTemplateString.MaxFormattedCount));
Assert.That(parsed.Operations, Is.Not.Null);
}
/// <summary>
/// Tests that the constructor correctly handles Max count const for each.
/// </summary>
[Test]
public void Constructor_WithLargeNonOverflowingValues_InitializesCorrectly()
{
// Arrange
const int literalLength = ParsedTemplateString.MaxLiteralLength;
const int formattedCount = ParsedTemplateString.MaxFormattedCount;
// Act
var parsed = new ParsedTemplateString(literalLength, formattedCount);
// Assert
Assert.That(parsed.LiteralLength, Is.EqualTo(ParsedTemplateString.MaxLiteralLength));
Assert.That(parsed.FormattedCount, Is.EqualTo(ParsedTemplateString.MaxFormattedCount));
Assert.That(parsed.Operations, Is.Not.Null);
}
/// <summary>
/// Tests that GetArgument returns the correct argument when a valid index is provided.
/// Input: Valid index (0, 1, 2) within the bounds of the arguments array.
/// Expected: Returns the correct argument value at the specified index.
/// </summary>
[TestCase(0, "FirstValue")]
[TestCase(1, "SecondValue")]
[TestCase(2, "ThirdValue")]
public void GetArgument_ValidIndex_ReturnsCorrectArgument(int index, string expectedValue)
{
// Arrange
var parsed = new ParsedTemplateString(0, 3);
parsed.AddFormatted("FirstValue", typeof(string));
parsed.AddFormatted("SecondValue", typeof(string));
parsed.AddFormatted("ThirdValue", typeof(string));
// Act
object argument = parsed.GetArgument(index);
// Assert
Assert.That(argument, Is.EqualTo(expectedValue));
}
/// <summary>
/// Tests that GetArgument throws IndexOutOfRangeException when an invalid index is provided.
/// Input: Invalid index values (negative, equal to or greater than array length, extreme values).
/// Expected: Throws IndexOutOfRangeException.
/// </summary>
[TestCase(-1)]
[TestCase(-100)]
[TestCase(0)]
[TestCase(1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void GetArgument_InvalidIndex_ThrowsIndexOutOfRangeException(int invalidIndex)
{
// Arrange
var parsed = new ParsedTemplateString(0, 0);
// Act & Assert
Assert.Throws<IndexOutOfRangeException>(() => parsed.GetArgument(invalidIndex));
}
/// <summary>
/// Tests that GetArgument throws IndexOutOfRangeException when index equals the array length.
/// Input: Index equal to the number of arguments.
/// Expected: Throws IndexOutOfRangeException.
/// </summary>
[Test]
public void GetArgument_IndexEqualToArrayLength_ThrowsIndexOutOfRangeException()
{
// Arrange
var parsed = new ParsedTemplateString(0, 2);
parsed.AddFormatted("Value1", typeof(string));
parsed.AddFormatted("Value2", typeof(string));
// Act & Assert
Assert.Throws<IndexOutOfRangeException>(() => parsed.GetArgument(2));
}
/// <summary>
/// Tests that GetArgument throws IndexOutOfRangeException when index is greater than array length.
/// Input: Index greater than the number of arguments.
/// Expected: Throws IndexOutOfRangeException.
/// </summary>
[Test]
public void GetArgument_IndexGreaterThanArrayLength_ThrowsIndexOutOfRangeException()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
parsed.AddFormatted("Value", typeof(string));
// Act & Assert
Assert.Throws<IndexOutOfRangeException>(() => parsed.GetArgument(5));
}
/// <summary>
/// Tests that GetArgument returns the correct argument for different value types.
/// Input: Valid index for non-string formatted values.
/// Expected: Returns the correct argument value.
/// </summary>
[Test]
public void GetArgument_NonStringValue_ReturnsCorrectArgument()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
parsed.AddFormatted("123", typeof(int));
// Act
object argument = parsed.GetArgument(0);
// Assert
Assert.That(argument, Is.EqualTo("123"));
}
/// <summary>
/// Tests that IsMultiLine returns false when no operations have been added to the ParsedTemplateString.
/// </summary>
[Test]
public void IsMultiLine_EmptyOperations_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns false when only literals without newlines are added.
/// </summary>
[Test]
public void IsMultiLine_SingleLineLiteral_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 0);
parsedTemplate.AddLiteral("Hello World");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns false when only whitespace literals without newlines are added.
/// </summary>
[Test]
public void IsMultiLine_WhitespaceOnly_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(5, 0);
parsedTemplate.AddLiteral(" ");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal contains a single newline character.
/// </summary>
[Test]
public void IsMultiLine_LiteralWithSingleNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(20, 0);
parsedTemplate.AddLiteral("First line\nSecond line");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal contains multiple newline characters.
/// </summary>
[Test]
public void IsMultiLine_LiteralWithMultipleNewlines_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(30, 0);
parsedTemplate.AddLiteral("Line 1\nLine 2\nLine 3\nLine 4");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal contains Windows-style line endings (CRLF).
/// </summary>
[Test]
public void IsMultiLine_LiteralWithWindowsNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(25, 0);
parsedTemplate.AddLiteral("First line\r\nSecond line");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns false when only formatted values are added without any newlines.
/// </summary>
[Test]
public void IsMultiLine_OnlyFormattedValues_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 2);
parsedTemplate.AddFormatted("value1", typeof(string));
parsedTemplate.AddFormatted("123", typeof(int));
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns false when a mix of literals and formatted values are added without any newlines.
/// </summary>
[Test]
public void IsMultiLine_MixedOperationsWithoutNewlines_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(15, 1);
parsedTemplate.AddLiteral("Hello ");
parsedTemplate.AddFormatted("World", typeof(string));
parsedTemplate.AddLiteral("!");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns true when operations include at least one newline among literals and formatted values.
/// </summary>
[Test]
public void IsMultiLine_MixedOperationsWithNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(20, 1);
parsedTemplate.AddLiteral("First line\n");
parsedTemplate.AddFormatted("value", typeof(string));
parsedTemplate.AddLiteral("\nLast line");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal starts with a newline character.
/// </summary>
[Test]
public void IsMultiLine_LiteralStartingWithNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 0);
parsedTemplate.AddLiteral("\nStarting text");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal ends with a newline character.
/// </summary>
[Test]
public void IsMultiLine_LiteralEndingWithNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 0);
parsedTemplate.AddLiteral("Ending text\n");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when a literal contains only a newline character.
/// </summary>
[Test]
public void IsMultiLine_LiteralOnlyNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(1, 0);
parsedTemplate.AddLiteral("\n");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns false when an empty string literal is added.
/// </summary>
[Test]
public void IsMultiLine_EmptyStringLiteral_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
parsedTemplate.AddLiteral(string.Empty);
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns false when multiple single-line literals are added.
/// </summary>
[Test]
public void IsMultiLine_MultipleSingleLineLiterals_ReturnsFalse()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(30, 0);
parsedTemplate.AddLiteral("First part");
parsedTemplate.AddLiteral("Second part");
parsedTemplate.AddLiteral("Third part");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.False);
}
/// <summary>
/// Tests that IsMultiLine returns true when one of multiple literals contains a newline.
/// </summary>
[Test]
public void IsMultiLine_OneOfMultipleLiteralsWithNewline_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(35, 0);
parsedTemplate.AddLiteral("First part");
parsedTemplate.AddLiteral("Second\npart");
parsedTemplate.AddLiteral("Third part");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when multiple literals each contain newlines.
/// </summary>
[Test]
public void IsMultiLine_MultipleLiteralsWithNewlines_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(40, 0);
parsedTemplate.AddLiteral("First\npart");
parsedTemplate.AddLiteral("Second\npart");
parsedTemplate.AddLiteral("Third\npart");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that IsMultiLine returns true when consecutive newlines are present in a literal.
/// </summary>
[Test]
public void IsMultiLine_ConsecutiveNewlines_ReturnsTrue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(15, 0);
parsedTemplate.AddLiteral("Text\n\n\nMore text");
// Act
bool result = parsedTemplate.IsMultiLine;
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that the Operations property returns an empty list when no operations have been added.
/// Input: A newly constructed ParsedTemplateString with no operations.
/// Expected: Returns an empty IReadOnlyList.
/// </summary>
[Test]
public void Operations_NoOperationsAdded_ReturnsEmptyList()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
// Act
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Is.Empty);
}
/// <summary>
/// Tests that the Operations property returns operations after AddLiteral is called.
/// Input: A ParsedTemplateString with a single literal added.
/// Expected: Returns a list containing one operation with the literal text.
/// </summary>
[Test]
public void Operations_AfterAddLiteral_ReturnsOperationsList()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 0);
const string literalText = "Hello";
// Act
parsedTemplate.AddLiteral(literalText);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(literalText));
}
/// <summary>
/// Tests that the Operations property returns operations after AddFormatted is called.
/// Input: A ParsedTemplateString with a formatted value added.
/// Expected: Returns a list containing one operation with the formatted value.
/// </summary>
[Test]
public void Operations_AfterAddFormatted_ReturnsOperationsList()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 1);
const string formattedValue = "42";
// Act
parsedTemplate.AddFormatted(formattedValue, typeof(int));
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(formattedValue));
}
/// <summary>
/// Tests that the Operations property returns operations in the correct order they were added.
/// Input: Multiple literals and formatted values added in sequence.
/// Expected: Returns a list with operations in the same order they were added.
/// </summary>
[Test]
public void Operations_MultipleOperations_ReturnsInCorrectOrder()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(20, 2);
const string literal1 = "Start";
const string formatted1 = "100";
const string literal2 = "End";
// Act
parsedTemplate.AddLiteral(literal1);
parsedTemplate.AddFormatted(formatted1, typeof(int));
parsedTemplate.AddLiteral(literal2);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(3));
Assert.That(operations[0].Item, Is.EqualTo(literal1));
Assert.That(operations[1].Item, Is.EqualTo(formatted1));
Assert.That(operations[2].Item, Is.EqualTo(literal2));
}
/// <summary>
/// Tests that the Operations property handles empty string literals correctly.
/// Input: An empty string added as a literal.
/// Expected: Returns an empty list as empty literals are not added.
/// </summary>
[Test]
public void Operations_EmptyStringLiteral_ReturnsEmptyList()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 0);
// Act
parsedTemplate.AddLiteral(string.Empty);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Is.Empty);
}
/// <summary>
/// Tests that the Operations property handles whitespace-only literals correctly.
/// Input: A whitespace-only string added as a literal.
/// Expected: Returns a list containing one operation with OpType.WhiteSpace.
/// </summary>
[Test]
public void Operations_WhitespaceLiteral_ReturnsWhiteSpaceOperation()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(5, 0);
const string whitespace = " ";
// Act
parsedTemplate.AddLiteral(whitespace);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(whitespace));
}
/// <summary>
/// Tests that the Operations property handles literals with line breaks correctly.
/// Input: A literal containing newline characters.
/// Expected: Returns a list with separate operations for text and line breaks.
/// </summary>
[Test]
public void Operations_LiteralWithLineBreaks_ReturnsMultipleOperations()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(20, 0);
const string literalWithNewline = "Line1\nLine2";
// Act
parsedTemplate.AddLiteral(literalWithNewline);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.GreaterThan(1));
Assert.That(operations.Any(op => op.Item == "Line1"), Is.True);
Assert.That(operations.Any(op => op.Item == "Line2"), Is.True);
}
/// <summary>
/// Tests that the Operations property is read-only and returns the same reference on multiple calls.
/// Input: A ParsedTemplateString with operations added.
/// Expected: Multiple calls to Operations return the same instance.
/// </summary>
[Test]
public void Operations_MultipleCalls_ReturnsSameReference()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10, 1);
parsedTemplate.AddLiteral("Test");
// Act
IReadOnlyList<ParsedTemplateString.Op> operations1 = parsedTemplate.Operations;
IReadOnlyList<ParsedTemplateString.Op> operations2 = parsedTemplate.Operations;
// Assert
Assert.That(ReferenceEquals(operations1, operations2), Is.True);
}
/// <summary>
/// Tests that the Operations property returns a list containing operations after FromString is used.
/// Input: A ParsedTemplateString created from a raw string.
/// Expected: Returns a list containing the operations from the raw string.
/// </summary>
[Test]
public void Operations_FromString_ReturnsOperationsFromRawString()
{
// Arrange
const string rawString = "Simple text";
// Act
var parsedTemplate = ParsedTemplateString.FromString(rawString);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Is.Not.Empty);
Assert.That(operations.Any(op => op.Item == rawString), Is.True);
}
/// <summary>
/// Tests that the Operations property handles very long literals correctly.
/// Input: A very long string added as a literal.
/// Expected: Returns a list containing the operation with the full long string.
/// </summary>
[Test]
public void Operations_VeryLongLiteral_ReturnsOperationWithFullString()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(10000, 0);
string longString = new('A', 10000);
// Act
parsedTemplate.AddLiteral(longString);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(longString));
}
/// <summary>
/// Tests that the Operations property handles special characters in literals correctly.
/// Input: A literal containing special characters like tabs, quotes, and backslashes.
/// Expected: Returns a list containing the operation with the special characters preserved.
/// </summary>
[Test]
public void Operations_SpecialCharactersInLiteral_PreservesCharacters()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(50, 0);
const string specialChars = "Tab:\t Quote:\" Backslash:\\ End";
// Act
parsedTemplate.AddLiteral(specialChars);
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations.Any(op => op.Item.Contains('\t', StringComparison.Ordinal)), Is.True);
}
/// <summary>
/// Tests that the Operations property handles formatted string types correctly.
/// Input: A formatted string value added with typeof(string).
/// Expected: Returns a list containing the operation as a Token type.
/// </summary>
[Test]
public void Operations_FormattedStringType_AddsAsToken()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 1);
const string stringValue = "TokenValue";
// Act
parsedTemplate.AddFormatted(stringValue, typeof(string));
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(stringValue));
}
/// <summary>
/// Tests that the Operations property handles formatted non-string types correctly.
/// Input: A formatted integer value added with typeof(int).
/// Expected: Returns a list containing the operation as a Value type.
/// </summary>
[Test]
public void Operations_FormattedNonStringType_AddsAsValue()
{
// Arrange
var parsedTemplate = new ParsedTemplateString(0, 1);
const string intValue = "123";
// Act
parsedTemplate.AddFormatted(intValue, typeof(int));
IReadOnlyList<ParsedTemplateString.Op> operations = parsedTemplate.Operations;
// Assert
Assert.That(operations, Is.Not.Null);
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Item, Is.EqualTo(intValue));
}
/// <summary>
/// Tests that FromString treats null input as an empty string.
/// </summary>
[Test]
public void FromString_NullString_ParsesAsEmptyString()
{
// Arrange
const string? input = null;
// Act
var parsed = ParsedTemplateString.FromString(input!);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.Zero);
Assert.That(parsed.FormattedCount, Is.Zero);
Assert.That(parsed.ArgumentCount, Is.Zero);
Assert.That(parsed.Format, Is.EqualTo(string.Empty));
Assert.That(parsed.Operations, Is.Empty);
}
/// <summary>
/// Tests that FromString correctly parses an empty string with zero length and no operations.
/// </summary>
[Test]
public void FromString_EmptyString_ParsesCorrectly()
{
// Arrange
const string input = "";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.Zero);
Assert.That(parsed.FormattedCount, Is.Zero);
Assert.That(parsed.ArgumentCount, Is.Zero);
Assert.That(parsed.Format, Is.EqualTo(string.Empty));
Assert.That(parsed.Operations, Is.Empty);
}
/// <summary>
/// Tests that FromString correctly parses a simple string without special characters.
/// </summary>
[Test]
public void FromString_SimpleString_ParsesCorrectly()
{
// Arrange
const string input = "Hello World";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.EqualTo(input.Length));
Assert.That(parsed.FormattedCount, Is.Zero);
Assert.That(parsed.ArgumentCount, Is.Zero);
Assert.That(parsed.Format, Is.EqualTo(input));
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
}
/// <summary>
/// Tests that FromString correctly identifies and parses whitespace-only strings (all spaces).
/// </summary>
[Test]
public void FromString_WhitespaceOnlySpaces_ParsesAsWhitespace()
{
// Arrange
const string input = " ";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.WhiteSpace));
Assert.That(parsed.Operations[0].Item, Is.EqualTo(input));
}
/// <summary>
/// Tests that FromString correctly handles tabs (not identified as all-spaces whitespace).
/// </summary>
[Test]
public void FromString_WhitespaceWithTabs_ParsesAsLiteral()
{
// Arrange
const string input = "\t\t\t";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
}
/// <summary>
/// Tests that FromString correctly parses strings with Unix-style line breaks (\n).
/// </summary>
[Test]
public void FromString_WithUnixNewlines_ParsesLineBreaksCorrectly()
{
// Arrange
const string input = "Line1\nLine2\nLine3";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations.Any(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.True);
Assert.That(operations.Count(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.EqualTo(2));
Assert.That(operations.Count(op => op.Type == ParsedTemplateString.OpType.Literal), Is.EqualTo(3));
}
/// <summary>
/// Tests that FromString correctly parses strings with Windows-style line breaks (\r\n).
/// </summary>
[Test]
public void FromString_WithWindowsNewlines_ParsesLineBreaksCorrectly()
{
// Arrange
const string input = "Line1\r\nLine2\r\nLine3";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations.Count(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.EqualTo(2));
}
/// <summary>
/// Tests that FromString correctly parses strings with mixed line break styles.
/// </summary>
[Test]
public void FromString_WithMixedNewlines_ParsesLineBreaksCorrectly()
{
// Arrange
const string input = "Line1\nLine2\r\nLine3";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations.Count(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.EqualTo(2));
}
/// <summary>
/// Tests that FromString correctly handles strings containing only a single newline character.
/// </summary>
[Test]
public void FromString_SingleNewline_ParsesCorrectly()
{
// Arrange
const string input = "\n";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
}
/// <summary>
/// Tests that FromString correctly handles strings containing only a Windows line break.
/// </summary>
[Test]
public void FromString_SingleWindowsNewline_ParsesCorrectly()
{
// Arrange
const string input = "\r\n";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
Assert.That(parsed.Operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
}
/// <summary>
/// Tests that FromString correctly handles strings with special and control characters.
/// </summary>
[Test]
public void FromString_WithSpecialCharacters_ParsesCorrectly()
{
// Arrange
const string input = "Hello\tWorld\0Test";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.EqualTo(input.Length));
Assert.That(parsed.Format, Is.EqualTo(input));
}
/// <summary>
/// Tests that FromString correctly handles strings with Unicode characters.
/// </summary>
[Test]
public void FromString_WithUnicodeCharacters_ParsesCorrectly()
{
// Arrange
const string input = "Hello 世界 🌍";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.EqualTo(input.Length));
Assert.That(parsed.Format, Is.EqualTo(input));
}
/// <summary>
/// Tests that FromString correctly handles very long strings.
/// </summary>
[Test]
public void FromString_VeryLongString_ParsesCorrectly()
{
// Arrange
string input = new('a', 10000);
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.EqualTo(input.Length));
Assert.That(parsed.Format, Is.EqualTo(input));
Assert.That(parsed.Operations, Has.Count.EqualTo(1));
}
/// <summary>
/// Tests that FromString correctly handles strings with mixed content including whitespace and text.
/// </summary>
[Test]
public void FromString_MixedWhitespaceAndContent_ParsesCorrectly()
{
// Arrange
const string input = " Hello World ";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.LiteralLength, Is.EqualTo(input.Length));
Assert.That(parsed.Format, Is.EqualTo(input));
}
/// <summary>
/// Tests that FromString correctly tracks line numbers when parsing multi-line strings.
/// </summary>
[Test]
public void FromString_MultiLine_TracksLineNumbersCorrectly()
{
// Arrange
const string input = "Line0\nLine1\nLine2";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
var operations = parsed.Operations.ToList();
Assert.That(operations[0].LineNumber, Is.Zero);
Assert.That(operations[1].LineNumber, Is.Zero);
Assert.That(operations[2].LineNumber, Is.EqualTo(1));
Assert.That(operations[3].LineNumber, Is.EqualTo(1));
Assert.That(operations[4].LineNumber, Is.EqualTo(2));
}
/// <summary>
/// Tests that FromString correctly resets offset at line breaks.
/// </summary>
[Test]
public void FromString_MultiLine_ResetsOffsetAtLineBreaks()
{
// Arrange
const string input = "ABC\nDEF";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
var operations = parsed.Operations.ToList();
var literalOps = operations.Where(op => op.Type == ParsedTemplateString.OpType.Literal).ToList();
Assert.That(literalOps[0].Offset, Is.Zero);
Assert.That(literalOps[1].Offset, Is.Zero); // Offset reset after line break
}
/// <summary>
/// Tests that FromString correctly handles strings ending with a newline.
/// </summary>
[Test]
public void FromString_EndsWithNewline_ParsesCorrectly()
{
// Arrange
const string input = "Line1\n";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(2));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
Assert.That(operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
}
/// <summary>
/// Tests that FromString correctly handles strings starting with a newline.
/// </summary>
[Test]
public void FromString_StartsWithNewline_ParsesCorrectly()
{
// Arrange
const string input = "\nLine1";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(2));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.LineBreak));
Assert.That(operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.Literal));
}
/// <summary>
/// Tests that FromString correctly handles consecutive newlines.
/// </summary>
[Test]
public void FromString_ConsecutiveNewlines_ParsesCorrectly()
{
// Arrange
const string input = "Line1\n\nLine2";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed, Is.Not.Null);
Assert.That(parsed.IsMultiLine, Is.True);
var operations = parsed.Operations.ToList();
Assert.That(operations.Count(op => op.Type == ParsedTemplateString.OpType.LineBreak), Is.EqualTo(2));
}
/// <summary>
/// Tests that FromString produces ArgumentCount of zero since no formatted arguments are present.
/// </summary>
[Test]
public void FromString_AnyString_HasZeroArgumentCount()
{
// Arrange
const string input = "Test String";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed.ArgumentCount, Is.Zero);
Assert.That(parsed.GetArguments(), Is.Empty);
}
/// <summary>
/// Tests that FromString produces FormattedCount of zero.
/// </summary>
[Test]
public void FromString_AnyString_HasZeroFormattedCount()
{
// Arrange
const string input = "Test String";
// Act
var parsed = ParsedTemplateString.FromString(input);
// Assert
Assert.That(parsed.FormattedCount, Is.Zero);
}
/// <summary>
/// Tests that GetArguments returns an empty array when no operations have been added.
/// </summary>
[Test]
public void GetArguments_NoOperations_ReturnsEmptyArray()
{
// Arrange
var parsed = new ParsedTemplateString(0, 0);
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Is.Not.Null);
Assert.That(arguments, Is.Empty);
}
/// <summary>
/// Tests that GetArguments returns an empty array when only literal operations exist.
/// </summary>
[Test]
public void GetArguments_OnlyLiterals_ReturnsEmptyArray()
{
// Arrange
var parsed = new ParsedTemplateString(20, 0);
parsed.AddLiteral("Hello World");
parsed.AddLiteral(" ");
parsed.AddLiteral("Test");
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Is.Not.Null);
Assert.That(arguments, Is.Empty);
}
/// <summary>
/// Tests that GetArguments returns only Token operations when all formatted items are strings.
/// </summary>
[Test]
public void GetArguments_OnlyTokens_ReturnsAllTokens()
{
// Arrange
var parsed = new ParsedTemplateString(0, 3);
parsed.AddFormatted("First", typeof(string));
parsed.AddFormatted("Second", typeof(string));
parsed.AddFormatted("Third", typeof(string));
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(3));
Assert.That(arguments[0], Is.EqualTo("First"));
Assert.That(arguments[1], Is.EqualTo("Second"));
Assert.That(arguments[2], Is.EqualTo("Third"));
}
/// <summary>
/// Tests that GetArguments returns only Value operations when all formatted items are non-strings.
/// </summary>
[Test]
public void GetArguments_OnlyValues_ReturnsAllValues()
{
// Arrange
var parsed = new ParsedTemplateString(0, 3);
parsed.AddFormatted("42", typeof(int));
parsed.AddFormatted("3.14", typeof(double));
parsed.AddFormatted("True", typeof(bool));
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(3));
Assert.That(arguments[0], Is.EqualTo("42"));
Assert.That(arguments[1], Is.EqualTo("3.14"));
Assert.That(arguments[2], Is.EqualTo("True"));
}
/// <summary>
/// Tests that GetArguments returns both Token and Value operations in correct order.
/// </summary>
[Test]
public void GetArguments_MixedTokensAndValues_ReturnsInCorrectOrder()
{
// Arrange
var parsed = new ParsedTemplateString(10, 4);
parsed.AddFormatted("First", typeof(string));
parsed.AddFormatted("42", typeof(int));
parsed.AddFormatted("Second", typeof(string));
parsed.AddFormatted("3.14", typeof(double));
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(4));
Assert.That(arguments[0], Is.EqualTo("First"));
Assert.That(arguments[1], Is.EqualTo("42"));
Assert.That(arguments[2], Is.EqualTo("Second"));
Assert.That(arguments[3], Is.EqualTo("3.14"));
}
/// <summary>
/// Tests that GetArguments filters out non-Token/Value operations and returns only formatted items.
/// </summary>
[Test]
public void GetArguments_MixedAllOperationTypes_ReturnsOnlyTokensAndValues()
{
// Arrange
var parsed = new ParsedTemplateString(30, 3);
parsed.AddLiteral("Start");
parsed.AddFormatted("Token1", typeof(string));
parsed.AddLiteral(" ");
parsed.AddFormatted("123", typeof(int));
parsed.AddLiteral("\n");
parsed.AddFormatted("Token2", typeof(string));
parsed.AddLiteral("End");
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(3));
Assert.That(arguments[0], Is.EqualTo("Token1"));
Assert.That(arguments[1], Is.EqualTo("123"));
Assert.That(arguments[2], Is.EqualTo("Token2"));
}
/// <summary>
/// Tests that GetArguments handles empty string items correctly.
/// </summary>
[Test]
public void GetArguments_EmptyStringItems_ReturnsEmptyStrings()
{
// Arrange
var parsed = new ParsedTemplateString(0, 3);
parsed.AddFormatted(string.Empty, typeof(string));
parsed.AddFormatted("NonEmpty", typeof(string));
parsed.AddFormatted(string.Empty, typeof(int));
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(3));
Assert.That(arguments[0], Is.EqualTo(string.Empty));
Assert.That(arguments[1], Is.EqualTo("NonEmpty"));
Assert.That(arguments[2], Is.EqualTo(string.Empty));
}
/// <summary>
/// Tests that GetArguments maintains order with multiple operations of same type.
/// </summary>
[Test]
public void GetArguments_MultipleOperationsSameType_MaintainsOrder()
{
// Arrange
var parsed = new ParsedTemplateString(0, 10);
for (int i = 0; i < 10; i++)
{
parsed.AddFormatted($"Item{i}", typeof(string));
}
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(10));
for (int i = 0; i < 10; i++)
{
Assert.That(arguments[i], Is.EqualTo($"Item{i}"));
}
}
/// <summary>
/// Tests that GetArguments returns items for various non-string types as Value operations.
/// </summary>
[TestCase(typeof(int), "42")]
[TestCase(typeof(double), "3.14")]
[TestCase(typeof(bool), "True")]
[TestCase(typeof(decimal), "99.99")]
[TestCase(typeof(long), "9223372036854775807")]
[TestCase(typeof(char), "A")]
[TestCase(typeof(object), "SomeObject")]
public void GetArguments_VariousNonStringTypes_ReturnsAsValues(Type type, string itemValue)
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
parsed.AddFormatted(itemValue, type);
// Act
object[] arguments = parsed.GetArguments();
// Assert
Assert.That(arguments, Has.Length.EqualTo(1));
Assert.That(arguments[0], Is.EqualTo(itemValue));
}
/// <summary>
/// Tests that AddFormatted with string type creates a Token operation.
/// Input: item = "TestToken", type = typeof(string)
/// Expected: Operation with OpType.Token is added to Operations list.
/// </summary>
[Test]
public void AddFormatted_WithStringType_AddsTokenOperation()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
const string item = "TestToken";
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
Assert.That(operations[0].Offset, Is.Zero);
Assert.That(operations[0].LineNumber, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted with non-string types creates a Value operation.
/// Input: item with various non-string types (int, double, object, bool, decimal)
/// Expected: Operation with OpType.Value is added for each type.
/// </summary>
[TestCase(typeof(int), "42")]
[TestCase(typeof(double), "3.14")]
[TestCase(typeof(object), "Object")]
[TestCase(typeof(bool), "True")]
[TestCase(typeof(decimal), "100.50")]
[TestCase(typeof(long), "9223372036854775807")]
[TestCase(typeof(char), "A")]
public void AddFormatted_WithNonStringType_AddsValueOperation(Type type, string item)
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
// Act
parsed.AddFormatted(item, type);
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Value));
Assert.That(operations[0].Item, Is.EqualTo(item));
}
/// <summary>
/// Tests that AddFormatted with empty string adds operation and maintains offset at zero.
/// Input: item = "", type = typeof(string)
/// Expected: Operation is added with empty item, offset remains 0.
/// </summary>
[Test]
public void AddFormatted_WithEmptyString_AddsOperationAndMaintainsOffset()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
const string item = "";
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
Assert.That(operations[0].Offset, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted correctly increments offset after adding operation.
/// Input: item = "Test" (length 4), type = typeof(string)
/// Expected: Offset in operation is 0, subsequent operation would have offset 4.
/// </summary>
[Test]
public void AddFormatted_SingleCall_IncrementsOffsetCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 2);
const string firstItem = "Test";
const string secondItem = "Value";
// Act
parsed.AddFormatted(firstItem, typeof(string));
parsed.AddFormatted(secondItem, typeof(int));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(2));
Assert.That(operations[0].Offset, Is.Zero);
Assert.That(operations[1].Offset, Is.EqualTo(firstItem.Length));
}
/// <summary>
/// Tests that multiple calls to AddFormatted accumulate operations correctly.
/// Input: Multiple AddFormatted calls with different items and types
/// Expected: All operations are added, offsets increment correctly.
/// </summary>
[Test]
public void AddFormatted_MultipleCalls_AccumulatesOperationsAndOffsetsCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 3);
const string item1 = "Hello";
const string item2 = "123";
const string item3 = "World";
// Act
parsed.AddFormatted(item1, typeof(string));
parsed.AddFormatted(item2, typeof(int));
parsed.AddFormatted(item3, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(3));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item1));
Assert.That(operations[0].Offset, Is.Zero);
Assert.That(operations[1].Type, Is.EqualTo(ParsedTemplateString.OpType.Value));
Assert.That(operations[1].Item, Is.EqualTo(item2));
Assert.That(operations[1].Offset, Is.EqualTo(item1.Length));
Assert.That(operations[2].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[2].Item, Is.EqualTo(item3));
Assert.That(operations[2].Offset, Is.EqualTo(item1.Length + item2.Length));
}
/// <summary>
/// Tests that AddFormatted handles strings with special characters correctly.
/// Input: item with special characters, type = typeof(string)
/// Expected: Operation is added with the exact string including special characters.
/// </summary>
[TestCase("Hello\tWorld")]
[TestCase("Line1\nLine2")]
[TestCase("Quote\"Test")]
[TestCase("Backslash\\Test")]
[TestCase("Unicode\u00A9\u00AE")]
[TestCase("Mixed!@#$%^&*()")]
public void AddFormatted_WithSpecialCharacters_AddsOperationCorrectly(string item)
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
Assert.That(operations[0].Offset, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted handles very long strings correctly.
/// Input: Very long string (1000+ characters), type = typeof(string)
/// Expected: Operation is added with correct offset increment.
/// </summary>
[Test]
public void AddFormatted_WithLongString_AddsOperationAndIncrementsOffsetCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
string longString = new('A', 10000);
// Act
parsed.AddFormatted(longString, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(longString));
Assert.That(operations[0].Item, Has.Length.EqualTo(10000));
Assert.That(operations[0].Offset, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted captures the correct line number in the operation.
/// Input: item = "Test", type = typeof(string)
/// Expected: Operation is added with LineNumber = 0 (default initial value).
/// </summary>
[Test]
public void AddFormatted_CapturesLineNumberCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
const string item = "Test";
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].LineNumber, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted with whitespace-only string adds operation correctly.
/// Input: item = " " (spaces only), type = typeof(string)
/// Expected: Operation is added as Token with the whitespace string.
/// </summary>
[Test]
public void AddFormatted_WithWhitespaceOnlyString_AddsOperationCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
const string item = " ";
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
Assert.That(operations[0].Offset, Is.Zero);
}
/// <summary>
/// Tests that AddFormatted with single character string adds operation correctly.
/// Input: item = "X" (single character), type = typeof(string)
/// Expected: Operation is added with offset incremented by 1.
/// </summary>
[Test]
public void AddFormatted_WithSingleCharacterString_AddsOperationCorrectly()
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
const string item = "X";
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
Assert.That(operations[0].Item, Has.Length.EqualTo(1));
}
/// <summary>
/// Tests that AddFormatted with control characters adds operation correctly.
/// Input: item with various control characters, type = typeof(string)
/// Expected: Operation is added with the exact string including control characters.
/// </summary>
[TestCase("\r")]
[TestCase("\n")]
[TestCase("\r\n")]
[TestCase("\t")]
[TestCase("\0")]
public void AddFormatted_WithControlCharacters_AddsOperationCorrectly(string item)
{
// Arrange
var parsed = new ParsedTemplateString(0, 1);
// Act
parsed.AddFormatted(item, typeof(string));
// Assert
var operations = parsed.Operations.ToList();
Assert.That(operations, Has.Count.EqualTo(1));
Assert.That(operations[0].Type, Is.EqualTo(ParsedTemplateString.OpType.Token));
Assert.That(operations[0].Item, Is.EqualTo(item));
}
}
}