From b3d23047b555c124631e03d1ebe86621b098954b Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 25 Feb 2025 19:15:12 -0800 Subject: [PATCH 1/3] Add a few Clipboard tests Adds a few Clipboard tests. Based somewhat on WinForms unit tests. WPF doesn't consistently swallow bad data on the clipboard. This is called out with comments and skips. Change PresentationCore.Tests so it can use `[WpfFact]` and shared test code. Add reference to drawing extensions so Clipboard image tests can be run. --- .../PresentationCore.Tests.csproj | 5 +- .../System/Windows/ClipboardTests.cs | 246 ++++++++++++++++++ 2 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj index 22b66899765..8540b5be814 100644 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj @@ -10,6 +10,7 @@ $(NoWarn);SYSLIB5005 true + $(TargetFramework)-windows @@ -19,6 +20,7 @@ + @@ -29,7 +31,8 @@ - + + diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs new file mode 100644 index 00000000000..dc0ad23102a --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Specialized; +using System.Runtime.InteropServices; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Color = System.Windows.Media.Color; + +namespace System.Windows; + +// Note: each registered Clipboard format is an OS singleton +// and we should not run this test at the same time as other tests using the same format. +[Collection("Sequential")] +[UISettings(MaxAttempts = 3)] +public class ClipboardTests +{ + [WpfFact] + public void SetText_InvokeString_GetReturnsExpected() + { + Clipboard.SetText("text"); + Clipboard.GetText().Should().Be("text"); + Clipboard.ContainsText().Should().BeTrue(); + } + + [WpfFact] + public void SetAudio_InvokeByteArray_GetReturnsExpected() + { + byte[] audioBytes = [1, 2, 3]; + Clipboard.SetAudio(audioBytes); + + Clipboard.GetAudioStream().Should().BeOfType().Which.ToArray().Should().Equal(audioBytes); + Clipboard.GetData(DataFormats.WaveAudio).Should().BeOfType().Which.ToArray().Should().Equal(audioBytes); + Clipboard.ContainsAudio().Should().BeTrue(); + Clipboard.ContainsData(DataFormats.WaveAudio).Should().BeTrue(); + } + + [WpfFact(Skip = "WinForms difference")] + public void SetAudio_InvokeEmptyByteArray_GetReturnsExpected() + { + byte[] audioBytes = Array.Empty(); + Clipboard.SetAudio(audioBytes); + + // Currently fails with CLIPBRD_E_BAD_DATA + Clipboard.GetAudioStream().Should().BeNull(); + Clipboard.GetData(DataFormats.WaveAudio).Should().BeNull(); + Clipboard.ContainsAudio().Should().BeTrue(); + Clipboard.ContainsData(DataFormats.WaveAudio).Should().BeTrue(); + } + + [WpfFact] + public void SetAudio_NullAudioBytes_ThrowsArgumentNullException() + { + Action action = () => Clipboard.SetAudio((byte[])null!); + action.Should().Throw().WithParameterName("audioBytes"); + } + + [WpfFact] + public void Clipboard_SetAudio_InvokeStream_GetReturnsExpected() + { + byte[] audioBytes = [1, 2, 3]; + using MemoryStream audioStream = new(audioBytes); + Clipboard.SetAudio(audioStream); + + Clipboard.GetAudioStream().Should().BeOfType().Which.ToArray().Should().Equal(audioBytes); + Clipboard.GetData(DataFormats.WaveAudio).Should().BeOfType().Which.ToArray().Should().Equal(audioBytes); + Clipboard.ContainsAudio().Should().BeTrue(); + Clipboard.ContainsData(DataFormats.WaveAudio).Should().BeTrue(); + } + + [WpfFact(Skip = "WinForms difference")] + public void SetAudio_InvokeEmptyStream_GetReturnsExpected() + { + using MemoryStream audioStream = new(); + Clipboard.SetAudio(audioStream); + + // Currently fails with CLIPBRD_E_BAD_DATA + Clipboard.GetAudioStream().Should().BeNull(); + Clipboard.GetData(DataFormats.WaveAudio).Should().BeNull(); + Clipboard.ContainsAudio().Should().BeTrue(); + Clipboard.ContainsData(DataFormats.WaveAudio).Should().BeTrue(); + } + + [WpfFact] + public void SetAudio_NullAudioStream_ThrowsArgumentNullException() + { + Action action = () => Clipboard.SetAudio((Stream)null!); + action.Should().Throw().WithParameterName("audioStream"); + } + + [WpfTheory] + // These three fail in WinForms, should probably fail in WPF as well. + // [InlineData("")] + // [InlineData(" ")] + // [InlineData("\t")] + [InlineData(null)] + public void SetData_EmptyOrWhitespaceFormat_ThrowsArgumentException(string? format) + { + Action action = () => Clipboard.SetData(format!, "data"); + action.Should().Throw().WithParameterName("format"); + } + + [WpfFact] + public void SetData_Null_Throws() + { + Action action = () => Clipboard.SetData("MyData", data: null!); + action.Should().Throw().WithParameterName("data"); + } + + [WpfFact] + public void SetData_NullData_ThrowsArgumentNullException() + { + Action action = () => Clipboard.SetData("MyData", data: null!); + action.Should().Throw().WithParameterName("data"); + } + + [WpfFact] + public void SetData_Int_GetReturnsExpected() + { + Clipboard.SetData("format", 1); + Clipboard.GetData("format").Should().Be(1); + Clipboard.ContainsData("format").Should().BeTrue(); + } + + [WpfFact] + public void SetFileDropList_Invoke_GetReturnsExpected() + { + StringCollection filePaths = + [ + "filePath", + "filePath2" + ]; + + Clipboard.SetFileDropList(filePaths); + + Clipboard.GetFileDropList().Should().BeEquivalentTo(filePaths); + Clipboard.ContainsFileDropList().Should().BeTrue(); + } + + [WpfFact] + public void SetFileDropList_NullFilePaths_ThrowsArgumentNullException() + { + Action action = () => Clipboard.SetFileDropList(null!); + // Note: The name will change with the WinForms shared code. + action.Should().Throw().WithParameterName("fileDropList"); + } + + [WpfFact] + public void SetFileDropList_EmptyFilePaths_ThrowsArgumentException() + { + Action action = static () => Clipboard.SetFileDropList([]); + action.Should().Throw(); + } + + [WpfTheory] + [InlineData("")] + [InlineData("\0")] + public void SetFileDropList_InvalidFileInPaths_ThrowsArgumentException(string filePath) + { + StringCollection filePaths = + [ + filePath + ]; + + Action action = () => Clipboard.SetFileDropList(filePaths); + action.Should().Throw(); + } + + [WpfFact] + public unsafe void SetImage_InvokeBitmap_VerifyPixelColor() + { + WriteableBitmap bitmap = new(10, 10, 96, 96, PixelFormats.Bgra32, palette: null); + + // Set a specific pixel to a given color (e.g., set pixel at (1, 2) to red) + Color color = Colors.Red; + byte[] colorData = [color.B, color.G, color.R, color.A]; + bitmap.WritePixels(new Int32Rect(1, 2, 1, 1), colorData, 4, 0); + + Clipboard.SetImage(bitmap); + Clipboard.ContainsImage().Should().BeTrue(); + InteropBitmap result = Clipboard.GetImage().Should().BeOfType().Subject; + + // Verify the pixel color + byte[] resultColorData = new byte[4]; + result.CopyPixels(new Int32Rect(1, 2, 1, 1), resultColorData, 4, 0); + resultColorData.Should().Equal(colorData); + + // Set back the image we just got from the clipboard + Clipboard.SetImage(result); + Clipboard.ContainsImage().Should().BeTrue(); + result = Clipboard.GetImage().Should().BeOfType().Subject; + + // Verify the pixel color + result.CopyPixels(new Int32Rect(1, 2, 1, 1), resultColorData, 4, 0); + resultColorData.Should().Equal(colorData); + } + + [WpfTheory] + [BoolData] + public void SetDataObject_WithMultipleData(bool copy) + { + string testData1 = "test data one"; + int testData2 = 42; + DataObject data = new(); + data.SetData("testData1", testData1); + data.SetData("testData2", testData2); + Clipboard.SetDataObject(data, copy); + + object? result1 = Clipboard.GetData("testData1"); + result1.Should().Be(testData1); + object? result2 = Clipboard.GetData("testData2"); + result2.Should().Be(testData2); + } + + [WpfFact] + public void SetData_Text_Format_AllUpper() + { + // The fact that casing on input matters is likely incorrect, but behavior has been this way. + Clipboard.SetData("TEXT", "Hello, World!"); + Clipboard.ContainsText().Should().BeTrue(); + Clipboard.ContainsData("TEXT").Should().BeTrue(); + Clipboard.ContainsData(DataFormats.Text).Should().BeTrue(); + Clipboard.ContainsData(DataFormats.UnicodeText).Should().BeTrue(); + + IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + string[] formats = dataObject.GetFormats(); + formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]); + + formats = dataObject.GetFormats(autoConvert: false); + formats.Should().BeEquivalentTo(["Text"]); + + // CLIPBRD_E_BAD_DATA returned when trying to get clipboard data. This will no longer throw when using + // the shared clipboard code. + Action action = () => Clipboard.GetText().Should().BeEmpty(); + action.Should().Throw(); + action = () => Clipboard.GetText(TextDataFormat.Text).Should().BeEmpty(); + action.Should().Throw(); + action = () => Clipboard.GetText(TextDataFormat.UnicodeText).Should().BeEmpty(); + action.Should().Throw(); + + Clipboard.GetData("System.String").Should().BeNull(); + action = () => Clipboard.GetData("TEXT").Should().BeNull(); + action.Should().Throw(); + } +} From 3b4cedf940114675ec6eec18c48a9a504fa47d92 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 25 Feb 2025 20:19:34 -0800 Subject: [PATCH 2/3] Remove code that duplicates what comes from the core test assembly --- .../TestUtilities/AppContextSwitchNames.cs | 21 ------- .../TestUtilities/AppContextSwitchScope.cs | 48 --------------- .../TestUtilities/BinaryFormatterScope.cs | 61 ------------------- 3 files changed, 130 deletions(-) delete mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchNames.cs delete mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchScope.cs delete mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/BinaryFormatterScope.cs diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchNames.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchNames.cs deleted file mode 100644 index caaa017208d..00000000000 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchNames.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.Serialization.Formatters.Binary; - -namespace System; - -public static class AppContextSwitchNames -{ - /// - /// The switch that controls whether or not the is enabled. - /// - public static string EnableUnsafeBinaryFormatterSerialization { get; } - = "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization"; - - /// - /// Switch that controls switch caching. - /// - public static string LocalAppContext_DisableCaching { get; } - = "TestSwitch.LocalAppContext.DisableCaching"; -} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchScope.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchScope.cs deleted file mode 100644 index 65087fda71e..00000000000 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/AppContextSwitchScope.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System; - -/// -/// Scope for temporarily setting an switch. Use in a statement. -/// -/// -/// -/// It is recommended to create wrappers for this struct for both simplicity and to allow adding synchronization. -/// See for an example of doing this. -/// -/// -public readonly ref struct AppContextSwitchScope -{ - private readonly string _switchName; - private readonly bool _originalState; - - public AppContextSwitchScope(string switchName, bool enable) - { - if (!AppContext.TryGetSwitch(AppContextSwitchNames.LocalAppContext_DisableCaching, out bool isEnabled) - || !isEnabled) - { - // It doesn't make sense to try messing with AppContext switches if they are going to be cached. - throw new InvalidOperationException("LocalAppContext switch caching is not disabled."); - } - - AppContext.TryGetSwitch(switchName, out _originalState); - - AppContext.SetSwitch(switchName, enable); - if (!AppContext.TryGetSwitch(switchName, out isEnabled) || isEnabled != enable) - { - throw new InvalidOperationException($"Could not set {switchName} to {enable}."); - } - - _switchName = switchName; - } - - public void Dispose() - { - AppContext.SetSwitch(_switchName, _originalState); - if (!AppContext.TryGetSwitch(_switchName, out bool isEnabled) || isEnabled != _originalState) - { - throw new InvalidOperationException($"Could not reset {_switchName} to {_originalState}."); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/BinaryFormatterScope.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/BinaryFormatterScope.cs deleted file mode 100644 index 3c73e69c7cf..00000000000 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/TestUtilities/BinaryFormatterScope.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.Serialization.Formatters.Binary; - -namespace System; - -/// -/// Scope for enabling / disabling the . Use in a statement. -/// -public readonly ref struct BinaryFormatterScope -{ - private readonly AppContextSwitchScope _switchScope; - - public BinaryFormatterScope(bool enable) - { - // Prevent multiple BinaryFormatterScopes from running simultaneously. Using Monitor to allow recursion on - // the same thread. - Monitor.Enter(typeof(BinaryFormatterScope)); - _switchScope = new AppContextSwitchScope(AppContextSwitchNames.EnableUnsafeBinaryFormatterSerialization, enable); - } - - public void Dispose() - { - try - { - _switchScope.Dispose(); - } - finally - { - Monitor.Exit(typeof(BinaryFormatterScope)); - } - } - - static BinaryFormatterScope() - { - // Need to explicitly set the switch to whatever the default is as its default value is in transition. - -#pragma warning disable SYSLIB0011 // Type or member is obsolete - BinaryFormatter formatter = new(); -#pragma warning restore SYSLIB0011 // Type or member is obsolete - try - { -#pragma warning disable SYSLIB0011 // Type or member is obsolete - formatter.Serialize(null!, null!); -#pragma warning restore SYSLIB0011 - } - catch (NotSupportedException) - { - AppContext.SetSwitch(AppContextSwitchNames.EnableUnsafeBinaryFormatterSerialization, false); - return; - } - catch (ArgumentNullException) - { - AppContext.SetSwitch(AppContextSwitchNames.EnableUnsafeBinaryFormatterSerialization, true); - return; - } - - throw new InvalidOperationException(); - } -} \ No newline at end of file From 78908860333e1448b221747a63fe35d127b3c2d4 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 26 Feb 2025 17:14:47 -0800 Subject: [PATCH 3/3] Remove a comment --- .../PresentationCore.Tests/System/Windows/ClipboardTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs index dc0ad23102a..d1c85eea300 100644 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/ClipboardTests.cs @@ -216,7 +216,6 @@ public void SetDataObject_WithMultipleData(bool copy) [WpfFact] public void SetData_Text_Format_AllUpper() { - // The fact that casing on input matters is likely incorrect, but behavior has been this way. Clipboard.SetData("TEXT", "Hello, World!"); Clipboard.ContainsText().Should().BeTrue(); Clipboard.ContainsData("TEXT").Should().BeTrue();