Skip to content

Commit

Permalink
Merge pull request #5207 from bclothier/MiscBugFixes2
Browse files Browse the repository at this point in the history
Make PermissiveAssertClass more permissive
  • Loading branch information
retailcoder committed Oct 18, 2019
2 parents 3d5370a + 4646c49 commit 65ad907
Show file tree
Hide file tree
Showing 5 changed files with 792 additions and 48 deletions.
42 changes: 10 additions & 32 deletions Rubberduck.UnitTesting/ComClientHelpers/PermissiveObjectComparer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Rubberduck.VBEditor.Variants;

namespace Rubberduck.UnitTesting.ComClientHelpers
{
Expand All @@ -13,42 +14,19 @@ public class PermissiveObjectComparer : IEqualityComparer<object>
/// <returns>VBA equity</returns>
public new bool Equals(object x, object y)
{
var expected = x;
var actual = y;

// try promoting integral types first.
if (expected is ulong && actual is ulong)
{
return (ulong)x == (ulong)y;
}
// then try promoting to floating point
if (expected is double && actual is double)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator - We're cool with that.
return (double)x == (double)y;
}
// that shouldn't actually happen, since decimal is the only numeric ValueType in its category
// this means we should've gotten the same types earlier in the Assert method
if (expected is decimal && actual is decimal)
if (x == null)
{
return (decimal)x == (decimal)y;
return y == null;
}
// worst case scenario for numbers
// since we're inside VBA though, double is the more appropriate type to compare,
// because that is what's used internally anyways, see https://support.microsoft.com/en-us/kb/78113
if ((expected is decimal && actual is double) || (expected is double && actual is decimal))
{
// ReSharper disable once CompareOfFloatsByEqualityOperator - We're still cool with that.
return (double)x == (double)y;
}
// no number-type promotions are applicable. 2nd to last straw: string "promotion"
if (expected is string || actual is string)

if (y == null)
{
expected = expected.ToString();
actual = actual.ToString();
return expected.Equals(actual);
return false;
}
return x.Equals(y);

var converted = VariantConverter.ChangeType(y, x.GetType());

return x.Equals(converted);
}

/// <summary>
Expand Down
220 changes: 220 additions & 0 deletions Rubberduck.VBEEditor/Variants/VariantConverter.cs
@@ -0,0 +1,220 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;

