Is the code below worth unit testing? - delphi

I'm new to unit testing. And I don't know whether it is worth to unit test the code below. Here's sample method written in Delphi:
function TCoreAudio.CreateAudioClient: IAudioClient;
var
MMDeviceEnumerator: IMMDeviceEnumerator;
MMDevice: IMMDevice;
MixFormat: PWaveFormatEx;
AudioClient: IAudioClient;
HR: HResult;
begin
Result := nil;
if CheckWin32Version(6, 0) then // The Core Audio APIs were introduced in Windows Vista.
begin
HR := GetInstance().CoCreateInstance(CLSID_MMDeviceEnumerator, nil, CLSCTX_ALL,
IMMDeviceEnumerator, MMDeviceEnumerator);
if Failed(HR) then
Exit;
HR := MMDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, MMDevice);
if Failed(HR) then
Exit;
HR := MMDevice.Activate(IAudioClient, CLSCTX_ALL, nil, AudioClient);
if Failed(HR) then
Exit;
HR := AudioClient.GetMixFormat(MixFormat);
if Failed(HR) then
Exit;
HR := AudioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 0, 0, MixFormat, nil);
CoTaskMemFree(MixFormat);
if Failed(HR) then
Exit;
Result := AudioClient;
end;
end;
Is that method worth unit testing? If it is, what parts of it need to be tested?
Thank You.

The problem you face is how to test it rather then whether it should be tested.
This is a wrapper to a number of COM calls which could fail for many different reasons. Those possible COM failure conditions are the most important aspects to test for this routine. But you can't easily provoke the COM routines to fail. In order to test these COM failure modes you'd need to use a mock and that's quite a leap from where you are.

Unit testing is usually a bottom-up approach. So you would start unit testing the classes that are used in your function. After having made sure that all these classes are covered by unit tests, you could create a unit test for your function CreateAudioClient. The unit test for this function is probably very easy, something like this:
AudioClient := CreateAudioClient;
CheckNotNil (AudioClient);
Note that you normally unit test the interface of a class and not the body of a function or procedure.
Hope that helps.
The question if it is worth it, depends on a number of factor:
How easy is it to build a unit test for it? How much effort would it be?
How important is this part? Depending on how critical this part is, the answer may be always "yes, it is definitely worth unit testing" - or not.
How likely is it to change? If you think this method might change somewhere in the future then adding a unit tests avoids introducing errors later.

When unit testing a class that acts as an interface between your application and a third party API (even a system API) you want to test that the class calls and responds to the API correctly. You can't do this without some way to sense what is being passed to the API and return an appropriate response.
In your case you are making a series of calls to obtain an IAudioClient. I'd say your doing too much. More than one conditional in a function is one conditional too many (I think I just confused myself with that one). I would break it into several functions that you can test individually.
function TCoreAudio.CreateAudioClient: IAudioClient;
var
MMDeviceEnumerator: IMMDeviceEnumerator;
MMDevice: IMMDevice;
MixFormat: PWaveFormatEx;
AudioClient: IAudioClient;
begin
Result := nil;
if IsVista then
try
MMDeviceEnumerator := GetMMDeviceEumerator;
MMDevice := GetMMDevice(MMDeviceEnumerator);
AudioClient := GetAudioClient(MMDevice);
MixFormat := GetMixFormat(AudioClient);
InitializeAudioClient(AudioClient, MixFormat);
Result := AudioClient;
except
//Handle exception
end;
end;
function TCoreAudio.IsVista: boolean;
begin
Result := CheckWin32Version(6, 0);
end;
function TCoreAudio.GetMMDeviceEnumerator: IMMDeviceEnumerator;
begin
HR := GetInstance().CoCreateInstance(CLSID_MMDeviceEnumerator, nil, CLSCTX_ALL,
IMMDeviceEnumerator, Result);
if Failed(HR) then
raise Exception.Create('Failed to create device enumerator');
end;
function TCoreAudio.GetMMDevice(ADeviceEnumerator: IMMDeviceEnumerator): IMMDevice;
begin
HR := MMDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve device');
end;
function TCoreAudio.GetAudioClient(ADevice: IMMDevice): IAudioClient;
begin
HR := MMDevice.Activate(IAudioClient, CLSCTX_ALL, nil, Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve audio client');
end;
function TCoreAudio.GetMixFormat(AAudioClient: IAudioClient): PWaveFormatEx
begin
HR := AudioClient.GetMixFormat(Result);
if Failed(HR) then
raise Exception.Create('Failed to retrieve mix format');
end;
procedure TCoreAudio.InitializeAudioClient(AAudioClient: IAudioClient, AMixFormat: PWaveFormatEx);
begin
HR := AudioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 0, 0, AMixFormat, nil);
CoTaskMemFree(MixFormat);
if Failed(HR) then
raise Exception.Create('Audio client failed to initialize');
end;
Now you can provide a mock/fake/stub to each function, ensuring the API is being called with appropriate arguments and forcing failure conditions to make sure your production code is handling them properly.
You don't need to ask if production code should be tested. The answer is always yes. (warning:shameless self-promotion) I wrote about this recently on my blog. Sometimes even the most innocuous of all statements, the assignment statement, doesn't work as expected.
Actually now that its broken down its starting to look like a new creational class just itching to break out.

