Browse Source

Merge pull request #2400 from stefannikolei/stefannikolei/arm/colorConvertercmyk

Add arm64 intrinsics for cmyk converter
pull/2412/head
Anton Firszov 3 years ago
committed by GitHub
parent
commit
d7cd46f503
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 95
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykArm64.cs
  3. 35
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterArm64.cs
  4. 5
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
  5. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs
  6. 282
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

2
README.md

@ -64,7 +64,7 @@ If you prefer, you can compile ImageSharp yourself (please do and help!)
- Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/)
- Make sure you have the latest version installed
- Make sure you have [the .NET 6 SDK](https://www.microsoft.com/net/core#windows) installed
- Make sure you have [the .NET 7 SDK](https://www.microsoft.com/net/core#windows) installed
Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**:

95
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykArm64.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal abstract partial class JpegColorConverterBase
{
internal sealed class CmykArm64 : JpegColorConverterArm64
{
public CmykArm64(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector128<float> c0Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> c1Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> c2Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector128<float> c3Base =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue));
nint n = (nint)(uint)values.Component0.Length / Vector128<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector128<float> c = ref Unsafe.Add(ref c0Base, i);
ref Vector128<float> m = ref Unsafe.Add(ref c1Base, i);
ref Vector128<float> y = ref Unsafe.Add(ref c2Base, i);
Vector128<float> k = Unsafe.Add(ref c3Base, i);
k = AdvSimd.Multiply(k, scale);
c = AdvSimd.Multiply(c, k);
m = AdvSimd.Multiply(m, k);
y = AdvSimd.Multiply(y, k);
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
=> ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane);
public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector128<float> destC =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector128<float> destM =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector128<float> destY =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector128<float> destK =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(values.Component3));
ref Vector128<float> srcR =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector128<float> srcG =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector128<float> srcB =
ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(bLane));
var scale = Vector128.Create(maxValue);
nint n = (nint)(uint)values.Component0.Length / Vector128<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector128<float> ctmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcR, i));
Vector128<float> mtmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcG, i));
Vector128<float> ytmp = AdvSimd.Subtract(scale, Unsafe.Add(ref srcB, i));
Vector128<float> ktmp = AdvSimd.Min(ctmp, AdvSimd.Min(mtmp, ytmp));
Vector128<float> kMask = AdvSimd.Not(AdvSimd.CompareEqual(ktmp, scale));
ctmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(ctmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask);
mtmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(mtmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask);
ytmp = AdvSimd.And(AdvSimd.Arm64.Divide(AdvSimd.Subtract(ytmp, ktmp), AdvSimd.Subtract(scale, ktmp)), kMask);
Unsafe.Add(ref destC, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(ctmp, scale));
Unsafe.Add(ref destM, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(mtmp, scale));
Unsafe.Add(ref destY, i) = AdvSimd.Subtract(scale, AdvSimd.Multiply(ytmp, scale));
Unsafe.Add(ref destK, i) = AdvSimd.Subtract(scale, ktmp);
}
}
}
}

35
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterArm64.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal abstract partial class JpegColorConverterBase
{
/// <summary>
/// <see cref="JpegColorConverterBase"/> abstract base for implementations
/// based on <see cref="Avx"/> instructions.
/// </summary>
/// <remarks>
/// Converters of this family would expect input buffers lengths to be
/// divisible by 8 without a remainder.
/// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks.
/// DO NOT pass test data of invalid size to these converters as they
/// potentially won't do a bound check and return a false positive result.
/// </remarks>
internal abstract class JpegColorConverterArm64 : JpegColorConverterBase
{
protected JpegColorConverterArm64(JpegColorSpace colorSpace, int precision)
: base(colorSpace, precision)
{
}
public static bool IsSupported => AdvSimd.Arm64.IsSupported;
public sealed override bool IsAvailable => IsSupported;
public sealed override int ElementsPerBatch => Vector128<float>.Count;
}
}

5
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs

@ -176,6 +176,11 @@ internal abstract partial class JpegColorConverterBase
return new CmykAvx(precision);
}
if (JpegColorConverterArm64.IsSupported)
{
return new CmykArm64(precision);
}
if (JpegColorConverterVector.IsSupported)
{
return new CmykVector(precision);

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs

@ -37,4 +37,12 @@ public class CmykColorConversion : ColorConversionBenchmark
new JpegColorConverterBase.CmykAvx(8).ConvertToRgbInplace(values);
}
[Benchmark]
public void SimdVectorArm64()
{
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.CmykArm64(8).ConvertToRgbInplace(values);
}
}

