From a08120f6e44fb68d600d94957bf71c73f1c2c7c1 Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Fri, 10 Apr 2020 10:56:00 +0100
Subject: [PATCH 1/8] Fixed issue #125 Access violation in
 TValueHelper.CompareValue for tkRecord

Added ability to register custom comparison routines which cann be used for all types but primarily designed to be used with records
---
 Source/Delphi.Mocks.Helpers.pas | 46 ++++++++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index cf4434f..b53e7e5 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -33,9 +33,20 @@
 interface
 
 uses
-  Rtti;
+  System.Generics.Collections,
+  System.Rtti;
 
 type
+  //Allow custom comparisons
+  TCustomValueComparer = reference to function(const a, b: TValue): Integer;
+  TCustomValueComparerStore = record
+  private
+    class var CustomComparers: TDictionary<Pointer, TCustomValueComparer>;
+  public
+    class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparer); static;
+    class procedure UnRegisterCustomComparer<T>; static;
+  end;
+
   //TValue really needs to have an Equals operator overload!
   TValueHelper = record helper for TValue
     private
@@ -64,6 +75,7 @@   TValueHelper = record helper for TValue
     function IsWord: Boolean;
   	function IsGuid: Boolean;
     function IsInterface : Boolean;
+    function IsRecord: Boolean;
     function AsDouble: Double;
     function AsFloat: Extended;
     function AsSingle: Single;
@@ -101,11 +113,14 @@ function CompareValue(const Left, Right: TValue): Integer;
   EmptyResults: array[Boolean, Boolean] of Integer = ((0, -1), (1, 0));
 var
   leftIsEmpty, rightIsEmpty: Boolean;
+  CustomComparer: TCustomValueComparer;
 begin
   leftIsEmpty := left.IsEmpty;
   rightIsEmpty := right.IsEmpty;
   if leftIsEmpty or rightIsEmpty then
     Result := EmptyResults[leftIsEmpty, rightIsEmpty]
+  else if (Left.TypeInfo = Right.TypeInfo) and TCustomValueComparerStore.CustomComparers.TryGetValue(Left.TypeInfo, CustomComparer) then
+    Result := CustomComparer(Left, Right)
   else if left.IsOrdinal and right.IsOrdinal then
     Result := Math.CompareValue(left.AsOrdinal, right.AsOrdinal)
   else if left.IsFloat and right.IsFloat then
@@ -116,6 +131,8 @@ function CompareValue(const Left, Right: TValue): Integer;
     Result := NativeInt(left.AsObject) - NativeInt(right.AsObject) // TODO: instance comparer
   else if Left.IsInterface and Right.IsInterface then
     Result := NativeInt(left.AsInterface) - NativeInt(right.AsInterface) // TODO: instance comparer
+  else if Left.IsRecord and Right.IsRecord then
+    raise Exception.Create('Use Delphi.Mocks.Helpers.TCustomValueComparerStore.RegisterCustomComparer<T> to add a method to compare records.')
   else if left.IsVariant and right.IsVariant then
   begin
     case VarCompareValue(left.AsVariant, right.AsVariant) of
@@ -236,6 +253,11 @@ function TValueHelper.IsPointer: Boolean;
   Result := Kind = tkPointer;
 end;
 
+function TValueHelper.IsRecord: Boolean;
+begin
+  Result := Kind = tkRecord;
+end;
+
 function TValueHelper.IsShortInt: Boolean;
 begin
   Result := TypeInfo = System.TypeInfo(ShortInt);
@@ -307,4 +329,26 @@ function TRttiTypeHelper.TryGetMethod(const AName: string; out AMethod: TRttiMet
   Result := Assigned(AMethod);
 end;
 
+
+
+{ TCustomValueComparerStore }
+
+class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparer);
+begin
+  CustomComparers.Add(System.TypeInfo(T), AComparer);
+end;
+
+class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;
+begin
+  CustomComparers.Remove(System.TypeInfo(T));
+end;
+
+
+
+initialization
+  TCustomValueComparerStore.CustomComparers := TDictionary<Pointer, TCustomValueComparer>.Create;
+
+finalization
+  TCustomValueComparerStore.CustomComparers.Free;
+
 end.

From 9d3fc3451547e14c7806424dd3a72d18be2c8bae Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Fri, 10 Apr 2020 10:57:38 +0100
Subject: [PATCH 2/8] Fixed E1234 compiler error when {$M+} is specified at
 project level

This was removed in commit e90fee5.
---
 Source/Delphi.Mocks.WeakReference.pas | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/Source/Delphi.Mocks.WeakReference.pas b/Source/Delphi.Mocks.WeakReference.pas