it really depends on how often you believe changes will be made to this method... if you already have some tests for it's unit, then yes do it, otherwise it's really up to you, but I'm not sure it's a good idea to test only this method from the whole unit, because it calls other methods from other units.
Bottom line, it's really up to you how you choose to do it, best way is to add testing to whole project, otherwise I can't really see any value in "partial testing".

Related

Delphi EInOutError exception is not raised

I am trying to copy directory:
procedure CopyBigDirWithSubdirs;
{$IOCHECKS ON}
begin
try
TDirectory.Copy(SrcPath, DstPath);
except
on E: EInOutError do something
end;
end;
In my case it is crucial to check disk full condition and I hoped that catching EInOutError exception would solve my problem. But as far as I could find out TDirectory methods do not notify of this condition at all. The situation is even worse because TDirectory.copy can write part of subdirs, face disk full condition and terminate, so I have to check the whole directory tree to be sure that my directory is copied properly. Does anybody know better solution?
{$IOCHECKS ON} isn't relevant here. That's for legacy Pascal I/O. And likewise for EInOutError, you aren't ever going to get that from functions in the IOUtils unit.
The real problem here is that TDirectory.Copy is, like so much of IOUtils, broken by design. There appears to be no error checking whatsoever implemented in TDirectory.Copy. For what it is worth, the rule at my place of work is that IOUtils must not be used in our code.
You are going to have to either write your own code which does include some error checking, or find a third party library to do the work.
Certainly on Windows then you should use IFileOperation to do this. As a benefit you'll even be able to show the standard system progress dialog. And because the code is provided by the system rather than by Embarcadero, you can expect it to work.
If you require support for other platforms then you may have to work a little harder to find suitable code.
As using IFileOperation interface looks like most practical solution I've written the function based on it:
function CopyItem(const Src, Dest: string ): HRESULT;
const
FOF_SILENT = $0004;
FOF_NOCONFIRMATION = $0010;
FOF_NOCONFIRMMKDIR = $0200;
FOF_NOERRORUI = $0400;
FOF_NO_UI =(FOF_SILENT or FOF_NOCONFIRMATION or FOF_NOERRORUI or FOF_NOCONFIRMMKDIR); // don't display any UI at all
var
lFileOperation: IFileOperation;
psiFrom: IShellItem;
psiTo: IShellItem;
opAborted : longbool;
begin
//We probably don't need to call CoInitializeEx/CoUninitialize pair as it could have been called by Delphi library
CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE);
// check arguments and create the IFileOperation interface,
if (Src='') or (Dest='') then Result := E_INVALIDARG
else Result := CoCreateInstance(CLSID_FileOperation, nil, CLSCTX_ALL, IFileOperation, lFileOperation);
// Set the operation flags. Turn off all UI from being shown to the user
if Succeeded(Result) then Result := lFileOperation.SetOperationFlags(FOF_NO_UI);
// Create IShellItem-s from the supplied source and dest paths.
if Succeeded(Result) then Result := SHCreateItemFromParsingName(PWideChar(wideString(Src)),
nil, IShellItem, psiFrom);
if Succeeded(Result) then Result := SHCreateItemFromParsingName(PWideChar(wideString(Dest)),
nil, IShellItem, psiTo);
// This method does not copy the item, it merely declares the item to be copied
if Succeeded(Result) then Result := lFileOperation.CopyItem(psiFrom, psiTo, nil, nil);
// This method is called last to execute those actions that have been specified earlier
if Succeeded(Result) then Result := lFileOperation.PerformOperations;
// Check now if the operation was aborted by the system
if Succeeded(Result) then
begin
lFileOperation.GetAnyOperationsAborted(opAborted);
if opAborted then Result := ERROR_WRITE_FAULT;
end;
CoUninitialize;
end;
As you can see from the code the solution is not complete because in case of disc full error (my reason for all this fiddling with lFileOperation) PerformOperations returns S_OK (!!!) and I can find the error only by calling GetAnyOperationsAborted which does not specify the error condition exactly but merely sets opAborted flag. Then I have to guess the real case of abortion.