namespace Rubberduck.VBEditor.Variants
{
public enum VARENUM
{
VT_EMPTY = 0x0000,
VT_NULL = 0x0001,
VT_I2 = 0x0002,
VT_I4 = 0x0003,
VT_R4 = 0x0004,
VT_R8 = 0x0005,
VT_CY = 0x0006,
VT_DATE = 0x0007,
VT_BSTR = 0x0008,
VT_DISPATCH = 0x0009,
VT_ERROR = 0x000A,
VT_BOOL = 0x000B,
VT_VARIANT = 0x000C,
VT_UNKNOWN = 0x000D,
VT_DECIMAL = 0x000E,
VT_I1 = 0x0010,
VT_UI1 = 0x0011,
VT_UI2 = 0x0012,
VT_UI4 = 0x0013,
VT_I8 = 0x0014,
VT_UI8 = 0x0015,
VT_INT = 0x0016,
VT_UINT = 0x0017,
VT_VOID = 0x0018,
VT_HRESULT = 0x0019,
VT_PTR = 0x001A,
VT_SAFEARRAY = 0x001B,
VT_CARRAY = 0x001C,
VT_USERDEFINED = 0x001D,
VT_LPSTR = 0x001E,
VT_LPWSTR = 0x001F,
VT_RECORD = 0x0024,
VT_INT_PTR = 0x0025,
VT_UINT_PTR = 0x0026,
VT_ARRAY = 0x2000,
VT_BYREF = 0x4000
}

[Flags]
public enum VariantConversionFlags : ushort
{
NO_FLAGS = 0x00,
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.
VARIANT_ALPHABOOL = 0x02, //Converts a VT_BOOL value to a string containing either "True" or "False".
VARIANT_NOUSEROVERRIDE = 0x04, //For conversions to or from VT_BSTR, passes LOCALE_NOUSEROVERRIDE to the core coercion routines.
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.
}

/// <summary>
/// Handles variant conversions, enabling us to have same implicit conversion behaviors within
/// .NET as we can observe it from VBA/VB6.
/// </summary>
/// <remarks>
/// The <see cref="VariantChangeType"/> function is the same one used internally by VBA/VB6.
/// However, we have to wrap the metadata, which the class helps with.
///
/// See the link for details on how marshaling are handled with <see cref="object"/>
/// https://docs.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-objects
/// </remarks>
public static class VariantConverter
{
private const string dllName = "oleaut32.dll";

// HRESULT VariantChangeType(
// VARIANTARG *pvargDest,
// const VARIANTARG *pvarSrc,
// USHORT wFlags,
// VARTYPE vt
// );
[DllImport(dllName, EntryPoint = "VariantChangeType", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
private static extern int VariantChangeType(ref object pvargDest, ref object pvarSrc, VariantConversionFlags wFlags, VARENUM vt);

// HRESULT VariantChangeTypeEx(
// VARIANTARG *pvargDest,
// const VARIANTARG *pvarSrc,
// LCID lcid,
// USHORT wFlags,
// VARTYPE vt
// );
[DllImport(dllName, EntryPoint = "VariantChangeTypeEx", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
private static extern int VariantChangeTypeEx(ref object pvargDest, ref object pvarSrc, int lcid, VariantConversionFlags wFlags, VARENUM vt);

public static object ChangeType(object value, VARENUM vt)
{
return ChangeType(value, vt, null);
}

private static bool HRESULT_FAILED(int hr) => hr < 0;
public static object ChangeType(object value, VARENUM vt, CultureInfo cultureInfo)
{
object result = null;
var hr = cultureInfo == null
? VariantChangeType(ref result, ref value, VariantConversionFlags.NO_FLAGS, vt)
: VariantChangeTypeEx(ref result, ref value, cultureInfo.LCID, VariantConversionFlags.NO_FLAGS, vt);
if (HRESULT_FAILED(hr))
{
throw Marshal.GetExceptionForHR(hr);
}

return result;
}

public static object ChangeType(object value, Type targetType)
{
return ChangeType(value, GetVarEnum(targetType));
}

public static object ChangeType(object value, Type targetType, CultureInfo culture)
{
return ChangeType(value, GetVarEnum(targetType), culture);
}

public static VARENUM GetVarEnum(Type target)
{
switch (target)
{
case null:
return VARENUM.VT_EMPTY;
case Type dbNull when dbNull == typeof(DBNull):
return VARENUM.VT_NULL;
case Type err when err == typeof(ErrorWrapper):
return VARENUM.VT_ERROR;
case Type disp when disp == typeof(DispatchWrapper):
return VARENUM.VT_DISPATCH;
case Type unk when unk == typeof(UnknownWrapper):
return VARENUM.VT_UNKNOWN;
case Type cy when cy == typeof(CurrencyWrapper):
return VARENUM.VT_CY;
case Type b when b == typeof(bool):
return VARENUM.VT_BOOL;
case Type s when s == typeof(sbyte):
return VARENUM.VT_I1;
case Type b when b == typeof(byte):
return VARENUM.VT_UI1;
case Type i16 when i16 == typeof(short):
return VARENUM.VT_I2;
case Type ui16 when ui16 == typeof(ushort):
return VARENUM.VT_UI2;
case Type i32 when i32 == typeof(int):
return VARENUM.VT_I4;
case Type ui32 when ui32 == typeof(uint):
return VARENUM.VT_UI4;
case Type i64 when i64 == typeof(long):
return VARENUM.VT_I8;
case Type ui64 when ui64 == typeof(ulong):
return VARENUM.VT_UI8;
case Type sng when sng == typeof(float):
return VARENUM.VT_R4;
case Type dbl when dbl == typeof(double):
return VARENUM.VT_R8;
case Type dec when dec == typeof(decimal):
return VARENUM.VT_DECIMAL;
case Type dt when dt == typeof(DateTime):
return VARENUM.VT_DATE;
case Type s when s == typeof(string):
return VARENUM.VT_BSTR;
//case Type a when a == typeof(Array):
// return VARENUM.VT_ARRAY;
case Type obj when obj == typeof(object):
case Type var when var == typeof(VariantWrapper):
return VARENUM.VT_VARIANT;
default:
throw new NotSupportedException("Unrecognized system type that cannot be mapped to a VARENUM out of the box.");
}
}

public static VARENUM GetVarEnum(TypeCode typeCode)
{
switch (typeCode)
{
case TypeCode.Empty:
return VARENUM.VT_EMPTY;
case TypeCode.Object:
return VARENUM.VT_UNKNOWN;
case TypeCode.DBNull:
return VARENUM.VT_NULL;
case TypeCode.Boolean:
return VARENUM.VT_BOOL;
case TypeCode.Char:
return VARENUM.VT_UI2;
case TypeCode.SByte:
return VARENUM.VT_I1;
case TypeCode.Byte:
return VARENUM.VT_UI1;
case TypeCode.Int16:
return VARENUM.VT_I2;
case TypeCode.UInt16:
return VARENUM.VT_UI2;
case TypeCode.Int32:
return VARENUM.VT_I4;
case TypeCode.UInt32:
return VARENUM.VT_UI4;
case TypeCode.Int64:
return VARENUM.VT_I8;
case TypeCode.UInt64:
return VARENUM.VT_UI8;
case TypeCode.Single:
return VARENUM.VT_R4;
case TypeCode.Double:
return VARENUM.VT_R8;
case TypeCode.Decimal:
return VARENUM.VT_DECIMAL;
case TypeCode.DateTime:
return VARENUM.VT_DATE;
case TypeCode.String:
return VARENUM.VT_BSTR;
default:
throw new ArgumentOutOfRangeException(nameof(typeCode), typeCode, null);
}
}
}
}
29 changes: 13 additions & 16 deletions RubberduckTests/UnitTesting/AssertTests.cs
@@ -1,3 +1,4 @@
using System;
using NUnit.Framework;
using Rubberduck.UnitTesting;

Expand Down Expand Up @@ -68,11 +69,10 @@ public void IsFalseFailsWithTrueExpression()

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreSameShouldSucceedWithSameReferences()
{
var assert = new AssertClass();
var obj1 = new object();
var obj1 = GetComObject();
var obj2 = obj1;
assert.AreSame(obj1, obj2);

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreSameShouldFailWithDifferentReferences()
{
var assert = new AssertClass();
var obj1 = new object();
var obj2 = new object();
var obj1 = GetComObject();
var obj2 = GetComObject();
assert.AreSame(obj1, obj2);

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreSameShouldFailWithActualNullReference()
{
var assert = new AssertClass();
assert.AreSame(new object(), null);
assert.AreSame(GetComObject(), null);

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreSameShouldFailWithExpectedNullReference()
{
var assert = new AssertClass();
assert.AreSame(null, new object());
assert.AreSame(null, GetComObject());

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreNotSameShouldSucceedWithDifferentReferences()
{
var assert = new AssertClass();
var obj1 = new object();
var obj2 = new object();
var obj1 = GetComObject();
var obj2 = GetComObject();
assert.AreNotSame(obj1, obj2);

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreNotSameShouldSuccedWithOneNullReference()
{
var assert = new AssertClass();
assert.AreNotSame(new object(), null);
assert.AreNotSame(GetComObject(), null);

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

[Category("Unit Testing")]
[Test]
[Ignore("Would require passing COM objects for proper verification")]
public void AreNotSameShouldFailWithSameReferences()
{
var assert = new AssertClass();
var obj1 = new object();
var obj1 = GetComObject();
var obj2 = obj1;
assert.AreNotSame(obj1, obj2);

Expand Down Expand Up @@ -355,5 +349,8 @@ public void OnAssertInconclusive_ReturnsResultInconclusive()

Assert.AreEqual(TestOutcome.Inconclusive, _args.Outcome);
}

private static Type GetComObjectType() => Type.GetTypeFromProgID("Scripting.FileSystemObject");
private object GetComObject() => Activator.CreateInstance(GetComObjectType());
}
}

0 comments on commit 65ad907

Please sign in to comment.