index 708ab04..9baf754 100644
--- a/Source/Delphi.Mocks.WeakReference.pas
+++ b/Source/Delphi.Mocks.WeakReference.pas
@@ -39,12 +39,20 @@ interface
 
 type
   /// Implemented by our weak referenced object base class
+  {$IFOPT M+}
+    {$M-}
+    {$DEFINE ENABLED_M+}
+  {$ENDIF}
   IWeakReferenceableObject = interface
     ['{3D7F9CB5-27F2-41BF-8C5F-F6195C578755}']
     procedure AddWeakRef(value : Pointer);
     procedure RemoveWeakRef(value : Pointer);
     function GetRefCount : integer;
   end;
+  {$IFOPT M+}
+    {$M+}
+    {$UNDEF ENABLED_M+}
+  {$ENDIF}
 
   ///  This is our base class for any object that can have a weak reference to
   ///  it. It implements IInterface so the object can also be used just like

From 936635e0c4171efa24fd0c45648d79c22a4ec528 Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Fri, 10 Apr 2020 11:26:18 +0100
Subject: [PATCH 3/8] Added better error message when custom comparer is not
 available

---
 Source/Delphi.Mocks.Helpers.pas | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index b53e7e5..4581ea9 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -114,6 +114,9 @@ function CompareValue(const Left, Right: TValue): Integer;
 var
   leftIsEmpty, rightIsEmpty: Boolean;
   CustomComparer: TCustomValueComparer;
+const
+  ErrorStr: String =  'Unable to compare %s. Use Delphi.Mocks.Helpers.TCustomValueComparerStore.RegisterCustomComparer<T> to add a ' +
+                      'method to compare records.';
 begin
   leftIsEmpty := left.IsEmpty;
   rightIsEmpty := right.IsEmpty;
@@ -132,7 +135,7 @@ function CompareValue(const Left, Right: TValue): Integer;
   else if Left.IsInterface and Right.IsInterface then
     Result := NativeInt(left.AsInterface) - NativeInt(right.AsInterface) // TODO: instance comparer
   else if Left.IsRecord and Right.IsRecord then
-    raise Exception.Create('Use Delphi.Mocks.Helpers.TCustomValueComparerStore.RegisterCustomComparer<T> to add a method to compare records.')
+    raise Exception.Create(Format(ErrorStr ,[Left.TypeInfo.Name]))
   else if left.IsVariant and right.IsVariant then
   begin
     case VarCompareValue(left.AsVariant, right.AsVariant) of

From f46ace6a65ece666a33ec15ae5d9ec69829e6e7f Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Fri, 10 Apr 2020 11:27:06 +0100
Subject: [PATCH 4/8] Avoid error in
 TCustomValueComparerStore.RegisterCustomComparer when comparer already exists

---
 Source/Delphi.Mocks.Helpers.pas | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index 4581ea9..e0353a3 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -338,7 +338,7 @@ function TRttiTypeHelper.TryGetMethod(const AName: string; out AMethod: TRttiMet
 
 class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparer);
 begin
-  CustomComparers.Add(System.TypeInfo(T), AComparer);
+  CustomComparers.AddOrSetValue(System.TypeInfo(T), AComparer);
 end;
 
 class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;

From f84d4491de84f0eb00c593cac17d4d1321a62753 Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Sat, 11 Apr 2020 22:13:54 +0100
Subject: [PATCH 5/8] Fixed typo as was checking {$IFOPT M+} rather than
 {$IFDEF ENABLED_M+}

---
 Source/Delphi.Mocks.WeakReference.pas | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Source/Delphi.Mocks.WeakReference.pas b/Source/Delphi.Mocks.WeakReference.pas
index 9baf754..1c5dc67 100644
--- a/Source/Delphi.Mocks.WeakReference.pas
+++ b/Source/Delphi.Mocks.WeakReference.pas
@@ -49,7 +49,7 @@ interface
     procedure RemoveWeakRef(value : Pointer);
     function GetRefCount : integer;
   end;
-  {$IFOPT M+}
+  {$IFDEF ENABLED_M+}
     {$M+}
     {$UNDEF ENABLED_M+}
   {$ENDIF}

From 613eccff2df3f5fb3494dc2335bd9fc81710714d Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Sat, 11 Apr 2020 22:16:00 +0100
Subject: [PATCH 6/8] Changed Pointer to PTypeInfo so it is stronger typed

---
 Source/Delphi.Mocks.Helpers.pas | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index e0353a3..6c9febe 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -34,14 +34,14 @@ interface
 
 uses
   System.Generics.Collections,
