Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

...

It was time to revisit John's changes. As usual, one thing led to about five others. 

An all-source ClarionTest

A while back I took the radical step of converting ClarionTest from an APP file to an all-source project. I wasn't entirely sure at the time why I was doing it. In part it was a wild impulse to get my hands on the source code in a way not possible as long as it remained in an APP. 

...

Given that premise, I ripped almost everything out of the ClarionTest source except for the window definitions and the data declarations necessary to compile those windows. And I started over.

 

Note

Don't for a minute think that whatever I'm describing in the following text is the "final" version of ClarionTest. If anything, ClarionTest has been, is and will be in a state of evolution. There are compromises and failures in the current code. When I notice them I'll comment on them, and try give some reason for my choices. And I'll come back to the code periodically and complain about the crap I wrote last time. To paraphrase Anil Dash, writing code is all about trying to suck less. That doesn't mean I'm not ever happy with my code - I frequently sit back and look at something I've written with a great deal of satisfaction. But I know that just because it looks good now doesn't mean it will look equally good down the road.

 

Starting over (again and again)

 

After commenting out all of the existing code and many of the data declarations, I had a bare bones, non-working app. So what were my requirements?

  • Be able to load up a test DLL
  • Remember the last test DLL loaded
  • Mark the individual tests that should be run, or run all
  • Display a summary of how many tests have failed and how many have passed
  • Option to display only the failed tests
  • Fix resizing so the last position/size is remembered

An accept loop

For the last couple of years I've been consulting on a very large Legacy system. And while Legacy is painful to work with, there is one thing I really do appreciate about it: there's an actual Accept loop in the generated code.   I sometimes think it was a mistake for ABC to bury that little bit of Clarion brilliance in the WindowManager.Ask method:

...

Maybe it's nostalgia, and maybe I'll regret not using WindowManager for ClarionTest, but for now I'm going to go with a regular old Accept loop and see where it takes me. If that means coming up with my own WindowManager replacement, I think I'm up for that. 

Resizing

For no particularly good reason I decided to tackle window resizing first. The first step was to find a way to remember the last window size and position.Here I did go with an ABC option: INIClass. 

INIClass is an example of a relatively benign ABC class. Although it conforms to the ABCInclude convention (which I generally despise), and although it's in a file with a bunch of other classes rather than in its own file (which I abhor) it does have the merit of being independent of any other ABC classes. And it provides a convenient way to store/retrieve configuration data. 

I added a relevant global include statement:

INCLUDE('ABUTIL.INC'),ONCE

...

Blech. There's that infamous '__Dont_Touch_Me__' string that shows up in every INI file. And I still don't see the rationale for an explicit kill - if it hasn't been done at destruct the class should be smart enough to do it. 

Oh well. I'll hold my nose on this one until I get around to writing something that suits me better. 

Here's my Accept loop so far:

Code Block
	open(window)
	Window{PROP:MinWidth} = 600  
	Window{PROP:MinHeight} = 300 
	ACCEPT
		case event()
		of EVENT:OpenWindow
			settings.Fetch(ProcedureName,window)
        of EVENT:CloseWindow
			settings.Update(ProcedureName,window)
		END
		case accepted()
		of ?Close
			post(event:closewindow)
		END
		
	END
	close(window)

...

Again, I resorted to an ABC class: WindowResizeClass. I might want to replace this at some point, but like INIClass it's fairly innocuous. That 'Class' suffix on the name is superfluous - I don't know that I've ever seen a class library where every class has that word in its name. For that matter, ABC is inconsistent - there are a number of class names that do not end on 'Class'. None of them should. 

I had to play around a little with the resizer to get it to do what I wanted.Here's my first cut of the window:

Image Modified

I have some flat buttons in the toolbar, and four controls in the window area: a prompt, an entry field, a lookup button and a list box. The prompt needs to stay fixed, and the entry field should only extend to the right. The lookup button needs to stay fixed to the right side of the entry field. The list box needs to resize to fill the window. 

I ended up with this override of the Init method:

Code Block
Resizer.Init                            PROCEDURE(BYTE AppStrategy=AppStrategy:Resize,BYTE SetWindowMinSize=False,BYTE SetWindowMaxSize=False)
	CODE
	PARENT.Init(AppStrategy,SetWindowMinSize,SetWindowMaxSize)
	SELF.SetParentDefaults()                                 ! Calculate default control parent-child relationships based upon their positions on the window
	SELF.SetStrategy(?TestList, Resize:FixLeft+Resize:FixTop, Resize:Resize) ! Override strategy for ?LIST1
	self.SetStrategy(?TestDll:Prompt,Resize:FixLeft+Resize:FixTop,Resize:LockSize)
	self.SetStrategy(?TestDll,Resize:FixLeft+Resize:FixTop,Resize:LockHeight+resize:resize)
	self.SetParentControl(?LookupTestDll,?TestDll)

...

I wouldn't be the first person to write my own resizer class, but again I can live with these limitations for now. 

Down the wrong rabbit hole

I had bit of a detour early on. In reviewing the original code I saw that there was a call to read the list of test DLLs from the directory. First there was a queue declaration:

Code Block
AllFiles                                    QUEUE(File:queue),PRE(FIL)            !
											END                                   !

...

Moral: Don't spend time on code that isn't needed! Although as noted it was probably time those classes made their way into DCL. 

