diff --git a/.editorconfig b/.editorconfig index 0b89996d58..8c636511f2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,15 +12,14 @@ indent_style = space tab_width = 4 # New line preferences -end_of_line = lf -insert_final_newline = false +insert_final_newline = true #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true -file_header_template = unset +file_header_template = ////////////////////////////////////////////////////////////////////////////\n//\n// Copyright 2021 Realm Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the "License")\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an "AS IS" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n//////////////////////////////////////////////////////////////////////////// # this. and Me. preferences dotnet_style_qualification_for_event = false:silent diff --git a/CHANGELOG.md b/CHANGELOG.md index 3940b49752..9706190220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ ``` * Added support for value substitution in string based queries. This enables expressions following [this syntax](https://github.com/realm/realm-js/blob/master/docs/tutorials/query-language.md): `realm.All().Filter("field1 = $0 && field2 = $1", 123, "some-string-value")`. (Issue [#1822](https://github.com/realm/realm-dotnet/issues/1822)) * Reduced the size of the native binaries by ~5%. (PR [#2239](https://github.com/realm/realm-dotnet/pull/2239)) +* Added a new class - `Logger`, which allows you to override the default logger implementation (previously writing to `stdout` or `stderr`) with a custom one by setting +`Logger.Default`. This replaces `AppConfiguration.CustomLogger` and `AppConfiguration.LogLevel` which will be removed in a future release. The built-in implementations are: + * `Console` - uses the `System.Console` for most projects and `UnityEngine.Debug` for Unity projects. + * `Null` - ignores all messages. + * `Function` - proxies calls to a supplied function. + + Custom loggers can derive from the `Logger` class and provide their own implementation for the `Log` method or use `Function` and provide an `Action`. (PR [#2276](https://github.com/realm/realm-dotnet/pull/2276)) ### Compatibility * Realm Studio: 10.0.0 or later. diff --git a/Realm/Realm/Handles/AppHandle.cs b/Realm/Realm/Handles/AppHandle.cs index a18a0f3e06..2253b52f3d 100644 --- a/Realm/Realm/Handles/AppHandle.cs +++ b/Realm/Realm/Handles/AppHandle.cs @@ -22,8 +22,8 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; +using Realms.Logging; using Realms.Native; using Realms.Sync.Exceptions; using Realms.Sync.Native; @@ -37,7 +37,7 @@ private static class NativeMethods #pragma warning disable IDE1006 // Naming Styles [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public unsafe delegate void LogMessageCallback(IntPtr managed_handler, byte* message_buf, IntPtr message_len, LogLevel logLevel); + public unsafe delegate void LogMessageCallback(IntPtr managed_handler, PrimitiveValue messageValue, LogLevel logLevel); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void UserCallback(IntPtr tcs_ptr, IntPtr user_ptr, AppError error); @@ -293,18 +293,18 @@ protected override void Unbind() } [MonoPInvokeCallback(typeof(NativeMethods.LogMessageCallback))] - private static unsafe void HandleLogMessage(IntPtr managedHandler, byte* messageBuffer, IntPtr messageLength, LogLevel level) + private static unsafe void HandleLogMessage(IntPtr managedHandler, PrimitiveValue messageValue, LogLevel level) { try { - var message = Encoding.UTF8.GetString(messageBuffer, (int)messageLength); - var logCallback = (Action)GCHandle.FromIntPtr(managedHandler).Target; - logCallback.Invoke(message, level); + var message = messageValue.AsString(); + var logger = (Logger)GCHandle.FromIntPtr(managedHandler).Target; + logger.Log(level, message); } catch (Exception ex) { var errorMessage = $"An error occurred while trying to log a message: {ex}"; - Console.Error.WriteLine(errorMessage); + Logger.Console.Log(LogLevel.Error, errorMessage); } } diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index 3a54fa77cc..5beb7a202b 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -25,6 +25,7 @@ using System.Threading.Tasks; using MongoDB.Bson; using Realms.Exceptions; +using Realms.Logging; using Realms.Native; using Realms.Schema; using Realms.Sync.Exceptions; @@ -49,6 +50,9 @@ private static class NativeMethods [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void OnBindingContextDestructedCallback(IntPtr handle); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void LogMessageCallback(PrimitiveValue message, LogLevel level); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_open", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr open(Configuration configuration, [MarshalAs(UnmanagedType.LPArray), In] SchemaObject[] objects, int objects_length, @@ -135,7 +139,12 @@ public static extern IntPtr create_object_unique(SharedRealmHandle sharedRealm, public static extern void get_schema(SharedRealmHandle sharedRealm, IntPtr callback, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_install_callbacks", CallingConvention = CallingConvention.Cdecl)] - public static extern void install_callbacks(NotifyRealmCallback notifyRealmCallback, GetNativeSchemaCallback nativeSchemaCallback, OpenRealmCallback openCallback, OnBindingContextDestructedCallback contextDestructedCallback); + public static extern void install_callbacks( + NotifyRealmCallback notifyRealmCallback, + GetNativeSchemaCallback nativeSchemaCallback, + OpenRealmCallback openCallback, + OnBindingContextDestructedCallback contextDestructedCallback, + LogMessageCallback logMessageCallback); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_has_changed", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] @@ -165,13 +174,15 @@ static unsafe SharedRealmHandle() NativeMethods.GetNativeSchemaCallback getNativeSchema = GetNativeSchema; NativeMethods.OpenRealmCallback openRealm = HandleOpenRealmCallback; NativeMethods.OnBindingContextDestructedCallback onBindingContextDestructed = OnBindingContextDestructed; + NativeMethods.LogMessageCallback logMessage = LogMessage; GCHandle.Alloc(notifyRealm); GCHandle.Alloc(getNativeSchema); GCHandle.Alloc(openRealm); GCHandle.Alloc(onBindingContextDestructed); + GCHandle.Alloc(logMessage); - NativeMethods.install_callbacks(notifyRealm, getNativeSchema, openRealm, onBindingContextDestructed); + NativeMethods.install_callbacks(notifyRealm, getNativeSchema, openRealm, onBindingContextDestructed, logMessage); } [Preserve] @@ -438,7 +449,7 @@ private static unsafe void HandleOpenRealmCallback(IntPtr taskCompletionSource, } } - [MonoPInvokeCallbackAttribute(typeof(NativeMethods.OnBindingContextDestructedCallback))] + [MonoPInvokeCallback(typeof(NativeMethods.OnBindingContextDestructedCallback))] public static void OnBindingContextDestructed(IntPtr handle) { if (handle != IntPtr.Zero) @@ -448,6 +459,12 @@ public static void OnBindingContextDestructed(IntPtr handle) } } + [MonoPInvokeCallback(typeof(NativeMethods.LogMessageCallback))] + private static void LogMessage(PrimitiveValue message, LogLevel level) + { + Logger.LogDefault(level, message.AsString()); + } + public class SchemaMarshaler { public readonly SchemaObject[] Objects; diff --git a/Realm/Realm/Sync/LogLevel.cs b/Realm/Realm/Logging/LogLevel.cs similarity index 98% rename from Realm/Realm/Sync/LogLevel.cs rename to Realm/Realm/Logging/LogLevel.cs index b6c86d6a91..aa6ae822ca 100644 --- a/Realm/Realm/Sync/LogLevel.cs +++ b/Realm/Realm/Logging/LogLevel.cs @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -namespace Realms.Sync +namespace Realms.Logging { /// /// Specifies the criticality level above which messages will be logged diff --git a/Realm/Realm/Logging/Logger.cs b/Realm/Realm/Logging/Logger.cs new file mode 100644 index 0000000000..fe78bf1b42 --- /dev/null +++ b/Realm/Realm/Logging/Logger.cs @@ -0,0 +1,181 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Runtime.InteropServices; +using System.Text; +using Realms.Sync; + +namespace Realms.Logging +{ + /// + /// A logger that logs messages originating from Realm. The default logger can be replaced by setting . + /// + /// + /// A few default implementations are provided by , , and , but you + /// can implement your own. + /// + public abstract class Logger + { + private readonly Lazy _gcHandle; + + /// + /// Gets a that outputs messages to the default console. For most project types, that will be + /// using but certain platforms may use different implementations. + /// + /// A instance that outputs to the platform's console. + public static Logger Console { get; internal set; } = new ConsoleLogger(); + + /// + /// Gets a that ignores all messages. + /// + /// A that doesn't output any messages. + public static Logger Null { get; } = new NullLogger(); + + /// + /// Gets a that proxies Log calls to the supplied function. + /// + /// Function to proxy log calls to. + /// + /// A instance that will invoke for each message. + /// + public static Logger Function(Action logFunction) => new FunctionLogger(logFunction); + + /// + /// Gets a that proxies Log calls to the supplied function. The message will + /// already be formatted with the default message formatting that includes a timestamp. + /// + /// Function to proxy log calls to. + /// + /// A instance that will invoke for each message. + /// + public static Logger Function(Action logFunction) => new FunctionLogger((level, message) => logFunction(FormatLog(level, message))); + + /// + /// Gets or sets the verbosity of log messages. + /// + /// + /// This replaces the deprecated . + /// + /// The log level for Realm-originating messages. + public static LogLevel LogLevel { get; set; } = LogLevel.Info; + + /// + /// Gets or sets a custom implementation that will be used by + /// Realm whenever information must be logged. + /// + /// + /// This is the logger that will be used to log diagnostic messages from Sync. It + /// replaces the deprecated . + /// + /// The logger to be used for Realm-originating messages. + public static Logger Default { get; set; } = Console; + + internal GCHandle GCHandle => _gcHandle.Value; + + // This is only needed for backward compatibility - the App logger sets its own level separately + // Once that is removed, we should use Logger.LogLevel across the board. + [Obsolete("Remove when we remove the AppConfiguration.CustomLogger")] + internal LogLevel _logLevel = LogLevel; + + /// + /// Initializes a new instance of the class. + /// + protected Logger() + { + _gcHandle = new Lazy(() => GCHandle.Alloc(this)); + } + + internal static void LogDefault(LogLevel level, string message) => Default?.Log(level, message); + + /// + /// Log a message at the supplied level. + /// + /// The criticality level for the message. + /// The message to log. + public void Log(LogLevel level, string message) + { + if (level < _logLevel) + { + return; + } + + try + { + LogImpl(level, message); + } + catch (Exception ex) + { + Console.Log(LogLevel.Error, $"An exception occurred while trying to log the message: '{message}' at level: {level}. Error: {ex}"); + } + } + + /// + /// The internal implementation being called from . + /// + /// The criticality level for the message. + /// The message to log. + protected abstract void LogImpl(LogLevel level, string message); + + internal static string FormatLog(LogLevel level, string message) => $"{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss.fff} {level}: {message}"; + + private class ConsoleLogger : Logger + { + protected override void LogImpl(LogLevel level, string message) + { + System.Console.WriteLine(FormatLog(level, message)); + } + } + + private class FunctionLogger : Logger + { + private readonly Action _logFunction; + + public FunctionLogger(Action logFunction) + { + _logFunction = logFunction; + } + + protected override void LogImpl(LogLevel level, string message) => _logFunction(level, message); + } + + private class NullLogger : Logger + { + protected override void LogImpl(LogLevel level, string message) + { + } + } + + internal class InMemoryLogger : Logger + { + private readonly StringBuilder _builder = new StringBuilder(); + + protected override void LogImpl(LogLevel level, string message) + { + lock (_builder) + { + _builder.AppendLine(FormatLog(level, message)); + } + } + + public string GetLog() => _builder.ToString(); + + public void Clear() => _builder.Clear(); + } + } +} diff --git a/Realm/Realm/Native/AppConfiguration.cs b/Realm/Realm/Native/AppConfiguration.cs index 62de793f32..8e3956b95d 100644 --- a/Realm/Realm/Native/AppConfiguration.cs +++ b/Realm/Realm/Native/AppConfiguration.cs @@ -18,6 +18,7 @@ using System; using System.Runtime.InteropServices; +using Realms.Logging; namespace Realms.Sync.Native { @@ -107,6 +108,6 @@ internal MetadataPersistenceMode? MetadataPersistence internal LogLevel log_level; - internal IntPtr managed_log_callback; + internal IntPtr managed_logger; } } diff --git a/Realm/Realm/Native/NativeCommon.cs b/Realm/Realm/Native/NativeCommon.cs index 415f2a5db1..3f4b3e1a10 100644 --- a/Realm/Realm/Native/NativeCommon.cs +++ b/Realm/Realm/Native/NativeCommon.cs @@ -21,38 +21,15 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; using System.Threading; -using Realms.Native; namespace Realms { internal static class NativeCommon { -#if DEBUG - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public unsafe delegate void DebugLoggerCallback(byte* utf8String, IntPtr stringLen); - - [MonoPInvokeCallback(typeof(DebugLoggerCallback))] - private static unsafe void DebugLogger(byte* utf8String, IntPtr stringLen) - { - var message = Encoding.UTF8.GetString(utf8String, (int)stringLen); - Console.WriteLine(message); - } - - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "set_debug_logger", CallingConvention = CallingConvention.Cdecl)] - public static extern void set_debug_logger(DebugLoggerCallback callback); -#endif // DEBUG - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "delete_pointer", CallingConvention = CallingConvention.Cdecl)] public static extern unsafe void delete_pointer(void* pointer); - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "delete_pointer", CallingConvention = CallingConvention.Cdecl)] - public static extern unsafe void delete_pointer(IntPtr pointer); - - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_reset_for_testing", CallingConvention = CallingConvention.Cdecl)] - public static extern void reset_for_testing(); - private static int _isInitialized; internal static unsafe void Initialize() @@ -73,12 +50,6 @@ internal static unsafe void Initialize() AddWindowsWrappersToPath("Windows", isUnityTarget: true); } -#if DEBUG - DebugLoggerCallback logger = DebugLogger; - GCHandle.Alloc(logger); - set_debug_logger(logger); -#endif - SynchronizationContextScheduler.Install(); } } diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index b943cc2c7e..60bfdf4da4 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -30,6 +30,7 @@ using Realms.Dynamic; using Realms.Exceptions; using Realms.Helpers; +using Realms.Logging; using Realms.Native; using Realms.Schema; using Realms.Sync; @@ -354,7 +355,7 @@ internal void NotifyError(Exception ex) { if (Error == null) { - Console.Error.WriteLine("A realm-level exception has occurred. To handle and react to those, subscribe to the Realm.Error event."); + Logger.LogDefault(LogLevel.Error, "A realm-level exception has occurred. To handle and react to those, subscribe to the Realm.Error event."); } Error?.Invoke(this, new ErrorEventArgs(ex)); diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index da4aa7f158..584444b4aa 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -22,6 +22,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Realms.Helpers; +using Realms.Logging; namespace Realms.Sync { @@ -159,14 +160,18 @@ public static App Create(AppConfiguration config) LocalAppVersion = config.LocalAppVersion, MetadataPersistence = config.MetadataPersistenceMode, default_request_timeout_ms = (ulong?)config.DefaultRequestTimeout?.TotalMilliseconds ?? 0, - log_level = config.LogLevel, + log_level = config.LogLevel != LogLevel.Info ? config.LogLevel : Logger.LogLevel, }; if (config.CustomLogger != null) { - // TODO: should we free this eventually? - var logHandle = GCHandle.Alloc(config.CustomLogger); - nativeConfig.managed_log_callback = GCHandle.ToIntPtr(logHandle); + var logger = Logger.Function((level, message) => config.CustomLogger(message, level)); + logger._logLevel = nativeConfig.log_level; + nativeConfig.managed_logger = GCHandle.ToIntPtr(logger.GCHandle); + } + else if (Logger.Default != null) + { + nativeConfig.managed_logger = GCHandle.ToIntPtr(Logger.Default.GCHandle); } var handle = AppHandle.CreateApp(nativeConfig, config.MetadataEncryptionKey); diff --git a/Realm/Realm/Sync/AppConfiguration.cs b/Realm/Realm/Sync/AppConfiguration.cs index e45c1d4c86..142d0191e3 100644 --- a/Realm/Realm/Sync/AppConfiguration.cs +++ b/Realm/Realm/Sync/AppConfiguration.cs @@ -18,6 +18,7 @@ using System; using Realms.Helpers; +using Realms.Logging; namespace Realms.Sync { @@ -113,16 +114,18 @@ public byte[] MetadataEncryptionKey /// Gets or sets a custom log function that will be invoked for each log message emitted by sync. /// /// - /// The first argument of the action is the log message itself, while the second one is the + /// The first argument of the action is the log message itself, while the second one is the /// at which the log message was emitted. /// /// The custom logger. + [Obsolete("Configure the global RealmConfiguration.Logger instead")] public Action CustomLogger { get; set; } /// /// Gets or sets the log level for sync operations. /// /// The sync log level. + [Obsolete("Configure the global RealmConfiguration.LogLevel instead")] public LogLevel LogLevel { get; set; } = LogLevel.Info; /// diff --git a/Tests/Realm.Tests/Database/APITests.cs b/Tests/Realm.Tests/Database/APITests.cs index 5f5f33b640..ce01f370a9 100644 --- a/Tests/Realm.Tests/Database/APITests.cs +++ b/Tests/Realm.Tests/Database/APITests.cs @@ -32,6 +32,7 @@ public class APITests [TestCase(typeof(IRealmCollection))] [TestCase(typeof(RealmResults))] [TestCase(typeof(RealmList))] + [TestCase(typeof(RealmSet))] public void RealmCollectionContravariance(Type type) { Assert.That(typeof(IRealmCollection).IsAssignableFrom(type)); diff --git a/Tests/Realm.Tests/Database/LoggerTests.cs b/Tests/Realm.Tests/Database/LoggerTests.cs new file mode 100644 index 0000000000..505fe9597a --- /dev/null +++ b/Tests/Realm.Tests/Database/LoggerTests.cs @@ -0,0 +1,93 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Realms.Logging; + +namespace Realms.Tests.Database +{ + [TestFixture, Preserve(AllMembers = true)] + public class LoggerTests + { + private Logger _originalLogger; + private LogLevel _originalLogLevel; + + [SetUp] + public void Setup() + { + _originalLogger = Logger.Default; + _originalLogLevel = Logger.LogLevel; + } + + [TearDown] + public void TearDown() + { + Logger.Default = _originalLogger; + Logger.LogLevel = _originalLogLevel; + } + + [Test] + public void Logger_CanSetDefaultLogger() + { + var messages = new List(); + Logger.Default = Logger.Function(message => messages.Add(message)); + + Logger.LogDefault(LogLevel.Warn, "This is very dangerous!"); + + Assert.That(messages.Count, Is.EqualTo(1)); + Assert.That(messages[0], Does.Contain(LogLevel.Warn.ToString())); + Assert.That(messages[0], Does.Contain(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"))); + Assert.That(messages[0], Does.Contain("This is very dangerous!")); + } + + [Test] + public void Logger_SkipsDebugMessagesByDefault() + { + var messages = new List(); + Logger.Default = Logger.Function(message => messages.Add(message)); + + Logger.LogDefault(LogLevel.Debug, "This is a debug message!"); + + Assert.That(messages.Count, Is.EqualTo(0)); + } + + [TestCase(LogLevel.Error)] + [TestCase(LogLevel.Info)] + [TestCase(LogLevel.Debug)] + public void Logger_WhenLevelIsSet_LogsOnlyExpectedLevels(LogLevel level) + { + var messages = new List(); + Logger.Default = Logger.Function(message => messages.Add(message)); + Logger.LogLevel = level; + + Logger.LogDefault(level - 1, "This is at level - 1"); + Logger.LogDefault(level, "This is at the same level"); + Logger.LogDefault(level + 1, "This is at level + 1"); + + Assert.That(messages.Count, Is.EqualTo(2)); + + Assert.That(messages[0], Does.Contain(level.ToString())); + Assert.That(messages[0], Does.Contain("This is at the same level")); + + Assert.That(messages[1], Does.Contain((level + 1).ToString())); + Assert.That(messages[1], Does.Contain("This is at level + 1")); + } + } +} diff --git a/Tests/Realm.Tests/Database/NotificationTests.cs b/Tests/Realm.Tests/Database/NotificationTests.cs index 1b8bb797ae..48e5b4d1c4 100644 --- a/Tests/Realm.Tests/Database/NotificationTests.cs +++ b/Tests/Realm.Tests/Database/NotificationTests.cs @@ -65,12 +65,12 @@ public void ShouldTriggerRealmChangedEvent() public void RealmError_WhenNoSubscribers_OutputsMessageInConsole() { using var sw = new StringWriter(); - var original = Console.Error; - Console.SetError(sw); + var original = Console.Out; + Console.SetOut(sw); _realm.NotifyError(new Exception()); Assert.That(sw.ToString(), Does.Contain("exception").And.Contains("Realm.Error")); - Console.SetError(original); + Console.SetOut(original); } [Test] @@ -764,4 +764,4 @@ public static IEnumerable CollectionChangedTestCases() yield return new TestCaseData(new int[] { 1, 2, 3, 4, 5 }, NotifyCollectionChangedAction.Remove, new int[] { 2, 4 }, -1); } } -} \ No newline at end of file +} diff --git a/Tests/Realm.Tests/RealmTest.cs b/Tests/Realm.Tests/RealmTest.cs index 3b5310f26a..84e6ab1ecf 100644 --- a/Tests/Realm.Tests/RealmTest.cs +++ b/Tests/Realm.Tests/RealmTest.cs @@ -22,6 +22,7 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using Realms.Logging; namespace Realms.Tests { @@ -29,6 +30,8 @@ namespace Realms.Tests public abstract class RealmTest { private readonly List _realms = new List(); + private Logger _originalLogger; + private LogLevel _originalLogLevel; private bool _isSetup; @@ -45,6 +48,9 @@ public void SetUp() { if (!_isSetup) { + _originalLogger = Logger.Default; + _originalLogLevel = Logger.LogLevel; + if (OverrideDefaultConfig) { RealmConfiguration.DefaultConfiguration = new RealmConfiguration(Guid.NewGuid().ToString()); @@ -71,6 +77,9 @@ public void TearDown() { CustomTearDown(); + Logger.Default = _originalLogger; + Logger.LogLevel = _originalLogLevel; + _isSetup = false; try { diff --git a/Tests/Realm.Tests/Sync/AppTests.cs b/Tests/Realm.Tests/Sync/AppTests.cs index 5cc607f8ed..2ac8a190c7 100644 --- a/Tests/Realm.Tests/Sync/AppTests.cs +++ b/Tests/Realm.Tests/Sync/AppTests.cs @@ -19,6 +19,7 @@ using System; using System.Text; using NUnit.Framework; +using Realms.Logging; using Realms.Sync; namespace Realms.Tests.Sync @@ -91,5 +92,35 @@ public void App_WithCustomLogger_LogsSyncOperations(LogLevel logLevel) Assert.That(log, Does.Not.Contain($"[{logLevel - 1}]")); }); } + + [TestCase(LogLevel.Debug)] + [TestCase(LogLevel.Info)] + public void RealmConfiguration_WithCustomLogger_LogsSyncOperations(LogLevel logLevel) + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + Logger.LogLevel = logLevel; + var logger = new Logger.InMemoryLogger(); + Logger.Default = logger; + + var appConfig = SyncTestHelpers.GetAppConfig(); + + var app = CreateApp(appConfig); + + var config = await GetIntegrationConfigAsync(Guid.NewGuid().ToString()); + using var realm = await GetRealmAsync(config); + realm.Write(() => + { + realm.Add(new PrimaryKeyStringObject { StringProperty = Guid.NewGuid().ToString() }); + }); + + await WaitForUploadAsync(realm); + + var log = logger.GetLog(); + + Assert.That(log, Does.Contain($"{logLevel}:")); + Assert.That(log, Does.Not.Contain($"{logLevel - 1}:")); + }); + } } } diff --git a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs index f771b56cba..3f3d7ebfaf 100644 --- a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs +++ b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs @@ -112,4 +112,4 @@ public static Task> SimulateSessionErrorAsync(Session sessi return tcs.Task; } } -} \ No newline at end of file +} diff --git a/Tools/SetupUnityPackage/UnityUtils/FileHelper.cs b/Tools/SetupUnityPackage/UnityUtils/FileHelper.cs index 6dfda304c5..885ae0a355 100644 --- a/Tools/SetupUnityPackage/UnityUtils/FileHelper.cs +++ b/Tools/SetupUnityPackage/UnityUtils/FileHelper.cs @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////// - using UnityEngine; namespace UnityUtils diff --git a/wrappers/src/CMakeLists.txt b/wrappers/src/CMakeLists.txt index 423e18100f..3fdf2cd553 100644 --- a/wrappers/src/CMakeLists.txt +++ b/wrappers/src/CMakeLists.txt @@ -1,5 +1,4 @@ set(SOURCES - debug.cpp error_handling.cpp list_cs.cpp set_cs.cpp diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index 2a98ea5833..382eb38446 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -46,7 +46,7 @@ namespace realm { std::string s_platform_version; std::string s_sdk_version; - void (*s_log_message_callback)(void* managed_handler, const char* message, size_t message_len, util::Logger::Level level); + void (*s_log_message_callback)(void* managed_handler, realm_value_t message, util::Logger::Level level); void (*s_user_callback)(void* tcs_ptr, SharedSyncUser* user, MarshaledAppError err); void (*s_void_callback)(void* tcs_ptr, MarshaledAppError err); void (*s_bson_callback)(void* tcs_ptr, BsonPayload response, MarshaledAppError err); @@ -76,38 +76,38 @@ namespace realm { util::Logger::Level log_level; - void* managed_log_handler; + void* managed_logger; }; class SyncLogger : public util::RootLogger { public: SyncLogger(void* delegate) - : m_log_message_delegate(delegate) + : managed_logger(delegate) { } void do_log(util::Logger::Level level, std::string message) { - s_log_message_callback(m_log_message_delegate, message.c_str(), message.length(), level); + s_log_message_callback(managed_logger, to_capi(Mixed(message)), level); } private: - void* m_log_message_delegate; + void* managed_logger; }; class SyncLoggerFactory : public realm::SyncLoggerFactory { public: - SyncLoggerFactory(void* managed_log_handler) - : m_managed_log_handler(managed_log_handler) + SyncLoggerFactory(void* managed_logger) + : managed_logger(managed_logger) { } std::unique_ptr make_logger(util::Logger::Level level) { - auto logger = std::make_unique(m_managed_log_handler); + auto logger = std::make_unique(managed_logger); logger->set_level_threshold(level); return std::unique_ptr(logger.release()); } private: - void* m_managed_log_handler; + void* managed_logger; }; } } @@ -177,8 +177,8 @@ extern "C" { sync_client_config.custom_encryption_key = std::vector(key.begin(), key.end()); } - if (app_config.managed_log_handler) { - sync_client_config.logger_factory = new realm::binding::SyncLoggerFactory(app_config.managed_log_handler); + if (app_config.managed_logger) { + sync_client_config.logger_factory = new realm::binding::SyncLoggerFactory(app_config.managed_logger); } return new SharedApp(App::get_shared_app(std::move(config), std::move(sync_client_config))); @@ -302,7 +302,7 @@ extern "C" { REALM_EXPORT void shared_app_reset_for_testing(SharedApp& app) { auto users = app->all_users(); for (size_t i = 0; i < users.size(); i++) { - auto user = users[i]; + auto &user = users[i]; user->log_out(); } diff --git a/wrappers/src/debug.cpp b/wrappers/src/debug.cpp deleted file mode 100644 index 9d8858f6f8..0000000000 --- a/wrappers/src/debug.cpp +++ /dev/null @@ -1,51 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// -#include "debug.hpp" -#include "realm_export_decls.hpp" - -#include - -#include -#include -#include - -namespace realm { - -using DebugLoggerT = void(*)(void* utf8Str, size_t strLen); -static DebugLoggerT debug_log_function = nullptr; - -void debug_log(const std::string message) -{ - // second check against -1 based on suspicions from stack traces of this as sentinel value - if (debug_log_function != nullptr && debug_log_function != reinterpret_cast(-1)) - debug_log_function((void*)message.data(), message.size()); -} - -} - -extern "C" { - REALM_EXPORT void set_debug_logger(realm::DebugLoggerT debug_logger) - { - realm::debug_log_function = debug_logger; - } - - REALM_EXPORT void realm_reset_for_testing() - { - realm::_impl::RealmCoordinator::clear_all_caches(); - } -} diff --git a/wrappers/src/debug.hpp b/wrappers/src/debug.hpp index 9f71faf94a..c31cb1692e 100644 --- a/wrappers/src/debug.hpp +++ b/wrappers/src/debug.hpp @@ -30,10 +30,6 @@ namespace realm { -#if defined(DEBUG) || !defined(NDEBUG) -void debug_log(std::string message); -#endif - // https://stackoverflow.com/a/28827188/1649102 inline void sleep_ms(int milliseconds){ // cross-platform sleep function #ifdef WIN32 diff --git a/wrappers/src/marshalling.cpp b/wrappers/src/marshalling.cpp index 2dce51185d..e32bf26a96 100644 --- a/wrappers/src/marshalling.cpp +++ b/wrappers/src/marshalling.cpp @@ -21,6 +21,7 @@ #include "marshalling.hpp" #include "error_handling.hpp" +#include "shared_realm_cs.hpp" using namespace realm; @@ -58,7 +59,7 @@ size_t realm::binding::stringdata_to_csharpstringbuffer(StringData str, uint16_t size_t size = Xcode::find_utf16_buf_size(in_begin, in_end);//Figure how much space is actually needed if (in_begin != in_end) { - std::cerr << "BAD UTF8 DATA IN stringdata_tocsharpbuffer :" << str.data() << "\n"; + realm::binding::log_message(util::format("BAD UTF8 DATA IN stringdata_tocsharpbuffer: %1", str.data())); return -1;//bad uft8 data } if (size > bufsize) diff --git a/wrappers/src/shared_realm_cs.cpp b/wrappers/src/shared_realm_cs.cpp index be6f1c4c75..6b50e647a4 100644 --- a/wrappers/src/shared_realm_cs.cpp +++ b/wrappers/src/shared_realm_cs.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,7 @@ namespace binding { void (*s_realm_changed)(void* managed_state_handle); void (*s_get_native_schema)(SchemaForMarshaling schema, void* managed_callback); void (*s_on_binding_context_destructed)(void* managed_handle); + void (*s_log_message)(realm_value_t message, util::Logger::Level level); CSharpBindingContext::CSharpBindingContext(void* managed_state_handle) : m_managed_state_handle(managed_state_handle) {} @@ -56,6 +58,11 @@ namespace binding { { s_on_binding_context_destructed(m_managed_state_handle); } + + void log_message(std::string message, util::Logger::Level level) + { + s_log_message(to_capi(Mixed(message)), level); + } } // the name of this class is an ugly hack to get around get_shared_group being private @@ -106,12 +113,18 @@ extern "C" { typedef uint32_t realm_table_key; -REALM_EXPORT void shared_realm_install_callbacks(decltype(s_realm_changed) realm_changed, decltype(s_get_native_schema) get_schema, decltype(s_open_realm_callback) open_callback, decltype(s_on_binding_context_destructed) on_binding_context_destructed) +REALM_EXPORT void shared_realm_install_callbacks( + decltype(s_realm_changed) realm_changed, + decltype(s_get_native_schema) get_schema, + decltype(s_open_realm_callback) open_callback, + decltype(s_on_binding_context_destructed) on_binding_context_destructed, + decltype(s_log_message) log_message) { s_realm_changed = realm_changed; s_get_native_schema = get_schema; s_open_realm_callback = open_callback; s_on_binding_context_destructed = on_binding_context_destructed; + s_log_message = log_message; } REALM_EXPORT SharedRealm* shared_realm_open(Configuration configuration, SchemaObject* objects, int objects_length, SchemaProperty* properties, uint8_t* encryption_key, NativeException::Marshallable& ex) @@ -249,6 +262,12 @@ REALM_EXPORT void shared_realm_close_realm(SharedRealm& realm, NativeException:: }); } +REALM_EXPORT void shared_realm_close_all_realms() +{ + realm::_impl::RealmCoordinator::clear_all_caches(); +} + + REALM_EXPORT realm_table_key shared_realm_get_table_key(SharedRealm& realm, uint16_t* object_type_buf, size_t object_type_len, NativeException::Marshallable& ex) { return handle_errors(ex, [&]() { diff --git a/wrappers/src/shared_realm_cs.hpp b/wrappers/src/shared_realm_cs.hpp index 8bd7c99dc5..7adbf36b6c 100644 --- a/wrappers/src/shared_realm_cs.hpp +++ b/wrappers/src/shared_realm_cs.hpp @@ -99,6 +99,8 @@ namespace binding { ~CSharpBindingContext(); }; + + void log_message(std::string message, util::Logger::Level level = util::Logger::Level::info); } }