-  System.Rtti;
+  System.Rtti, System.TypInfo;
 
 type
   //Allow custom comparisons
   TCustomValueComparer = reference to function(const a, b: TValue): Integer;
   TCustomValueComparerStore = record
   private
-    class var CustomComparers: TDictionary<Pointer, TCustomValueComparer>;
+    class var CustomComparers: TDictionary<PTypeInfo, TCustomValueComparer>;
   public
     class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparer); static;
     class procedure UnRegisterCustomComparer<T>; static;
@@ -99,7 +99,6 @@ implementation
 uses
   SysUtils,
   Math,
-  TypInfo,
   Variants,
   StrUtils;
 
@@ -349,7 +348,7 @@ class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;
 
 
 initialization
-  TCustomValueComparerStore.CustomComparers := TDictionary<Pointer, TCustomValueComparer>.Create;
+  TCustomValueComparerStore.CustomComparers := TDictionary<PTypeInfo, TCustomValueComparer>.Create;
 
 finalization
   TCustomValueComparerStore.CustomComparers.Free;

From ccd3c6a9ee64ff74f6b71bd5afe17e0b71a9981a Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Sat, 11 Apr 2020 22:17:03 +0100
Subject: [PATCH 7/8] Updated TCustomValueComparer so that it can be used
 without TValue and directly with T

---
 Source/Delphi.Mocks.Helpers.pas | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index 6c9febe..9e19b21 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -38,12 +38,22 @@ interface
 
 type
   //Allow custom comparisons
-  TCustomValueComparer = reference to function(const a, b: TValue): Integer;
+  {$IFOPT M+}
+    {$M-}
+    {$DEFINE ENABLED_M+}
+  {$ENDIF}
+  TCustomValueComparer = reference to function(const a, b): Integer;
+  {$IFDEF ENABLED_M+}
+    {$M+}
+    {$UNDEF ENABLED_M+}
+  {$ENDIF}
+
+  TCustomValueComparer<T> = reference to function(const a, b: T): Integer;
   TCustomValueComparerStore = record
   private
     class var CustomComparers: TDictionary<PTypeInfo, TCustomValueComparer>;
   public
-    class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparer); static;
+    class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparer<T>); static;
     class procedure UnRegisterCustomComparer<T>; static;
   end;
 
@@ -122,7 +132,7 @@ function CompareValue(const Left, Right: TValue): Integer;
   if leftIsEmpty or rightIsEmpty then
     Result := EmptyResults[leftIsEmpty, rightIsEmpty]
   else if (Left.TypeInfo = Right.TypeInfo) and TCustomValueComparerStore.CustomComparers.TryGetValue(Left.TypeInfo, CustomComparer) then
-    Result := CustomComparer(Left, Right)
+    Result := CustomComparer(Left.GetReferenceToRawData^, Right.GetReferenceToRawData^)
   else if left.IsOrdinal and right.IsOrdinal then
     Result := Math.CompareValue(left.AsOrdinal, right.AsOrdinal)
   else if left.IsFloat and right.IsFloat then
@@ -335,9 +345,9 @@ function TRttiTypeHelper.TryGetMethod(const AName: string; out AMethod: TRttiMet
 
 { TCustomValueComparerStore }
 
-class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparer);
+class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparer<T>);
 begin
-  CustomComparers.AddOrSetValue(System.TypeInfo(T), AComparer);
+  CustomComparers.AddOrSetValue(System.TypeInfo(T), TCustomValueComparer(AComparer));
 end;
 
 class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;

From ec5e5b6d08608dca8e381dbbd59522c50a19b029 Mon Sep 17 00:00:00 2001
From: Aly <alasdair.douglas@brookesbell.com>
Date: Mon, 13 Apr 2020 10:37:28 +0100
Subject: [PATCH 8/8] Updated TCustomValueComparer behaviour to use generics
 and not rely on a function with untyped parameters

---
 Source/Delphi.Mocks.Helpers.pas | 62 ++++++++++++++++++++++-----------
 1 file changed, 42 insertions(+), 20 deletions(-)

diff --git a/Source/Delphi.Mocks.Helpers.pas b/Source/Delphi.Mocks.Helpers.pas
index 9e19b21..e0a887d 100644
--- a/Source/Delphi.Mocks.Helpers.pas
+++ b/Source/Delphi.Mocks.Helpers.pas
@@ -37,23 +37,29 @@ interface
   System.Rtti, System.TypInfo;
 
 type