282
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -20,7 +20,7 @@ public class JpegColorConverterTests
private const int TestBufferLength = 40;
private const HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX;
private const HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2;
private static readonly ApproximateColorSpaceComparer ColorSpaceComparer = new(epsilon: Precision);
@ -36,7 +36,7 @@ public class JpegColorConverterTests
[Fact]
public void GetConverterThrowsExceptionOnInvalidColorSpace()
{
var invalidColorSpace = (JpegColorSpace)(-1);
JpegColorSpace invalidColorSpace = (JpegColorSpace)(-1);
Assert.Throws<InvalidImageContentException>(() => JpegColorConverterBase.GetConverter(invalidColorSpace, 8));
}
@ -61,7 +61,7 @@ public class JpegColorConverterTests
[InlineData(JpegColorSpace.YCbCr, 12)]
internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int precision)
{
var converter = JpegColorConverterBase.GetConverter(colorSpace, precision);
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision);
Assert.NotNull(converter);
Assert.True(converter.IsAvailable);
@ -75,10 +75,10 @@ public class JpegColorConverterTests
[InlineData(JpegColorSpace.Cmyk, 4)]
[InlineData(JpegColorSpace.RGB, 3)]
[InlineData(JpegColorSpace.YCbCr, 3)]
internal void ConvertWithSelectedConverter(JpegColorSpace colorSpace, int componentCount)
internal void ConvertToRgbWithSelectedConverter(JpegColorSpace colorSpace, int componentCount)
{
var converter = JpegColorConverterBase.GetConverter(colorSpace, 8);
ValidateConversion(
JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8);
ValidateConversionToRgb(
converter,
componentCount,
1);
@ -87,13 +87,13 @@ public class JpegColorConverterTests
[Theory]
[MemberData(nameof(Seeds))]
public void FromYCbCrBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.YCbCrScalar(8), 3, seed);
this.TestConversionToRgb(new JpegColorConverterBase.YCbCrScalar(8), 3, seed);
[Theory]
[MemberData(nameof(Seeds))]
public void FromYCbCrVector(int seed)
{
var converter = new JpegColorConverterBase.YCbCrVector(8);
JpegColorConverterBase.YCbCrVector converter = new(8);
if (!converter.IsAvailable)
{
@ -108,22 +108,23 @@ public class JpegColorConverterTests
IntrinsicsConfig);
static void RunTest(string arg) =>
ValidateConversion(
ValidateConversionToRgb(
new JpegColorConverterBase.YCbCrVector(8),
3,
FeatureTestRunner.Deserialize<int>(arg));
FeatureTestRunner.Deserialize<int>(arg),
new JpegColorConverterBase.YCbCrScalar(8));
}
[Theory]
[MemberData(nameof(Seeds))]
public void FromCmykBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.CmykScalar(8), 4, seed);
this.TestConversionToRgb(new JpegColorConverterBase.CmykScalar(8), 4, seed);
[Theory]
[MemberData(nameof(Seeds))]
public void FromCmykVector(int seed)
{
var converter = new JpegColorConverterBase.CmykVector(8);
JpegColorConverterBase.CmykVector converter = new(8);
if (!converter.IsAvailable)
{
@ -138,22 +139,23 @@ public class JpegColorConverterTests
IntrinsicsConfig);
static void RunTest(string arg) =>
ValidateConversion(
ValidateConversionToRgb(
new JpegColorConverterBase.CmykVector(8),
4,
FeatureTestRunner.Deserialize<int>(arg));
FeatureTestRunner.Deserialize<int>(arg),
new JpegColorConverterBase.CmykScalar(8));
}
[Theory]
[MemberData(nameof(Seeds))]
public void FromGrayscaleBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.GrayscaleScalar(8), 1, seed);
this.TestConversionToRgb(new JpegColorConverterBase.GrayscaleScalar(8), 1, seed);
[Theory]
[MemberData(nameof(Seeds))]
public void FromGrayscaleVector(int seed)
{
var converter = new JpegColorConverterBase.GrayScaleVector(8);
JpegColorConverterBase.GrayScaleVector converter = new(8);
if (!converter.IsAvailable)
{
@ -168,22 +170,23 @@ public class JpegColorConverterTests
IntrinsicsConfig);
static void RunTest(string arg) =>
ValidateConversion(
ValidateConversionToRgb(
new JpegColorConverterBase.GrayScaleVector(8),
1,
FeatureTestRunner.Deserialize<int>(arg));
FeatureTestRunner.Deserialize<int>(arg),
new JpegColorConverterBase.GrayscaleScalar(8));
}
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.RgbScalar(8), 3, seed);
this.TestConversionToRgb(new JpegColorConverterBase.RgbScalar(8), 3, seed);
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbVector(int seed)
{
var converter = new JpegColorConverterBase.RgbVector(8);
JpegColorConverterBase.RgbVector converter = new(8);
if (!converter.IsAvailable)
{
@ -198,22 +201,23 @@ public class JpegColorConverterTests
IntrinsicsConfig);
static void RunTest(string arg) =>
ValidateConversion(
ValidateConversionToRgb(
new JpegColorConverterBase.RgbVector(8),
3,
FeatureTestRunner.Deserialize<int>(arg));
FeatureTestRunner.Deserialize<int>(arg),
new JpegColorConverterBase.RgbScalar(8));
}
[Theory]
[MemberData(nameof(Seeds))]
public void FromYccKBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.YccKScalar(8), 4, seed);
this.TestConversionToRgb(new JpegColorConverterBase.YccKScalar(8), 4, seed);
[Theory]
[MemberData(nameof(Seeds))]
public void FromYccKVector(int seed)
{
var converter = new JpegColorConverterBase.YccKVector(8);
JpegColorConverterBase.YccKVector converter = new(8);
if (!converter.IsAvailable)
{
@ -228,41 +232,119 @@ public class JpegColorConverterTests
IntrinsicsConfig);
static void RunTest(string arg) =>
ValidateConversion(
ValidateConversionToRgb(
new JpegColorConverterBase.YccKVector(8),
4,
FeatureTestRunner.Deserialize<int>(arg));
FeatureTestRunner.Deserialize<int>(arg),
new JpegColorConverterBase.YccKScalar(8));
}
[Theory]
[MemberData(nameof(Seeds))]
public void FromYCbCrAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.YCbCrAvx(8), 3, seed);
this.TestConversionToRgb(new JpegColorConverterBase.YCbCrAvx(8),
3,
seed,
new JpegColorConverterBase.YCbCrScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToYCbCrAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YCbCrAvx(8),
3,
seed,
new JpegColorConverterBase.YCbCrScalar(8),
precísion: 2);
[Theory]
[MemberData(nameof(Seeds))]
public void FromCmykAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.CmykAvx(8), 4, seed);
this.TestConversionToRgb(new JpegColorConverterBase.CmykAvx(8),
4,
seed,
new JpegColorConverterBase.CmykScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToCmykAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.CmykAvx(8),
4,
seed,
new JpegColorConverterBase.CmykScalar(8),
precísion: 4);
[Theory]
[MemberData(nameof(Seeds))]
public void FromCmykArm(int seed) =>
this.TestConversionToRgb( new JpegColorConverterBase.CmykArm64(8),
4,
seed,
new JpegColorConverterBase.CmykScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToCmykArm(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.CmykArm64(8),
4,
seed,
new JpegColorConverterBase.CmykScalar(8),
precísion: 4);
[Theory]
[MemberData(nameof(Seeds))]
public void FromGrayscaleAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.GrayscaleAvx(8), 1, seed);
this.TestConversionToRgb(new JpegColorConverterBase.GrayscaleAvx(8),
1,
seed,
new JpegColorConverterBase.GrayscaleScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToGrayscaleAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.GrayscaleAvx(8),
1,
seed,
new JpegColorConverterBase.GrayscaleScalar(8),
precísion: 3);
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.RgbAvx(8), 3, seed);
this.TestConversionToRgb(new JpegColorConverterBase.RgbAvx(8),
3,
seed,
new JpegColorConverterBase.RgbScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbArm(int seed) =>
this.TestConversionToRgb(new JpegColorConverterBase.RgbArm(8),
3,
seed,
new JpegColorConverterBase.RgbScalar(8));
[Theory]
[MemberData(nameof(Seeds))]
public void FromYccKAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.YccKAvx(8), 4, seed);
this.TestConversionToRgb( new JpegColorConverterBase.YccKAvx(8),
4,
seed,
new JpegColorConverterBase.YccKScalar(8));
private void TestConverter(
[Theory]
[MemberData(nameof(Seeds))]
public void FromRgbToYccKAvx2(int seed) =>
this.TestConversionFromRgb(new JpegColorConverterBase.YccKAvx(8),
4,
seed,
new JpegColorConverterBase.YccKScalar(8),
precísion: 4);
private void TestConversionToRgb(
JpegColorConverterBase converter,
int componentCount,
int seed)
int seed,
JpegColorConverterBase baseLineConverter = null)
{
if (!converter.IsAvailable)
{
@ -271,10 +353,33 @@ public class JpegColorConverterTests
return;
}
ValidateConversion(
ValidateConversionToRgb(
converter,
componentCount,
seed);
seed,
baseLineConverter);
}
private void TestConversionFromRgb(
JpegColorConverterBase converter,
int componentCount,
int seed,
JpegColorConverterBase baseLineConverter,
int precísion)
{
if (!converter.IsAvailable)
{
this.Output.WriteLine(
$"Skipping test - {converter.GetType().Name} is not supported on current hardware.");
return;
}
ValidateConversionFromRgb(
converter,
componentCount,
seed,
baseLineConverter,
precísion);
}
private static JpegColorConverterBase.ComponentValues CreateRandomValues(
@ -303,24 +408,117 @@ public class JpegColorConverterTests
return new JpegColorConverterBase.ComponentValues(buffers, 0);
}
private static void ValidateConversion(
private static float[] CreateRandomValues(int length, Random rnd)
{
float[] values = new float[length];
for (int j = 0; j < values.Length; j++)
{
values[j] = (float)rnd.NextDouble() * MaxColorChannelValue;
}
return values;
}
private static void ValidateConversionToRgb(
JpegColorConverterBase converter,
int componentCount,
int seed)
int seed,
JpegColorConverterBase baseLineConverter = null)
{
JpegColorConverterBase.ComponentValues original = CreateRandomValues(TestBufferLength, componentCount, seed);
JpegColorConverterBase.ComponentValues values = new(
JpegColorConverterBase.ComponentValues actual = new(
original.ComponentCount,
original.Component0.ToArray(),
original.Component1.ToArray(),
original.Component2.ToArray(),
original.Component3.ToArray());
converter.ConvertToRgbInplace(values);
converter.ConvertToRgbInplace(actual);
for (int i = 0; i < TestBufferLength; i++)
{
Validate(converter.ColorSpace, original, values, i);
Validate(converter.ColorSpace, original, actual, i);
}
// Compare conversion result to a baseline, should be the scalar version.
if (baseLineConverter != null)
{
JpegColorConverterBase.ComponentValues expected = new(
original.ComponentCount,
original.Component0.ToArray(),
original.Component1.ToArray(),
original.Component2.ToArray(),
original.Component3.ToArray());
baseLineConverter.ConvertToRgbInplace(expected);
if (componentCount == 1)
{
Assert.True(expected.Component0.SequenceEqual(actual.Component0));
}
if (componentCount == 2)
{
Assert.True(expected.Component1.SequenceEqual(actual.Component1));
}
if (componentCount == 3)
{
Assert.True(expected.Component2.SequenceEqual(actual.Component2));
}
if (componentCount == 4)
{
Assert.True(expected.Component3.SequenceEqual(actual.Component3));
}
}
}
private static void ValidateConversionFromRgb(
JpegColorConverterBase converter,
int componentCount,
int seed,
JpegColorConverterBase baseLineConverter,
int precision = 4)
{
// arrange
JpegColorConverterBase.ComponentValues actual = CreateRandomValues(TestBufferLength, componentCount, seed);
JpegColorConverterBase.ComponentValues expected = CreateRandomValues(TestBufferLength, componentCount, seed);
Random rnd = new(seed);
float[] rLane = CreateRandomValues(TestBufferLength, rnd);
float[] gLane = CreateRandomValues(TestBufferLength, rnd);
float[] bLane = CreateRandomValues(TestBufferLength, rnd);
// act
converter.ConvertFromRgb(actual, rLane, gLane, bLane);
baseLineConverter.ConvertFromRgb(expected, rLane, gLane, bLane);
// assert
if (componentCount == 1)
{
CompareSequenceWithTolerance(expected.Component0, actual.Component0, precision);
}
if (componentCount == 2)
{
CompareSequenceWithTolerance(expected.Component1, actual.Component1, precision);
}
if (componentCount == 3)
{
CompareSequenceWithTolerance(expected.Component2, actual.Component2, precision);
}
if (componentCount == 4)
{
CompareSequenceWithTolerance(expected.Component3, actual.Component3, precision);
}
}
private static void CompareSequenceWithTolerance(Span<float> expected, Span<float> actual, int precision)
{
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], actual[i], precision: precision);
}
}
@ -358,9 +556,9 @@ public class JpegColorConverterTests
float y = values.Component0[i];
float cb = values.Component1[i];
float cr = values.Component2[i];
var expected = ColorSpaceConverter.ToRgb(new YCbCr(y, cb, cr));
Rgb expected = ColorSpaceConverter.ToRgb(new YCbCr(y, cb, cr));
var actual = new Rgb(result.Component0[i], result.Component1[i], result.Component2[i]);
Rgb actual = new(result.Component0[i], result.Component1[i], result.Component2[i]);
bool equal = ColorSpaceComparer.Equals(expected, actual);
Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}");

Loading…
Cancel
Save