|
// 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.Diagnostics.CodeAnalysis;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using ClipboardCore = System.Private.Windows.Ole.ClipboardCore<System.Private.Windows.Ole.MockOleServices<System.Private.Windows.Ole.ClipboardCoreTests>>;
using ClipboardScope = System.Private.Windows.Ole.ClipboardScope<System.Private.Windows.Ole.MockOleServices<System.Private.Windows.Ole.ClipboardCoreTests>>;
using DataObject = System.Private.Windows.Ole.TestDataObject<System.Private.Windows.Ole.MockOleServices<System.Private.Windows.Ole.ClipboardCoreTests>>;
namespace System.Private.Windows.Ole;
public unsafe class ClipboardCoreTests
{
[Fact]
public void Clear_ChecksThreadState()
{
Assert.Throws<ThreadStateException>(() => ClipboardCore<InvalidThreadOleServices>.Clear());
}
[Fact]
public void SetData_ChecksThreadState()
{
Assert.Throws<ThreadStateException>(() => ClipboardCore<InvalidThreadOleServices>.SetData(null!, false));
}
[Fact]
public void TryGetData_ChecksThreadState()
{
Assert.Throws<ThreadStateException>(() => ClipboardCore<InvalidThreadOleServices>.TryGetData(out _, out _));
}
private class InvalidThreadOleServices() : IOleServices
{
static bool IOleServices.AllowTypeWithoutResolver<T>() => throw new NotImplementedException();
static IComVisibleDataObject IOleServices.CreateDataObject() => throw new NotImplementedException();
static void IOleServices.EnsureThreadState() => throw new ThreadStateException();
static unsafe HRESULT IOleServices.GetDataHere(string format, object data, FORMATETC* pformatetc, STGMEDIUM* pmedium) => throw new NotImplementedException();
static bool IOleServices.IsValidTypeForFormat(Type type, string format) => throw new NotImplementedException();
static HRESULT IOleServices.OleFlushClipboard() => throw new NotImplementedException();
static unsafe HRESULT IOleServices.OleGetClipboard(IDataObject** dataObject) => throw new NotImplementedException();
static unsafe HRESULT IOleServices.OleSetClipboard(IDataObject* dataObject) => throw new NotImplementedException();
static unsafe bool IOleServices.TryGetObjectFromDataObject<T>(IDataObject* dataObject, string requestedFormat, [NotNullWhen(true)] out T data) => throw new NotImplementedException();
static void IOleServices.ValidateDataStoreData(ref string format, bool autoConvert, object? data) => throw new NotImplementedException();
}
[Fact]
public void MockOleServices_ValidatePointerBehavior()
{
DataObject dataObject = new();
using ComScope<IDataObject> iDataObject = ComHelpers.GetComScope<IDataObject>(dataObject);
using AgileComPointer<IDataObject> agileComPointer = new(iDataObject.Value, takeOwnership: false);
using ComScope<IDataObject> fetched = agileComPointer.GetInterface();
// We don't get a proxy when in process. Faking a proxy would require not using ComWrappers as we
// cannot control QueryInterface behavior (it depends on IUnknown being it's pointer).
Assert.Equal((nint)iDataObject.Value, (nint)fetched.Value);
}
[Fact]
public void SetData_SetsClipboard()
{
using ClipboardScope scope = new();
DataObject dataObject = new();
HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
result = ClipboardCore.TryGetData(out var data, out var original, retryTimes: 1, retryDelay: 0);
using (data)
{
result.Should().Be(HRESULT.S_OK);
data.IsNull.Should().BeFalse();
original.Should().BeSameAs(dataObject);
}
}
[Fact]
public void Clear_ClearsClipboard()
{
HRESULT result;
DataObject dataObject = new();
using (ClipboardScope scope = new())
{
result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
}
result = ClipboardCore.TryGetData(out var data, out var original, retryTimes: 1, retryDelay: 0);
using (data)
{
result.Should().Be(HRESULT.CLIPBRD_E_BAD_DATA);
data.IsNull.Should().BeTrue();
original.Should().BeNull();
}
}
[Fact]
public void RoundTrip_Text()
{
using ClipboardScope scope = new();
DataObject dataObject = new();
dataObject.SetData(DataFormatNames.Text, "Hello, World!");
HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
result = ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var data, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
data.Should().NotBeNull();
data!.GetDataPresent(DataFormatNames.Text).Should().BeTrue();
data.GetData(DataFormatNames.Text).Should().Be("Hello, World!");
data.GetDataPresent(DataFormatNames.UnicodeText).Should().BeTrue();
data.GetData(DataFormatNames.UnicodeText).Should().Be("Hello, World!");
data.GetDataPresent(DataFormatNames.UnicodeText, autoConvert: false).Should().BeFalse();
data.GetData(DataFormatNames.UnicodeText, autoConvert: false).Should().BeNull();
IDataObjectInternal iDataObject = data.Should().BeAssignableTo<IDataObjectInternal>().Subject;
iDataObject.TryGetData(out string? text).Should().BeTrue();
text.Should().Be("Hello, World!");
iDataObject.TryGetData(DataFormatNames.Text, out text).Should().BeTrue();
text.Should().Be("Hello, World!");
iDataObject.TryGetData(DataFormatNames.UnicodeText, out text).Should().BeTrue();
text.Should().Be("Hello, World!");
iDataObject.TryGetData(DataFormatNames.UnicodeText, autoConvert: false, out text).Should().BeFalse();
text.Should().BeNull();
}
[Fact]
public void DerivedDataObject_DataPresent()
{
// https://github.com/dotnet/winforms/issues/12789
SomeDataObject data = new();
// This was provided as a workaround for the above and should not break, but should
// also work without it.
data.SetData(SomeDataObject.Format, data);
ClipboardCore.SetData(data, copy: false, retryTimes: 1, retryDelay: 0);
ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var outData).Should().Be(HRESULT.S_OK);
outData!.GetDataPresent(SomeDataObject.Format).Should().BeTrue();
}
internal class SomeDataObject : DataObject
{
public static string Format => "SomeDataObjectId";
public override string[] GetFormats() => [Format];
public override bool GetDataPresent(string format, bool autoConvert)
=> format == Format || base.GetDataPresent(format, autoConvert);
}
[Fact]
public void SerializableObject_InProcess_DoesNotUseBinaryFormatter()
{
// This test ensures that the SerializableObject does not use BinaryFormatter when running in process.
using ClipboardScope scope = new();
DataObject dataObject = new();
SerializablePerson person = new() { Name = "John Doe", Age = 30 };
dataObject.SetData(person);
HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
result = ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var data, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
data.Should().NotBeNull();
data!.GetDataPresent(typeof(SerializablePerson).FullName!).Should().BeTrue();
data.GetData(typeof(SerializablePerson).FullName!).Should().BeSameAs(person);
}
[Serializable]
internal class SerializablePerson
{
public string Name { get; set; } = "DefaultName";
public int Age { get; set; }
}
[Fact]
public void Clear_InvokeMultipleTimes_Success()
{
ClipboardCore.Clear();
HRESULT result = ClipboardCore.TryGetData(out var data, out var original, retryTimes: 1, retryDelay: 0);
using (data)
{
result.Should().Be(HRESULT.CLIPBRD_E_BAD_DATA);
data.IsNull.Should().BeTrue();
original.Should().BeNull();
}
ClipboardCore.Clear();
result = ClipboardCore.TryGetData(out var data1, out var original1, retryTimes: 1, retryDelay: 0);
using (data1)
{
result.Should().Be(HRESULT.CLIPBRD_E_BAD_DATA);
data1.IsNull.Should().BeTrue();
original1.Should().BeNull();
}
}
[Fact]
public void Contains_InvokeMultipleTimes_Success()
{
DataObject dataObject = new();
HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
ClipboardCore.Clear();
result = ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var data, retryTimes: 1, retryDelay: 0);
HRESULT result2 = ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var data2, retryTimes: 1, retryDelay: 0);
result.Should().Be(result2);
result.Should().Be(HRESULT.CLIPBRD_E_BAD_DATA);
data.Should().Be(data2);
data.Should().BeNull();
}
[Theory]
[StringWithNullData]
public void ContainsData_InvokeMultipleTimes_Success(string? format)
{
SetAndGetClipboardDataMultipleTimes(format, null!, out ITestDataObject? outData1, out ITestDataObject? outData2);
outData1!.GetFormats().Should().BeEquivalentTo(outData2!.GetFormats());
}
[Theory]
[EnumData<TextDataFormat>]
public void ContainsText_TextDataFormat_InvokeMultipleTimes_Success(string format)
{
SetAndGetClipboardDataMultipleTimes(format, null!, out ITestDataObject? outData1, out ITestDataObject? outData2);
outData1!.GetFormats().Contains(format).Should().BeTrue();
outData1!.GetFormats().Should().BeEquivalentTo(outData2!.GetFormats());
}
[Fact]
public void GetAudioStream_InvokeMultipleTimes_Success()
{
string testData = "WaveAudio";
string format = DataFormatNames.WaveAudio;
SetAndGetClipboardDataMultipleTimes(format, testData, out ITestDataObject? outData1, out ITestDataObject? outData2);
VerifyResult(testData, format, outData1, outData2);
}
[Fact]
public void GetDataObject_InvokeMultipleTimes_Success()
{
SetAndGetClipboardDataMultipleTimes(null, null!, out ITestDataObject? outData1, out ITestDataObject? outData2);
outData1.Should().NotBeNull();
outData2.Should().NotBeNull();
outData1.GetFormats().Should().BeEquivalentTo(outData2.GetFormats());
}
[Fact]
public void GetFileDropList_InvokeMultipleTimes_Success()
{
string testData = "FileDrop";
string format = DataFormatNames.FileDrop;
SetAndGetClipboardDataMultipleTimes(format, testData, out ITestDataObject? outData1, out ITestDataObject? outData2);
VerifyResult(testData, format, outData1, outData2);
}
[Fact]
public void GetImage_InvokeMultipleTimes_Success()
{
string testData = "Bitmap";
string format = DataFormatNames.Bitmap;
SetAndGetClipboardDataMultipleTimes(format, testData, out ITestDataObject? outData1, out ITestDataObject? outData2);
VerifyResult(testData, format, outData1, outData2);
}
[Fact]
public void GetText_InvokeMultipleTimes_Success()
{
string testData = "Text";
string format = DataFormatNames.UnicodeText;
SetAndGetClipboardDataMultipleTimes(format, testData, out ITestDataObject? outData1, out ITestDataObject? outData2);
VerifyResult(testData, format, outData1, outData2);
}
[Theory]
[EnumData<TextDataFormat>]
public void GetText_TextDataFormat_InvokeMultipleTimes_Success(string format)
{
string testData = "Text";
SetAndGetClipboardDataMultipleTimes(format, testData, out ITestDataObject? outData1, out ITestDataObject? outData2);
VerifyResult(testData, format, outData1, outData2);
}
private static void SetAndGetClipboardDataMultipleTimes(string? format, string data, out ITestDataObject? outData1, out ITestDataObject? outData2)
{
DataObject dataObject = string.IsNullOrEmpty(format) ? new() : new(format, data);
HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0);
result.Should().Be(HRESULT.S_OK);
ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out outData1, retryTimes: 1, retryDelay: 0);
ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out outData2, retryTimes: 1, retryDelay: 0);
}
private static void VerifyResult(string testData, string format, ITestDataObject? outData1, ITestDataObject? outData2)
{
outData1.Should().NotBeNull();
outData2.Should().NotBeNull();
outData1.GetDataPresent(format).Should().BeTrue();
outData1.GetData(format, autoConvert: false).Should().Be(testData);
outData1.GetData(format, autoConvert: false).Should().Be(outData2.GetData(format, autoConvert: false));
}
[Fact]
public void SetData_Int_GetReturnsExpected()
{
ClipboardCore.SetData(new DataObject(SomeDataObject.Format, 1), copy: false, retryTimes: 1, retryDelay: 0);
ClipboardCore.GetDataObject<DataObject, ITestDataObject>(out var outData, retryTimes: 1, retryDelay: 0);
outData.Should().NotBeNull();
outData.GetDataPresent("SomeDataObjectId").Should().BeTrue();
outData.GetData("SomeDataObjectId").Should().Be(1);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
[InlineData(null)]
public void SetData_EmptyOrWhitespaceFormat_ThrowsArgumentException(string? format)
{
Action action = () => ClipboardCore.SetData(new DataObject(format!, "data"), copy: true);
action.Should().Throw<ArgumentException>().WithParameterName("format");
}
[Fact]
public void SetFileDropList_NullFilePaths_ThrowsArgumentNullException()
{
Action action = () => ClipboardCore.SetFileDropList(null!);
action.Should().Throw<ArgumentNullException>().WithParameterName("filePaths");
}
[Fact]
public void SetFileDropList_EmptyFilePaths_ThrowsArgumentException()
{
Action action = static () => ClipboardCore.SetFileDropList([]);
action.Should().Throw<ArgumentException>();
}
[Theory]
[InlineData("")]
[InlineData("\0")]
public void SetFileDropList_InvalidFileInPaths_ThrowsArgumentException(string filePath)
{
StringCollection filePaths =
[
filePath
];
Action action = () => ClipboardCore.SetFileDropList(filePaths);
action.Should().Throw<ArgumentException>();
}
}
|