Skip to content

Commit 65ad907

Browse files
authored
Merge pull request #5207 from bclothier/MiscBugFixes2
Make PermissiveAssertClass more permissive
2 parents 3d5370a + 4646c49 commit 65ad907

File tree

5 files changed

+792
-48
lines changed

5 files changed

+792
-48
lines changed

Rubberduck.UnitTesting/ComClientHelpers/PermissiveObjectComparer.cs

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Rubberduck.VBEditor.Variants;
34

45
namespace Rubberduck.UnitTesting.ComClientHelpers
56
{
@@ -13,42 +14,19 @@ public class PermissiveObjectComparer : IEqualityComparer<object>
1314
/// <returns>VBA equity</returns>
1415
public new bool Equals(object x, object y)
1516
{
16-
var expected = x;
17-
var actual = y;
18-
19-
// try promoting integral types first.
20-
if (expected is ulong && actual is ulong)
21-
{
22-
return (ulong)x == (ulong)y;
23-
}
24-
// then try promoting to floating point
25-
if (expected is double && actual is double)
26-
{
27-
// ReSharper disable once CompareOfFloatsByEqualityOperator - We're cool with that.
28-
return (double)x == (double)y;
29-
}
30-
// that shouldn't actually happen, since decimal is the only numeric ValueType in its category
31-
// this means we should've gotten the same types earlier in the Assert method
32-
if (expected is decimal && actual is decimal)
17+
if (x == null)
3318
{
34-
return (decimal)x == (decimal)y;
19+
return y == null;
3520
}
36-
// worst case scenario for numbers
37-
// since we're inside VBA though, double is the more appropriate type to compare,
38-
// because that is what's used internally anyways, see https://support.microsoft.com/en-us/kb/78113
39-
if ((expected is decimal && actual is double) || (expected is double && actual is decimal))
40-
{
41-
// ReSharper disable once CompareOfFloatsByEqualityOperator - We're still cool with that.
42-
return (double)x == (double)y;
43-
}
44-
// no number-type promotions are applicable. 2nd to last straw: string "promotion"
45-
if (expected is string || actual is string)
21+
22+
if (y == null)
4623
{
47-
expected = expected.ToString();
48-
actual = actual.ToString();
49-
return expected.Equals(actual);
24+
return false;
5025
}
51-
return x.Equals(y);
26+
27+
var converted = VariantConverter.ChangeType(y, x.GetType());
28+
29+
return x.Equals(converted);
5230
}
5331

5432
/// <summary>
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Rubberduck.VBEditor.Variants
6+
{
7+
public enum VARENUM
8+
{
9+
VT_EMPTY = 0x0000,
10+
VT_NULL = 0x0001,
11+
VT_I2 = 0x0002,
12+
VT_I4 = 0x0003,
13+
VT_R4 = 0x0004,
14+
VT_R8 = 0x0005,
15+
VT_CY = 0x0006,
16+
VT_DATE = 0x0007,
17+
VT_BSTR = 0x0008,
18+
VT_DISPATCH = 0x0009,
19+
VT_ERROR = 0x000A,
20+
VT_BOOL = 0x000B,
21+
VT_VARIANT = 0x000C,
22+
VT_UNKNOWN = 0x000D,
23+
VT_DECIMAL = 0x000E,
24+
VT_I1 = 0x0010,
25+
VT_UI1 = 0x0011,
26+
VT_UI2 = 0x0012,
27+
VT_UI4 = 0x0013,
28+
VT_I8 = 0x0014,
29+
VT_UI8 = 0x0015,
30+
VT_INT = 0x0016,
31+
VT_UINT = 0x0017,
32+
VT_VOID = 0x0018,
33+
VT_HRESULT = 0x0019,
34+
VT_PTR = 0x001A,
35+
VT_SAFEARRAY = 0x001B,
36+
VT_CARRAY = 0x001C,
37+
VT_USERDEFINED = 0x001D,
38+
VT_LPSTR = 0x001E,
39+
VT_LPWSTR = 0x001F,
40+
VT_RECORD = 0x0024,
41+
VT_INT_PTR = 0x0025,
42+
VT_UINT_PTR = 0x0026,
43+
VT_ARRAY = 0x2000,
44+
VT_BYREF = 0x4000
45+
}
46+
47+
[Flags]
48+
public enum VariantConversionFlags : ushort
49+
{
50+
NO_FLAGS = 0x00,
51+
VARIANT_NOVALUEPROP = 0x01, //Prevents the function from attempting to coerce an object to a fundamental type by getting the Value property. Applications should set this flag only if necessary, because it makes their behavior inconsistent with other applications.
52+
VARIANT_ALPHABOOL = 0x02, //Converts a VT_BOOL value to a string containing either "True" or "False".
53+
VARIANT_NOUSEROVERRIDE = 0x04, //For conversions to or from VT_BSTR, passes LOCALE_NOUSEROVERRIDE to the core coercion routines.
54+
VARIANT_LOCALBOOL = 0x08 //For conversions from VT_BOOL to VT_BSTR and back, uses the language specified by the locale in use on the local computer.
55+
}
56+
57+
/// <summary>
58+
/// Handles variant conversions, enabling us to have same implicit conversion behaviors within
59+
/// .NET as we can observe it from VBA/VB6.
60+
/// </summary>
61+
/// <remarks>
62+
/// The <see cref="VariantChangeType"/> function is the same one used internally by VBA/VB6.
63+
/// However, we have to wrap the metadata, which the class helps with.
64+
///
65+
/// See the link for details on how marshaling are handled with <see cref="object"/>
66+
/// https://docs.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-objects
67+
/// </remarks>
68+
public static class VariantConverter
69+
{
70+
private const string dllName = "oleaut32.dll";
71+
72+
// HRESULT VariantChangeType(
73+
// VARIANTARG *pvargDest,
74+
// const VARIANTARG *pvarSrc,
75+
// USHORT wFlags,
76+
// VARTYPE vt
77+
// );
78+
[DllImport(dllName, EntryPoint = "VariantChangeType", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
79+
private static extern int VariantChangeType(ref object pvargDest, ref object pvarSrc, VariantConversionFlags wFlags, VARENUM vt);
80+
81+
// HRESULT VariantChangeTypeEx(
82+
// VARIANTARG *pvargDest,
83+
// const VARIANTARG *pvarSrc,
84+
// LCID lcid,
85+
// USHORT wFlags,
86+
// VARTYPE vt
87+
// );
88+
[DllImport(dllName, EntryPoint = "VariantChangeTypeEx", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
89+
private static extern int VariantChangeTypeEx(ref object pvargDest, ref object pvarSrc, int lcid, VariantConversionFlags wFlags, VARENUM vt);
90+
91+
public static object ChangeType(object value, VARENUM vt)
92+
{
93+
return ChangeType(value, vt, null);
94+
}
95+
96+
private static bool HRESULT_FAILED(int hr) => hr < 0;
97+
public static object ChangeType(object value, VARENUM vt, CultureInfo cultureInfo)
98+
{
99+
object result = null;
100+
var hr = cultureInfo == null
101+
? VariantChangeType(ref result, ref value, VariantConversionFlags.NO_FLAGS, vt)
102+
: VariantChangeTypeEx(ref result, ref value, cultureInfo.LCID, VariantConversionFlags.NO_FLAGS, vt);
103+
if (HRESULT_FAILED(hr))
104+
{
105+
throw Marshal.GetExceptionForHR(hr);
106+
}
107+
108+
return result;
109+
}
110+
111+
public static object ChangeType(object value, Type targetType)
112+
{
113+
return ChangeType(value, GetVarEnum(targetType));
114+
}
115+
116+
public static object ChangeType(object value, Type targetType, CultureInfo culture)
117+
{
118+
return ChangeType(value, GetVarEnum(targetType), culture);
119+
}
120+
121+
public static VARENUM GetVarEnum(Type target)
122+
{
123+
switch (target)
124+
{
125+
case null:
126+
return VARENUM.VT_EMPTY;
127+
case Type dbNull when dbNull == typeof(DBNull):
128+
return VARENUM.VT_NULL;
129+
case Type err when err == typeof(ErrorWrapper):
130+
return VARENUM.VT_ERROR;
131+
case Type disp when disp == typeof(DispatchWrapper):
132+
return VARENUM.VT_DISPATCH;
133+
case Type unk when unk == typeof(UnknownWrapper):
134+
return VARENUM.VT_UNKNOWN;
135+
case Type cy when cy == typeof(CurrencyWrapper):
136+
return VARENUM.VT_CY;
137+
case Type b when b == typeof(bool):
138+
return VARENUM.VT_BOOL;
139+
case Type s when s == typeof(sbyte):
140+
return VARENUM.VT_I1;
141+
case Type b when b == typeof(byte):
142+
return VARENUM.VT_UI1;
143+
case Type i16 when i16 == typeof(short):
144+
return VARENUM.VT_I2;
145+
case Type ui16 when ui16 == typeof(ushort):
146+
return VARENUM.VT_UI2;
147+
case Type i32 when i32 == typeof(int):
148+
return VARENUM.VT_I4;
149+
case Type ui32 when ui32 == typeof(uint):
150+
return VARENUM.VT_UI4;
151+
case Type i64 when i64 == typeof(long):
152+
return VARENUM.VT_I8;
153+
case Type ui64 when ui64 == typeof(ulong):
154+
return VARENUM.VT_UI8;
155+
case Type sng when sng == typeof(float):
156+
return VARENUM.VT_R4;
157+
case Type dbl when dbl == typeof(double):
158+
return VARENUM.VT_R8;
159+
case Type dec when dec == typeof(decimal):
160+
return VARENUM.VT_DECIMAL;
161+
case Type dt when dt == typeof(DateTime):
162+
return VARENUM.VT_DATE;
163+
case Type s when s == typeof(string):
164+
return VARENUM.VT_BSTR;
165+
//case Type a when a == typeof(Array):
166+
// return VARENUM.VT_ARRAY;
167+
case Type obj when obj == typeof(object):
168+
case Type var when var == typeof(VariantWrapper):
169+
return VARENUM.VT_VARIANT;
170+
default:
171+
throw new NotSupportedException("Unrecognized system type that cannot be mapped to a VARENUM out of the box.");
172+
}
173+
}
174+
175+
public static VARENUM GetVarEnum(TypeCode typeCode)
176+
{
177+
switch (typeCode)
178+
{
179+
case TypeCode.Empty:
180+
return VARENUM.VT_EMPTY;
181+
case TypeCode.Object:
182+
return VARENUM.VT_UNKNOWN;
183+
case TypeCode.DBNull:
184+
return VARENUM.VT_NULL;
185+
case TypeCode.Boolean:
186+
return VARENUM.VT_BOOL;
187+
case TypeCode.Char:
188+
return VARENUM.VT_UI2;
189+
case TypeCode.SByte:
190+
return VARENUM.VT_I1;
191+
case TypeCode.Byte:
192+
return VARENUM.VT_UI1;
193+
case TypeCode.Int16:
194+
return VARENUM.VT_I2;
195+
case TypeCode.UInt16:
196+
return VARENUM.VT_UI2;
197+
case TypeCode.Int32:
198+
return VARENUM.VT_I4;
199+
case TypeCode.UInt32:
200+
return VARENUM.VT_UI4;
201+
case TypeCode.Int64:
202+
return VARENUM.VT_I8;
203+
case TypeCode.UInt64:
204+
return VARENUM.VT_UI8;
205+
case TypeCode.Single:
206+
return VARENUM.VT_R4;
207+
case TypeCode.Double:
208+
return VARENUM.VT_R8;
209+
case TypeCode.Decimal:
210+
return VARENUM.VT_DECIMAL;
211+
case TypeCode.DateTime:
212+
return VARENUM.VT_DATE;
213+
case TypeCode.String:
214+
return VARENUM.VT_BSTR;
215+
default:
216+
throw new ArgumentOutOfRangeException(nameof(typeCode), typeCode, null);
217+
}
218+
}
219+
}
220+
}

RubberduckTests/UnitTesting/AssertTests.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using NUnit.Framework;
23
using Rubberduck.UnitTesting;
34

@@ -68,11 +69,10 @@ public void IsFalseFailsWithTrueExpression()
6869

6970
[Category("Unit Testing")]
7071
[Test]
71-
[Ignore("Would require passing COM objects for proper verification")]
7272
public void AreSameShouldSucceedWithSameReferences()
7373
{
7474
var assert = new AssertClass();
75-
var obj1 = new object();
75+
var obj1 = GetComObject();
7676
var obj2 = obj1;
7777
assert.AreSame(obj1, obj2);
7878

@@ -81,12 +81,11 @@ public void AreSameShouldSucceedWithSameReferences()
8181

8282
[Category("Unit Testing")]
8383
[Test]
84-
[Ignore("Would require passing COM objects for proper verification")]
8584
public void AreSameShouldFailWithDifferentReferences()
8685
{
8786
var assert = new AssertClass();
88-
var obj1 = new object();
89-
var obj2 = new object();
87+
var obj1 = GetComObject();
88+
var obj2 = GetComObject();
9089
assert.AreSame(obj1, obj2);
9190

9291
Assert.AreEqual(TestOutcome.Failed, _args.Outcome);
@@ -104,46 +103,42 @@ public void AreSameShouldSucceedWithTwoNullReferences()
104103

105104
[Category("Unit Testing")]
106105
[Test]
107-
[Ignore("Would require passing COM objects for proper verification")]
108106
public void AreSameShouldFailWithActualNullReference()
109107
{
110108
var assert = new AssertClass();
111-
assert.AreSame(new object(), null);
109+
assert.AreSame(GetComObject(), null);
112110

113111
Assert.AreEqual(TestOutcome.Failed, _args.Outcome);
114112
}
115113

116114
[Category("Unit Testing")]
117115
[Test]
118-
[Ignore("Would require passing COM objects for proper verification")]
119116
public void AreSameShouldFailWithExpectedNullReference()
120117
{
121118
var assert = new AssertClass();
122-
assert.AreSame(null, new object());
119+
assert.AreSame(null, GetComObject());
123120

124121
Assert.AreEqual(TestOutcome.Failed, _args.Outcome);
125122
}
126123

127124
[Category("Unit Testing")]
128125
[Test]
129-
[Ignore("Would require passing COM objects for proper verification")]
130126
public void AreNotSameShouldSucceedWithDifferentReferences()
131127
{
132128
var assert = new AssertClass();
133-
var obj1 = new object();
134-
var obj2 = new object();
129+
var obj1 = GetComObject();
130+
var obj2 = GetComObject();
135131
assert.AreNotSame(obj1, obj2);
136132

137133
Assert.AreEqual(TestOutcome.Succeeded, _args.Outcome);
138134
}
139135

140136
[Category("Unit Testing")]
141137
[Test]
142-
[Ignore("Would require passing COM objects for proper verification")]
143138
public void AreNotSameShouldSuccedWithOneNullReference()
144139
{
145140
var assert = new AssertClass();
146-
assert.AreNotSame(new object(), null);
141+
assert.AreNotSame(GetComObject(), null);
147142

148143
Assert.AreEqual(TestOutcome.Succeeded, _args.Outcome);
149144
}
@@ -160,11 +155,10 @@ public void AreNotSameShouldFailWithBothNullReferences()
160155

161156
[Category("Unit Testing")]
162157
[Test]
163-
[Ignore("Would require passing COM objects for proper verification")]
164158
public void AreNotSameShouldFailWithSameReferences()
165159
{
166160
var assert = new AssertClass();
167-
var obj1 = new object();
161+
var obj1 = GetComObject();
168162
var obj2 = obj1;
169163
assert.AreNotSame(obj1, obj2);
170164

@@ -355,5 +349,8 @@ public void OnAssertInconclusive_ReturnsResultInconclusive()
355349

356350
Assert.AreEqual(TestOutcome.Inconclusive, _args.Outcome);
357351
}
352+
353+
private static Type GetComObjectType() => Type.GetTypeFromProgID("Scripting.FileSystemObject");
354+
private object GetComObject() => Activator.CreateInstance(GetComObjectType());
358355
}
359356
}

0 commit comments

Comments
 (0)