Is globalalloc with GMEM_MOVEABLE dangerous for local variables in Delphi?

Our programming dept just spent about a non-mythical man-month tracking down what we think is a bug in a 3rd party component, here's their copyrighted source code:
function TGDIPPicture.GetImageSizes: boolean;
var
multi: TGPImage;
pstm: IStream;
hGlobal: THandle;
pcbWrite: Longint;
begin
result := false;
if Empty then
Exit;
if FDataStream.Size = 0 then
Exit;
hGlobal := GlobalAlloc(GMEM_MOVEABLE, FDataStream.Size);
if (hGlobal = 0) then
raise Exception.Create('Could not allocate memory for image');
try
pstm := nil;
// Create IStream* from global memory
CreateStreamOnHGlobal(hGlobal, TRUE, pstm);
pstm.Write(FDataStream.Memory, FDataStream.Size,#pcbWrite);
multi := TGPImage.Create(pstm);
FWidth := multi.GetWidth;
FHeight := multi.GetHeight;
Result := true;
multi.Free;
finally
GlobalFree(hGlobal);
end;
end;
We found the problem was with TMS's AdvOfficeTabSet. If we added tabs, then it crashed, if we didn't add tabs then it didn't crash. (the crash was one of those un-debuggable app hangs that hits you 10 steps after the real problem).
Following Raymond Chen's advice I replaced GMEM_MOVEABLE with GPTR and it appears to have fixed the problem.
I'm wondering if anyone can tell me if the above code had any legitimate reason for using GMEM_MOVEABLE. AFAIK it's only for the clipboard and it should always be used with GlobalAlloc.
while I was typing this another programmer got an error in the GlobalFree function using my code. So, apparently this doesn't work either. Could really use some help here!
*CreateStreamOnHGlobal is a Windows API function. (which apparently prefers GMEM_MOVEABLE)
*TGPImage is part of TMS's implementation of the GDI+ library.
Jonathan has identified the obvious problem, that being the double free of the HGLOBAL. But as you have found, the use is GMEM_MOVEABLE is correct.
Frankly, the code seems needlessly complex. I suggest you use the built in stream adapter and avoid any GlobalAlloc. To get an IStream you just need to do this:
pstm := TStreamAdapter.Create(FDataStream);
That's it.

How do I wait to delete a file until after the program I started has finished using it?

I have been looking for a way to open a file saved to my computer via a Delphi app with its appropriate application. The file is stored in a Varbinary field in a SQL database, and is loaded into a memory stream and then saved via the TMemoryStream's SavetoFile method. What I would like to accomplish is to open the saved file in its appropriate application without knowing the filepath to that application's executable. I have had some success using ShellExecuteEx, but certain applications don't return an HProcess (Windows Live Photo Gallery, for example), so I can't (or at least don't know how to) wait for the application to close before moving on when a handle isn't returned. Is there a way to ensure I receive a handle when calling ShellExecuteEx? If this is not the best way how should I go about doing this?
I only need to know the external app's status because I plan on deleting the file after it closes, and I only need to write it because I'm fairly certain I can't load the file stored in the SQL table into memory (by way of a MemoryStream, FileStream, etc.) and launch its associated program directly from my Delphi app. (I've asked about that separately.)
Trying to detect that the displaying process has closed is brittle and fraught with problems, as you learnt in your previous question. Often times, it's hard to find the process that is used to view the file, and even if you can, there's no certainty the closing the view of the file will close the process. The process may be used to view other files which the user leaves open. I think the lesson that you should take from that is that the system does not want you to do what you are trying to do.
So, what's the better way to solve the problem? I think the best you can do is to create the temporary files in the temporary directory and not attempt to delete them when the user has finished with them. You could:
Remember the files you created and when you create, say the 21st file, delete the first one you made. Then delete the 2nd when you create the 22nd and so on.
Or, delete all temporary files on startup. This would remove files from a previous session.
Or run a separate tidy up thread that, every ten minutes, say, deleted files that were created more than an hour ago.
You get the idea. The point is that it is an intractable problem to detect when the viewer has finished with the file, in full generality. So you need to think creatively. Find a different way around the road block.
Hers a snip from a unit I use for a similar purpose. I found these functions online somewhere over the the years so I take no credit and make no promises.
I personally use the WaitExec() function to launch a pdf (retrieved from a database) in Acrobat for editing and then re-save it to our database when done.
I have used the two other functions at other times as well so I know they all work to one degree or another but I think WaitExec() worked best in an interactive mode, while Launch() worked better from a thread or non-interactive mode.
The IsFileInUse function can tell you if the file you created is in use by any other processes and may be a viable option as well.
uses SysUtils, Windows, ShellAPI, Forms, Registry, Classes, Messages, Printers,
PSAPI, TlHelp32, SHFolder;
function IsFileInUse(fName: string): boolean;
var
HFileRes: HFILE;
begin
Result := False;
if not FileExists(fName) then
Exit;
HFileRes := CreateFile(pchar(fName), GENERIC_READ or GENERIC_WRITE,
0 {this is the trick!}, nil, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, 0);
Result := (HFileRes = INVALID_HANDLE_VALUE);
if not Result then
CloseHandle(HFileRes);
end;
function Launch(sCommandLine: string; bWait: Boolean; AppHandle: HWND): Boolean;
var
SEI: TShellExecuteInfo;
Mask: Longint;
begin
Mask := SEE_MASK_NOCLOSEPROCESS;
FillChar(SEI, Sizeof(SEI), #0);
SEI.cbsize := Sizeof(SEI);
SEI.wnd := AppHandle;
SEI.fmask := Mask;
//if FExeStyleString<>'' then SEI.LPVERB:=pchar(FExeStyleString);
SEI.LPFile := pchar(sCommandline);
//SEI.LPParameters := pchar(FExeParameters);
//SEI.LPDirectory := pchar(FExepath);
SEI.nshow := SW_SHOWNORMAL; // SW_SHOWMINIMIZED, SW_SHOWMAXIMIZED
ShellexecuteEx(#SEI);
if bWait then
WaitforSingleObject(SEI.hProcess, INFINITE);
Result := True;
end;
function WaitExec(const CmdLine:AnsiString;const DisplayMode:Integer):Integer;
{Execute an app, wait for it to terminate then return exit code. Returns -1
if execution fails. DisplayMode is usually either sw_ShowNormal or sw_Hide.}
var
S:TStartupInfo;
P:TProcessInformation;
M:TMsg;
R:DWord;
begin
FillChar(P,SizeOf(P),#0);
FillChar(S,Sizeof(S),#0);
S.cb := Sizeof(S);
S.dwFlags := STARTF_USESHOWWINDOW;
S.wShowWindow := DisplayMode;
if not CreateProcess(nil,
PChar(CmdLine), { pointer to command line string }
nil, { pointer to process security attributes }
nil, { pointer to thread security attributes }
False, { handle inheritance flag }
CREATE_NEW_CONSOLE or { creation flags }
NORMAL_PRIORITY_CLASS,
nil, { pointer to new environment block }
nil, { pointer to current directory name }
S, { pointer to STARTUPINFO }
P) { pointer to PROCESS_INF }
then begin
ShowMessage('Create Process failed. Save this message for IT: ' + CmdLine);
Result:=-1
end
else begin
// WaitforSingleObject(P.hProcess,INFINITE);
// The following replacement better satisfies DDE requirements
repeat
R := MsgWaitForMultipleObjects(1, // One event to wait for
P.hProcess, // The array of events
FALSE, // Wait for 1 event
INFINITE, // Timeout value
QS_ALLINPUT); // Any message wakes up
if R>WAIT_OBJECT_0 then begin
M.Message := 0;
while PeekMessage(M,0,0,0,PM_REMOVE) do begin
TranslateMessage(M);
DispatchMessage(M);
end
end;
until R=WAIT_OBJECT_0;
// put value into Result.... non zero = success
GetExitCodeProcess(P.hProcess,DWord(Result));
CloseHandle(P.hProcess);
CloseHandle(P.hThread);
P.hProcess:=0;
P.hThread:=0;
end;
end;

What does "free" do in Delphi?

I found the following code snippet here:
with TClipper.Create do
try
AddPolygon(subject, ptSubject);
AddPolygon(clip, ptClip);
Execute(ctIntersection, solution);
finally
free;
end
Just curious, what does the free statement/function (between finally and end) do here? Google did not help.
The code
with TClipper.Create do
try
AddPolygon(subject, ptSubject);
AddPolygon(clip, ptClip);
Execute(ctIntersection, solution);
finally
free;
end
is shorthand for
with TClipper.Create do
begin
try
AddPolygon(subject, ptSubject);
AddPolygon(clip, ptClip);
Execute(ctIntersection, solution);
finally
free;
end;
end;
TClipper.Create creates an object of type TClipper, and returns this, and the with statement, which works as in most languages, lets you access the methods and properties of this TClipper object without using the NameOfObject.MethodOrProperty syntax.
(A simpler example:
MyPoint.X := 0;
MyPoint.Y := 0;
MyPoint.Z := 0;
MyPoint.IsSet := true;
can be simplified to
with MyPoint do
begin
X := 0;
Y := 0;
Z := 0;
IsSet := true;
end;
)
But in your case, you never need to declare a TClipper object as a variable, because you create it and can access its methods and properties by means of the with construct.
So your code is almost equivelant to
var
Clipper: TClipper;
Clipper := TClipper.Create;
Clipper.AddPolygon(subject, ptSubject);
Clipper.AddPolygon(clip, ptClip);
Clipper.Execute(ctIntersection, solution);
Clipper.Free;
The first line, Clipper := TClipper.Create, creates a TClipper object. The following three lines work with this object, and then Clipper.Free destroys the object, freeing RAM and possibly also CPU time and OS resources, used by the TClipper object.
But the above code is not good, because if an error occurrs (an exception is created) within AddPolygon or Execute, then the Clipper.Free will never be called, and so you have a memory leak. To prevent this, Delphi uses the try...finally...end construct:
Clipper := TClipper.Create;
try
Clipper.AddPolygon(subject, ptSubject);
Clipper.AddPolygon(clip, ptClip);
Clipper.Execute(ctIntersection, solution);
finally
Clipper.Free;
end;
The code between finally and end is guaranteed to run, even if an exception is created, and even if you call Exit, between try and finally.
What Mason means is that sometimes the with construct can be a paint in the ... brain, because of identifier conflicts. For instance, consider
MyObject.Caption := 'My test';
If you write this inside a with construct, i.e. if you write
with MyObect do
begin
// A lot of code
Caption := 'My test';
// A lot of code
end;
then you might get confused. Indeed, most often Caption := changes the caption of the current form, but now, due to the with statement, it will change the caption of MyObject instead.
Even worse, if
MyObject.Title := 'My test';
and MyObject has no Caption property, and you forget this (and think that the property is called Caption), then
MyObject.Caption := 'My test';
will not even compile, whereas
with MyObect do
begin
// A lot of code
Caption := 'My test';
// A lot of code
end;
will compile just fine, but it won't do what you expect.
In addition, constructs like
with MyObj1, MyObj2, ..., MyObjN do
or nested with statements as in
with MyConverter do
with MyOptionsDialog do
with MyConverterExtension do
..
can produce a lot of conflicts.
In Defence of The With Statement
I notice that there almost is a consensus (at least in this thread) that the with statement is more evil than good. Although I am aware of the potential confusion, and have fallen for it a couple of times, I cannot agree. Careful use of the with statement can make the code look much prettier. And this lessens the risk of confusion due to "barfcode".
For example:
Compare
var
verdata: TVerInfo;
verdata := GetFileVerNumbers(FileName);
result := IntToStr(verdata.vMajor) + '.' + IntToStr(verdata.vMinor) + '.' + IntToStr(verdata.vRelease) + '.' + IntToStr(verdata.vBuild);
with
with GetFileVerNumbers(FileName) do
result := IntToStr(vMajor) + '.' + IntToStr(vMinor) + '.' + IntToStr(vRelease) + '.' + IntToStr(vBuild);
There is absolutely no risk of confusion, and not only do we save a temporaray variable in the last case - it also is far more readable.
Or what about this very, very, standard code:
with TAboutDlg.Create(self) do
try
ShowModal;
finally
Free;
end;
Exactly where is the risk of confusion? From my own code I could give hundreds of more examples of with statements, all simplifying code.
Furthermore, as have been stated above, there is no risk of using with at all, as long as you know what you are doing. But what if you want to use a with statement together with the MyObject in the example above: then, inside the with statement, Caption is equal to MyObject.Caption. How do you change the caption of the form, then? Simple!
with MyObject do
begin
Caption := 'This is the caption of MyObject.';
Self.Caption := 'This is the caption of Form1 (say).';
end;
Another place where with can be useful is when working with a property or function result that takes a non-trivial amount of time to execute.
To work with the TClipper example above, suppose that you have a list of TClipper objects with a slow method that returns the clipper for a particular TabSheet.
Ideally you should only call this getter once, so you can either use an explicit local variable, or an implicit one using with.
var
Clipper : TClipper;
begin
Clipper := ClipList.GetClipperForTab(TabSheet);
Clipper.AddPolygon(subject, ptSubject);
Clipper.AddPolygon(clip, ptClip);
Clipper.Execute(ctIntersection, solution);
end;
OR
begin
with ClipList.GetClipperForTab(TabSheet)do
begin
AddPolygon(subject, ptSubject);
AddPolygon(clip, ptClip);
Execute(ctIntersection, solution);
end;
end;
In a case like this, either method would do, but in some circumstances, typically in complex conditionals a with can be clearer.
var
Clipper : TClipper;
begin
Clipper := ClipList.GetClipperForTab(TabSheet);
if (Clipper.X = 0) and (Clipper.Height = 0) and .... then
Clipper.AddPolygon(subject, ptSubject);
end;
OR
begin
with ClipList.GetClipperForTab(TabSheet) do
if (X = 0) and (Height = 0) and .... then
AddPolygon(subject, ptSubject);
end;
In the end is is matter of personal taste. I generally will only use a with with a very tight scope, and never nest them. Used this way they are a useful tool to reduce barfcode.
It's a call to TObject.Free, which is basically defined as:
if self <> nil then
self.Destroy;
It's being executed on the unnamed TClipper object created in the with statement.
This is a very good example of why you shouldn't use with. It tends to make the code harder to read.
Free calls the destructor of the object, and releases the memory occupied by the instance of the object.
I don't know anything about Delphi but I would assume that it is releasing the resources used by TClipper much like a using statement in C#. That is just a guess....
Any dinamicly created object must call free to free at object creation alocated memory after use. TClipper object is a desktop content creation, capture and management tool. So it is some kind of Delphi connection object with Clipper. The create (object creation) is handled in try finaly end; statment what mean, if connection with Clipper isn't successful the object TClipper will not be created and can not be freed after after of try finaly end; statement.
If "with" is as evil as some posters are suggesting, could they please explain
1. why Borland created this language construct, and
2. why they (Borland/Embarcadero/CodeGear) use it extensively in their own code?
While I certainly understand that some Delphi programmers don't like "with", and while acknowledging that some users abuse it, I think it's silly to say "you shouldn't use it".
angusj - author of the offending code :)

Improve speed of own debug visualizer for Delphi 2010

I wrote Delphi debug visualizer for TDataSet to display values of current row, source + screenshot: http://delphi.netcode.cz/text/tdataset-debug-visualizer.aspx . Working good, but very slow. I did some optimalization (how to get fieldnames) but still for only 20 fields takes 10 seconds to show - very bad.
Main problem seems to be slow IOTAThread90.Evaluate used by main code shown below, this procedure cost most of time, line with ** about 80% time. FExpression is name of TDataset in code.
procedure TDataSetViewerFrame.mFillData;
var
iCount: Integer;
I: Integer;
// sw: TStopwatch;
s: string;
begin
// sw := TStopwatch.StartNew;
iCount := StrToIntDef(Evaluate(FExpression+'.Fields.Count'), 0);
for I := 0 to iCount - 1 do
begin
s:= s + Format('%s.Fields[%d].FieldName+'',''+', [FExpression, I]);
// FFields.Add(Evaluate(Format('%s.Fields[%d].FieldName', [FExpression, I])));
FValues.Add(Evaluate(Format('%s.Fields[%d].Value', [FExpression, I]))); //**
end;
if s<> '' then
Delete(s, length(s)-4, 5);
s := Evaluate(s);
s:= Copy(s, 2, Length(s) -2);
FFields.CommaText := s;
{ sw.Stop;
s := sw.Elapsed;
Application.MessageBox(Pchar(s), '');}
end;
Now I have no idea how to improve performance.
That Evaluate needs to do a surprising amount of work. The compiler needs to compile it, resolving symbols to memory addresses, while evaluating properties may cause functions to be called, which needs the debugger to copy the arguments across into the debugee, set up a stack frame, invoke the function to be called, collect the results - and this involves pausing and resuming the debugee.
I can only suggest trying to pack more work into the Evaluate call. I'm not 100% sure how the interaction between the debugger and the evaluator (which is part of the compiler) works for these visualizers, but batching up as much work as possible may help. Try building up a more complicated expression before calling Evaluate after the loop. You may need to use some escaping or delimiting convention to unpack the results. For example, imagine what an expression that built the list of field values and returned them as a comma separated string would look like - but you would need to escape commas in the values themselves.
Because Delphi is a different process than your debugged exe, you cannot direct use the memory pointers of your exe, so you need to use ".Evaluate" for everything.
You can use 2 different approaches:
Add special debug dump function into executable, which does all value retrieving in one call
Inject special dll into exe with does the same as 1 (more hacking etc)
I got option 1 working, 2 should also be possible but a little bit more complicated and "ugly" because of hacking tactics...
With code below (just add to dpr) you can use:
Result := 'Dump=' + Evaluate('TObjectDumper.SpecialDump(' + FExpression + ')');
Demo code of option 1, change it for your TDataset (maybe make CSV string of all values?):
unit Unit1;
interface
type
TObjectDumper = class
public
class function SpecialDump(aObj: TObject): string;
end;
implementation
class function TObjectDumper.SpecialDump(aObj: TObject): string;
begin
Result := '';
if aObj <> nil then
Result := 'Special dump: ' + aObj.Classname;
end;
initialization
//dummy call, just to ensure it is linked c.q. used by compiler
TObjectDumper.SpecialDump(nil);
end.
Edit: in case someone is interested: I got option 2 working too (bpl injection)
I have not had a chance to play with the debug visualizers yet, so I do not know if this work, but have you tried using Evaluate() to convert FExpression into its actual memory address? If you can do that, then type-cast that memory address to a TDataSet pointer and use its properties normally without going through additional Evaluate() calls. For example:
procedure TDataSetViewerFrame.mFillData;
var
DS: TDataSet;
I: Integer;
// sw: TStopwatch;
begin
// sw := TStopwatch.StartNew;
DS := TDataSet(StrToInt(Evaluate(FExpression)); // this line may need tweaking
for I := 0 to DS.Fields.Count - 1 do
begin
with DS.Fields[I] do begin
FFields.Add(FieldName);
FValues.Add(VarToStr(Value));
end;
end;
{
sw.Stop;
s := sw.Elapsed;
Application.MessageBox(Pchar(s), '');
}
end;

Resources