-  //Allow custom comparisons
-  {$IFOPT M+}
-    {$M-}
-    {$DEFINE ENABLED_M+}
-  {$ENDIF}
-  TCustomValueComparer = reference to function(const a, b): Integer;
-  {$IFDEF ENABLED_M+}
-    {$M+}
-    {$UNDEF ENABLED_M+}
-  {$ENDIF}
-
-  TCustomValueComparer<T> = reference to function(const a, b: T): Integer;
+  // Need to define a common 'non-generic' version and using interface gives bonus of reference counting for clean-up
+  ICustomValueComparer = Interface
+  ['{AA4E862E-F83E-4438-B8E3-BAE2BD0E9475}']
+    function Compare(const ALeft, ARight: TValue): Integer;
+  End;
+
+  TCustomValueComparerFunction<T> = reference to function(const a, b: T): Integer;
+  TCustomValueComparer<T> = class(TInterfacedObject, ICustomValueComparer)
+  private
+    FComparer: TCustomValueComparerFunction<T>;
+  public
+    constructor Create(const ACustomComparer: TCustomValueComparerFunction<T>);
+
+    {$REGION 'ICustomValueComparer'}
+    function Compare(const ALeft, ARight: TValue): Integer;
+    {$ENDREGION}
+  end;
+
   TCustomValueComparerStore = record
   private
-    class var CustomComparers: TDictionary<PTypeInfo, TCustomValueComparer>;
+    class var CustomComparers: TDictionary<PTypeInfo, ICustomValueComparer>;
   public
-    class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparer<T>); static;
+    class procedure RegisterCustomComparer<T>(const AComparer: TCustomValueComparerFunction<T>); static;
     class procedure UnRegisterCustomComparer<T>; static;
   end;
 
@@ -122,7 +128,7 @@ function CompareValue(const Left, Right: TValue): Integer;
   EmptyResults: array[Boolean, Boolean] of Integer = ((0, -1), (1, 0));
 var
   leftIsEmpty, rightIsEmpty: Boolean;
-  CustomComparer: TCustomValueComparer;
+  CustomComparer: ICustomValueComparer;
 const
   ErrorStr: String =  'Unable to compare %s. Use Delphi.Mocks.Helpers.TCustomValueComparerStore.RegisterCustomComparer<T> to add a ' +
                       'method to compare records.';
@@ -132,7 +138,7 @@ function CompareValue(const Left, Right: TValue): Integer;
   if leftIsEmpty or rightIsEmpty then
     Result := EmptyResults[leftIsEmpty, rightIsEmpty]
   else if (Left.TypeInfo = Right.TypeInfo) and TCustomValueComparerStore.CustomComparers.TryGetValue(Left.TypeInfo, CustomComparer) then
-    Result := CustomComparer(Left.GetReferenceToRawData^, Right.GetReferenceToRawData^)
+    Result := CustomComparer.Compare(Left, Right)
   else if left.IsOrdinal and right.IsOrdinal then
     Result := Math.CompareValue(left.AsOrdinal, right.AsOrdinal)
   else if left.IsFloat and right.IsFloat then
@@ -341,13 +347,30 @@ function TRttiTypeHelper.TryGetMethod(const AName: string; out AMethod: TRttiMet
   Result := Assigned(AMethod);
 end;
 
+{ TCustomValueComparer<T> }
+
+function TCustomValueComparer<T>.Compare(const ALeft, ARight: TValue): Integer;
+var
+  Left, Right: T;
+begin
+  Left := ALeft.AsType<T>;
+  Right := ARight.AsType<T>;
+
+  Result := FComparer(Left, Right);
+end;
+
+constructor TCustomValueComparer<T>.Create(const ACustomComparer: TCustomValueComparerFunction<T>);
+begin
+  inherited Create;
 
+  FComparer := ACustomComparer;
+end;
 
 { TCustomValueComparerStore }
 
-class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparer<T>);
+class procedure TCustomValueComparerStore.RegisterCustomComparer<T>(const AComparer: TCustomValueComparerFunction<T>);
 begin
-  CustomComparers.AddOrSetValue(System.TypeInfo(T), TCustomValueComparer(AComparer));
+  CustomComparers.AddOrSetValue(TypeInfo(T), TCustomValueComparer<T>.Create(AComparer))
 end;
 
 class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;
@@ -356,9 +379,8 @@ class procedure TCustomValueComparerStore.UnRegisterCustomComparer<T>;
 end;
 
 
-
 initialization
-  TCustomValueComparerStore.CustomComparers := TDictionary<PTypeInfo, TCustomValueComparer>.Create;
+  TCustomValueComparerStore.CustomComparers := TDictionary<PTypeInfo, ICustomValueComparer>.Create;
 
 finalization
   TCustomValueComparerStore.CustomComparers.Free;