Loading up a test DLL

One line of code lets the user look up a DLL with FileDialog:

...

settings.Update(ProcedureName,'TestDLL',TestDll)

This is all pretty standard stuff in ABC apps, so even if you're writing procedural code, Clarion is constantly writing OO code for you. 

Now, how about loading up the test DLL? Really this just takes two lines of code:

Code Block
			TestRunner.Init(TestDllName)
			TestRunner.GetTestProcedures(ProceduresQ)

TestRunner loads up the test procedures into a predefined queue that's part of the ClarionTest classes. I am going to want to do some formatting of the queue, which raises an important question about where those kinds of changes should be made. If I want to use my TestRunner's queue for display purposes then I'm going to need to add some UI-related fields into the queue (for colors, styles, etc). That's inevitably going to make things messy and will blur the lines between the display of testing information to the user and the code that actually execute tests. 

I decided pretty quickly to follow the approach previously used in ClarionTest, which was to create a separate display queue in the application itself, and copy the relevant information over from the queue populated by the TestRunner object. That's a bit of extra work, but it keeps UI separate from business logic and it means I don't have to make changes to my class library to handle UI improvements. 

Here's the LoadTests routine with TestsQ as the UI queue and ProceduresQ as the list of test procedures:

Code Block
LoadTests                               ROUTINE
	do SetDirectoryWatcher
	free(TestsQ)
	logger.Write('Loading tests from ' & TestDllPathAndName)
	str.Assign(TestDllPathAndName)
	TestDllDirectory = str.SubString(1,str.LastIndexOf('\'))
	if TestDllDirectory <> ''
		if not exists(TestDllDirectory)
			message('The test directory ' & TestDllDirectory & ' does not exist')
		ELSE
			setpath(TestDllDirectory)
			TestDllName = str.SubString(str.LastIndexOf('\') + 1,str.Length())
			TestRunner.Init(TestDllName)
			TestRunner.GetTestProcedures(ProceduresQ)
			logger.Write('Found ' & records(ProceduresQ) & ' tests')
		END
	END
	loop x = 1 to records(ProceduresQ)
		get(ProceduresQ,x)
		IF ProceduresQ.TestGroupName <> PreviousGroupOrTestName
			PreviousGroupOrTestName = ProceduresQ.TestGroupName
			CLEAR(TestsQ)
			TestsQ.GroupOrTestName = ProceduresQ.TestGroupName
			TestsQ.GroupOrTestLevel = 1
			TestsQ.ProcedureQIndex = 0
			TestsQ.GroupOrTestStyle = Style:Bold
			ADD(TestsQ)
		END
		TestsQ.GroupOrTestStyle = Style:Default
		TestsQ.GroupOrTestName = ProceduresQ.TestName
		TestsQ.GroupOrTestLevel = 2
		TestsQ.ProcedureQIndex = x
		TestsQ.TestResult = ''
		TestsQ.TestResultStyle = Style:Default
		TestsQ.Mark = false
		Add(TestsQ)
	END

The extra ADD at the top half of the loop is to handle test groups, e.g.:

Image Modified

Directory monitoring

With most of the core functionality in place, I still needed to re-implement directory monitoring which uses the DCL_System_Runtime_DirectoryWatcher class. The original class was written by Alan Telford (building on work by Jim Kane) and is described in Clarion Magazine article Waiting For Files With Clarion Threads. I've made a few minor changes, including moving the suggested event trapping code into a TakeEvent() method which you place in the Accept loop. Then you just need to derive the DoTask method which will be triggered any time a file in the monitored directory changes. 

Initially I run the unit tests any time there was a directory change, but this had two problems. First, I was getting a number of events each time I built a new test DLL and copied it into my UnitTests directory. I really only wanted a single event. My initial solution is a bit of a hack: I set a variable called TimeOfLastDirectoryChange whenever a change is detected, and then I use the following code to delay the test execution until I've had a quiet time of somewhere around a second (the value of DelayBeforeAutoRun is configurable via INI file). 

Code Block
			if TimeOfLastDirectoryChange > 0
				if clock() < TimeOfLastDirectoryChange or clock() >  TimeOfLastDirectoryChange + DelayBeforeAutorun
					TimeOfLastDirectoryChange = 0
					do RunTests
				end
			end

That's a bit of a hack. What I'd really like is a way to handle this inside the class itself, but I haven't come up with a clean solution yet. 

The other problem is that the directory watcher is indiscriminate - it triggers when any file changes. One of my unit tests updates an INI file, and each time the INI file is updated the tests are triggered, so ClarionTests sits in a loop executing all the selected tests every second (provided directory monitoring is enabled). 

Clearly I need to filter out only the DLL changes. I'm not sure of the best way to do this, although if nothing else I may yet use my directory class. 

Also I'm not too impressed with the code I'm using to manage the on-screen list of test procedures.  I need to add icons and some more sophisticated marking code, and I can see that becoming a mess in a hurry. 

Still work to be done

There's always still work to be done. Besides monitoring for DLL changes only, I need to find some way of presenting summary information and showing only, say, failed tests. The UI is pretty ugly, so if you have some suggestions for how it can be improved please pass them along. Oh, and not all the buttons work yet. Some of that may have to wait until next week. 

...

As always you can download the latest and greatest DCL including ClarionTest at GitHub.