diff --git a/src/ZeroLog.Tests/LogEventTests.EdgeCases.cs b/src/ZeroLog.Tests/LogEventTests.EdgeCases.cs index a17162ec..3f409798 100644 --- a/src/ZeroLog.Tests/LogEventTests.EdgeCases.cs +++ b/src/ZeroLog.Tests/LogEventTests.EdgeCases.cs @@ -41,8 +41,7 @@ public void should_truncate_ascii_string_if_buffer_is_not_large_enough() [Test] public void should_ignore_ascii_string_if_buffer_is_not_large_enough_for_header( - [Range(_bufferSize - 2 * _asciiHeaderSize, _bufferSize)] - int firstStringLength) + [Range(_bufferSize - 2 * _asciiHeaderSize, _bufferSize)] int firstStringLength) { var largeString1 = new string('a', firstStringLength); var asciiBytes1 = Encoding.ASCII.GetBytes(largeString1); @@ -214,6 +213,54 @@ public void should_ignore_append_time_span_if_buffer_is_full() Check.That(string.IsNullOrWhiteSpace(_output.ToString())); } + [Test] + public void should_truncate_key_value_ascii_if_too_long_bytes() + { + var largeString = new string('a', 2000); + var bytes = Encoding.ASCII.GetBytes(largeString); + _logEvent.AppendKeyValue("key1", "value1"); + _logEvent.AppendKeyValueAscii("key2", bytes, bytes.Length); + _logEvent.WriteToStringBuffer(_output); + Assert.AreEqual(" ~~ { \"key1\": \"value1\" } [TRUNCATED]", _output.ToString()); + } + + [Test] + public void should_truncate_key_value_ascii_if_too_long_raw_bytes() + { + var largeString = new string('a', 2000); + var asciiBytes = Encoding.ASCII.GetBytes(largeString); + _logEvent.AppendKeyValue("key1", "value1"); + + fixed (byte* pAsciiBytes = asciiBytes) + { + _logEvent.AppendKeyValueAscii("key2", pAsciiBytes, asciiBytes.Length); + } + + _logEvent.WriteToStringBuffer(_output); + Assert.AreEqual(" ~~ { \"key1\": \"value1\" } [TRUNCATED]", _output.ToString()); + } + + [Test] + public void should_truncate_key_value_ascii_if_too_long_byte_span() + { + var largeString = new string('a', 2000); + var bytes = Encoding.ASCII.GetBytes(largeString); + _logEvent.AppendKeyValue("key1", "value1"); + _logEvent.AppendKeyValueAscii("key2", bytes.AsSpan()); + _logEvent.WriteToStringBuffer(_output); + Assert.AreEqual(" ~~ { \"key1\": \"value1\" } [TRUNCATED]", _output.ToString()); + } + + [Test] + public void should_truncate_key_value_ascii_if_too_long_char_span() + { + var largeString = new string('a', 2000); + _logEvent.AppendKeyValue("key1", "value1"); + _logEvent.AppendKeyValueAscii("key2", largeString.AsSpan()); + _logEvent.WriteToStringBuffer(_output); + Assert.AreEqual(" ~~ { \"key1\": \"value1\" } [TRUNCATED]", _output.ToString()); + } + [Test] public void should_ignore_append_key_values_if_buffer_is_full() { diff --git a/src/ZeroLog.Tests/LogEventTests.KeyValues.cs b/src/ZeroLog.Tests/LogEventTests.KeyValues.cs index d2843f7c..e5de1288 100644 --- a/src/ZeroLog.Tests/LogEventTests.KeyValues.cs +++ b/src/ZeroLog.Tests/LogEventTests.KeyValues.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using NUnit.Framework; namespace ZeroLog.Tests @@ -27,14 +28,115 @@ public void should_append_multiple_key_values() [Test] public void should_append_formatted_string_mixed_with_key_values() { - // TODO(lmanners): There are more edge cases here. _logEvent.AppendKeyValue("myKey", "myValue"); _logEvent.AppendFormat("Some {} message"); _logEvent.Append("formatted"); _logEvent.AppendKeyValue("otherKey", 2); + _logEvent.Append("..."); _logEvent.WriteToStringBuffer(_output); - Assert.AreEqual("Some formatted message ~~ { \"myKey\": \"myValue\", \"otherKey\": 2 }", _output.ToString()); + Assert.AreEqual("Some formatted message... ~~ { \"myKey\": \"myValue\", \"otherKey\": 2 }", _output.ToString()); + } + + [Test] + public void should_append_key_byte_array_value() + { + var bytes = Encoding.ASCII.GetBytes("myValue"); + _logEvent.AppendKeyValueAscii("myKey", bytes, bytes.Length); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"myValue\" }", _output.ToString()); + } + + [Test] + public void should_append_key_byte_span_value() + { + var bytes = Encoding.ASCII.GetBytes("myValue"); + _logEvent.AppendKeyValueAscii("myKey", bytes.AsSpan()); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"myValue\" }", _output.ToString()); + } + + [Test] + public void should_append_key_char_span_value() + { + _logEvent.AppendKeyValueAscii("myKey", "myValue".AsSpan()); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"myValue\" }", _output.ToString()); + } + + [Test] + public void should_ignore_byte_array_value_with_negative_length() + { + var bytes = Encoding.Default.GetBytes("myValue"); + _logEvent.AppendKeyValueAscii("myKey", bytes, -1); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual("", _output.ToString()); + } + + [Test] + public void should_support_byte_array_value_that_is_empty() + { + _logEvent.AppendKeyValueAscii("myKey", new byte[0], 0); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"\" }", _output.ToString()); + } + + [Test] + public void should_support_null_byte_array() + { + _logEvent.AppendKeyValueAscii("myKey", (byte[])null, 0); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": null }", _output.ToString()); + } + + [Test] + public void should_support_empty_byte_pointer() + { + var bytes = new byte[1]; + unsafe + { + fixed (byte* pBytes = bytes) + { + _logEvent.AppendKeyValueAscii("myKey", pBytes, 0); + } + } + + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"\" }", _output.ToString()); + } + + [Test] + public unsafe void should_support_null_byte_pointer() + { + _logEvent.AppendKeyValueAscii("myKey", (byte*)null, 0); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": null }", _output.ToString()); + } + + [Test] + public void should_support_byte_span_value_that_is_empty() + { + _logEvent.AppendKeyValueAscii("myKey", ReadOnlySpan.Empty); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"\" }", _output.ToString()); + } + + [Test] + public void should_support_char_span_value_that_is_empty() + { + _logEvent.AppendKeyValueAscii("myKey", ReadOnlySpan.Empty); + _logEvent.WriteToStringBuffer(_output); + + Assert.AreEqual(" ~~ { \"myKey\": \"\" }", _output.ToString()); } [Test] diff --git a/src/ZeroLog/LogEvent.cs b/src/ZeroLog/LogEvent.cs index d5c80978..c8a67008 100644 --- a/src/ZeroLog/LogEvent.cs +++ b/src/ZeroLog/LogEvent.cs @@ -151,6 +151,86 @@ public ILogEvent AppendKeyValue(string key, T? value) return this; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ILogEvent AppendKeyValueAscii(string key, byte[]? bytes, int length) + { + if (length < 0 || !PrepareAppend(sizeof(ArgumentType) + sizeof(byte) + sizeof(ArgumentType) + sizeof(int) + length, 2)) + return this; + + AppendArgumentType(ArgumentType.KeyString); + AppendString(key); + + if (bytes is null) + { + AppendArgumentType(ArgumentType.Null); + return this; + } + + AppendArgumentType(ArgumentType.AsciiString); + AppendInt32(length); + + if (length != 0) + AppendBytes(bytes, length); + + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ILogEvent AppendKeyValueAscii(string key, byte* bytes, int length) + { + if (length < 0 || !PrepareAppend(sizeof(ArgumentType) + sizeof(byte) + sizeof(ArgumentType) + sizeof(int) + length, 2)) + return this; + + AppendArgumentType(ArgumentType.KeyString); + AppendString(key); + + if (bytes == null) + { + AppendArgumentType(ArgumentType.Null); + return this; + } + + AppendArgumentType(ArgumentType.AsciiString); + AppendInt32(length); + AppendBytes(bytes, length); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ILogEvent AppendKeyValueAscii(string key, ReadOnlySpan bytes) + { + var length = bytes.Length; + if (!PrepareAppend(sizeof(ArgumentType) + sizeof(byte) + sizeof(ArgumentType) + sizeof(int) + length, 2)) + return this; + + AppendArgumentType(ArgumentType.KeyString); + AppendString(key); + + AppendArgumentType(ArgumentType.AsciiString); + AppendInt32(length); + AppendBytes(bytes); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ILogEvent AppendKeyValueAscii(string key, ReadOnlySpan chars) + { + var length = chars.Length; + if (!PrepareAppend(sizeof(ArgumentType) + sizeof(byte) + sizeof(ArgumentType) + sizeof(int) + length, 2)) + return this; + + AppendArgumentType(ArgumentType.KeyString); + AppendString(key); + + AppendArgumentType(ArgumentType.AsciiString); + AppendInt32(length); + + foreach (var c in chars) + *_dataPointer++ = (byte)c; + + return this; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ILogEvent AppendAsciiString(byte[]? bytes, int length) {