diff --git a/.github/workflows/dotnet_test_runner.yml b/.github/workflows/dotnet_test_runner.yml index a8ba471..43de8be 100644 --- a/.github/workflows/dotnet_test_runner.yml +++ b/.github/workflows/dotnet_test_runner.yml @@ -3,7 +3,9 @@ name: .NET Test Runner on: workflow_dispatch: push: - branches: [ "main" ] + branches: [ "main" ] + pull_request: + branches: [ "main" ] jobs: @@ -34,10 +36,38 @@ jobs: with: dotnet-version: 10.x - - name: Run tests + - name: Run tests with coverage run: > dotnet test --configuration Release --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" + --collect:"XPlat Code Coverage" + --results-directory ./coverage -- - RunConfiguration.CollectSourceInformation=true \ No newline at end of file + RunConfiguration.CollectSourceInformation=true + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 + with: + directory: ./Tests/TyKonKet.BarcodeGenerator.Tests/coverage + files: '**/*.xml' + fail_ci_if_error: false + verbose: true + + - name: Generate coverage report + if: always() + run: | + dotnet tool install -g dotnet-reportgenerator-globaltool + reportgenerator \ + -reports:"coverage/**/*.xml" \ + -targetdir:"coverage/report" \ + -reporttypes:"Html;Badges" \ + -verbosity:Info + + - name: Upload coverage report artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: Tests/TyKonKet.BarcodeGenerator.Tests/coverage/report diff --git a/README.md b/README.md index d614d0c..5d4e718 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,12 @@ We welcome contributions from the community! Here's how you can help: - Improve documentation and examples - Share your use cases and feedback +**Testing:** +- Run the comprehensive test suite: `dotnet test --configuration Release` +- Generate code coverage reports: `./scripts/run-coverage.sh` (Linux/macOS) or `.\scripts\run-coverage.ps1` (Windows) +- Review [test documentation](Tests/TyKonKet.BarcodeGenerator.Tests/README.md) for testing guidelines +- Ensure 80%+ code coverage for new features + --- ## πŸ“„ License diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/.gitignore b/Tests/TyKonKet.BarcodeGenerator.Tests/.gitignore new file mode 100644 index 0000000..4b4d863 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/.gitignore @@ -0,0 +1 @@ +coverage/ \ No newline at end of file diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTest.cs deleted file mode 100644 index 1544de4..0000000 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTest.cs +++ /dev/null @@ -1,175 +0,0 @@ -ο»Ώusing SkiaSharp; -using System; -using System.IO; -using Xunit; - -namespace TyKonKet.BarcodeGenerator.Tests -{ - public class BarcodeOptionsTest - { - [Fact] - public void DefaultValues_ShouldBeCorrect() - { - var options = new BarcodeOptions(); - Assert.Equal(2, options.Margins); - Assert.Equal(BarcodeTypes.Ean8, options.Type); - Assert.Equal(30, options.Height); - Assert.Equal(5, options.Scaling); - Assert.Equal(SKColors.White, options.BackgroundColor); - Assert.Equal(SKColors.Black, options.ForegroundColor); - Assert.Null(options.TextColor); - Assert.Equal(SKColors.Black, options.EffectiveTextColor); // Should fallback to ForegroundColor - Assert.True(options.RenderText); - Assert.Equal(SKTypeface.Default, options.Typeface); - } - - [Fact] - public void TextColor_WhenSet_ShouldReturnSetValue() - { - var options = new BarcodeOptions(); - options.TextColor = SKColors.Red; - Assert.Equal(SKColors.Red, options.TextColor); - Assert.Equal(SKColors.Red, options.EffectiveTextColor); - } - - [Fact] - public void TextColor_WhenNull_ShouldFallbackToForegroundColor() - { - var options = new BarcodeOptions(); - options.ForegroundColor = SKColors.Blue; - options.TextColor = null; - Assert.Null(options.TextColor); - Assert.Equal(SKColors.Blue, options.EffectiveTextColor); - } - - [Fact] - public void TextColor_ShouldBeIndependentOfForegroundColor() - { - var options = new BarcodeOptions(); - options.ForegroundColor = SKColors.Green; - options.TextColor = SKColors.Yellow; - Assert.Equal(SKColors.Green, options.ForegroundColor); - Assert.Equal(SKColors.Yellow, options.TextColor); - Assert.Equal(SKColors.Yellow, options.EffectiveTextColor); - } - - [Fact] - public void TextColor_IntegrationTest_ShouldUseCorrectColors() - { - // Test that TextColor is actually used in barcode generation - using var barcode = new Barcode(options => - { - options.Type = BarcodeTypes.Ean13; - options.Height = 30; - options.Scaling = 1; - options.ForegroundColor = SKColors.Black; - options.TextColor = SKColors.Red; - options.RenderText = true; - }); - - var result = barcode.Encode("1234567890123"); - Assert.NotNull(barcode.Image); - Assert.Equal("1234567890128", result); // Includes check digit - } - - [Fact] - public void TextColor_BackwardCompatibility_ShouldDefaultToForegroundColor() - { - // Test that when TextColor is not set, it behaves like before - using var barcode = new Barcode(options => - { - options.Type = BarcodeTypes.Ean13; - options.Height = 30; - options.Scaling = 1; - options.ForegroundColor = SKColors.Blue; - // TextColor not set - should use ForegroundColor - options.RenderText = true; - }); - - var result = barcode.Encode("1234567890123"); - Assert.NotNull(barcode.Image); - Assert.Equal("1234567890128", result); - } - - [Fact] - public void UseTypeface_ShouldSetTypeface() - { - var options = new BarcodeOptions(); - var typeface = SKTypeface.FromFamilyName("Arial"); - options.UseTypeface(typeface); - Assert.Equal(typeface, options.Typeface); - } - - [Fact] - public void UseTypeface_ShouldThrowException_WhenLocked() - { - var options = new BarcodeOptions(); - options.Lock(); - Assert.Throws(() => options.UseTypeface(SKTypeface.FromFamilyName("Arial"))); - } - - [Fact] - public void UseTypefaceFromFile_ShouldSetTypeface() - { - var options = new BarcodeOptions(); - var path = "path/to/font.ttf"; - options.UseTypefaceFromFile(path); - Assert.NotNull(options.Typeface); - } - - [Fact] - public void UseTypefaceFromFile_ShouldThrowException_WhenLocked() - { - var options = new BarcodeOptions(); - options.Lock(); - Assert.Throws(() => options.UseTypefaceFromFile("path/to/font.ttf")); - } - - [Fact] - public void UseTypefaceFromData_ShouldSetTypeface() - { - var options = new BarcodeOptions(); - var data = SKData.Create(1); - options.UseTypefaceFromData(data); - Assert.NotNull(options.Typeface); - } - - [Fact] - public void UseTypefaceFromData_ShouldThrowException_WhenLocked() - { - var options = new BarcodeOptions(); - options.Lock(); - Assert.Throws(() => options.UseTypefaceFromData(SKData.Create(1))); - } - - [Fact] - public void UseTypefaceFromStream_ShouldSetTypeface() - { - var options = new BarcodeOptions(); - using var stream = new MemoryStream([0]); - options.UseTypefaceFromStream(stream); - Assert.NotNull(options.Typeface); - } - - [Fact] - public void UseTypefaceFromStream_ShouldThrowException_WhenLocked() - { - var options = new BarcodeOptions(); - options.Lock(); - using var stream = new MemoryStream([0]); - Assert.Throws(() => options.UseTypefaceFromStream(stream)); - } - - [Fact] - public void Lock_ShouldPreventModifications() - { - var options = new BarcodeOptions(); - options.Lock(); - Assert.Throws(() => options.UseTypeface(SKTypeface.FromFamilyName("Arial"))); - Assert.Throws(() => options.UseTypefaceFromFile("path/to/font.ttf")); - Assert.Throws(() => options.UseTypefaceFromData(SKData.Create(1))); - using var stream = new MemoryStream([0]); - Assert.Throws(() => options.UseTypefaceFromStream(stream)); - } - } -} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTests.cs new file mode 100644 index 0000000..4a7b0f8 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/BarcodeOptionsTests.cs @@ -0,0 +1,371 @@ +ο»Ώusing SkiaSharp; +using System; +using System.IO; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests +{ + /// + /// Contains unit tests for the class. + /// Tests configuration options, default values, validation, and option interactions. + /// + public class BarcodeOptionsTests + { + [Fact] + public void DefaultValues_ShouldBeCorrect() + { + var options = new BarcodeOptions(); + Assert.Equal(2, options.Margins); + Assert.Equal(BarcodeTypes.Ean8, options.Type); + Assert.Equal(30, options.Height); + Assert.Equal(5, options.Scaling); + Assert.Equal(SKColors.White, options.BackgroundColor); + Assert.Equal(SKColors.Black, options.ForegroundColor); + Assert.Null(options.TextColor); + Assert.Equal(SKColors.Black, options.EffectiveTextColor); // Should fallback to ForegroundColor + Assert.True(options.RenderText); + Assert.Equal(SKTypeface.Default, options.Typeface); + } + + [Fact] + public void TextColor_WhenSet_ShouldReturnSetValue() + { + var options = new BarcodeOptions + { + TextColor = SKColors.Red + }; + Assert.Equal(SKColors.Red, options.TextColor); + Assert.Equal(SKColors.Red, options.EffectiveTextColor); + } + + [Fact] + public void TextColor_WhenNull_ShouldFallbackToForegroundColor() + { + var options = new BarcodeOptions + { + ForegroundColor = SKColors.Blue, + TextColor = null + }; + Assert.Null(options.TextColor); + Assert.Equal(SKColors.Blue, options.EffectiveTextColor); + } + + [Fact] + public void TextColor_ShouldBeIndependentOfForegroundColor() + { + var options = new BarcodeOptions + { + ForegroundColor = SKColors.Green, + TextColor = SKColors.Yellow + }; + Assert.Equal(SKColors.Green, options.ForegroundColor); + Assert.Equal(SKColors.Yellow, options.TextColor); + Assert.Equal(SKColors.Yellow, options.EffectiveTextColor); + } + + [Fact] + public void TextColor_IntegrationTest_ShouldUseCorrectColors() + { + // Test that TextColor is actually used in barcode generation + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean13; + options.Height = 30; + options.Scaling = 1; + options.ForegroundColor = SKColors.Black; + options.TextColor = SKColors.Red; + options.RenderText = true; + }); + + var result = barcode.Encode("1234567890123"); + Assert.NotNull(barcode.Image); + Assert.Equal("1234567890128", result); // Includes check digit + } + + [Fact] + public void TextColor_BackwardCompatibility_ShouldDefaultToForegroundColor() + { + // Test that when TextColor is not set, it behaves like before + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean13; + options.Height = 30; + options.Scaling = 1; + options.ForegroundColor = SKColors.Blue; + // TextColor not set - should use ForegroundColor + options.RenderText = true; + }); + + var result = barcode.Encode("1234567890123"); + Assert.NotNull(barcode.Image); + Assert.Equal("1234567890128", result); + } + + [Fact] + public void UseTypeface_ShouldSetTypeface() + { + var options = new BarcodeOptions(); + var typeface = SKTypeface.FromFamilyName("Arial"); + options.UseTypeface(typeface); + Assert.Equal(typeface, options.Typeface); + } + + [Fact] + public void UseTypeface_ShouldThrowException_WhenLocked() + { + var options = new BarcodeOptions(); + options.Lock(); + Assert.Throws(() => options.UseTypeface(SKTypeface.FromFamilyName("Arial"))); + } + + [Fact] + public void UseTypefaceFromFile_ShouldSetTypeface() + { + var options = new BarcodeOptions(); + var path = "path/to/font.ttf"; + options.UseTypefaceFromFile(path); + Assert.NotNull(options.Typeface); + } + + [Fact] + public void UseTypefaceFromFile_ShouldThrowException_WhenLocked() + { + var options = new BarcodeOptions(); + options.Lock(); + Assert.Throws(() => options.UseTypefaceFromFile("path/to/font.ttf")); + } + + [Fact] + public void UseTypefaceFromData_ShouldSetTypeface() + { + var options = new BarcodeOptions(); + const int testDataSize = 1; + var data = SKData.Create(testDataSize); + options.UseTypefaceFromData(data); + Assert.NotNull(options.Typeface); + } + + [Fact] + public void UseTypefaceFromData_ShouldThrowException_WhenLocked() + { + var options = new BarcodeOptions(); + options.Lock(); + const int testDataSize = 1; + Assert.Throws(() => options.UseTypefaceFromData(SKData.Create(testDataSize))); + } + + [Fact] + public void UseTypefaceFromStream_ShouldSetTypeface() + { + var options = new BarcodeOptions(); + var testData = new byte[] { 0 }; + using var stream = new MemoryStream(testData); + options.UseTypefaceFromStream(stream); + Assert.NotNull(options.Typeface); + } + + [Fact] + public void UseTypefaceFromStream_ShouldThrowException_WhenLocked() + { + var options = new BarcodeOptions(); + options.Lock(); + var testData = new byte[] { 0 }; + using var stream = new MemoryStream(testData); + Assert.Throws(() => options.UseTypefaceFromStream(stream)); + } + + [Fact] + public void Lock_ShouldPreventModifications() + { + var options = new BarcodeOptions(); + options.Lock(); + Assert.Throws(() => options.UseTypeface(SKTypeface.FromFamilyName("Arial"))); + Assert.Throws(() => options.UseTypefaceFromFile("path/to/font.ttf")); + const int testDataSize = 1; + Assert.Throws(() => options.UseTypefaceFromData(SKData.Create(testDataSize))); + var testData = new byte[] { 0 }; + using var stream = new MemoryStream(testData); + Assert.Throws(() => options.UseTypefaceFromStream(stream)); + } + + #region Boundary and Negative Tests + + [Theory] + [InlineData(-1)] + [InlineData(-10)] + [InlineData(int.MinValue)] + public void Height_ShouldAcceptNegativeValues_ForEdgeCaseTesting(int height) + { + // Note: Testing that negative values are accepted by the options class + // The actual validation may occur at encoding time + var options = new BarcodeOptions + { + Height = height + }; + Assert.Equal(height, options.Height); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(int.MinValue)] + public void Scaling_ShouldNotAcceptZeroAndNegativeValues_ForEdgeCaseTesting(int scaling) + { + // Note: Testing that zero/negative values are accepted by the options class + // The actual validation may occur at encoding time + var options = new BarcodeOptions + { + Scaling = scaling + }; + Assert.Equal(1, options.Scaling); + } + + [Theory] + [InlineData(-5)] + [InlineData(-1)] + [InlineData(int.MinValue)] + public void Margins_ShouldAcceptNegativeValues_ForEdgeCaseTesting(int margins) + { + // Note: Testing that negative values are accepted by the options class + var options = new BarcodeOptions + { + Margins = margins + }; + Assert.Equal(margins, options.Margins); + } + + [Fact] + public void BarcodeOptions_ShouldHandleExtremeColorValues() + { + var options = new BarcodeOptions + { + // Test with extreme color values + BackgroundColor = new SKColor(0, 0, 0, 0), // Transparent + ForegroundColor = new SKColor(255, 255, 255, 255), // Opaque white + TextColor = new SKColor(128, 128, 128, 128) // Semi-transparent gray + }; + + Assert.Equal(new SKColor(0, 0, 0, 0), options.BackgroundColor); + Assert.Equal(new SKColor(255, 255, 255, 255), options.ForegroundColor); + Assert.Equal(new SKColor(128, 128, 128, 128), options.TextColor); + } + + [Fact] + public void UseTypeface_ShouldThrowArgumentNullException_WhenTypefaceIsNull() + { + var options = new BarcodeOptions(); + SKTypeface typeface = null!; + Assert.Throws(() => options.UseTypeface(typeface)); + } + + [Fact] + public void UseTypefaceFromFile_ShouldThrowArgumentNullException_WhenPathIsNull() + { + var options = new BarcodeOptions(); + Assert.Throws(() => options.UseTypefaceFromFile(null!)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void UseTypefaceFromFile_ShouldThrowArgumentException_WhenPathIsEmptyOrWhitespace(string path) + { + var options = new BarcodeOptions(); + Assert.Throws(() => options.UseTypefaceFromFile(path)); + } + + [Fact] + public void UseTypefaceFromData_ShouldThrowArgumentNullException_WhenDataIsNull() + { + var options = new BarcodeOptions(); + Assert.Throws(() => options.UseTypefaceFromData(null!)); + } + + [Fact] + public void UseTypefaceFromStream_ShouldThrowArgumentNullException_WhenStreamIsNull() + { + var options = new BarcodeOptions(); + SKStreamAsset stream = null!; + Assert.Throws(() => options.UseTypefaceFromStream(stream)); + } + + [Fact] + public void UseTypeface_WithFontFamily_ShouldSetTypeface() + { + // On Unix, Arial may not be available, so we use DejaVu Sans as an alternative. + var fontName = Environment.OSVersion.Platform == PlatformID.Unix ? "DejaVu Sans" : "Arial"; + + var options = new BarcodeOptions(); + options.UseTypeface(fontName); + Assert.NotNull(options.Typeface); + Assert.Equal(fontName, options.Typeface.FamilyName); + } + + [Fact] + public void UseTypeface_WithFontFamilyAndStyle_ShouldSetTypeface() + { + // On Unix, Times New Roman may not be available, so we use DejaVu Sans as an alternative. + var fontName = Environment.OSVersion.Platform == PlatformID.Unix ? "DejaVu Sans" : "Times New Roman"; + + var options = new BarcodeOptions(); + var style = new SKFontStyle(SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic); + options.UseTypeface(fontName, style); + Assert.NotNull(options.Typeface); + Assert.Equal(fontName, options.Typeface.FamilyName); + Assert.Equal(style.Weight, options.Typeface.FontWeight); + Assert.Equal(style.Slant, options.Typeface.FontSlant); + } + + [Fact] + public void UseTypeface_WithFontFamilyAndDetailedStyle_ShouldSetTypeface() + { + // On Unix, Courier New may not be available, so we use DejaVu Sans as an alternative. + var fontName = Environment.OSVersion.Platform == PlatformID.Unix ? "DejaVu Sans" : "Courier New"; + + var options = new BarcodeOptions(); + options.UseTypeface(fontName, 700, 5, SKFontStyleSlant.Upright); + Assert.NotNull(options.Typeface); + Assert.Equal(fontName, options.Typeface.FamilyName); + Assert.Equal(700, options.Typeface.FontWeight); + Assert.Equal(5, options.Typeface.FontWidth); + Assert.Equal(SKFontStyleSlant.Upright, options.Typeface.FontSlant); + } + + [Fact] + public void UseTypeface_WithFontFamilyAndEnumStyle_ShouldSetTypeface() + { + // On Unix, Arial may not be available, so we use DejaVu Sans as an alternative. + var fontName = Environment.OSVersion.Platform == PlatformID.Unix ? "DejaVu Sans" : "Arial"; + + var options = new BarcodeOptions(); + options.UseTypeface(fontName, SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic); + Assert.NotNull(options.Typeface); + Assert.Equal(fontName, options.Typeface.FamilyName); + Assert.Equal((int)SKFontStyleWeight.Normal, options.Typeface.FontWeight); + Assert.Equal((int)SKFontStyleWidth.Normal, options.Typeface.FontWidth); + Assert.Equal(SKFontStyleSlant.Italic, options.Typeface.FontSlant); + } + + [Fact] + public void UseTypefaceFromStream_WithSKStreamAsset_ShouldSetTypeface() + { + var options = new BarcodeOptions(); + var testData = new byte[] { 0, 1, 2, 3, 4 }; + using var stream = new SKMemoryStream(testData); + options.UseTypefaceFromStream(stream); + Assert.NotNull(options.Typeface); + } + + [Fact] + public void UseTypefaceFromStream_WithSKStreamAsset_ShouldThrowException_WhenLocked() + { + var options = new BarcodeOptions(); + options.Lock(); + var testData = new byte[] { 0, 1, 2, 3, 4 }; + using var stream = new SKMemoryStream(testData); + Assert.Throws(() => options.UseTypefaceFromStream(stream)); + } + + #endregion + } +} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTests.cs similarity index 97% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTests.cs index f4d41ba..46dc83c 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EanEncoderTests.cs @@ -3,7 +3,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders.Abstract { - public class EanEncodersTest + public class EanEncoderTests { [Theory] // Random diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTests.cs similarity index 99% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTests.cs index a1e9a1a..df42e2d 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Abstract/EncoderTests.cs @@ -6,7 +6,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders.Abstract { - public class EncoderTest + public class EncoderTests { [Fact] public void GetSafeFilename_ShouldRemoveInvalidCharacters() diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTests.cs similarity index 99% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTests.cs index e8698f2..b451ea3 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Code93EncoderTests.cs @@ -4,7 +4,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders { - public class Code93EncoderTest + public class Code93EncoderTests { [Theory] [InlineData("ABC-1234-ABC", "DX")] diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTest.cs deleted file mode 100644 index 05123f4..0000000 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -ο»Ώusing System; -using TyKonKet.BarcodeGenerator.Encoders; -using Xunit; - -namespace TyKonKet.BarcodeGenerator.Tests.Encoders -{ - public class Ean13EncoderTest - { - [Theory] - [InlineData("9781234567897", "9781234567897")] - [InlineData("978123456789", "9781234567897")] - [InlineData("123456789", "0001234567895")] - [InlineData("9781234567897532", "9781234567897")] - public void FormatBarcode_ShouldReturnExpectedFormattedBarcode(string input, string expected) - { - Assert.Equal(expected, Ean13Encoder.FormatBarcode(input)); - } - - [Theory] - [InlineData("6461524a")] - [InlineData("6461A524")] - [InlineData("646-1524")] - [InlineData("64.61524")] - public void ValidateCharset_ShouldThrowFormatException_ForInvalidCharset(string barcode) - { - Assert.Throws(() => { new Ean13Encoder().EnsureValidCharset(barcode); }); - } - - [Theory] - [InlineData("90361012")] - [InlineData("9781234567897")] - [InlineData("978123456789")] - [InlineData("978123456786")] - public void ValidateCharset_ShouldReturnTrue_ForValidCharset(string barcode) - { - Assert.True(new Ean13Encoder().EnsureValidCharset(barcode)); - } - - [Theory] - [InlineData("9780201379624", "10101110110001001010011100100110100111001100101010100001010001001110100101000011011001011100101")] - [InlineData("2837491746340", "10101101110111101001000100111010001011011001101010100010010111001010000100001010111001110010101")] - [InlineData("8829647458294", "10101101110011011000101100001010011101011101101010101110010011101001000110110011101001011100101")] - [InlineData("1234567891231", "10100100110111101001110101100010000101001000101010100100011101001100110110110010000101100110101")] - [InlineData("7352837294767", "10101111010111001001001100010010111101001000101010110110011101001011100100010010100001000100101")] - public void EncodeBars_ShouldReturnExpectedEncodedBars(string input, string expected) - { - Assert.Equal(expected, Ean13Encoder.EncodeBars(input)); - } - } -} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTests.cs new file mode 100644 index 0000000..4d4cd7c --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean13EncoderTests.cs @@ -0,0 +1,141 @@ +ο»Ώusing System; +using TyKonKet.BarcodeGenerator.Encoders; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Encoders +{ + /// + /// Contains unit tests for the class. + /// Tests barcode formatting, validation, encoding, and error handling for EAN-13 barcodes. + /// + public class Ean13EncoderTests + { + [Theory] + [InlineData("9781234567897", "9781234567897")] + [InlineData("978123456789", "9781234567897")] + [InlineData("123456789", "0001234567895")] + [InlineData("9781234567897532", "9781234567897")] + public void FormatBarcode_ShouldReturnExpectedFormattedBarcode(string input, string expected) + { + Assert.Equal(expected, Ean13Encoder.FormatBarcode(input)); + } + + [Theory] + [InlineData("6461524a")] + [InlineData("6461A524")] + [InlineData("646-1524")] + [InlineData("64.61524")] + public void ValidateCharset_ShouldThrowFormatException_ForInvalidCharset(string barcode) + { + Assert.Throws(() => { new Ean13Encoder().EnsureValidCharset(barcode); }); + } + + [Theory] + [InlineData("90361012")] + [InlineData("9781234567897")] + [InlineData("978123456789")] + [InlineData("978123456786")] + public void ValidateCharset_ShouldReturnTrue_ForValidCharset(string barcode) + { + Assert.True(new Ean13Encoder().EnsureValidCharset(barcode)); + } + + [Theory] + [InlineData("9780201379624", "10101110110001001010011100100110100111001100101010100001010001001110100101000011011001011100101")] + [InlineData("2837491746340", "10101101110111101001000100111010001011011001101010100010010111001010000100001010111001110010101")] + [InlineData("8829647458294", "10101101110011011000101100001010011101011101101010101110010011101001000110110011101001011100101")] + [InlineData("1234567891231", "10100100110111101001110101100010000101001000101010100100011101001100110110110010000101100110101")] + [InlineData("7352837294767", "10101111010111001001001100010010111101001000101010110110011101001011100100010010100001000100101")] + public void EncodeBars_ShouldReturnExpectedEncodedBars(string input, string expected) + { + Assert.Equal(expected, Ean13Encoder.EncodeBars(input)); + } + + #region Boundary and Negative Tests + + [Fact] + public void FormatBarcode_ShouldThrowArgumentNullException_WhenInputIsNull() + { + Assert.Throws(() => Ean13Encoder.FormatBarcode(null!)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void FormatBarcode_ShouldThrowArgumentException_WhenInputIsEmptyOrWhitespace(string input) + { + Assert.Throws(() => Ean13Encoder.FormatBarcode(input)); + } + + [Theory] + [InlineData("abc")] + [InlineData("12a456")] + [InlineData("!@#$%")] + [InlineData("123-456")] + [InlineData("123.456.789")] + public void ValidateCharset_ShouldThrowFormatException_ForVariousInvalidInputs(string barcode) + { + Assert.Throws(() => new Ean13Encoder().EnsureValidCharset(barcode)); + } + + [Theory] + [InlineData("0")] + [InlineData("12")] + [InlineData("123")] + [InlineData("1234")] + [InlineData("12345")] + [InlineData("123456")] + [InlineData("1234567")] + [InlineData("12345678")] + [InlineData("123456789")] + [InlineData("1234567890")] + [InlineData("12345678901")] + [InlineData("123456789012")] + [InlineData("1234567890123")] + public void FormatBarcode_ShouldHandleVariousLengths(string input) + { + // Should not throw for numeric inputs of various lengths + var result = Ean13Encoder.FormatBarcode(input); + + // Result should always be 13 digits + Assert.Equal(13, result.Length); + Assert.All(result, c => Assert.True(char.IsDigit(c))); + } + + [Theory] + [InlineData("0000000000000")] + [InlineData("9999999999999")] + [InlineData("1111111111111")] + public void FormatBarcode_ShouldHandleRepeatingDigits(string input) + { + var result = Ean13Encoder.FormatBarcode(input); + Assert.Equal(13, result.Length); + Assert.All(result, c => Assert.True(char.IsDigit(c))); + } + + [Fact] + public void EncodeBars_ShouldThrowArgumentNullException_WhenInputIsNull() + { + Assert.Throws(() => Ean13Encoder.EncodeBars(null!)); + } + + [Theory] + [InlineData("")] + [InlineData("123456789012")] // 12 digits instead of 13 + [InlineData("12345678901234")] // 14 digits instead of 13 + public void EncodeBars_ShouldThrowArgumentException_ForInvalidLength(string input) + { + Assert.Throws(() => Ean13Encoder.EncodeBars(input)); + } + + [Fact] + public void Constructor_ShouldCreateInstanceWithoutException() + { + // Test that the encoder can be instantiated + var encoder = new Ean13Encoder(); + Assert.NotNull(encoder); + } + + #endregion + } +} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTests.cs similarity index 98% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTests.cs index e560e38..8d70358 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Ean8EncoderTests.cs @@ -4,7 +4,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders { - public class Ean8EncoderTest + public class Ean8EncoderTests { [Theory] diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTests.cs similarity index 98% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTests.cs index c8f0586..60b18b6 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/Isbn13EncoderTests.cs @@ -4,7 +4,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders { - public class Isbn13EncoderTest + public class Isbn13EncoderTests { [Theory] [InlineData("9781234567897", "978123456789")] diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTests.cs similarity index 98% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTests.cs index dd41561..592c1d7 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Encoders/UpcaEncoderTests.cs @@ -4,7 +4,7 @@ namespace TyKonKet.BarcodeGenerator.Tests.Encoders { - public class UpcaEncoderTest + public class UpcaEncoderTests { [Theory] [InlineData("978123456786", "978123456786")] diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTest.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTests.cs similarity index 93% rename from Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTest.cs rename to Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTests.cs index 47ef7c3..9da1064 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTest.cs +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Fonts/FontFamilyTests.cs @@ -6,7 +6,11 @@ namespace TyKonKet.BarcodeGenerator.Tests.Fonts { - public class FontFamilyTest + /// + /// Contains unit tests for the class and enum. + /// Tests font family implicit conversions, platform-specific behavior, and enum mappings. + /// + public class FontFamilyTests { public static IEnumerable ValidateFontFamily_ShouldReturnExpectedFamilyName_Data() { diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Integration/BarcodeIntegrationTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Integration/BarcodeIntegrationTests.cs new file mode 100644 index 0000000..ec546d7 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Integration/BarcodeIntegrationTests.cs @@ -0,0 +1,269 @@ +using SkiaSharp; +using System; +using System.IO; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Integration +{ + /// + /// Contains integration tests for the complete barcode generation workflow. + /// Tests end-to-end functionality including encoding, rendering, and exporting. + /// + public class BarcodeIntegrationTests : IDisposable + { + private readonly string tempDirectory; + + public BarcodeIntegrationTests() + { + tempDirectory = Path.Combine(Path.GetTempPath(), "BarcodeGenerator_Tests", Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDirectory); + } + + public void Dispose() + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + + #region Full Workflow Tests + + [Theory] + [InlineData(BarcodeTypes.Ean13, "1234567890123", "1234567890128")] + [InlineData(BarcodeTypes.Ean8, "12345678", "12345670")] + [InlineData(BarcodeTypes.Upca, "123456789012", "123456789012")] + [InlineData(BarcodeTypes.Isbn13, "9781234567897", "9781234567897")] + [InlineData(BarcodeTypes.Code93, "HELLO123", "HELLO1237N")] + public void FullWorkflow_ShouldEncodeAndGenerateImage_ForAllBarcodeTypes(BarcodeTypes type, string input, string expectedOutput) + { + // Arrange & Act + using var barcode = new Barcode(options => + { + options.Type = type; + options.Height = 50; + options.Scaling = 2; + options.RenderText = true; + options.BackgroundColor = SKColors.White; + options.ForegroundColor = SKColors.Black; + }); + + var result = barcode.Encode(input); + + // Assert + Assert.Equal(expectedOutput, result); + Assert.NotNull(barcode.Image); + Assert.True(barcode.Image.Width > 0); + Assert.True(barcode.Image.Height > 0); + } + + [Fact] + public void ExportToPng_ShouldCreateValidImageFile() + { + // Arrange + var fileName = Path.Combine(tempDirectory, "test_barcode.png"); + + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean13; + options.Height = 30; + options.Scaling = 5; + options.RenderText = true; + }); + + // Act + var result = barcode.Encode("1234567890123"); + barcode.Export(fileName, SKEncodedImageFormat.Png, 100); + + // Assert + Assert.True(File.Exists(fileName)); + + var fileInfo = new FileInfo(fileName); + Assert.True(fileInfo.Length > 0); + + // Verify it's a valid PNG by reading the header + using var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + const int pngHeaderSize = 8; + var pngHeader = new byte[pngHeaderSize]; + var bytesRead = fileStream.Read(pngHeader, 0, pngHeaderSize); + + // Ensure we read the expected number of bytes + Assert.Equal(pngHeaderSize, bytesRead); + + // PNG signature: 137 80 78 71 13 10 26 10 + var expectedPngSignature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; + Assert.Equal(expectedPngSignature, pngHeader); + } + + [Fact] + public void ExportToJpeg_ShouldCreateValidImageFile() + { + // Arrange + var fileName = Path.Combine(tempDirectory, "test_barcode.jpg"); + + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Code93; + options.Height = 40; + options.Scaling = 3; + options.RenderText = false; + }); + + // Act + var result = barcode.Encode("HELLO"); + barcode.Export(fileName, SKEncodedImageFormat.Jpeg, 85); + + // Assert + Assert.True(File.Exists(fileName)); + + var fileInfo = new FileInfo(fileName); + Assert.True(fileInfo.Length > 0); + + // Verify it's a valid JPEG by checking the header + using var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + const int jpegHeaderSize = 2; + var jpegHeader = new byte[jpegHeaderSize]; + var bytesRead = fileStream.Read(jpegHeader, 0, jpegHeaderSize); + + // Ensure we read the expected number of bytes + Assert.Equal(jpegHeaderSize, bytesRead); + + // JPEG header: FF D8 + var expectedJpegSignature = new byte[] { 0xFF, 0xD8 }; + Assert.Equal(expectedJpegSignature, jpegHeader); + } + + [Fact] + public void ExportWithPlaceholders_ShouldReplaceTokensCorrectly() + { + // Arrange + var fileName = Path.Combine(tempDirectory, "{barcode}_{quality}.{format}"); + + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean8; + options.Height = 25; + options.Scaling = 4; + }); + + // Act + var result = barcode.Encode("12345678"); + barcode.Export(fileName, SKEncodedImageFormat.Png, 95); + + // Assert + var expectedFileName = Path.Combine(tempDirectory, "12345670_95.png"); + Assert.True(File.Exists(expectedFileName)); + } + + #endregion + + #region Error Handling Tests + + [Fact] + public void Encode_ShouldThrowArgumentNullException_WhenBarcodeIsNull() + { + using var barcode = new Barcode(); + Assert.Throws(() => barcode.Encode(null!)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Encode_ShouldThrowArgumentException_WhenBarcodeIsEmptyOrWhitespace(string input) + { + using var barcode = new Barcode(); + Assert.Throws(() => barcode.Encode(input)); + } + + [Fact] + public void Export_ShouldThrowInvalidOperationException_WhenNoImageGenerated() + { + // Arrange + var fileName = Path.Combine(tempDirectory, "test.png"); + + using var barcode = new Barcode(); + // Don't encode anything - Image should be null + + // Act & Assert + Assert.Throws(() => + barcode.Export(fileName, SKEncodedImageFormat.Png, 100)); + } + + [Fact] + public void Export_ShouldThrowArgumentNullException_WhenFileNameIsNull() + { + using var barcode = new Barcode(); + barcode.Encode("12345678"); + + string fileName = null!; + Assert.Throws(() => + barcode.Export(fileName, SKEncodedImageFormat.Png, 100)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Export_ShouldThrowArgumentException_WhenFileNameIsEmptyOrWhitespace(string fileName) + { + using var barcode = new Barcode(); + barcode.Encode("12345678"); + + Assert.Throws(() => + barcode.Export(fileName, SKEncodedImageFormat.Png, 100)); + } + + #endregion + + #region Performance and Stress Tests + + [Fact] + public void MultipleEncodings_ShouldWorkWithSameBarcodeInstance() + { + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean13; + options.Height = 30; + options.Scaling = 2; + }); + + // First encoding + var result1 = barcode.Encode("1111111111116"); + Assert.NotNull(barcode.Image); + var firstImageHash = barcode.Image.GetHashCode(); + + // Second encoding should replace the first + var result2 = barcode.Encode("2222222222228"); + Assert.NotNull(barcode.Image); + var secondImageHash = barcode.Image.GetHashCode(); + + // Images should be different + Assert.NotEqual(firstImageHash, secondImageHash); + Assert.Equal("1111111111116", result1); + Assert.Equal("2222222222222", result2); + } + + [Theory] + [InlineData(1, 1)] + [InlineData(10, 10)] + [InlineData(100, 5)] + [InlineData(500, 1)] + public void ExtremeScalingAndHeight_ShouldNotCrash(int height, int scaling) + { + // Test that extreme values don't cause crashes + using var barcode = new Barcode(options => + { + options.Type = BarcodeTypes.Ean8; + options.Height = height; + options.Scaling = scaling; + options.RenderText = false; // Disable text for extreme cases + }); + + // Should not throw + var result = barcode.Encode("12345678"); + Assert.NotNull(result); + Assert.NotNull(barcode.Image); + } + + #endregion + } +} \ No newline at end of file diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/README.md b/Tests/TyKonKet.BarcodeGenerator.Tests/README.md new file mode 100644 index 0000000..899f717 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/README.md @@ -0,0 +1,200 @@ +# BarcodeGenerator Test Suite Documentation + +## Overview + +This document describes the testing approach, conventions, and organization for the BarcodeGenerator library test suite. + +## Test Structure and Organization + +### File Naming Convention +All test files follow the `Tests.cs` naming convention: +- `BarcodeOptionsTests.cs` - Tests for `BarcodeOptions` class +- `Ean13EncoderTests.cs` - Tests for `Ean13Encoder` class +- `FontFamilyTests.cs` - Tests for `FontFamily` class + +### Directory Structure +``` +Tests/TyKonKet.BarcodeGenerator.Tests/ +β”œβ”€β”€ BarcodeOptionsTests.cs # Core options configuration tests +β”œβ”€β”€ EncodesExtensionsTests.cs # Extension methods tests +β”œβ”€β”€ Integration/ +β”‚ └── BarcodeIntegrationTests.cs # End-to-end workflow tests +β”œβ”€β”€ Encoders/ +β”‚ β”œβ”€β”€ Abstract/ +β”‚ β”‚ β”œβ”€β”€ EncoderTests.cs # Base encoder functionality tests +β”‚ β”‚ └── EanEncoderTests.cs # Common EAN encoder tests +β”‚ β”œβ”€β”€ Code93EncoderTests.cs # CODE-93 specific tests +β”‚ β”œβ”€β”€ Ean13EncoderTests.cs # EAN-13 specific tests +β”‚ β”œβ”€β”€ Ean8EncoderTests.cs # EAN-8 specific tests +β”‚ β”œβ”€β”€ Isbn13EncoderTests.cs # ISBN-13 specific tests +β”‚ └── UpcaEncoderTests.cs # UPC-A specific tests +└── Fonts/ + └── FontFamilyTests.cs # Font system tests +``` + +## Test Categories + +### 1. Unit Tests +- **Location**: Individual encoder test files and options tests +- **Purpose**: Test individual components in isolation +- **Coverage**: Core functionality, validation, formatting, encoding + +### 2. Integration Tests +- **Location**: `Integration/BarcodeIntegrationTests.cs` +- **Purpose**: Test complete workflows end-to-end +- **Coverage**: Full barcode generation, image export, file validation + +### 3. Boundary and Negative Tests +- **Location**: Embedded within relevant test files using `#region` sections +- **Purpose**: Test edge cases, invalid inputs, and error conditions +- **Coverage**: Null values, empty strings, extreme values, invalid formats + +## Test Method Naming Conventions + +### Pattern: `Should__When_` +- `DefaultValues_ShouldBeCorrect()` - Tests default value initialization +- `FormatBarcode_ShouldReturnExpectedFormattedBarcode()` - Tests formatting behavior +- `ValidateCharset_ShouldThrowFormatException_ForInvalidCharset()` - Tests error conditions + +### Pattern: `_` +- `Encode_ShouldThrowArgumentNullException_WhenBarcodeIsNull()` - Specific method scenario testing + +## Test Data and Parameterization + +### Theory Tests with InlineData +Preferred for testing multiple scenarios with the same logic: +```csharp +[Theory] +[InlineData("1234567890123", "1234567890128")] +[InlineData("978123456789", "9781234567897")] +public void FormatBarcode_ShouldReturnExpectedFormattedBarcode(string input, string expected) +``` + +### MemberData for Complex Test Cases +Used when test data requires complex setup or when data is reused: +```csharp +[Theory] +[MemberData(nameof(GetTestData))] +public void ComplexTest(ComplexType data) +``` + +## Code Coverage + +### Configuration +- **Tool**: Coverlet with multiple output formats (OpenCover, Cobertura, JSON, LCOV) +- **Threshold**: 80% minimum for line, branch, and method coverage +- **Reports**: Generated in `$(OutputPath)coverage/` directory +- **CI Integration**: Coverage data collected automatically during test runs + +### Coverage Areas +1. **Core Library**: All public APIs and critical internal logic +2. **Encoders**: All barcode type implementations +3. **Options**: Configuration and validation logic +4. **Error Handling**: Exception paths and edge cases + +## Error Handling Test Patterns + +### Null Parameter Tests +```csharp +[Fact] +public void Method_ShouldThrowArgumentNullException_WhenParameterIsNull() +{ + Assert.Throws(() => Method(null!)); +} +``` + +### Invalid Input Tests +```csharp +[Theory] +[InlineData("")] +[InlineData(" ")] +public void Method_ShouldThrowArgumentException_WhenInputIsInvalid(string input) +{ + Assert.Throws(() => Method(input)); +} +``` + +### Format Validation Tests +```csharp +[Theory] +[InlineData("abc")] +[InlineData("123-456")] +public void ValidateCharset_ShouldThrowFormatException_ForInvalidCharset(string barcode) +{ + Assert.Throws(() => encoder.EnsureValidCharset(barcode)); +} +``` + +## Integration Test Patterns + +### File System Tests +- Use temporary directories for all file operations +- Implement `IDisposable` to clean up test artifacts +- Validate actual file contents, not just existence + +### Image Validation +- Verify file headers for correct format (PNG, JPEG) +- Check file size and dimensions +- Validate metadata when applicable + +### End-to-End Workflows +- Test complete barcode generation pipeline +- Include encoding, rendering, and export steps +- Verify both input validation and output correctness + +## Best Practices + +### 1. Test Independence +- Each test should be able to run in isolation +- No dependencies between test methods +- Use `IDisposable` for cleanup when needed + +### 2. Descriptive Assertions +- Use specific assertion messages when helpful +- Prefer multiple specific asserts over single complex ones +- Test both positive and negative cases + +### 3. Performance Considerations +- Keep individual tests fast (< 100ms typically) +- Use `[Theory]` to reduce test method count +- Group related assertions in single test methods + +### 4. Documentation +- Add XML documentation to test classes describing their purpose +- Use regions to organize related test methods +- Comment complex test setups or unusual scenarios + +## Running Tests + +### Local Development +```bash +# Run all tests +dotnet test --configuration Release + +# Run with coverage +dotnet test --configuration Release --collect:"XPlat Code Coverage" + +# Run specific test class +dotnet test --filter "ClassName=BarcodeOptionsTests" +``` + +### CI/CD Integration +Tests are automatically run in GitHub Actions with: +- Code coverage collection +- Test result reporting +- Performance regression detection (via benchmarks) + +## Future Improvements + +### Planned Enhancements +1. **Mocking**: Add mocking for external dependencies (file system, graphics) +2. **Property-Based Testing**: Add property-based tests for encoder validation +3. **Visual Regression Testing**: Add image comparison tests for barcode output +4. **Performance Testing**: Expand benchmark coverage for all encoders +5. **Mutation Testing**: Add mutation testing to validate test quality + +### Coverage Goals +- Maintain 90%+ line coverage +- 85%+ branch coverage +- 100% coverage of public APIs +- Full error path coverage for validation logic \ No newline at end of file diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/TyKonKet.BarcodeGenerator.Tests.csproj b/Tests/TyKonKet.BarcodeGenerator.Tests/TyKonKet.BarcodeGenerator.Tests.csproj index ed7489e..b80bc09 100644 --- a/Tests/TyKonKet.BarcodeGenerator.Tests/TyKonKet.BarcodeGenerator.Tests.csproj +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/TyKonKet.BarcodeGenerator.Tests.csproj @@ -4,6 +4,14 @@ net10.0 false True + + true + $(OutputPath)coverage/ + opencover,cobertura,json,lcov + 80 + line,branch,method + minimum + **/bin/**/*,**/obj/**/* @@ -14,8 +22,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/EncodersFactoryTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/EncodersFactoryTests.cs new file mode 100644 index 0000000..874fdd9 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/EncodersFactoryTests.cs @@ -0,0 +1,58 @@ +using System; +using TyKonKet.BarcodeGenerator.Encoders; +using TyKonKet.BarcodeGenerator.Utils; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Utils +{ + public class EncodersFactoryTests + { + [Theory] + [InlineData(BarcodeTypes.Ean13, typeof(Ean13Encoder))] + [InlineData(BarcodeTypes.Ean8, typeof(Ean8Encoder))] + [InlineData(BarcodeTypes.Upca, typeof(UpcaEncoder))] + [InlineData(BarcodeTypes.Isbn13, typeof(Isbn13Encoder))] + [InlineData(BarcodeTypes.Code93, typeof(Code93Encoder))] + public void Create_ShouldReturnCorrectEncoderInstance(BarcodeTypes barcodeType, Type expectedEncoderType) + { + // Arrange + var options = new BarcodeOptions { Type = barcodeType }; + + // Act + var encoder = EncodersFactory.Create(options); + + // Assert + Assert.NotNull(encoder); + Assert.IsAssignableFrom(expectedEncoderType, encoder); + } + + [Fact] + public void Create_ShouldThrowInvalidOperationException_ForUnknownEncoderType() + { + // Arrange + var options = new BarcodeOptions { Type = (BarcodeTypes)999 }; // An invalid enum value + + // Act & Assert + var exception = Assert.Throws(() => EncodersFactory.Create(options)); + Assert.Contains("isn't a known Encoder type", exception.Message); + } + + [Fact] + public void Create_ShouldUseCacheForRepeatedCalls() + { + // Arrange + var options = new BarcodeOptions { Type = BarcodeTypes.Ean13 }; + + // Act + var encoder1 = EncodersFactory.Create(options); + var encoder2 = EncodersFactory.Create(options); + + // Assert + Assert.NotNull(encoder1); + Assert.NotNull(encoder2); + Assert.IsType(encoder1); + Assert.IsType(encoder2); + Assert.NotSame(encoder1, encoder2); // Should be new instances + } + } +} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/ExtensionsTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/ExtensionsTests.cs new file mode 100644 index 0000000..19021cd --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/ExtensionsTests.cs @@ -0,0 +1,42 @@ +using System; +using TyKonKet.BarcodeGenerator.Utils; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Utils +{ + public class ExtensionsTests + { + [Theory] + [InlineData('0', 0)] + [InlineData('1', 1)] + [InlineData('2', 2)] + [InlineData('3', 3)] + [InlineData('4', 4)] + [InlineData('5', 5)] + [InlineData('6', 6)] + [InlineData('7', 7)] + [InlineData('8', 8)] + [InlineData('9', 9)] + public void ToInt_ShouldConvertNumericCharToInteger(char input, int expected) + { + // Act + var result = input.ToInt(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData('a')] + [InlineData('Z')] + [InlineData(' ')] + [InlineData('-')] + [InlineData('!')] + public void ToInt_ShouldThrowArgumentOutOfRangeException_ForNonNumericChar(char input) + { + // Act & Assert + var exception = Assert.Throws(() => input.ToInt()); + Assert.Equal("digitChar", exception.ParamName); + } + } +} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/RegexCacheTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/RegexCacheTests.cs new file mode 100644 index 0000000..0175b24 --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/RegexCacheTests.cs @@ -0,0 +1,79 @@ +using TyKonKet.BarcodeGenerator.Utils; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Utils +{ + public class RegexCacheTests + { + [Theory] + [InlineData("1234567890")] + [InlineData("0")] + [InlineData("9")] + [InlineData("01234567890123456789")] + public void EanAllowedCharsetRegex_ShouldMatch_ValidEanStrings(string input) + { + // Act + var isMatch = RegexCache.EanAllowedCharsetRegex.IsMatch(input); + + // Assert + Assert.True(isMatch); + } + + [Theory] + [InlineData("123a")] + [InlineData("abc")] + [InlineData("123-456")] + [InlineData("123 456")] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void EanAllowedCharsetRegex_ShouldNotMatch_InvalidEanStrings(string input) + { + // Act & Assert + if (input is null) + { + Assert.Throws(() => RegexCache.EanAllowedCharsetRegex.IsMatch(input)); + } + else + { + var isMatch = RegexCache.EanAllowedCharsetRegex.IsMatch(input); + Assert.False(isMatch); + } + } + + [Theory] + [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZ")] + [InlineData("0123456789")] + [InlineData(" .$+%/-")] + [InlineData("CODE 93 TEST")] + [InlineData("TEST-1/A+B$C.")] + public void Code93AllowedCharsetRegex_ShouldMatch_ValidCode93Strings(string input) + { + // Act + var isMatch = RegexCache.Code93AllowedCharsetRegex.IsMatch(input); + + // Assert + Assert.True(isMatch); + } + + [Theory] + [InlineData("abcdefghijklmnopqrstuvwxyz")] // Lowercase + [InlineData("Test")] // Mixed case + [InlineData("CODE_93")] // Underscore + [InlineData("TEST!")] // Exclamation mark + [InlineData(null)] + public void Code93AllowedCharsetRegex_ShouldNotMatch_InvalidCode93Strings(string input) + { + // Act & Assert + if (input is null) + { + Assert.Throws(() => RegexCache.Code93AllowedCharsetRegex.IsMatch(input)); + } + else + { + var isMatch = RegexCache.Code93AllowedCharsetRegex.IsMatch(input); + Assert.False(isMatch); + } + } + } +} diff --git a/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/SkiaExtensionsTests.cs b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/SkiaExtensionsTests.cs new file mode 100644 index 0000000..b60b97e --- /dev/null +++ b/Tests/TyKonKet.BarcodeGenerator.Tests/Utils/SkiaExtensionsTests.cs @@ -0,0 +1,46 @@ +using SkiaSharp; +using TyKonKet.BarcodeGenerator.Utils; +using Xunit; + +namespace TyKonKet.BarcodeGenerator.Tests.Utils +{ + public class SkiaExtensionsTests + { + [Theory] + [InlineData(SKEncodedImageFormat.Jpeg, "jpg")] + [InlineData(SKEncodedImageFormat.Png, "png")] + [InlineData(SKEncodedImageFormat.Webp, "webp")] + [InlineData(SKEncodedImageFormat.Bmp, "bmp")] + [InlineData(SKEncodedImageFormat.Gif, "gif")] + [InlineData(SKEncodedImageFormat.Ico, "ico")] + [InlineData(SKEncodedImageFormat.Heif, "heif")] + [InlineData(SKEncodedImageFormat.Jpegxl, "jxl")] + [InlineData(SKEncodedImageFormat.Avif, "avif")] + [InlineData(SKEncodedImageFormat.Dng, "dng")] + [InlineData(SKEncodedImageFormat.Astc, "astc")] + [InlineData(SKEncodedImageFormat.Ktx, "ktx")] + [InlineData(SKEncodedImageFormat.Pkm, "pkm")] + [InlineData(SKEncodedImageFormat.Wbmp, "wbmp")] + public void ToFileExtension_ShouldReturnCorrectExtension(SKEncodedImageFormat format, string expectedExtension) + { + // Act + var extension = format.ToFileExtension(); + + // Assert + Assert.Equal(expectedExtension, extension); + } + + [Fact] + public void ToFileExtension_ShouldHandleUndefinedFormat() + { + // Arrange + var undefinedFormat = (SKEncodedImageFormat)999; // An undefined value + + // Act + var extension = undefinedFormat.ToFileExtension(); + + // Assert + Assert.Equal("999", extension.ToLower()); + } + } +} diff --git a/TyKonKet.BarcodeGenerator/Barcode.cs b/TyKonKet.BarcodeGenerator/Barcode.cs index ba85906..07d581e 100644 --- a/TyKonKet.BarcodeGenerator/Barcode.cs +++ b/TyKonKet.BarcodeGenerator/Barcode.cs @@ -68,6 +68,7 @@ public string Encode(string barcode) /// Thrown when is . /// Thrown when the directory for the file path does not exist and cannot be created. /// Thrown when an I/O error occurs during file operations. + /// Thrown when is empty or whitespace. public void Export(string filePath, SKEncodedImageFormat format = SKEncodedImageFormat.Png, int quality = 100) { this.barcodeEncoder.Export(filePath, format, quality); diff --git a/TyKonKet.BarcodeGenerator/BarcodeOptions.cs b/TyKonKet.BarcodeGenerator/BarcodeOptions.cs index c38da76..949957b 100644 --- a/TyKonKet.BarcodeGenerator/BarcodeOptions.cs +++ b/TyKonKet.BarcodeGenerator/BarcodeOptions.cs @@ -61,7 +61,7 @@ public SKTypeface Typeface public int Scaling { get => this.scalingFactor; - set => this.scalingFactor = Math.Max(Math.Abs(value), 1); + set => this.scalingFactor = Math.Max(value, 1); } /// @@ -96,6 +96,7 @@ public int Scaling /// /// The typeface to use. /// Thrown when the options are locked and cannot be modified. + /// Thrown when is null. public void UseTypeface(SKTypeface typeface) { if (this.locked) @@ -103,6 +104,11 @@ public void UseTypeface(SKTypeface typeface) throw new InvalidOperationException("Options are locked and cannot be modified."); } + if (typeface is null) + { + throw new ArgumentNullException(nameof(typeface), "Typeface cannot be null."); + } + this.typeface = typeface; } @@ -191,6 +197,8 @@ public void UseTypeface(FontFamily fontFamily, SKFontStyleWeight weight, SKFontS /// The path to the font file. /// The index of the font in the file. /// Thrown when the options are locked and cannot be modified. + /// Thrown when is null. + /// Thrown when is empty or whitespace. public void UseTypefaceFromFile(string path, int index = 0) { if (this.locked) @@ -198,6 +206,16 @@ public void UseTypefaceFromFile(string path, int index = 0) throw new InvalidOperationException("Options are locked and cannot be modified."); } + if (path is null) + { + throw new ArgumentNullException(nameof(path), "Font file path cannot be null."); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Font file path cannot be null or whitespace.", nameof(path)); + } + this.typeface = SKTypeface.FromFile(path, index); } diff --git a/TyKonKet.BarcodeGenerator/Encoders/Abstract/Encoder.cs b/TyKonKet.BarcodeGenerator/Encoders/Abstract/Encoder.cs index 6ba82bb..b231041 100644 --- a/TyKonKet.BarcodeGenerator/Encoders/Abstract/Encoder.cs +++ b/TyKonKet.BarcodeGenerator/Encoders/Abstract/Encoder.cs @@ -83,8 +83,19 @@ protected Encoder(BarcodeOptions options) /// Thrown when is . /// Thrown when the directory for the file path does not exist and cannot be created. /// Thrown when an I/O error occurs during file operations. + /// Thrown when is empty or whitespace. public virtual void Export(string filePath, SKEncodedImageFormat format, int quality) { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException("File path cannot be empty or whitespace.", nameof(filePath)); + } + if (this.Image == null || this.Barcode == null) { throw new InvalidOperationException("The barcode must be encoded before exported"); diff --git a/TyKonKet.BarcodeGenerator/Encoders/Ean13Encoder.cs b/TyKonKet.BarcodeGenerator/Encoders/Ean13Encoder.cs index e23f114..6f2485a 100644 --- a/TyKonKet.BarcodeGenerator/Encoders/Ean13Encoder.cs +++ b/TyKonKet.BarcodeGenerator/Encoders/Ean13Encoder.cs @@ -294,8 +294,20 @@ private void RenderBarcodeText() /// /// The barcode string to format. /// The formatted barcode string. + /// Thrown when the barcode is null. + /// Thrown when the barcode is not 13 digits. internal static string FormatBarcode(string barcode) { + if (barcode is null) + { + throw new ArgumentNullException(nameof(barcode)); + } + + if (string.IsNullOrWhiteSpace(barcode)) + { + throw new ArgumentException("Barcode cannot be null or whitespace.", nameof(barcode)); + } + return FormatBarcode(barcode, 13); } @@ -304,8 +316,20 @@ internal static string FormatBarcode(string barcode) /// /// The barcode string to encode. /// The encoded bars string. + /// Thrown when the barcode is null. + /// Thrown when the barcode is not 13 digits. internal static string EncodeBars(string barcode) { + if (barcode is null) + { + throw new ArgumentNullException(nameof(barcode)); + } + + if (barcode.Length != 13) + { + throw new ArgumentException("EAN-13 barcode must be 13 digits.", nameof(barcode)); + } + var left = new StringBuilder(42); var right = new StringBuilder(42); var encodingTable = EncodingTable[barcode[0].ToInt()]; diff --git a/scripts/run-coverage.ps1 b/scripts/run-coverage.ps1 new file mode 100644 index 0000000..05e5c83 --- /dev/null +++ b/scripts/run-coverage.ps1 @@ -0,0 +1,121 @@ +# Code Coverage Script for BarcodeGenerator Tests (PowerShell) +# This script runs tests with code coverage and generates HTML reports + +param( + [switch]$OpenReport = $true +) + +Write-Host "πŸ§ͺ Running BarcodeGenerator Tests with Code Coverage" -ForegroundColor Green +Write-Host "==============================================" + +# Navigate to test directory +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$testPath = Join-Path (Split-Path -Parent $scriptPath) "Tests\TyKonKet.BarcodeGenerator.Tests" + +if (-not (Test-Path $testPath)) { + Write-Host "❌ Test directory not found: $testPath" -ForegroundColor Red + exit 1 +} + +Set-Location $testPath + +# Clean previous coverage results +Write-Host "🧹 Cleaning previous coverage results..." -ForegroundColor Yellow +try { + if (Test-Path "coverage") { Remove-Item -Recurse -Force "coverage" } + if (Test-Path "TestResults") { Remove-Item -Recurse -Force "TestResults" } +} +catch { + Write-Host "❌ Failed to clean previous results: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} + +# Run tests with coverage +Write-Host "πŸš€ Running tests with coverage collection..." -ForegroundColor Yellow +try { + dotnet test ` + --configuration Release ` + --collect:"XPlat Code Coverage" ` + --results-directory ./coverage ` + --logger "console;verbosity=minimal" ` + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Tests failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE + } +} +catch { + Write-Host "❌ Failed to run tests: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} + +# Check if reportgenerator is installed +$reportGeneratorInstalled = $false +try { + $null = Get-Command reportgenerator -ErrorAction Stop + $reportGeneratorInstalled = $true +} +catch { + Write-Host "πŸ“¦ Installing ReportGenerator global tool..." -ForegroundColor Yellow + try { + dotnet tool install -g dotnet-reportgenerator-globaltool + if ($LASTEXITCODE -eq 0) { + $reportGeneratorInstalled = $true + } + else { + Write-Host "❌ Failed to install ReportGenerator tool with exit code $LASTEXITCODE" -ForegroundColor Red + exit 1 + } + } + catch { + Write-Host "❌ Failed to install ReportGenerator tool: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } +} + +if ($reportGeneratorInstalled) { + # Generate HTML report + Write-Host "πŸ“Š Generating HTML coverage report..." -ForegroundColor Yellow + try { + reportgenerator ` + -reports:"coverage/**/coverage.opencover.xml" ` + -targetdir:"coverage/report" ` + -reporttypes:"Html;Badges;TextSummary" ` + -verbosity:Info + + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to generate coverage report with exit code $LASTEXITCODE" -ForegroundColor Red + exit 1 + } + } + catch { + Write-Host "❌ Failed to generate coverage report: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } + + # Display summary + Write-Host "βœ… Coverage report generated successfully!" -ForegroundColor Green + Write-Host "" + Write-Host "πŸ“ Report location: $(Get-Location)\coverage\report\index.html" + Write-Host "🏷️ Coverage badges: $(Get-Location)\coverage\report\badge_*.svg" + Write-Host "" + + # Open the report + if ($OpenReport) { + $reportPath = Join-Path (Get-Location) "coverage\report\index.html" + if (Test-Path $reportPath) { + Write-Host "🌐 Opening coverage report in browser..." -ForegroundColor Yellow + Start-Process $reportPath + } + } else { + Write-Host "πŸ’‘ Open this file in your browser to view the report:" -ForegroundColor Yellow + Write-Host " file://$(Get-Location)\coverage\report\index.html" + } + + Write-Host "" + Write-Host "πŸŽ‰ Done! Check the coverage report for detailed analysis." -ForegroundColor Green +} else { + Write-Host "❌ Failed to install or find ReportGenerator tool." -ForegroundColor Red + exit 1 +} \ No newline at end of file diff --git a/scripts/run-coverage.sh b/scripts/run-coverage.sh new file mode 100755 index 0000000..2f06546 --- /dev/null +++ b/scripts/run-coverage.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Code Coverage Script for BarcodeGenerator Tests +# This script runs tests with code coverage and generates HTML reports + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}πŸ§ͺ Running BarcodeGenerator Tests with Code Coverage${NC}" +echo "==============================================" + +# Navigate to test directory +SCRIPT_DIR="$(dirname "$0")" +TEST_DIR="$SCRIPT_DIR/../Tests/TyKonKet.BarcodeGenerator.Tests" +cd "$TEST_DIR" || { echo -e "${RED}❌ Failed to navigate to test directory${NC}"; exit 1; } + +# Clean previous coverage results +echo -e "${YELLOW}🧹 Cleaning previous coverage results...${NC}" +if [ -d "coverage" ]; then + rm -rf coverage/ || { echo -e "${RED}❌ Failed to clean coverage directory${NC}"; exit 1; } +fi +if [ -d "TestResults" ]; then + rm -rf TestResults/ || { echo -e "${RED}❌ Failed to clean TestResults directory${NC}"; exit 1; } +fi + +# Run tests with coverage +echo -e "${YELLOW}πŸš€ Running tests with coverage collection...${NC}" +dotnet test \ + --configuration Release \ + --collect:"XPlat Code Coverage" \ + --results-directory ./coverage \ + --logger "console;verbosity=minimal" \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + +# Check if reportgenerator is installed +if ! command -v reportgenerator &> /dev/null; then + echo -e "${YELLOW}πŸ“¦ Installing ReportGenerator global tool...${NC}" + dotnet tool install -g dotnet-reportgenerator-globaltool || { echo -e "${RED}❌ Failed to install ReportGenerator${NC}"; exit 1; } +fi + +# Generate HTML report +echo -e "${YELLOW}πŸ“Š Generating HTML coverage report...${NC}" +reportgenerator \ + -reports:"coverage/**/coverage.opencover.xml" \ + -targetdir:"coverage/report" \ + -reporttypes:"Html;Badges;TextSummary" \ + -verbosity:Info || { echo -e "${RED}❌ Failed to generate coverage report${NC}"; exit 1; } + +# Display summary +echo -e "${GREEN}βœ… Coverage report generated successfully!${NC}" +echo "" +echo "πŸ“ Report location: $(pwd)/coverage/report/index.html" +echo "🏷️ Coverage badges: $(pwd)/coverage/report/badge_*.svg" +echo "" + +# Try to open the report (cross-platform) +if command -v xdg-open &> /dev/null; then + echo -e "${YELLOW}🌐 Opening coverage report in browser...${NC}" + xdg-open "coverage/report/index.html" +elif command -v open &> /dev/null; then + echo -e "${YELLOW}🌐 Opening coverage report in browser...${NC}" + open "coverage/report/index.html" +elif command -v start &> /dev/null; then + echo -e "${YELLOW}🌐 Opening coverage report in browser...${NC}" + start "coverage/report/index.html" +else + echo -e "${YELLOW}πŸ’‘ Open this file in your browser to view the report:${NC}" + echo " file://$(pwd)/coverage/report/index.html" +fi + +echo "" +echo -e "${GREEN}πŸŽ‰ Done! Check the coverage report for detailed analysis.${NC}" \ No newline at end of file