BindingTests_Logging.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using Avalonia.Controls;
  5. using Avalonia.Controls.Converters;
  6. using Avalonia.Data;
  7. using Avalonia.Data.Converters;
  8. using Avalonia.Data.Core;
  9. using Avalonia.Data.Core.Plugins;
  10. using Avalonia.Input;
  11. using Avalonia.Logging;
  12. using Avalonia.LogicalTree;
  13. using Avalonia.Markup.Xaml.MarkupExtensions;
  14. using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
  15. using Avalonia.Reactive;
  16. using Avalonia.UnitTests;
  17. using Xunit;
  18. #nullable enable
  19. namespace Avalonia.Markup.UnitTests.Data
  20. {
  21. public class BindingTests_Logging : ScopedTestBase
  22. {
  23. public class DataContext : ScopedTestBase
  24. {
  25. [Fact]
  26. public void Should_Not_Log_Missing_Member_On_Null_DataContext()
  27. {
  28. var target = new Decorator { };
  29. var root = new TestRoot(target);
  30. var binding = new Binding("Foo");
  31. using (AssertNoLog())
  32. {
  33. target.Bind(Control.TagProperty, binding);
  34. }
  35. }
  36. [Fact]
  37. public void Should_Log_Missing_Member_On_DataContext()
  38. {
  39. var target = new Decorator { DataContext = new TestClass("foo") };
  40. var root = new TestRoot(target);
  41. var binding = new Binding("Foo.Bar");
  42. using (AssertLog(
  43. target,
  44. binding.Path,
  45. "Could not find a matching property accessor for 'Bar' on 'System.String'.",
  46. "Bar"))
  47. {
  48. target.Bind(Control.TagProperty, binding);
  49. }
  50. }
  51. [Fact]
  52. public void Should_Log_Null_In_Binding_Chain()
  53. {
  54. var target = new Decorator { DataContext = new TestClass() };
  55. var root = new TestRoot(target);
  56. var binding = new Binding("Foo.Length");
  57. using (AssertLog(target, binding.Path, "Value is null.", "Foo"))
  58. {
  59. target.Bind(Control.TagProperty, binding);
  60. }
  61. }
  62. }
  63. public class Source : ScopedTestBase
  64. {
  65. [Fact]
  66. public void Should_Log_Null_Source()
  67. {
  68. var target = new Decorator { };
  69. var root = new TestRoot(target);
  70. var binding = new Binding("Foo") { Source = null };
  71. using (AssertLog(target, binding.Path, "Binding Source is null.", "(source)"))
  72. {
  73. target.Bind(Control.TagProperty, binding);
  74. }
  75. }
  76. [Fact]
  77. public void Should_Log_Null_Source_For_Unrooted_Control()
  78. {
  79. var target = new Decorator { };
  80. var binding = new Binding("Foo") { Source = null };
  81. using (AssertLog(target, binding.Path, "Binding Source is null.", "(source)"))
  82. {
  83. target.Bind(Control.TagProperty, binding);
  84. }
  85. }
  86. }
  87. public class LogicalAncestor : ScopedTestBase
  88. {
  89. [Fact]
  90. public void Should_Log_Ancestor_Not_Found()
  91. {
  92. var target = new Decorator { };
  93. var root = new TestRoot(target);
  94. var binding = new Binding("$parent[TextBlock]") { TypeResolver = ResolveType };
  95. using (AssertLog(target, binding.Path, "Ancestor not found.", "$parent[TextBlock]"))
  96. {
  97. target.Bind(Control.TagProperty, binding);
  98. }
  99. }
  100. [Fact]
  101. public void Should_Not_Log_Ancestor_Not_Found_For_Unrooted_Control()
  102. {
  103. var target = new Decorator { };
  104. var binding = new Binding("$parent[TextBlock]") { TypeResolver = ResolveType };
  105. using (AssertNoLog())
  106. {
  107. target.Bind(Control.TagProperty, binding);
  108. }
  109. }
  110. }
  111. public class VisualAncestor : ScopedTestBase
  112. {
  113. [Fact]
  114. public void Should_Log_Ancestor_Not_Found()
  115. {
  116. var target = new Decorator { };
  117. var root = new TestRoot(target);
  118. var binding = new Binding
  119. {
  120. RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
  121. {
  122. AncestorType = typeof(TextBlock),
  123. }
  124. };
  125. using (AssertLog(target, "$visualParent[TextBlock]", "Ancestor not found.", "$visualParent[TextBlock]"))
  126. {
  127. target.Bind(Control.TagProperty, binding);
  128. }
  129. }
  130. [Fact]
  131. public void Should_Log_Ancestor_Property_Not_Found()
  132. {
  133. var target = new Decorator { };
  134. var root = new TestRoot(target);
  135. var binding = new Binding("Foo")
  136. {
  137. RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
  138. {
  139. AncestorType = typeof(TestRoot),
  140. }
  141. };
  142. using (AssertLog(
  143. target,
  144. "$visualParent[TestRoot].Foo",
  145. "Could not find a matching property accessor for 'Foo' on 'Avalonia.UnitTests.TestRoot'.",
  146. "Foo"))
  147. {
  148. target.Bind(Control.TagProperty, binding);
  149. }
  150. }
  151. [Fact]
  152. public void Should_Not_Log_Ancestor_Not_Found_For_Unrooted_Control()
  153. {
  154. var target = new Decorator { };
  155. var binding = new Binding
  156. {
  157. RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
  158. {
  159. AncestorType = typeof(Window),
  160. }
  161. };
  162. using (AssertNoLog())
  163. {
  164. target.Bind(Control.TagProperty, binding);
  165. }
  166. }
  167. }
  168. public class NamedElement : ScopedTestBase
  169. {
  170. [Fact]
  171. public void Should_Log_NameScope_Not_Found()
  172. {
  173. var target = new Decorator { };
  174. var root = new TestRoot(target);
  175. var binding = new Binding("#source") { TypeResolver = ResolveType };
  176. using (AssertLog(target, binding.Path, "NameScope not found.", "#source"))
  177. {
  178. target.Bind(Control.TagProperty, binding);
  179. }
  180. }
  181. [Fact]
  182. public void Should_Not_Log_Element_Property_Null_For_Unrooted_Control()
  183. {
  184. var ns = new NameScope();
  185. var source = new Canvas { Name = "source" };
  186. var target = new Decorator { };
  187. var binding = new Binding("#source.DataContext.Foo") { TypeResolver = ResolveType, NameScope = new(ns) };
  188. var container = new StackPanel
  189. {
  190. [NameScope.NameScopeProperty] = ns,
  191. Children = { source, target }
  192. };
  193. ns.Register(source.Name, source);
  194. using (AssertNoLog())
  195. {
  196. target.Bind(Control.TagProperty, binding);
  197. }
  198. // Sanity check to that the binding works when rooted: make sure that we're not just testing a broken
  199. // binding!
  200. using (AssertNoLog())
  201. {
  202. var root = new TestRoot(container);
  203. root.DataContext = new { Foo = "foo" };
  204. Assert.Equal("foo", target.Tag);
  205. }
  206. }
  207. }
  208. public class Converter : ScopedTestBase
  209. {
  210. [Fact]
  211. public void Should_Log_Error_For_Unconvertible_Type()
  212. {
  213. var target = new Decorator { DataContext = new { Foo = new System.Version() } };
  214. var root = new TestRoot(target);
  215. var binding = new Binding("Foo");
  216. using (AssertLog(
  217. target,
  218. binding.Path,
  219. "Could not convert '0.0' (System.Version) to 'Avalonia.Thickness'.",
  220. property: Control.MarginProperty))
  221. {
  222. target.Bind(Control.MarginProperty, binding);
  223. }
  224. }
  225. [Fact]
  226. public void Should_Log_Error_For_Unconvertible_Type_With_Converter()
  227. {
  228. var target = new Decorator { DataContext = new { Foo = new System.Version() } };
  229. var root = new TestRoot(target);
  230. var binding = new Binding("Foo")
  231. {
  232. Converter = new ThrowingConverter(),
  233. };
  234. using (AssertLog(
  235. target,
  236. binding.Path,
  237. "Could not convert '0.0' (System.Version) to 'Avalonia.Thickness' " +
  238. "using 'Avalonia.Markup.UnitTests.Data.BindingTests_Logging+ThrowingConverter': " +
  239. "The method or operation is not implemented.",
  240. property: Control.MarginProperty))
  241. {
  242. target.Bind(Control.MarginProperty, binding);
  243. }
  244. }
  245. }
  246. public class Fallback : ScopedTestBase
  247. {
  248. [Theory]
  249. [InlineData(true)]
  250. [InlineData(false)]
  251. public void Should_Log_Invalid_FallbackValue(bool rooted)
  252. {
  253. var target = new Decorator { };
  254. var binding = new Binding("foo") { FallbackValue = "bar" };
  255. if (rooted)
  256. new TestRoot(target);
  257. // An invalid fallback value is invalid whether the control is rooted or not.
  258. using (AssertLog(
  259. target,
  260. binding.Path,
  261. "Could not convert FallbackValue 'bar' to 'System.Double'.",
  262. level: LogEventLevel.Error,
  263. property: Visual.OpacityProperty))
  264. {
  265. target.Bind(Visual.OpacityProperty, binding);
  266. }
  267. }
  268. [Theory]
  269. [InlineData(true)]
  270. [InlineData(false)]
  271. public void Should_Log_Invalid_TargetNullValue(bool rooted)
  272. {
  273. var target = new Decorator { DataContext = new { Bar = (string?) null } };
  274. var binding = new Binding("Bar") { TargetNullValue = "foo" };
  275. if (rooted)
  276. new TestRoot(target);
  277. // An invalid target null value is invalid whether the control is rooted or not.
  278. using (AssertLog(
  279. target,
  280. binding.Path,
  281. "Could not convert TargetNullValue 'foo' to 'System.Double'.",
  282. level: LogEventLevel.Error,
  283. property: Visual.OpacityProperty))
  284. {
  285. target.Bind(Visual.OpacityProperty, binding);
  286. }
  287. }
  288. }
  289. public class NonControlDataContext : ScopedTestBase
  290. {
  291. [Fact]
  292. public void Should_Not_Log_Missing_Member_On_Null_DataContext()
  293. {
  294. var target = new TestRoot();
  295. var binding = new Binding("Foo") { DefaultAnchor = new(target) };
  296. target.KeyBindings.Add(new KeyBinding
  297. {
  298. Gesture = new KeyGesture(Key.A),
  299. [!KeyBinding.CommandProperty] = binding
  300. });
  301. using (AssertNoLog())
  302. {
  303. target.Bind(Control.TagProperty, binding);
  304. }
  305. }
  306. [Fact]
  307. public void Should_Log_Missing_Member_On_DataContext()
  308. {
  309. var target = new TestRoot();
  310. var binding = new Binding("Foo") { DefaultAnchor = new(target) };
  311. target.KeyBindings.Add(new KeyBinding
  312. {
  313. Gesture = new KeyGesture(Key.A),
  314. [!KeyBinding.CommandProperty] = binding
  315. });
  316. target.DataContext = new object();
  317. using (AssertLog(
  318. target,
  319. binding.Path,
  320. "Could not find a matching property accessor for 'Foo' on 'System.Object'.",
  321. "Foo"))
  322. {
  323. target.Bind(Control.TagProperty, binding);
  324. }
  325. }
  326. }
  327. public class CompiledBinding : ScopedTestBase
  328. {
  329. [Fact]
  330. public void Should_Log_For_Invalid_DataContext_Type()
  331. {
  332. var target = new TestRoot { DataContext = 48 };
  333. var stringLengthProperty = new ClrPropertyInfo(
  334. "Length",
  335. x => ((string)x).Length,
  336. null,
  337. typeof(int));
  338. var bindingPath = new CompiledBindingPathBuilder()
  339. .Property(stringLengthProperty, PropertyInfoAccessorFactory.CreateInpcPropertyAccessor)
  340. .Build();
  341. var binding = new CompiledBindingExtension(bindingPath);
  342. using (AssertLog(
  343. target,
  344. bindingPath.ToString(),
  345. "Unable to cast object of type 'System.Int32' to type 'System.String'.",
  346. "Length"))
  347. {
  348. target.Bind(Control.TagProperty, binding);
  349. }
  350. }
  351. }
  352. private static IDisposable AssertLog(
  353. AvaloniaObject target,
  354. string expression,
  355. string message,
  356. string? errorPoint = null,
  357. LogEventLevel level = LogEventLevel.Warning,
  358. AvaloniaProperty? property = null)
  359. {
  360. var logs = new List<LogMessage>();
  361. var sink = TestLogSink.Start((l, a, s, m, p) =>
  362. {
  363. if (l >= level)
  364. logs.Add(new(l, a, s, m, p));
  365. });
  366. return Disposable.Create(() =>
  367. {
  368. sink.Dispose();
  369. Assert.Equal(1, logs.Count);
  370. var l = logs[0];
  371. var messageTemplate = errorPoint is not null ?
  372. "An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}" :
  373. "An error occurred binding {Property} to {Expression}: {Message}";
  374. Assert.Equal(level, l.level);
  375. Assert.Equal(LogArea.Binding, l.area);
  376. Assert.Equal(target, l.source);
  377. Assert.Equal(messageTemplate, l.messageTemplate);
  378. Assert.Equal(property ?? Control.TagProperty, l.propertyValues[0]);
  379. Assert.Equal(expression, l.propertyValues[1]);
  380. if (errorPoint is not null)
  381. {
  382. Assert.Equal(errorPoint, l.propertyValues[2]);
  383. Assert.Equal(message, l.propertyValues[3]);
  384. }
  385. else
  386. {
  387. Assert.Equal(message, l.propertyValues[2]);
  388. }
  389. });
  390. }
  391. private static IDisposable AssertNoLog()
  392. {
  393. var count = 0;
  394. var sink = TestLogSink.Start((l, a, s, m, p) =>
  395. {
  396. if (l >= LogEventLevel.Warning)
  397. ++count;
  398. });
  399. return Disposable.Create(() =>
  400. {
  401. sink.Dispose();
  402. Assert.Equal(0, count);
  403. });
  404. }
  405. private static Type ResolveType(string? ns, string typeName)
  406. {
  407. return typeName switch
  408. {
  409. "TextBlock" => typeof(TextBlock),
  410. "TestRoot" => typeof(TestRoot),
  411. _ => throw new InvalidOperationException($"Could not resolve type {typeName}.")
  412. };
  413. }
  414. private class TestClass
  415. {
  416. public TestClass(string? foo = null) => Foo = foo;
  417. public string? Foo { get; set; }
  418. }
  419. private class ThrowingConverter : IValueConverter
  420. {
  421. public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
  422. {
  423. throw new NotImplementedException();
  424. }
  425. public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
  426. {
  427. throw new NotImplementedException();
  428. }
  429. }
  430. private record LogMessage(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues);
  431. }
  432. }