Book.txt
Copyright(c) Richard J. Wartner
7610 North Via De Manana
Scottsdale, Arizona 85258-3517 USA
Email RWartner@aol.com

Version 1.05   May 30, 1998


"A Modular Approach To Visual Test 4.0 Programming"
Richard J. Wartner

Thank you Tom Arnold for including this in your new
book.  I've made very few changes to the text of the
book but I've added several functions to the GenUtil
and GenAPI include files.
						 
This guide is FREE.  There are no warranties, NONE.

What is meant by free?  You have the right to use the
information that is contained in this book and in the
attached sample libraries.  This is not intended as
a transfer of ownership however.  More like a free lease.

The purpose of the included libraries is to give you a
start and examples to study.  You may use free any or 
all of the code in your projects.

What you can't do is sell this book to anyone, but you
may make copies and give them away.

All the code within was written on Win95 with Visual Test 4.0r.

See Table of Contents: TOC.txt

Preface
--------

Visual Test is a programming language.  What a shock, you can't just turn the
recorder on and magically create a program. You've got to write the code.

Believe it or not Visual Test is a simple language, it's relatively easy.  But 
you must learn some concepts and experiment.  After that you're only limited 
by your imagination.

This book or guide is not intended to turn anyone into an expert programmer.
We have to start somewhere.  I felt that it would be helpful for beginning 
programmers and new VT users to see code examples and working programs.

This is an unfinished work. Once the foundations are laid and if others
are willing to help we can add chapters on more complex programming topics.

None of the code that is contained within is really original.  The only credit
I can take is the presentation and some personal touches.  None of the code
is "written in stone".  Have your way with it.  Just remember that I still
own all the rights to the book and you can't sell it.


If you find this guide/software useful then please take the following 
guidelines found on a Visual Basic forum (author unknown) to heart.

	If we all agree to some simple ettiquete, I think we will all find the 
	use of the list to be more productive.  I am not the list administrator, 
	but feel that some of the following suggestions are	in no way 
	inappropriate.  Here are my suggestions:

	1.  If you wish to thank someone who helped you, do not send that notice
	to the list.  Send it directly to the persons who helped you.

	2.  Only answer a question if you have a definite answer, or can tell
	the person exactly where to find an answer.  Simply commenting , "
	Yeah..I'd like to know the answer to that one too..." does not help
	anyone, and clutters the list.

	3.  Be very descriptive in your Subject line.  'Help me please!' forces
	one to read a mail, regardless of whether or not one is knowledgeable in
	that area.  Something like 'Help with DBGrid please..' will attract
	readers who are knowledgeable about the grid, or readers who are
	interested in it.

	4.  Specify the version of VT you are using, and what approaches you
	have already tried to solving your problem. Show us some code.

	5.  Unless your joke or humorous comment is extraordinarily good, please
	spare us.  Test it on some of your colleagues first.

	Thank you in advance to all of those who comply with these suggestions.
	I look forward to helping, and receiving help from all of you!


There are of course, NO RULES.  These are only SUGGESTIONS.  You may like 
some of the guidelines and hate or refuse to comply with others.  That's up 
to you, I'll do the best I can.


Suggested Reading list:
-----------------------

"Software Testing with Visual Test 4.0"
Thomas R. Arnold II

ISBN 0-7645-8000-0

Tom's book is the only reference book on Visual Test
that I'm aware of. Thanks Tom for your effort.

If you're new to Visual Test this is REQUIRED READING.

You can order books via the internet if you have a credit card.
Here's a place I've ordered a couple of books from:

	books@compubooks.com
	http://www.abyssbooks.com
	800-880-6818


MSTest 3.0 and VT 4.0 have installation manuals but are not
reference books. The manuals were replaced by online help.

MSTest 2.0 had a manual that's the last one.
It's a nice reference for the basics. I don't know if you can still 
order a copy but here's the info:

"Microsoft Test Software Testing Automation Tool for Windows"
Version 2.0
Document No. DB51776-0493
It's not at the bookstore so talk to Microsoft.

The following is a great book on the API
There is a 16 bit version as well.
HIGHLY RECOMMENDED reading.

"Visual Basic Programmers Guide to the Win32 API"
Daniel Appleman
ISBN 1-56276-287-7	

General Reference Books:

"Programmers Guide to Microsoft Windows 95"
ISBN 1-55615-834-3

"Programming Windows 95"
Charles Petzold
ISBN 1-55615-676-6 

As VT is a BASIC language, books on Visual Basic, Quick Basic etc.
are helpful to understanding fundamentals.

In addition VT is conceptually similiar to C++
so books on C and C++ are helpful.

I think the creators of VT expected VT programmers to have a background 
in C/C++ and Visual Basic.

If you don't understand fundamentals such as conditional branching,
loops, arrays etc then you have a challenge in front of you.

There are many books on Beginning Visual Basic.  If you are an inexperienced
programmer READ AS MANY AS YOU CAN.  The fundamentals are the same.

Discover online help.  Sometimes the answer is right there.

Books on Software Testing:

Testing Computer Software 
Kaner, Falk & Nguyen - 1993
ISBN: 0-442-01361-2

The Craft of Software Testing, Subsystem Testing (includes Object 
Oriented) 
Marick - 1995
SBN: 0-13-177411-5

Software System Testing and Quality Assurance
Beizer - 1984
ISBN: 0-442-21306-9


Internet User Groups that I know about
--------------------------------------
Rational Software:
http://WWW.Rational.com

Just in case you don't know, it's "Rational" Visual Test.

Newsgroups:

	Comp.Software.Testing
	Microsoft.Public.VisualTest

Email List Server:

	mt_info-request@eskimo.com

	subscribe    (in the subject line)


This is without a doubt the best source of information on VT.
Thank you InDepth Productions, Inc., for your sponsorship
and maintaining the list.

Training:

People ask about training.  STLabs is a good place to start.
http://WWW.STLABS.COM.  Another place to look for video training
is InDepth Productions, Inc., at http://WWW.VTINDEPTH.COM."


==========
Section 1:  Procedural Programming
==========
		 

Chapter 1.	 Overview
----------

What is Visual Test (VT)?
-------------------------

Procedures
----------

Visual Test (VT) is a BASIC language similiar to Visual Basic (VB).

VT is a procedural language whereas VB is event driven.

VT is primarily used as an automation tool for Windows applications. 
However, as you will see it has functionality beyond just manipulating 
windows.

I will stress the procedural aspects and modular design of VT which I will
eventually incorporate as projects.

I learned how to write programs by studying books, finding examples that suited 
my needs, copying them, making modifications until I learned how it all fit 
together.

I'm going to show you some examples, explain them as best I can, try to show
how it all fits together and hope you can figure out the rest.

Because VT is a procedural language, the key is understanding how to use
subroutines.  First how to write them and then how to put them 
together as a program.


Windows
-------

Windows IS windows.  Consider every object on the screen to be a window.
A button is a collection of windows. VT has specialized functions
to manipulate these windows.

These specialized functions are what makes VT unique.

Many Windows applications can be manipulated by these VT functions.  As a
result, by manipulating the windows of an application you can test it
or automate it as you wish.  Besides being a procedural language VT is a
Windows automation tool.

VT 4.0 is a 32 bit language and can only manipulate Win95 and NT Windows 
applications.  By comparision MSTest 3.0 has a 16 and 32 bit version.

Not all	Win95 and NT Windows applications can be automated and not every object 
can be manipulated with VT functions.  Functions to manipulate objects are 
limited to standard	Windows objects.  With out getting to deep in this subject,
custom controls such as grids are not standard therefore can't be directly
manipulated.

Generally speaking C++, Visual Basic and Delphi are examples of applications 
that can be automated.

By comparison, Visual Foxpro applications can't. (at least not very well with
VT)  That is because Visual Foxpro uses "painted controls".  Painted controls 
are like a picture of an object rather than an object. A "bitmap 
representation" of an object.

With a custom control such as a grid you have 2 options:

	1. Have the developer place the MSTEST.OCX on the form containing 
	the control, exposing the control to VT OLE commands.

	2. Use PLAY commands that perform simulated keyboard inputs and mouse
	actions.


With a painted control you can only use PLAY commands.


Play Statement
-------------
It is too early to get into this discussion, but because PLAY was brought up
here is some information about it found in online help.

Purpose:

Sends keyboard or mouse events to the active window as if they had been 
entered by a user.

Syntax
PLAY [ hwnd&, ] string$
  
hwnd&
Mouse coordinates are relative to a windows hwnd 
(value used to identify a window). If hwnd is omitted, the mouse coordinates 
are absolute (screen) coordinates.

string$
String expression sent to the active window. The string can contain sequences 
of key text, action codes, and characters with special meaning to the PLAY 
statement.

An example of the PLAY statement can be found in the ClickGrid() function 
which is located in the GenUtil.inc library file.

Play statements are usually not the preferred method of object manipulation as
VT offers other functions that provide more control.  Object manipulation 
techniques demonstrating VT functions will be discussed later.


Jigsaw puzzle
-------------

A test application is just like a development application.  It takes 
just as much organization and planning.

Communication is the key.

The ideal way to do this is have a specification, goals, a design.  In a 
development project, after the design phase, you may start with a menu system 
and proceed down.  Creating the specific steps or programs. With Visual Test 
you don't necessarily start with a menu but the concept is the same. Start at 
the top and work down.  This is referred to as Top Down programming.

A bottom up approach would be to create the libraries of functions you need and
build an application up from it.  Like solving a jigsaw puzzle.  Ok, I've got all
the pieces how do they fit together?

If you are experienced and have designed several applications the jigsaw approach
can work very well.  Programming becomes cutting and pasting and creating more 
puzzle pieces.

In order to explain how VT programming works I will have to use a bottom up
approach.  I will take a project apart and show you how the pieces work.  Some
work will be left up to you to examine the project code to see how it all fits
together.  I'm not going to explain every VT function or regurgitate all of the 
online help.  

It's like I have to reverse engineer a VT project, which may be good 
practice.  Testing an application involves reverse engineering it.

So I will give you the "box cover" or picture of the puzzle in the form of a 
project or program.  I will explain the tricky pieces and hopefully you will 
gain enough understanding so that you can then put your own puzzle together
yourself.


Example of a very simple program:
--------------------------------

	Program 1 Test1.mst Using a procedure:
	--------------------------------------

	Declare Sub Test1

	Test1
	End

	Sub	Test1
		MsgBox "Test1"
	End Sub


	Program 2 Test2.mst Using a function:
	-------------------------------------

	Dim ReturnInteger As Integer

	Declare Function Test1() As Integer

	ReturnInteger = Test1()

	If ReturnInteger = True
		MsgBox "Test is True"
	Else
		MsgBox "Test is False"
	End


	Function Test1() As Integer
		
		Dim RetInt  As Integer
		Test1 = True
			
		RetInt = MsgBox "Test1"

		If RetInt <> 1 Then
			Test1 = False
		End If

	End Function


With proceduralized programming nearly all of the work can be done by
subroutines.

The 2 lines of code above:

	Test1
	End


Would more likely look like this:

	Test1
	Test2
	Test3
	Test4
	Test5
	Test6
	End


Meaning that a program would usually have more than 1 subroutine.

The idea is the same whether you have one subroutine or many.  The program is
broken up in to parts.  The parts are called one by one in the proper order.
If the subroutines are created properly they can be mixed and matched.


Modular program design
----------------------

	1. Smaller is better.
	2. Only one function should be performed by a subroutine.
	3. The subroutine should be completely debugged before being
	   integrated into a program.


	Here is some advice:

	Do not create gigantic programs! Break your programs into small routines. 
	Comment your code.  Consider what you will have to do to debug the program 
	and maintain it later.

	Create readable code.  Indent, capitalize, give variables names that make 
	sense.

	Gigantic programs that are not broken up into their proper components i.e.
	subroutines read like gibberish.  Spending a bit more time upfront will
	pay huge divendends in the back end.

	Consider programming analogous to writing a book. Subroutines are like
	small chapters.  Within the book are characters which are like variables.

	This is art, design beautiful programs be an ARTIST. Have pride in your
	work, not only it's practicality but the care and beauty of it's design.
	Your programs are a reflection of you.


Subroutines
-----------
		   
I refer to the collection of procedures(SUBS) and functions as subroutines.  
They represent the pieces of the puzzle. VT has many inherent subroutines 
to do a variety of things but can't do everything.  The beauty is that VT 
gives us the capability to create our own routines for those things it can't 
do.  Even so, of course, it can't do everything.  But if you are creative...

There are two simple differences between procedures(SUBS) and functions.

	1. Functions return a value, procedures do not
	2. Functions can be used within a line of code, procedures can't

A program may be composed of functions and procedures.  If so, there could be
any number used.  I prefer to break up programs with subroutines each having a
specialized task.

Subroutines may be kept in library files that are named with the extension INC.
This way the subroutines are made available to all programs that use that library.
We will discuss how an INC file is used later.

For our first project we will not use external library files but instead 
concentrate on creating a program and subroutines within it.


What is a subroutine?
--------------------

A subroutine either a SUB or a FUNCTION is a small program.


	Declarations
	------------

Subroutine declarations are not required by VT. MAKE THEM ANYWAY.
Trust me this is a good programming practice and makes for more readable
code.

We declare all of our subroutines at the beginning of the program and put the 
body of it, the actual routine, at the end of the program.

Here are the declarations for 2 Functions:

	Declare Function CheckCommandLine() As Integer
	Declare Function ParseDN(DName$) As String

Here are the declarations for 2 Subs:

	Declare Sub LogError(Message$,ApplicationToClose%,LogPath$,CritErrPath$)
	Declare Sub TestLogVars(LogPath$)


	Return Values
	-------------

Notice that for the Functions we declared them AS INTEGER or AS STRING.
We declare a function as a TYPE, and have it return a VALUE of that type.

The point being that the function is going to return some value when 
it has completed it's mission and we must assign a TYPE i.e. Integer,
String, Variant etc to this return value.

As you can see CheckCommandLine() returns an Integer and ParseDN() returns
a string.

	CheckCommandLine() returns -1 = meaning TRUE or 0 meaning FALSE

	ParseDN("c:\dirname\dirname\data.dat") would return	"c:\dirname\dirname"


The declarations for the SUBs don't have the statement AS "--" because SUBs 
don't return values.

	Parameters
	----------

Functions and Subs can contain up to 32 parameters.

Parameters are variables that are used inside of the subroutine.  You're 
passing some known thing to the subroutine to use.

	Using the ParseDN(DName$) function as an example

	If we pass the parameter like this: ParseDN("c:\dirname\dirname\data.dat")

		Within the function

		DName$ = "c:\dirname\dirname\data.dat"


We must define all of the parameter TYPES.  Integer, String etc.

We can spell out the type definition or use the character that represents it.

	$ = String
	% = Integer
	& = Long
	etc.

	There are many TYPES, you can look up the rest and each will have it's
	own character designator.  See VARIABLE DATA TYPES.
	
	The reason I use the character designator instead of spelling it out 
	is simply to conserve space on that line of code.

	Declare Sub TestLogVars(LogPath$)
	Declare Sub TestLogVars(LogPath As String)

	The 2 declarations for LogPath above work the same either way.


If you declare a parameter as a string, you can't pass an integer and vice versa.
The compiler will generate an error and you will not be able to run the program.

How functions and procedures work will hopefully become apparent to you as you
work with the examples in our project.

Once you do a few, it's a piece of cake.

The important thing to understand is that if you write your programs using the
procedural approach using subs and functions, your code will become 
transportable, easier to understand and easier to maintain.



Chapter 2.  The WBImport Project
----------

A project is a collection of programs with a common goal.  A good example would
be to test a certain application and could contain many programs.  It may 
however have a simpler objective and contain one program with one objective in 
mind.

Our first project will be relatively simple and contain one program.  That program
will contain a number of subroutines which I'll explain as we go.


WBImport Project
----------------

Our WBImport project will consist of the following:

	WBImport.VTP
	WBImport.CSV
	WBImport.INI
	WBImport.MST

In addition the project will create as a byproduct:

	WBImport.CSV (new version in a separate directory)
	Error.log
	Criterr.log


Here are some sample lines of text from WBImport.csv.

*** In the fourth line note the word "none"
*** Note that commas separate the fields.


"Wed Jun  4 14:21:00 EDT 1997","Web Info Request","Power Computers",
"Wed Jun  4 15:35:59 EDT 1997","Web Info Request","TESTCO Technologies
"Wed Jun  4 15:50:23 EDT 1997","Web Info Request","FisiCom",999-228-5556"
"Wed Jun  4 15:56:12 EDT 1997","Web Info Request","none","3623726","3623728"

A comma delimited file is often referred to as a CSV file.


Objective:
---------

The objective of our project will be to manipulate a text file: WBImport.CSV.
We need to do a search and replace operation.

The project will demonstrate how a program is constructed and provide samples
of common subroutines.

Aside from the boiler plate routines, the program will demonstrate some string
manipulation techniques.  There will be several examples of conditional logic,
loops and arrays etc.


Here is our Specification:
--------------------------

1.  Where the word "none" appears we must replace it with the name which is 
found after	commas 23 and 24.  I've cut that portion of the lines off so you 
will have to believe me or examine the WBImport.CSV file.

	An example name at commas 23 and 24 is: "randy","walkin"

2. While doing so we need to reverse the name order.

	So in this example, "randy","walkin" becomes "walkin","randy".

3. Create a text file with the changes.


Desired Result
--------------
			  
"none" becomes	"walkin","randy".

The resulting line would look like this:

	From this:

"Wed Jun  4 15:56:12 EDT 1997","Web Info Request","none","3623726","3623728"

	To this:

"Wed Jun  4 15:56:12 EDT 1997","Web Info Request","walkin","randy","3623726","3623728"


The new file containing the changes will be created in a separate directory from the
original file.


Setting up our project
----------------------

Please create a directory on your computer named WBIMPORT.

Copy the zip file to that directory and unzip it.

Make sure that your directory contains these files:

	WBImport.VTP
	WBImport.CSV
	WBImport.INI
	WBImport.MST


Later we will discuss INI files. After reading that section, 
make sure your WBImport.INI agrees with the directory structure 
that you have created and copied the above files.


Chapter 3.  WBImport.mst
----------

So we have a clear objective in mind.  Find the file that contains the nones
replace none with the name found later in the string.  Create a file with
the changes.

I bet we can do this in under 1200 lines of code.

Here we go.


Relationships
-------------
In our WBImport.mst we will have many subroutines.  When you become familiar 
with it all you will find that there are dependencies.  An order or sequence
of events.  One particular routine must go first and then another second and
so on.

For instance before we can write to a log file we must tell the program its
name and where it's located.  If we want to write the current program's name
to the log file then we must tell the program that name before we call the
function.

So we have many routines and there is a definite order of events.  Sometimes
we will have some sort of error even though we have debugged all the 
subroutines, this could be because we didn't call the routines in the proper
order.  So in writing your code think about these dependencies.

In trying to explain how WBImport works, I'm taking some liberties or artistic
license so to speak with the order of events.  The order I explain it may not
be in the actual order of how the program works, close but maybe not exactly.

So the order in which I explain it is a bit more conceptual but I'll try to
explain it in program order as best I can.


INI file
--------

What I need to know from an external source:

	1. Could the source and output files change?
	2. Do we want to delete the source file?
	3. Error log names and locations.

The only thing you can rely on is change.

Important directory paths and names should be kept in an external file.
I prefer to use an INI file to keep these names.  By not hard coding these
variables within the program if the paths change you don't have to change
the code.

This information could be written to and read from the registry as well.
I prefer an INI file because it's easier to explain and maintain.  In a later
chapter we'll explain the registry functions.

In addition we could put all the variables in an include file (INC). Using INC
files will be explained later. There are pros and cons to all 3 approaches.

For now, let's learn how an INI file works, later you can pick the best 
technique for the job at hand.

Try to keep INI variables to a reasonable number.  20 to 30 maybe.  

All of the INI file variables should be declared globally.  So you must
declare the variables before you gather their contents from the INI file.  

These global variables are then available to your entire program.  If the 
variable is used as a private variable in a subroutine it shouldn't be 
necessary to put it in the INI file.


Here is a sample of the WBImport.INI file:


	[Defaults]

	; Logging directories
	; Error.log
	LogPath = C:\WBIMPORT\ERROR.LOG

	; Criterr.log
	CritErrPath = C:\WBIMPORT\CRITERR.LOG

	; Directory to import files from  
	; Include Backslash at end of path
	SourceDir = C:\WBIMPORT\

	; Directory to place resulting files
	; Include Backslash at end of path
	TargetDir =	C:\WBIMPORT\COMPLETE\

	; Delete the imported files?
	; YES or NO
	DeleteOldFiles = NO


We have a section [Defaults] and several variables.  Lines beginning with 
semicolons are ignored and are used as comments.  You may create any number of 
sections and variables.  If there is a physical limitation I don't know what 
it is.  Don't get carried away.  20 to 30 variables is certainly reasonble.

The idea here is that the directory where our import file lives may change 
so we nominate it within the INI file.  The INI file is a simple text file 
and easily modified.


Command Line
------------

So trick question:  Where is the INI file?  It's location could change too.

The way I deal with that is to pass it's name and location via the program's 
command line.


Here is the syntax to run our program:

	\FullPATH\mtrun.exe \FullPATH\WBImport.pcd /C WBImport.INI

Using my directory structure for example would be:

	C:\MSDEV\BIN\MTRUN.EXE C:\WBIMPORT\WBIMPORT.PCD /C C:\WBIMPORT\WBIMPORT.INI


In order to run a PCD file we must first call MTRUN.EXE then the PCD itself and
then we may use optional parameters.

By following the called PCD with the /C we can pass to the program a command 
line (optional parameters).


	Our entire run syntax should look like this:
	---------------------------------------------

	C:\MSDEV\BIN\MTRUN.EXE C:\WBIMPORT\WBIMPORT.PCD /C C:\WBIMPORT\WBIMPORT.INI


	This portion is what the program receives as the command line:

	C:\WBIMPORT\WBIMPORT.INI


The command line is a text string which can mean anything to the program that we
decide. In this program I'm creating a parameter from the command line which is
the path and name of an INI file.


	1.  We create a variable to contain the command lines contents.

	Global TheIniFile As String     ' INI file


	2.  Within the program the command line will become any string we pass after /C

	So we could initialize our variable this way:

		TheIniFile = Command$

		and eventually our variable will become:

		TheIniFile = "C:\WBIMPORT\WBIMPORT.INI" 


There are 2 ways to pass the command line parameter to the program.

	1.  An icon/shortcut:
	
		If you don't know what a shortcut is, we're in trouble.
		The command line syntax for your shortcut should look like this:

		C:\MSDEV\BIN\MTRUN.EXE C:\WBIMPORT\WBIMPORT.PCD /C C:\WBIMPORT\WBIMPORT.INI

	2.  If you're running WBIMPORT.MST from inside of developers studio you can pass
		the command line parameter as follows:

		a. File/New		 
		b. Open Workspace
		c. Select the WBIMPORT project (included in the zip file)
		d. In the TestView window the WBIMPORT project should appear
		e. Right Click on WBIMPORT the very top line
		f. select properties
		g. select the parameters tab
		h. In the Command$ edit box type in:

			C:\WBIMPORT\WBIMPORT.INI



*********************
OUR FIRST SUBROUTINE!
*********************


Extracting the command line:
----------------------------

I use a function to extract the command line.


First we declare the function:

	Function CheckCommandLine() As Integer


Then we execute the function:

	ReturnInteger = CheckCommandLine()


'*************************************************************************
'  CheckCommandLine() Parse command line for INI file
'*************************************************************************
Function CheckCommandLine() As Integer

    Dim CommandCounter  As Integer
    Dim Dummy           As String

	CheckCommandLine = True

	' Create dummy string from command line
    Dummy = UCase$(Command$)

	' Test Dummy
	If Len(Trim(Dummy))	= 0 Then
        LogWrite("1     Ini file not passed to program!",LogPath,CritErrPath)
		CheckCommandLine = False
        Exit Function
	End If

	' Check for parameter separator - comma
    CommandCounter = InStr(1, Dummy, ",")

    If CommandCounter = 0 Then
        TheIniFile = Dummy
        If Not Exists(TheIniFile) Then
            LogWrite("1     Ini file: "+TheIniFile+" does not exist!",LogPath,CritErrPath)
			CheckCommandLine = False
            Exit Function
        End If
    Else
        TheIniFile = Mid$(Dummy, 1, CommandCounter - 1)
        If Not Exists(TheIniFile) Then
            LogWrite("1     Ini file: "+TheIniFile+" does not exist!",LogPath,CritErrPath)
			CheckCommandLine = False
            Exit Function
        End If
	End If

End Function
'*************************************************************************


CheckCommandLine() will return an Integer either TRUE or FALSE.

By default the CheckCommandLine variable is false (0).  Initially we force the 
variable to true (-1).

		CheckCommandLine = True


If anything bad happens in our function:

		1. set CheckCommandLine to false
		2. write an error message
		3. exit the function.

		The order of the above events is not important except exiting the 
		function has to be the last step.


CheckCommandLine creates a dummy variable to place the command line.

Our command line equals the string 	"C:\WBIMPORT\WBIMPORT.INI"


		' Create dummy string from command line
		Dummy = UCase$(Command$)


As a result our Dummy variable will now equal the command line string.
i.e.

	   Dummy = "C:\WBIMPORT\WBIMPORT.INI"


Notice that we used the VT UCASE command.  This forces the Dummy string to be
all in capital letters.  This makes sure that the string is in a known state.

Make sure the Dummy is not empty.

		' Test Dummy
		If Len(Trim(Dummy))	= 0 Then
			LogWrite("1     Ini file not passed to program!",LogPath,CritErrPath)
			CheckCommandLine = False
			Exit Function
		End If

We now check the Dummy for commas with the VT INSTR() function.

		' Check for parameter separator - comma
		CommandCounter = InStr(1, Dummy, ",")


The reason for this is that there may be more than one parameter in
the command line.  I use the convention that command line parameters
are separated by a comma. CheckCommandLine decides what that
separator is.  The command line syntax must agree. 

You can use whatever separator you wish.  What I'm saying is that the 
CheckCommandLine function must agree.

If there are no commas that means that there is only one parameter and
the entire contents of the command line contains the INI path.

If it finds a comma that means that there are 2 parameters and the first
paramenter is the INI file.  It parses out the text from the begining of the
command line to the comma with the MID function.

If the function is successful it returns a True to the calling program
now our program knows the name and path of our INI file.

TheIniFile is a GLOBAL variable and it's contents now contain the name and 
path of our INI file.

Because it is a GLOBAL variable, TheIniFile can be used anywhere in our program
and it's contents will always be "C:\WBIMPORT\WBIMPORT.INI".


KeyPress event notification
---------------------------

	' Sets Virtual Key for KeyPress event to terminate program
	Const VK_ESCAPE     = &H1B&

	' Set trap for escape key to end program
	On KeyPress(VK_ESCAPE) Call StopScripts

This is a keypress notification trap.  What it means is that if you press the
escape key while this program is running the program is instructed to call the
StopScripts subroutine.

If you search WINAPI.INC for VK_ you will find a list of key assignments.  You
can set a trap for any key you wish and you can name your trap and program it
however you like.

But first we must declare the trap procedure.


StopScripts procedure
---------------------

Keypress notifications have a standard format and must be declared as follows:
We must use this format but we can name the routine and the parameter variable
as we wish.

	Declare Sub StopScripts(vKeyInfo As Variant)

There is one parameter and it must be passed as a variant. Variant means that
the variable can be of any type i.e. string, integer, double etc.


'*************************************************************************
' Sub StopScripts(vKeyInfo As Variant)
'*************************************************************************
Sub StopScripts(vKeyInfo As Variant)
		
	Dim EndProgram As Integer
		
	EndProgram = MsgBox("Escape pressed."+ Chr(10)+"Click OK to end program, 
	Cancel to continue.",1 OR 64 OR 0 OR 4096,"End "+CurPrgName)

	If EndProgram = 1 Then

		LogWrite("0     'Escape' Pressed, Ending "+CurPrgName+".",LogPath,CritErrPath)

		' End program
		StatusBox "Ending "+CurPrgName
		Sleep 2
		LogClose(LogPath,CritErrPath)
		LogWrite("",LogPath,CritErrPath)
		End

	End If

End Sub
'*************************************************************************


StopScripts is a simple routine that ENDS the program.  Along the way it puts 
up a message box to confirm the action and writes a message to the error log 
file.

Note the useage of the Global Variable CurPrgName. 

Look it up in the MST file. This is one	variable that you must set by typing 
in the name of your program. Here, it is WBImport.MST.



Logging procedures
-------------------


TestLogVars
-----------

All of the logging procedures as a first step call TestLogVars which tests to 
make sure the logfile names and paths are initialized.

We can't write to a logfile if we don't know it's name or path.

Early in WBImport.mst we created the variable LogPath and assigned it a 
default location.

	LogPath = CurDir+"\ERROR.LOG"

We then retrieve it's value from our INI file.

	; Logging directories
	; Error.log
	LogPath = C:\WBIMPORT\ERROR.LOG


Our log file name and path is very important.  This is the place that 
our program writes it's messages.

So we're taking another extra step to verify that this variable is OK.

We pass	TestLogVars() LogPath which by now should contain.

	"C:\WBIMPORT\ERROR.LOG"

Our function tests it as follows:


'*****************************************************
'  TestLogVars() - Test to make sure logfile
'                  Variables are initialized
'
'*****************************************************
Sub TestLogVars(LogPath$)

	Dim LogDir As String

	' Detail Error Log File path and name
	If Len(LogPath) = 0 Then
		LogPath = Curdir+"\ERROR.LOG"
	End If

	' Critical Error Log File
	If Len(CritErrPath) = 0 Then
		CritErrPath = Curdir+"\CRITERR.LOG"
	End If

	' Status Log File e.g. the suite manager log file
	If Len(SMLogPath) = 0 Then
		SMLogPath = Curdir+"\STATUS.LOG"
	End If

	' Detail Error Log File path only
	If Len(LogDir) = 0 Then
		LogDir = Curdir+"\"
	End If

	' Make sure Logging Directory exists
	If Not Exists(Mid(LogDir,1,Len(LogDir)-1),"+d") Then
		MakeDirs(LogDir)
	End If

End Sub
'*****************************************************


We simply test the length of the variables and create them if necessary.



MakeDirs
--------

At the end of TestLogVars the MakeDirs procedure is called.  This procedure 
will make a directory if it doesn't exist.

The declaration for MakeDirs is as follows:

Declare Sub MakeDirs(DirName As String)


'*****************************************************
'  MakeDirs() - Create directories from a string
'
'  Parameters:
'
'  DirName$  The directory path that you wish to create.
'
'    \MSDEV\PROJECTS\TESTDIR\PROJECT          
'
'	 creates the following directories:
'	 \msdev
'	 \msdev\projects
'	 \msdev\projects\testdir
'	 \msdev\projects\testdir\project
'
'*****************************************************
Sub MakeDirs(DirName As String)

	Dim SlashInString As Integer
	Dim NewDirString  As String
	Dim DirCounter    As Integer
	Dim LoopCounter   As Integer

	' Create Array to store directory names
	Dim DirArray(1 To 1) As String 

	NewDirString  = DirName
	SlashInString = Instr(2,NewDirString,"\")

	' Parse elements from DirName
	' and put each element into an array
	Do Until SlashInString = 0
		NewDirString  = ParseDN(NewDirString)
		SlashInString = Instr(2,NewDirString,"\")
		If Len(NewDirString) > 2 Then
			DirCounter = DirCounter + 1
			ReDim Preserve DirArray(1 To DirCounter) As String
			DirArray(DirCounter) = NewDirString 
		End If
	Loop

	' Create directories from array starting at the end
	If DirCounter > 0 Then
		For LoopCounter = DirCounter To 1 Step -1
			If Not Exists(DirArray(LoopCounter),"+d") Then
				MkDir(DirArray(LoopCounter))
			End If
		Next
	End If

	' Write directories created to Log File
	If DirCounter > 0 Then
		For LoopCounter = DirCounter To 1 Step -1
			If Exists(DirArray(LoopCounter),"+d") Then
				LogWrite("0     Created Test Log File Directory: "
				+DirArray(LoopCounter)+".",LogPath,CritErrPath)
			End If
		Next
	End If

End Sub

'*****************************************************


MakeDirs is an interesting procedure because it utilizes three important basics:

An Array
A While Loop
A For Loop


Let's take a look at the array portion of the procedure.

We are constructing a single dimension array to hold directories.

Using our example the eventual array elements would look like this:

	DirArray(1) = c:\msdev\projects\testdir\project
	DirArray(2) = c:\msdev\projects\testdir
	DirArray(3) = c:\msdev\projects
	DirArray(4) = c:\msdev


To do this we must first create a blank array.
The first element of an array always defaults to zero. 

	' Example of default array
	Dim DirArray(0) As String 


I prefer to start the array at one so when I created the array with the 
DIM statement I used 1 to 1.

	' Create Array to store directory names
	Dim DirArray(1 To 1) As String 


This creates an array with the lower boundary (LBound) as 1 and an upper 
boundary (UBound) of 1.

The second 1 or UBound is not necessary in the declaration but I do it as
a convention.

Dim DirArray(1) As String would achieve the same result.  I prefer to nominate
both initial boundaries.


We must now parse out the subdirectories from the directory path that was 
passed to the procedure.

Our example:  \MSDEV\PROJECTS\TESTDIR\PROJECT including itself, contains 4
subdirectories.

By searching for the second \ in the string we can parse out the subdirectories
inside a DO LOOP.

We need only then resize the array with the REDIM PRESERVE command and add
each subdirectory to the array.


The REDIM PRESERVE command allows us to keep what is inside the existing array
while adding more elements to it.  We can dynamically grow our array to its 
desired size.  So we start the array with one element and add as many elements
as are necessary.


	NewDirString  = DirName
	SlashInString = Instr(2,NewDirString,"\")

	' Parse elements from DirName
	' and put each element into an array
	Do Until SlashInString = 0
		NewDirString  = ParseDN(NewDirString)
		SlashInString = Instr(2,NewDirString,"\")
		If Len(NewDirString) > 2 Then
			DirCounter = DirCounter + 1
			ReDim Preserve DirArray(1 To DirCounter) As String
			DirArray(DirCounter) = NewDirString 
		End If
	Loop


The resulting array would look like this:

	DirArray(1) = c:\msdev\projects\testdir\project
	DirArray(2) = c:\msdev\projects\testdir
	DirArray(3) = c:\msdev\projects
	DirArray(4) = c:\msdev


Now that we have all the subdirectory names in the array we can create 
subdirectories with the VT MKDIR function.


This uses a FOR NEXT to loop through the array.

Note the useage of DirCounter To 1 Step -1.  This means that it starts at
the end of the array and works down i.e. from 4 to 1.  This is because we
must create the root directory first.  i.e.  c:\msdev


Before we create a directory we test to see if it's already there by using
the VT function Exists(). The "+d" parameter means check for subdirectories
as opposed to a file name.

So we pass the directory name to the Exist() and MkDir() functions.


	' Create directories from array starting at the end
	If DirCounter > 0 Then
		For LoopCounter = DirCounter To 1 Step -1
			If Not Exists(DirArray(LoopCounter),"+d") Then
				MkDir(DirArray(LoopCounter))
			End If
		Next
	End If



So from our example we start at array element 4 and count down to 1.
We know that our array contains 4 elements because as we created the array
we remembered the count.

		DirCounter = DirCounter + 1


In our FOR LOOP	DirCounter now represents the total elements in the array.

		For LoopCounter = DirCounter To 1 Step -1


Counting down from 4 to 1 the array looks like this:

	DirArray(4) = c:\msdev
	DirArray(3) = c:\msdev\projects
	DirArray(2) = c:\msdev\projects\testdir
	DirArray(1) = c:\msdev\projects\testdir\project


In effect we do the following commands:

	MkDir("c:\msdev")
	MkDir("c:\msdev\projects")
	MkDir("c:\msdev\projects\testdir")
	MkDir("c:\msdev\projects\testdir\project")


Now all that is left is to write the files created to our log file
using the same FOR NEXT loop.

	' Write directories created to Log File
	If DirCounter > 0 Then
		For LoopCounter = DirCounter To 1 Step -1
			If Exists(DirArray(LoopCounter),"+d") Then
				LogWrite("0     Created Test Log File Directory: "+DirArray(LoopCounter)+".",LogPath,CritErrPath)
			End If
		Next
	End If


This time we passed the directory name to our Logging Function LogWrite().


Logging Procedures
-------------------

Declare Sub LogOpen(LogPath$,CritErrPath$)
Declare Sub LogClose(LogPath$,CritErrPath$)
Declare Sub LogEnter(Message$,LogPath$,CritErrPath$)
Declare Sub LogWrite(Message$,LogPath$,CritErrPath$)
Declare Sub LogOnly(Message$,LogPath$,CritErrPath$)
Declare Sub LogError(Message$,ApplicationToClose%,LogPath$,CritErrPath$)

Above are the declarations for 6 subroutines that are quite similiar but have
specialized tasks.


	LogWrite() is the most commonly used routine of my entire library.


Note the useage of the LogWrite sub in the preceding example.

	LogWrite("0     Created Test Log File Directory: "+DirArray(LoopCounter)
	+".",LogPath,CritErrPath)


'*************************************************************
'  LogWrite() - Write Entry to the Error Log - with date/time
'
'  Parameters:
'
'  Message$     - What is the message you want to write.
'                 The first character of the message is the
'                 Severity level of the message.
'  LogPath$     - Error.log path and name.
'  CritErrPath$ - CritErr.log path and name.
'
'*************************************************************
Sub LogWrite(Message$,LogPath$,CritErrPath$)
 
	Dim LogFH      As Integer
 	Dim LogMessage As String

	' Make sure log file variables are initialized
	TestLogVars(LogPath)

	' get next available file handle
	LogFH=FREEFILE

	' open file and append message
    Open LogPath for Append As #LogFH
    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
    Close #LogFH

	' Write message to Suite Manager Log (Status Log)
	LogMessage = Mid(Message,2,Len(Message))

	Select Case	Left$(Message,1)

		Case ""

	'	Case " "
	'		Log #0,+LogMessage
		Case "0"
			Log #0,+LogMessage
		Case "1"
			Log #1,+LogMessage
			' Write Error message to Critical Error Log
			LogFH=FREEFILE 
			Open CritErrPath for Append As #LogFH
		    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
			Close #LogFH
		Case "2"
			Log #2,+LogMessage
			' Write Error message to Critical Error Log
			LogFH=FREEFILE 
			Open CritErrPath for Append As #LogFH
		    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
			Close #LogFH
		Case "3"
			Log #3,+LogMessage
			' Write Error message to Critical Error Log
			LogFH=FREEFILE 
			Open CritErrPath for Append As #LogFH
		    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
			Close #LogFH
		Case Else
			Log #4,+LogMessage
			' Write Error message to Critical Error Log
			LogFH=FREEFILE 
			Open CritErrPath for Append As #LogFH
		    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
			Close #LogFH

	End Select

End Sub

'*************************************************************


We previously discussed TestLogVars(LogPath).

The remaining code uses VT low level file commands to Create or Append
to the log files passed as parameters.

	LogPath$     - Error.log path and name.
	CritErrPath$ - CritErr.log path and name.


For example LogPath = "C:\WBImport\Error.log"


We get a file handle, and use the VT function Open() for Append As

	' get next available file handle
	LogFH=FREEFILE

	' open file and append message
    Open LogPath for Append As #LogFH
    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
    Close #LogFH


The following is from the online help for OPEN:

		"OPEN STATEMENT"

		Purpose
		Enables input/output (I/O) to/from a file.

		Syntax
		OPEN filename$ [ FOR mode ] AS [ # ] filenumber [ LEN = reclength ]
  
		Remarks

		You must open a file before any I/O operation can be performed on it. 
		The OPEN statement allocates a buffer for I/O to the file and 
		determines the mode of access used with the buffer. The filename$ 
		is the name of the file you want to open. If filename$ does 
		not exist, it is created when a file is opened for either APPEND, 
		BINARY, OUTPUT, or RANDOM mode.

		The filenumber is an integer expression with a value between 1 and 20, 
		inclusive. When an OPEN statement is carried out, filenumber is 
		associated with the file as long as it is open. Other I/O statements 
		can use filenumber to refer to the file.


Opening a file for append can be compared to opening an existing file in a
text editor such as Notepad, except we're doing it with a program.

I choose to open the file with the MODE of APPEND.  This is because I want to 
WRITE to the end of the file.

We want to open our Error.log and write to it by appending text to the BOTTOM
of the file instead of overwriting it.

If we want to OVERWRITE the text by starting at the TOP of the file we would 
use the OUTPUT mode instead.  i.e.

		Open LogPath for OUTPUT As #LogFH

		This mode will be demonstrated later.


If I want to READ the file instead of WRITING to it I would use the MODE of 
INPUT.   

		Open LogPath for INPUT As #LogFH

		This mode will be demonstrated later.


My first step is to create the FILENUMBER variable.  This must be an integer
from 1 to 20 as defined by the OPEN function.

What this means is that you can open 20 files at the same time, but each file 
must have a unique number.  The problem then is how do you know which number
to use?  Did we already use number 5 or 6, etc?


		' get next available file handle
		LogFH=FREEFILE


The VT FREEFILE function will do just that.  It will return us an UNUSED number
between 1 and 20.  It remembers for us, and the range of numbers it will return
is exactly the range we're looking for.  What a coincidence!


The VT function Print() prints our message on the next available line in our
designated file.

		Print #LogFH, Date$ + " " + Time$ + "  "+ Message


If we did not specify where to Print i.e. Print #LogFH, it would print to the
ViewPort window, which is the default.


The VT function Close() is then used to close the file.

		Close #LogFH

Here again we specify the file to close.  If we don't it will close all open
files.


	I could open the file once at the beginning of my program using LogWrite()
	and close the file at the end of the program.  However, I prefer to open 
	and then immediately close.  This way if anything goes wrong, chances are 
	I haven't left any files open.

	This means of course that I open and close the log file many times during
	the program.  Don't worry, it's so fast you don't know its happening.

	Never worry about speed, except going too fast.  If anything we have to 
	slow our program down.


The LogMessage parameter is shortened for usage with the Suite Manager log.

	LogMessage = Mid(Message,2,Len(Message))


The LogMessage parameter is then examined to determine the severity of the 
message.  Error messages can have levels of severity.  

For instance:

	0 = No error informational message
	1 = Fatal Error
	2 = Error but not Fatal
	3 = Not a very bad error
	4 =	Minor inconvenience
	5 = You decide


Depending on the level of severity the Case structure reports in different ways.

	Select Case	Left$(Message,1)


The important part of this is that this gives you the flexibility to design your 
error reporting on a severity basis.  You can decide how many levels by adding or
subtracting Cases.

For the most part I use 0 and 1, which is equivalent to pass and fail.  But I
have the flexibility by changing a parameter to report several levels.

In addition you can program in any action based upon this paramenter.

Each of the cases logs the message to the Suite Manager log file which I name
STATUS.LOG.

	SMLogPath = Curdir+"\STATUS.LOG"

This is an example of the Log code.

	Log #0,+LogMessage


However the WBImport program does not turn this feature on unless we run the
program from within the Suite Manager. 

If you run the program within the developer studio, in for instance the 
debug mode, the VT LOG() function will Log to the Viewport.

Later we will create a program to emulate the Suite Manager and turn on the
"Custom Logging Functions".  This will then cause the VT LOG() function to 
write to our Status.log.

So for the time being we are not writing to the Status.log.

It is important to understand that if and when we do activate the Logging
functions by program or using the Suite Manager, the Log function will be 
automatically activated.  


Back to the INI file
--------------------

Ok we discussed what to put in the INI file and how it is structured.
Now we have to retrieve what is inside of it.

In order to do that we must use an API call. I'm not going to explain
the API, maybe later.  But VT comes with WINAPI.INC that declares many API 
functions. The WinAPI.inc file is about 9000 lines.

We only need to declare the following function so by declaring it ourself
instead of including WINAPI.INC we save 9000 lines of code from being 
included in our PCD. 

Here is our declaration:

	Declare Function GetPrivateProfileString Lib "kernel32.dll" Alias 
	"GetPrivateProfileStringA" (lpAppName$, lpKeyName As Any, lpDefault$, 
	lpReturnedString$, nSize&, lpFileName$) As Long

For nearly all of the API functions that we use, we create a "Wrapper". A
Wrapper is simply a routine that we create to use the API.

SetWBDefaults() is a "wrapper" function that calls the GetPrivateProfileString 
API function.

This is the syntax I use to call the function.

	' Retrieve INI file defaults
	ReturnInteger = SetWBDefaults("Defaults",TheIniFile)
	' Test Return Value
	If ReturnInteger = False Then
		LogError("Error Setting Defaults!",0,LogPath,CritErrPath)
	End If

SetWBDefaults() is a function which means that it returns a value.  I programmed
it to return an Integer True/False.

So I call SetWBDefaults and test the return value. If that value is False I then call
my LogError function. 


LogError()
----------

The LogError() function makes use of the WGetInfo() statement.

	
	Dim lpInfo As Info
    WGetInfo (WGetActWnd(0), lpInfo)


By calling the INFO structure, we can retrieve information about the current 
window.  We can then write this information to our error file.


8/4/97 9:51:58 PM  1 ********************************************************
8/4/97 9:51:58 PM  1 ***               FATAL ERROR!
8/4/97 9:51:58 PM  1 *** C:\WBImport\TstExp03.PCD Could Not Continue!
8/4/97 9:51:58 PM  1 *** Error Testing View Options Menu!
8/4/97 9:51:58 PM  1 *** Current Window Information Follows: 
8/4/97 9:51:58 PM  1 *** Window Handle:          1156 
8/4/97 9:51:58 PM  1 *** Window ID:              0 
8/4/97 9:51:58 PM  1 *** Visible:               TRUE
8/4/97 9:51:58 PM  1 *** Enabled:               TRUE
8/4/97 9:51:58 PM  1 *** Window Caption:        Options
8/4/97 9:51:58 PM  1 *** Window Classname:      #32770
8/4/97 9:51:58 PM  1 *** Name of Owning Module: EXPLORER.EXE
8/4/97 9:51:58 PM  1 *** Associated Label:      
8/4/97 9:51:58 PM  1 ********************************************************


By doing so, we know more about the window that was on the screen at the time
of the error.

LogError is used only for FATAL ERRORS.  I try to avoid calling it from within 
subroutines. The reason is that I use LogError to end programs besides writing 
information to a file.  When something goes wrong within a subroutine it should 
return a false value.  The calling program can then decide what to do.

So if the calling program wants to end things because a function returned false
that is the time to call LogError().
 

	 ' Retrieve INI file name and path from Command Line
	ReturnInteger = CheckCommandLine() 

	' Test Return Value
	If ReturnInteger = False Then
		LogError("Error Retreiving Command Line!",0,LogPath,CritErrPath)
	End If


The above example calls CheckCommandLine().  If it returns FALSE then
LogError() is called.


		LogError("Error Retreiving Command Line!",0,LogPath,CritErrPath)


Notice the 2nd parameter i.e. 0, which is the ApplicationToClose parameter.


By testing the ApplicationToClose parameter the LogError() function can then
run a close routine.


	' Close desired application
	Select Case	ApplicationToClose

		Case 0
			' Do nothing
		Case 1
			'Run Close application 1 procedure
		Case 2
			'Run Close application 2 procedure
			' Do nothing

	End Select


In our particular program the application to test hasn't been opened yet so
there is no reason to try and close it.

If the application was open we could pass a 1 or 2 etc. Which would mean to run
a close routine.

At this point we don't have a close routine.  But we're thinking ahead and 
making a place to put one.



Here's our function to retrieve INI file variables:


'*************************************************************************
' Function SetWBDefaults(SectionName$,TheIniFile$)
'
' Retrieves default values from INI file.
'
'*************************************************************************
Function SetWBDefaults(SectionName$,TheIniFile$) As Integer

	Dim tmpLogDir      As String
	Dim tmpLogPath     As String
	Dim tmpCritErrPath As String

	SetWBDefaults = True

	' Test parameters
	If SectionName = "" Then
		tmpLogDir		= CurDir+"\"
		tmpLogPath		= tmpLogDir + "ERROR.LOG"
		tmpCritErrPath  = tmpLogDir + "CRITERR.LOG"
		SetWBDefaults = False
		LogWrite("1     Bad Section Name parameter passed to SetDefaults()!",tmpLogPath,tmpCritErrPath)
		Exit Function
	End If

	' Create temporary defaults
	tmpLogDir		= CurDir+"\"
	tmpLogPath		= TheIniFile
	tmpCritErrPath  = tmpLogDir + "CRITERR.LOG"

	' Retrieve INI file variables
	' ---------------------------
	LogPath = ReadIni(TheIniFile,SectionName,"LogPath")
	' Test value returned
	If LogPath = "NotFnd" Then
		SetWBDefaults = False
		LogWrite("1     Can't find LogPath in: " + TheIniFile+"!",tmpLogPath,tmpCritErrPath)
		Exit Function
	End If

	CritErrPath = ReadIni(TheIniFile,SectionName,"CritErrPath")
	' Test value returned
	If CritErrPath = "NotFnd" Then
		SetWBDefaults = False
		LogWrite("1     Can't find CritErrPath in: " + TheIniFile+"!",tmpLogPath,tmpCritErrPath)
		Exit Function
	End If

	SourceDir = ReadIni(TheIniFile,SectionName,"SourceDir")
	' Test value returned
	If SourceDir = "NotFnd" Then
		SetWBDefaults = False
		LogWrite("1     Can't find SourceDir in: " + TheIniFile+"!",LogPath,CritErrPath)
		Exit Function
	End If

	TargetDir = ReadIni(TheIniFile,SectionName,"TargetDir")
	' Test value returned
	If TargetDir = "NotFnd" Then
		SetWBDefaults = False
		LogWrite("1     Can't find TargetDir in: " + TheIniFile+"!",LogPath,CritErrPath)
		Exit Function
	End If

	DeleteOldFiles = ReadIni(TheIniFile,SectionName,"DeleteOldFiles")
	' Test value returned
	If DeleteOldFiles = "NotFnd" Then
		SetWBDefaults = False
		LogWrite("1     Can't find DeleteOldFiles in: " + TheIniFile+"!",LogPath,CritErrPath)
		Exit Function
	End If

End Function
'*************************************************************************


Functions must return a value.  I declare most of my functions to return an
Integer which is almost always TRUE or FALSE. This of course relates to success
or failure.

So if the function returns TRUE then we know it completed it's task.

I immediately set the function return value to True.

	SetWBDefaults = True

If in the function an error is encountered the return value is set to FALSE

	SetWBDefaults = False

and we exit the function.


I usually write an error message with a severity error number of 1 when an 
error occurs.  Here is an example of it all together.


		SetWBDefaults = False
		LogWrite("1     Bad Section Name parameter passed to SetDefaults()!"
		,tmpLogPath,tmpCritErrPath)
		Exit Function


SetWBDefaults first creates temporary names and paths for the error logging
files.  Just in case something goes wrong we need a place to write messages
to.


It then retrieves the values from the INI file with ReadINI().	ReadINI() is a
"wrapper".  It encapsulates an API function.


The Declaration for ReadINI is as follows:

Function ReadINI(TheIniFile As String,TheSection As String,TheVariable As String) As String



'*************************************************************************
' Function ReadINI()
'	Retrieve variables from INI file
' 	Parameters:
'		TheIniFile  - The name of the INI file
'		TheSection  - The section within the INI file
'		TheVariable	- The variable name within the Section
'   Returns retrieved variable or NotFnd
'*************************************************************************
Function ReadINI(TheIniFile As String,TheSection As String,TheVariable As 
String) As String

Dim ReturnBuffer    As String 
Dim ReturnInteger   As Integer
Dim ReturnString    As String 

ReturnBuffer  = String$(256,0)

ReturnInteger = GetPrivateProfileString(TheSection,TheVariable,"NotFnd",
ReturnBuffer,Len(ReturnBuffer),TheIniFile)

ReturnString  = Left$(ReturnBuffer,ReturnInteger)

ReadINI = ReturnString

End Function
'*****************************************************


I'm not going to go in to a discussion of the API at this point.

If you're interested in the WINAPI read this:

		Visual Basic Programmers Guide to the Win32 API
		Daniel Appleman
		ISBN 1-56276-287-7


ReadINI's job is to get a nominated variable from an INI file.

The tricky part is creating a buffer to hold the value.
We've declared a buffer of 256 zeros.

	ReturnBuffer  = String$(256,0)

GetPrivateProfileString puts the value of the INI file variable into this buffer.
The value is retrieved from the buffer and sent back to the calling program.


Error Traps:
-------------

Here are error trap calls.
Each call needs its own procedure to handle deal with the error.


On UnhandledException Call UAEErrorTrap	 ' Unhandled Exception Error
On Error GoTo ErrorHandler               ' Error Handler routine


The following is email discussing ON ERROR from the ESKIMO group provided
by STLABS and Tom Arnold.  If you're not subscribed to this group, do it.

Arthur does a great job of explaining this particular subject and I've 
included it here.

Thanks to everybody who participates in the group, we all get lots of help from
each other.  


>>>>>---------->

	>Subject: 	RE: On Error Goto .... Resume not functioning correctly

	From:	JAARSMA@sqeez.lkg.dec.com
	To:	mt_info@eskimo.com

	I believe your problem may be due to the way Vt4.0 handles runtime
	errors. 

	With MSTest 3.x you could define the On error goto statement in the main
	script. When a runtime error occurred the resume or resume next would be
	from the line of code where the error occurred, regardless if that line
	of code was in a main script or a subroutine.

	With VT 4.0 you need to define the On error goto statement for each
	subroutine that you want the resume or resume next to work. For example
	if a program had sub1 calling sub2 which called sub3. If sub1 contained
	the On error statement but a runtime error occurred in sub3 the resume
	command would be in reference to the line of code in sub1 that called
	sub2. Note when defining the On error in a subroutine you must use the
	On Local error. For more info refer to the online help.

	Hope this helps ...Arthur 

>>>>>---------->

Here's another email from Arthur.  These are great ideas.  But Art, where's the
code dude?


>>>>>---------->

	You may want to add more information about the how and why's of
	the On Error usage. For example, a routine tests file copy operations, a
	network time-out condition could occur generating a runtime error. The
	on error for this routine should log an error message, sleep for several
	seconds and retry the operation without any operator intervention.
	Another example, a routine performs a setup operation for the entire
	test suite, the on error routine should force operator intervention by
	displaying a retry or abort dialog.

	When coding the On Error, each On Error routine should be in it's own
	include file. This file can be included, via the include statement at
	the top of each sub-routine. Doing it this way makes for easier
	maintainable code.

	Well that's my two cents,

	Regards Art

>>>>>---------->



So consider the above email when implementing this error handler.  You may wish
to create additional error handlers and incorporate them into your local
procedures.

For the most part having a "global error handler" works fine.  This handler 
doesn't do a RESUME NEXT, instead ENDs the program.  Your error handler may 
require a Resume Next or code to deal with specific problems.


'*****************************************************
' Error Handler
'
'*****************************************************
ErrorHandler:

    Close #fh

	' Run CloseOnSystemError procedure
	CloseOnSystemError(0,LogPath,CritErrPath)



The ErrorHandler closes the variable fh.  This is the file handle variable
I use in all the logging functions.  This statement makes sure that no file
is accidently left open.
						

CloseOnSystemError is then called.  The reason for doing this is that the
ErrorHandler: statement must be in your program because it is called by a
GOTO statement.  

	On Error GoTo ErrorHandler

The GOTO statement will look for the string following the word GoTo which
is "ErrorHandler" but only if that string is immediately followed by a colon ":".

	i.e.  ErrorHandler:

When it finds that string it executes the commands following it.  The important
thing to note is that "ErrorHandler:" must be in your program.  We can't put it
into an external library like a function or sub. 

Imagine that you have 100 programs to maintain.  You put your ErrorHandler
trap in every one.  One day a light goes on and you decide you need to change
your error routine.  You now have to go to 100 programs to change it.

Or you merely have all 100 error routines call the same procedure.  Now all you
have to do is change that one procedure and you are done. Oops, you have to compile
all 100 programs, but it's still a heck of a lot easier.

So	"ErrorHandler:" must be in your program but any subroutines it calls can be
placed in an external library.


'************************************************************
' Sub CloseOnSystemError - Reports and closes on System Error
'                          Called by Error Handler Routine.
'
' Parameters:
'
'	1) ApplicationToClose
'
'		0 None
'		1 close routine for application 1
'		2 close routine for application 2
'
'************************************************************
Sub CloseOnSystemError(ApplicationToClose&,LogPath$,CritErrPath$)

    Dim ErrFileNum    As Integer
	Dim ReturnInteger As Integer
	Dim DialogInteger As Integer

    ' Get next available handle for Error Log
	ErrFileNum = FREEFILE

	' Append to Error Log
    Open LogPath For Append As #ErrFileNum

	' If error exists write to Error Log
    If Err > 0 Then

        BEEP
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  ********************************************************"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** MS-TEST Reports the following ERROR:"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** "+Error$(Err)+" online: "Trim$(Str$(Erl(0)))
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** In Program: "+Erf(0)
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** This may cause unexpected results."
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** Attempting to end "+CurPrgName+"!"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  ********************************************************"

    End If

	' Close Error Log
    Close #ErrFileNum

	' Append to Critical Error Log
    Open CritErrPath For Append As #ErrFileNum

	' If error exists write to Critical Error Log
    If Err > 0 Then

        BEEP
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  ********************************************************"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** MS-TEST Reports the following ERROR:"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** "+Error$(Err)+" online: "Trim$(Str$(Erl(0)))
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** In Program: "+Erf(0)
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** This may cause unexpected results."
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  *** Attempting to end "+CurPrgName+"!"
        Print #ErrFileNum, Date$ + " " + Time$ + " "+ Str$(Err)+"  ********************************************************"

    End If

	' Close Critical Error Log
    Close #ErrFileNum

	' Write error notice to Status Log
	Log #1, "     SYSTEM ERROR "+ Trim$(Str$(Err))+": Check "+LogPath+"!"
	StatusBox "SYSTEM ERROR "+Error$(Err)+"!"
	Sleep 3
	StatusBox Close

	' Close desired application
	Select Case	ApplicationToClose
		Case 0
			' Do nothing
		Case 1
			' Run Close application 1 procedure
		Case 2
			' Run Close application 2 procedure
			' LogWrite("1  *** System Error Attempting to Close Application 2! ***",LogPath,CritErrPath)
			' Do nothing
	End Select

	' End program
    LogClose(LogPath,CritErrPath)
	LogWrite("",LogPath,CritErrPath)
    End

	' Instead of ending the program, resume
	' Resume Next

End Sub
'************************************************************


CloseOnSystemError is similiar LogError and LogWrite.  It writes error 
messages to a file.
	

		Str$(Err)+"  *** "+Error$(Err)+" online: "Trim$(Str$(Erl(0))
		Str$(Err)+"  *** In Program: "+Erf(0)


The above is an example of Using Error Indicators: 

	ERR  - the number of the error
	ERL  - the line where the error occurred
	ERF  - the program where the error occurred


	' If error exists write to Error Log
	If Err > 0 Then

The error number can be "trapped".  The value of ERR is a number which relates 
to a list of errors that is kept somewhere.  The idea of an error handler is to
catch the error and respond to it.

This is a fairly simple error handler because it doesn't look/catch/trap 
specific errors.  You could build in a case structure to examine the ERR 
variable and take whatever action you please.
		

	' Close desired application
	Select Case	ApplicationToClose
		Case 0
			' Do nothing
		Case 1
			' Run Close application 1 procedure
			' Do nothing
		Case 2
			' Run Close application 2 procedure
			' Do nothing
	End Select


By passing an ApplicationToClose parameter you can have the routine do a close 
routine on a designated application by using a SELECT CASE.  You choose the 
application to close and of course, have to write the close routine.

		Case 0
			' Do nothing


Always have one of the cases do nothing.  My convention is Case 0.

You may have as many close routines as you wish by adding additional Cases.
As you test more and more applications you just add them to the list.

What I have done is given you the place to put your close routines, now it's 
up to you.

	' End program
    LogClose(LogPath,CritErrPath)
	LogWrite("",LogPath,CritErrPath)
    End

So here we have an END statement. Which means this procedure will end the 
current program as a result.  This is a choice on my part.  You may not
always want to include an End statement here.  But I usually do.

Instead you can use the RESUME NEXT statement that will return you to the 
calling program.

	' Instead of ending the program, resume
	' Resume Next

The idea is that you would resolve the error and continue.


************************
MEAT AND POTATOES TIME!
************************

Up to now everything has been "BOILER PLATE". All of the concepts and routines
are standard.  The stuff I've gone through could be included in every program.  
Nearly all of the routines could be kept in an external library file (INC).

Now we get to do the specific requirement.

	' Get the files to fix
	GetFiles()


'*************************************************************************
' Function GetFiles()
'
' Fix Import files and send to new directory
'
' Richard J. Wartner
'*************************************************************************
Function GetFiles() As Integer

    Dim FileName      As String
    Dim TargetName    As String
    Dim ReturnInteger As Integer

	GetFiles = True

	' Make sure TargetDir exists
	If Not Exists(Mid(TargetDir,1,Len(TargetDir)-1),"+d") Then
		MakeDirs(TargetDir)
	End If

    ' Find import files and put into list
	FileList Clear
	FileList Add SourceDir + "import*.txt","?AR-D"

    ' Loop through import files
	For FileName In FileList
        If Exists(FileName) Then

            ' Extrapolate Target Name
            TargetName = TargetDir+Mid(FileName,Len(SourceDir)+1)

            LogWrite("0     Fixing file:  "+FileName+".",LogPath,CritErrPath)
            StatusBox "Fixing file:  "+FileName+"."
            Sleep .5

            ' Fix Import file and send to new directory
            ReturnInteger = WBImport(FileName,TargetName)

            ' Test Return Value
            If ReturnInteger = False Then
				GetFiles = False
	            LogWrite("1     Error changing file: "+FileName+"!",LogPath,CritErrPath)
                Exit Function
            End If
  
        End If
	Next

    ' Delete old import file
    If UCase$(DeleteOldFiles) = "YES" Then

	    For FileName In FileList
            If Exists(FileName) Then
                Kill FileName
                LogWrite("0     Deleted file: "+FileName+".",LogPath,CritErrPath)
            End If
	    Next

    End If

	FileList Clear

End Function
'*************************************************************************


GetFiles() demonstrates the VT FILELIST command.


Here we gather a list of the source files in a FileList array.  If files exist
we loop through the FileList array sending the files one by one to our function
WBImport() to be modified per our specification.

The FILELIST command that is a VT function that is used specifically to build
an array of files.  Unusual name huh?


	FileList Add SourceDir + "import*.txt","?AR-D"


If SourceDir = "C:\WBIMPORT\" then we're telling FILELIST to build us an array
of files in the C:\WBIMPORT\ directory that begin with import and end with 
the extension .txt

	?AR-D

The plus sign (+) means that the attribute must be on. The minus sign (-) means 
that the attribute must be off. The question mark (?) means that the attribute 
can be either on or off.

Read the online help for FILELIST for a complete explanation.  The following 
is good to remember.

	The -D means we're we're looking for files.
	+D means we are looking for subdirectories.


As we only have one WBImport.csv.  That will be the only file in our filelist
array.  Of course as many files that meet the search criteria will be included
in the array.


Notice this code at the beginning which tests to make sure our Target Directory
\WBIMPORT\COMPLETE\ exists.  

If the subdirectory does not exist it calls the MakeDirs() sub to create it.

	' Make sure TargetDir exists
	If Not Exists(Mid(TargetDir,1,Len(TargetDir)-1),"+d") Then
		MakeDirs(TargetDir)
	End If


The line

	If Not Exists(Mid(TargetDir,1,Len(TargetDir)-1),"+d") Then

makes use of the +D parameter discussed above.  So the syntax of the EXIST 
command works the same way as the FILELIST in that regard.  Here we are
searching for a subdirectory rather than a file.  I hope you noticed that the
D can be d, i.e. case insensitive.


It is necessary to use a string operation on our TargetDir variable.

	Mid(TargetDir,1,Len(TargetDir)-1)


This is because we must remove the backslash from the end of our varible.

	From this:

		\WBIMPORT\COMPLETE\

	To this:

		\WBIMPORT\COMPLETE



If the variable DeleteOldFiles = YES then we delete the source file after
modifying it. We need only loop through our filelist again and KILL the
files.


    ' Delete old import file
    If UCase$(DeleteOldFiles) = "YES" Then

	    For FileName In FileList
            If Exists(FileName) Then
                Kill FileName
                LogWrite("0     Deleted file: "+FileName+".",LogPath,CritErrPath)
            End If
	    Next

    End If


When we are finished we clear the filelist with the following command.

	FileList Clear

This should always be done before and after creating a filelist.



$$$$$$$$$$$$$$$$$$
$ Earn The Dough $
$$$$$$$$$$$$$$$$$$


OK here's where we earn the big bucks and actually start doing what our 
client wants.


'*************************************************************************
' Function WBImport
' Fix our files
' Richard J. Wartner
'
' Parameters:
'
' FileName1$  - Source File Name
'
' FileName2$  - Target File Name
'
' The function converts the Source into the Target
'
'*************************************************************************
Function WBImport(FileName1$,FileName2$)

    Dim RdBuffer      As String     ' Buffer to hold input records
    Dim LineCounter   As Integer    ' Counter
	Dim ArrayCounter  As Integer
    Dim NonePos       As Integer
    Dim EndNonePos    As Integer
    Dim FNPos         As Integer
    Dim LNPos         As Integer
    Dim EndLNPos      As Integer
    Dim FNString      As String
    Dim LNString      As String
    Dim StartString   As String
    Dim EndString     As String
    Dim PrintString   As String

    WBImport = True

    ' Make sure file exists
    If Not Exists(FileName1) Then
        LogWrite("1     Can't Find "+FileName1+"!",LogPath,CritErrPath)
        WBImport = False
        Exit Function 
    End If

    ' get a free file handle
    fh = FreeFile

    ' open file
    Open FileName1 For Input As #fh   

    Do

        ' Read string into buffer variable
		Line Input #fh, RdBuffer
  
        If Len(RdBuffer) > 1 Then

            ' Increment counter
		    LineCounter = LineCounter + 1

            ' Test for String signifying need to change
            NonePos = Instr(1,RdBuffer,"none")

        End If
  

        If NonePos > 0 Then

            ' Find end position of none
            EndNonePos = CountCommas(RdBuffer,3)

            ' Test Return Value
            If EndNonePos < 1 Then
	            LogWrite("1     Error counting commas !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Find position of first name
            FNPos = CountCommas(RdBuffer,22)

            ' Test Return Value
            If FNPos < 1 Then
	            LogWrite("1     Error counting commas !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Find position of last name
            LNPos = CountCommas(RdBuffer,23)

            ' Test Return Value
            If LNPos < 1 Then
	            LogWrite("1     Error counting commas !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Find end position of last name
            EndLNPos = CountCommas(RdBuffer,24)
  
            ' Test Return Value
            If EndLNPos < 1 Then
	            LogWrite("1     Error counting commas !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Create first name string
            FNString = Mid(RdBuffer,FNPos+2,(LNPos-1)-(FNPos+2))

            ' Test Return Value
            If Len(FNString) < 1 Then
	            LogWrite("1     Error creating first name string !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Create last name string
            LNString = Mid(RdBuffer,LNPos+2,(EndLNPos-1)-(LNPos+2))

            ' Test Return Value
            If Len(LNString) < 1 Then
	            LogWrite("1     Error creating last name string !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Create string from beginning to word none
            StartString = Mid(RdBuffer,1,NonePos-1)

            ' Test Return Value
            If Len(StartString) < 1 Then
	            LogWrite("1     Error creating start string !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Create string from none to end
            EndString = Mid(RdBuffer,EndNonePos-1)

            ' Test Return Value
            If Len(EndString) < 1 Then
	            LogWrite("1     Error creating end string !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

            ' Put all the strings together
            PrintString = StartString + LNString + ", "+ FNString + EndString

            ' Test Return Value
            If Len(PrintString) < 1 Then
	            LogWrite("1     Error creating print string !",LogPath,CritErrPath)
                WBImport = False
                Exit Function 
            End If

'          	LogOnly(PrintString,LogPath,CritErrPath)

            ' Redim array to hold new string
            ReDim Preserve StringArray(1 To LineCounter) As String
          
            ' Put final string into array
            StringArray(LineCounter) = PrintString

        ' Put strings without word none into array
        ElseIf Len(RdBuffer) > 1 Then

            ' Redim array to hold new string
            ReDim Preserve StringArray(1 To LineCounter) As String
          
            ' Put string into array
            StringArray(LineCounter) = RdBuffer

        End If

    Loop While Not EOF(fh)

	' Close file
    Close #fh

    ' get a free file handle
    fh = FreeFile

    ' open file
    Open FileName2 For Append As #fh   

    ' Write array to new file
    For ArrayCounter = 1 To UBound(StringArray)

		Print #fh, StringArray(ArrayCounter)

    '	LogOnly(StringArray(ArrayCounter),LogPath,CritErrPath)

    Next

    LogWrite("0     Created file: "+FileName2+".",LogPath,CritErrPath)
    StatusBox "Created file:  "+FileName2+"."
    Sleep .5

    Close #fh

End Function

'*************************************************************************


We pass to WBImport FileName1$ and FileName2$ parameters which are the 
Source File and Target File.


	FileName1$ = Source File = "C:\WBIMPORT\WBIMPORT.CSV"
	FileName2$ = Target File = "C:\WBIMPORT\COMPLETE\WBIMPORT.CSV"


We open up the Source File and examine it line by line.  We do this by reading
each line into a buffer.  


        ' Read string into buffer variable
		Line Input #fh, RdBuffer


The VT OPEN function was discussed earlier.  This is an example of opening a 
file for INPUT instead of APPEND.  Here we are reading the file line by line
instead of writing to it.

The buffer is examined for the string "none". If the buffer contains the string
"none" then we know we have to replace it.

To replace it we must pick the string apart.

We determine the beginning and end position of "none",
and we determine the beginning and end position of the first and last name.

This is done by calling the CountCommas() function which will be eplained
later.


			'Starting position of none:
            NonePos = Instr(1,RdBuffer,"none")

            ' Find end position of none
            EndNonePos = CountCommas(RdBuffer,3)

            ' Find position of first name
            FNPos = CountCommas(RdBuffer,22)

            ' Find position of last name
			LNPos = CountCommas(RdBuffer,23)


When we have the positions we can pull out those portions of the string creating
several smaller portions.  Then recombine the portions the way we want them to
end up.

	
            ' Create first name string
            FNString = Mid(RdBuffer,FNPos+2,(LNPos-1)-(FNPos+2))

            ' Create last name string
            LNString = Mid(RdBuffer,LNPos+2,(EndLNPos-1)-(LNPos+2))

            ' Create string from beginning to word none
            StartString = Mid(RdBuffer,1,NonePos-1)

            ' Create string from none to end
            EndString = Mid(RdBuffer,EndNonePos-1)

            ' Put all the strings together
            PrintString = StartString + LNString + ", "+ FNString + EndString


When we have the reassembled string the way we want we put it into an array.
We add another element to the array with REDIM PRESERVE command then put the
final string into that element of the array.


            ' Redim array to hold new string
            ReDim Preserve StringArray(1 To LineCounter) As String
          
            ' Put final string into array
            StringArray(LineCounter) = PrintString



Most of the lines in our Source file do not have the string "none" in them. So
the lines that don't need to be changed must be put into our array as well.
This is because we want to include those lines in our Target file.  To be
printed to the Target file they must be in the array.


        ' Put strings without word none into array
        ElseIf Len(RdBuffer) > 1 Then

            ' Redim array to hold new string
            ReDim Preserve StringArray(1 To LineCounter) As String
          
            ' Put string into array
            StringArray(LineCounter) = RdBuffer

        End If



So we've created an array with our corrected file.  Now we must create the new
file from it.  i.e. the Target File.


    ' get a free file handle
    fh = FreeFile

    ' open file
    Open FileName2 For Append As #fh   

    ' Write array to new file
    For ArrayCounter = 1 To UBound(StringArray)

		Print #fh, StringArray(ArrayCounter)

    '	LogOnly(StringArray(ArrayCounter),LogPath,CritErrPath)

    Next

    LogWrite("0     Created file: "+FileName2+".",LogPath,CritErrPath)
    StatusBox "Created file:  "+FileName2+"."
    Sleep .5

    Close #fh


To do that we just loop through the array we created, with a FOR NEXT and we 
PRINT each element of the array as a line in our Target File.  As mentioned 
earlier	the entire array is sent to the Target File, which includes the 
lines that were	fixed and the lines that were OK.

Notice here the use of OPEN for APPEND instead of OPEN for INPUT.  Writing vs
reading the file.


'***************************************************************
' CountCommas(InputString$)	As Integer
' Counts the number of commas  (",")  in a string
'***************************************************************
Function CountCommas(InputString$,CountTo%) As Integer

    Dim CommaPosition  As Integer
    Dim CommaCounter   As Integer

	CommaPosition = 1
    Do While CommaPosition <> 0 And CommaCounter < CountTo                         
		' Find Comma                       
        CommaPosition=Instr(CommaPosition,InputString,Chr(34)+","+Chr(34)) 
		' Found Then
        If CommaPosition <> 0 Then 
			' Increment past ','
            CommaPosition=CommaPosition+1
			' Remember comma count
            CommaCounter=CommaCounter+1                  
        End If
    Loop ' Do it again

	' Return Comma position
    CountCommas=CommaPosition  

End Function
'***************************************************************

In order to determine the positions of some of the strings we needed to count
commas.  And return the position.

			none       = comma 3
			first name = comma 22
			last name  = comma 23

            ' Find end position of none
            EndNonePos = CountCommas(RdBuffer,3)

            ' Find position of first name
            FNPos = CountCommas(RdBuffer,22)

            ' Find position of last name
			LNPos = CountCommas(RdBuffer,23)


To do this we use a DO WHILE loop.

We loop through the line of text counting commas until there are no more or
the count does not excede the CountTo variable.

So if the CountTo variable is 3 it will search for 3 commas and exit the loop.


The line that I find most interesting is the following:

		' Find Comma                       
        CommaPosition=Instr(CommaPosition,InputString,Chr(34)+","+Chr(34)) 


We're not just looking for a commas we're actually looking for a comma 
surrounded by quotes.  

	i.e.  ","


To tell that to the INSTR command we must do it this way:

	Chr(34)+","+Chr(34)

	Chr(34) is the equivalent of a double quote (") stated in a different way.
	Every keyboard character has a CHR equivalent.

	Don't ask me what they all are.  I always ask someone else. "Hey what's
	the CHR for a double quote?"


	We CAN'T do it this way:

		"",""

	or like this

		'","'


I hope that by showing this to you, you can understand because the "quote" 
problem is hard to explain.  The 3rd parameter of the INSTR command is the
character we wish to look for.

Quoting from online help:

	INSTR ( [ start , ] stringexp1$, stringexp2$ )
  
	Return Value
	The position of the first occurrence of stringexp2$ in stringexp1$, 
	optionally starting at the location start.


The 3rd parameter is a string.  So if we want it to look for a comma we need to
use ",".  Because to designate a string we surround the character with quotes.

But in our case we want it to look for:

		 double quote comma double quote

Thus:

		Chr(34)+","+Chr(34)

Is the only way to make it work.  Trust me on this if you don't understand it.
Just remember CHR(34) is a double quote because someday someone will ask and
they'll think you're really smart when you know.  "Hey Rich it's 34."



Running our program
-------------------

First of all you should compile WBImport.mst. As a note the only time I run 
MSTs is from inside Developers Studio.  I want to compile my MSTs into PCDs
and run PCDs.  If I compile them all then I know that the program has been 
checked for syntax. 

RUN WBImport.pcd from a shortcut icon or RUN WBImport.mst from within the 
Developers Studio.

Do I need to explain how to do this?

Start Microsoft Developer Studio and open the WBImport project.

		a. Select Menu File/New		 
		b. Open Workspace
		c. Select the WBIMPORT project (included in the zip file)
		d. Double click on WBImport.mst in the TestView window
		e. Select Menu Test/Go



SUCCESS!
--------

Open \WBIMPORT\Error.log


If the program is successful the error.log will look like this:


7/24/97 12:21:26 PM  0   WBImport.PCD STARTED. Rev 1.0
7/24/97 12:21:26 PM  0     Created Directory: C:\WBIMPORT.
7/24/97 12:21:26 PM  0     Created Directory: C:\WBIMPORT\COMPLETE.
7/24/97 12:21:27 PM  0     Fixing file:  C:\WBIMPORT\WBImport.csv.
7/24/97 12:21:28 PM  0     Created file: C:\WBIMPORT\COMPLETE\WBImport.csv.
7/24/97 12:21:28 PM  0     Successfully Fixed Files.
7/24/97 12:21:28 PM  0   WBImport.PCD ENDED.
7/24/97 12:21:31 PM  

Yes, of course the date and time will be different.


If it doesn't look like that, hopefully you will find an error message 
to give you a clue why it didn't work.

Hey we're testers right?  If there's a bug, we'll find it.


	Possible errors
	---------------

If the program didn't work make sure that directory path settings in 
WBImport.ini agree with your directory structure.

The main reason for error that I've discovered is not passing the command line
to the program properly.  Review instructions and make sure you're passing
the command line properly.



 C:\WBIMPORT\COMPLETE\WBIMPORT.CSV
 ---------------------------------


Here is what your newly created file should look like:


"Wed Jun  4 12:48:23 EDT 1997","Web Info Request","Antegrate Inc","514-421-0004","514-421-0007","ACT!, ","Macintosh","32 Warren Street","","Columbus","OH","432150-1525","usa","32 Warren Street","","Columbus","OH","43215-1525","USA","Sally@Antegrateinc.com","http://","Ms.","Sally","Perkins","VP","From a friend or associate, ","yes","5","","","Vendor evaluation","ACT!, ","Auto agents, Auto processes, Ease-of-use, ","30 days","30 days","","Phone call","Yes","No"
"Wed Jun  4 13:54:26 EDT 1997","Web Info Request","BBB Corp","666-870-3570","666-870-3600","TeleMagic, ","Windows 3.1, Windows 95, NT, DOS, UNIX","same","","","","","","1700 West park Drive","","Westborough","MA","01581","USA","Joe_smith@BBB.com","http://","Mr.","Joe","Smith","Network Analyst","Searching the Web, ","yes","23","","","Planning","ACT!, GoldMine, Lotus Notes, TeleMagic, ","Auto agents, Auto processes, Ease-of-use, Opportunity tracking, ","30-90 days","90-180 days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 14:07:44 EDT 1997","Web Info Request","ABC Publications","(211) 630-4411","(211) 630-4412","ACT!, ","Windows 3.1, Windows 95, NT","","","","","","","7 West 34th Street","","New York","NY","23001-8191","USA","baylesr@ABCpub.com","http://www.ABCpub.com","Mr.","Harry","Bayles","Sr. VP, Operations","From a friend or associate, ","yes","30","20","","Pilot","ACT!, ","Ease-of-use, SynchMartinization, ","30-90 days","90-180 days","$600-$800","Mail literature","Yes","No"
"Wed Jun  4 14:21:00 EDT 1997","Web Info Request","XYZ Computing","413-388-6751","","ACT!, ","Macintosh","2400 S. IH-35","","Round Rock","TX","78681-2015","USA","2400 S. IH-35","","Round Rock","TX","78681-2015","USA","djudd@XYZcc.com","http://www.XYZcc.com","Mr.","Arthur","Judd","Business Analyst","American Way, ","yes","30-50","23","","Idea","","Auto agents, Auto processes, Conflict resolution, Customization, Ease-of-use, Opportunity tracking, SynchMartinization, ","","","","Phone call","Yes","No"
"Wed Jun  4 15:35:59 EDT 1997","Web Info Request","Super Technologies ","333-472-7000","333-229-1669","None, ","Windows 95","11126 McCormick Road","","Hunt Valley","Md","22330","","11126 McCormick Road","","Hunt Valley","Md","22330","USA","Barnhill, albert ","333-472-7000","333-229-1669","None, ","Windows 95","11126 McCormick Road","","Hunt Valley","Md","22330","","11126 McCormick Road","","Hunt Valley","Md","22330","USA","none","http://www.Super.com","Mr.","albert ","Barnhill","Chairman & CEO","US Air, ","no","","","23","Idea","None, ","","30-90 days","30-90 days","$600-$800","Fax literature","Yes","No"
"Wed Jun  4 15:50:23 EDT 1997","Web Info Request","TestCom","333-228-5556","333-228-5484","ACT!, Tracker, ","Windows 95","201 Loudon Road","","Concord","NH","03301","USA","201 Loudon Road","","Concord","NH","03301","USA","lhunt@TestCom.com","http://WWW.TestCom.com","Mr.","Sam","Hunt","Eastern Reg.Sls.mGR.","America West, ","yes","23","4","3","Planning","ACT!, ORACLE","Ease-of-use, Opportunity tracking, SynchMartinization, ","30-90 days","90-180 days","","","Yes","No"
"Wed Jun  4 15:56:12 EDT 1997","Web Info Request","WalKen, randy","3683736","3683798","ACT!, ","NT","","","","","","","8800 n. gainey center drive","","scottsdale","az","85258","","rWalKen@saleslogix.com","http://","Mr.","randy","WalKen","manager","Delta Sky, ","yes","12","4","","Vendor evaluation","Aurum, ","Auto agents, ","30-90 days","180+ days","over $1,600","E-mail information","Yes","No"
"Wed Jun  4 16:16:52 EDT 1997","Web Info Request","Comp Corp","222-963-8878","222-963-9525","ACT!, ","Windows 95","same","","","","","","615 Griswold, Suite 2320","","Detroit","MI","48226","United States","ccorps1@rust.net","search Comp Corp ","Mr.","Harvey","Hoskow","Business Manager","Selling XYZ Magazine, ","yes","3","","","Planning","","","90-180 days","90-180 days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 16:40:35 EDT 1997","Web Info Request","BAZ Services","555-541-1820","555-279-3441","ACT!, GoldMine, TeleMagic, ","Windows 95","Same as above","","","","","","1565 North Rim Court","Suite 287","San Diego","CA","92111","USA","John@rbdata.com","http://www.rbdata.com","Mr.","John","Bellefond","President","From a friend or associate, ","no","","","1","Vendor evaluation","ACT!, GoldMine, TeleMagic, ","Auto agents, Auto processes, Customization, Ease-of-use, Opportunity tracking, SynchMartinization, ","30-90 days","30-90 days","$900-$1,000","Mail literature","Yes","No"
"Wed Jun  4 16:46:18 EDT 1997","Web Info Request","Institute of Fitness","111-673-4905","111-652-5755","None, ","Windows 95","202 North Snow Canyon Road","","Ivins","UT","84738","USA","202 North Snow Canyon Road","","Ivins","UT","84738","USA","mlp@infowest.com","http://www.fqif.com","Mr.","Jeff","Peterson","Marketing Supervisor","Sales & Marketing Management, ","no","","","5","Vendor evaluation","","Auto agents, Auto processes, Customization, Ease-of-use, Opportunity tracking, ","30-90 days","90-180 days","$900-$1,000","E-mail information","Yes","No"
"Wed Jun  4 18:13:21 EDT 1997","Web Info Request","LW, Inc.","(666) 736-0302","(666) 736-9802","None, ","Windows 3.1, NT","","","","","","","8701 W. Gage Blvd","","Kennewick","WA","99336","USA","rbradshaw@LW.com","http://","Mr.","Martin","Bradshaw","Project Manager","Forbes, ","no","","","25","Interest","None, ","Ease-of-use, ","180+ days","180+ days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 18:34:04 EDT 1997","Web Info Request","Spiro","567/432-7650 ext 209","567/432-8415","None","Windows 95","2460 N. First St.","Ste. 230","San Jose","CA","95131-2323","USA","2460 N. First St.","Ste. 230","San Jose","CA","95131-2323","USA","Mary@Spiro.com","http://","Ms.","Mary","De Los Reyes","Mgr., Product Licensing","Sales Technics","yes","20","12","","Vendor evaluation","ACT!, GoldMine, Maximizer","Auto agents, Auto processes, Conflict resolution, Customization, Ease-of-use, Opportunity tracking, SynchMartinization, Integration w/central relational database for ad hoc reporting","30 days","30 days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 19:03:30 EDT 1997","Web Info Request","Dynamic Public Relations","543/830-2639","543/830-1639","None, ","NT","201 E. Carpenter Freeway","","Irving","TX","75062","USA","201 E. Carpenter Freeway","","Irving","TX","75062","USA","pstreibe@temmc.com","http://www.temmc.com","Mr.","Ted","Streiber","Director","Consultant","yes","23","5","","Interest","ACT!, ","Auto processes, Customization, Ease-of-use, Opportunity tracking, ","30 days","30-90 days","","E-mail information","Yes","No"
"Wed Jun  4 19:51:58 EDT 1997","Web Info Request","Trendacosta, Richard","765248-7937","765248-6437","GoldMine, ","Windows 95","","","","","","","1814 Glencoe Way","","Glendale","CA","91208","","altrenda@pacbell.net","http://","Mr.","Richard","Trendacosta","Sales Manager","Sales & Marketing Management, ","yes","8","","","Vendor evaluation","ACT!, GoldMine, TeleMagic, ","Auto processes, Customization, Ease-of-use, Opportunity tracking, ","30-90 days","30-90 days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 19:52:49 EDT 1997","Web Info Request","Arthur ElectMartinics","765248-7937","765248-6437","GoldMine, ","Windows 95","","","","","","","1814 Glencoe Way","","Glendale","CA","91208","","altrenda@pacbell.net","http://","Mr.","Richard","Trendacosta","Sales Manager","Sales & Marketing Management, ","yes","8","","","Vendor evaluation","ACT!, GoldMine, TeleMagic, ","Auto processes, Customization, Ease-of-use, Opportunity tracking, ","30-90 days","30-90 days","$600-$800","E-mail information","Yes","No"
"Wed Jun  4 21:42:59 EDT 1997","Web Info Request","BigTime Industries","(323) 639-7411","(323) 639-7411","ACT!, ","Windows 95","","","","","","","365 W. Victoria St.","","Compton","CA","90220","","KSiverts@aol.com","http://","Mr.","Fred ","Siverts","VP Sales","America West, American Way, Sales & Field Force Automation, Sales & Marketing Management, ","no","","","1","","ACT!, ","Auto agents, Ease-of-use, Opportunity tracking, ","30 days","30 days","$600-$800","Mail literature","Yes","No"
"Wed Jun  4 22:37:45 EDT 1997","Web Info Request","starusta, George","8797495535","8797427895","ACT!, ","Windows 95","","","","","","","235 ketch dr","","sunrise","fl","33326","usa","Georgell@ix.netcom.com","http://","Mr.","George","starusta","manager","Searching the Web, ","no","","","1","Interest","GoldMine, Tracker, ","Customization, Ease-of-use, ","90-180 days","90-180 days","$600-$800","Mail literature","Yes","No"
"Wed Jun  4 22:40:38 EDT 1997","Web Info Request","TheSource","5672606054","5672418270","ACT!, GoldMine, ","Windows 95","","","","","","","550 S Winchester","suite 301","San Jose","CA","94138","","Andyk@TheSource.com","http://TheSource.com","Mr.","Andy","Klebe","VP Sales","Delta Sky, ","yes","23","23","","Planning","ACT!, GoldMine, ","Ease-of-use, ","90-180 days","180+ days","$600-$800","Mail literature","Yes","No"



When you have sucessfully completed this project and understand it completely, 
we'll continue on with bigger and better(?) projects.


StdAlone.mst
------------

What have we really accomplished?

We now have a rudimentary program design that we can turn into a TEMPLATE.
Merely strip out the specific functions such as GetFiles(), WBImport() and 
CountCommas(). Then do a search and replace on WBImport and change it to a new 
name.

I've created a "stand alone" template named StdAlone.mst.  The reason I've 
named it this way is that the program is complete within itself and doesn't
call external libraries.

Now we have template program that we can create new programs containing
many of the standard features that every program needs. 

Examples are our logging and on error routines.  Every program will need this.

Many programs will need to read an INI file and so on.

Hopefully the concept and reasons for using a procedural approach now makes 
more sense.  As we progress you'll see that the only real difference in our 
programs is the number of procedures utilized.



Chapter 4.  Template Project
----------


Here are the file names for our Template project:

Template.vtp
Template.mst
Template.ini


TDeclare.inc
GenUtil.inc
GenAPI.inc


You may open up the Template project if you wish.

The objective of this project is to understand how libraries are created
and create a new Template program to utilize them.


Template.mst
------------

As we continue we will build on our Template, adding additional standard 
features. Instead of keeping all of our standard subroutines within the 
Template we will begin to build a library to put them in instead.

So let's create Template.mst and start building our library files.

Library files have the extension .INC.  Which stands for INCLUDE files.

Include files are an important concept in our modular design. The idea is
to group similiar functions, variables etc. into one file.  We can then 
incorporate this file into any program we wish.

I don't know if there is a maximum number of include files your program can
contain.  Some people like to create many, I use only a few.  Personal 
preference.  It's less confusing for me to keep the number down.  I usually
use 3 or 4.


Include files
-------------

	TDeclare.inc - Header file
	GenUtil.inc  - Will contain variables, path names and subroutines.
	GenAPI.inc   - Will replace WINAPI.  Contain API declarations 
	               and API Wrappers.



Precompiled Header file
------------------------

TDeclare.inc is a HEADER file. 
This is all the actual code it contains:


	'$HEADER START 'TDeclare.HDR'
	'$INCLUDE 'MSTEST.INC'
	'$INCLUDE 'GenUtil.INC'
	'$INCLUDE 'GenAPI.INC'
	'$HEADER STOP


Notice the single quote dollar sign ('$) that begins every line.  This is 
required.

We first nominate the Header name, the Header name may be different than the 
INC file name that contains it. Because it has a different extension you can 
give it essentially the same name which as the INC file that it is within.


Here I named the Header "TDeclare.HDR" which is assigned with the HEADER START
command.


	'$HEADER START 'TDeclare.HDR'
	'$HEADER STOP


We nominate the INC files that we want in our Header between the HEADER START
and HEADER STOP.

OK so what's all the fuss about or what the point of Headers anyway?

By "including a file" we are in effect inserting that code into our program at 
that insertion point.  So instead of typing or cut and paste, we need only 
"include it".
															  
If we include it, everytime we recompile our program all of the include 
files will have to be recompiled as well.  If you have many include files or
just a few large ones this operation will take some time.

A "Precompiled Header" is a collection of designated include files that are 
only compiled if one or more of the include files within it changes.  

If you never change your include files then your header is inserted 
immediately instead of having to be recompiled.

Big Deal, I change my include files everyday, how will that help me?  

Let's say that you have 30 test programs and they all use the same include 
files via the same header file.  The header need only be rebuilt/recompiled 
for the first program, the remaining 29 programs will then compile much 
faster.
 

In our Template.mst the syntax to include our precompiled header file is as
follows:


	'=============================================
	' Metacommand declarations follow
	'=============================================
	'$INCLUDE 'TDECLARE.INC'


Notice that this is the first actual line of code in our program.

For argument sake, let's say that we had an additional INC file named: 
Test.INC. Suppose the include files within our header rarely changed but 
Test.INC changed frequently.

	We could do it this way instead:

	'=============================================
	' Metacommand declarations follow
	'=============================================
	'$INCLUDE 'TDECLARE.INC'
	'$INCLUDE 'TEST.INC'


We can include as many INC files as we wish.  If you don't want to use a
header file at all then just list your INCs like this:
													 
	'=============================================
	' Metacommand declarations follow
	'=============================================
	'$INCLUDE 'MSTEST.INC'
	'$INCLUDE 'GenUtil.INC'
	'$INCLUDE 'GenAPI.INC'



Include files are a very important aspect of our modular design concept.

If you can use precompiled headers do so because it will save you time.



MSTEST.INC
----------

MSTEST.INC is a library file that comes with VT.  It contains necessary
functions that VT relys on.  Assume that it is necessary and always include it.
If you're curious open it up and take a look.


GenUtil.INC
-----------

This is our own library file.  Here we will put all of the standard subroutines
that we used in WBImport.mst.  If we use a subroutine in more than one program
it probably belongs in a library.  This is the place. 

You can create as many library files as you wish.  Some people like to group 
their subroutines by subject into individual INC files.  As I said this is
personal preference.  For now this file will do.


Once again we have a beginning and end to our file as shown below.


	' ====================================================================
	' Metacommand Declarations
	' ====================================================================
	'$IFNDEF _GenUtil_INCLUDED	   
	'$DEFINE _GenUtil_INCLUDED


	'*****************************************************
	' EOF End Of GenUtil.INC
	'*****************************************************
	'$ENDIF _GenUtil_INCLUDED


Look up METACOMMAND in your online help.

What we have here is an IF ENDIF statement that's used in a special way.

It says to the compiler IF what you find here isn't defined elsewhere THEN
define it now.

This prevents duplication.  All of your INC files should contain this syntax.
The syntax is the same in all of the INC files except that you give it a unique
name in each one  i.e. _GenUtil_.  I use the same name as the INC file.


The next thing to do is create variables for our windows search function.


	'============================================================
	' Construct flag variables for windows search
	' Variables on right side of equal sign override default settings
	'============================================================
	Const FNDWINDFLGS = FW_FOCUS Or FW_PART Or FW_HIDDENOK	' Partial Match OK
	Const FNDINSTFLGS = FW_FOCUS Or FW_HIDDENOK		        ' No Partial Match
	Const FNDNOFOCUSFLGS = FW_NOFOCUS Or FW_HIDDENOK        ' No Focus Given
	Const FNDPARTNOFOCUSFLGS = FW_PART Or FW_NOFOCUS Or FW_HIDDENOK ' Partial No Focus


I've created 4 different variables each having slightly different search criteria.


	'============================================================
	' Window Handle Variables
	'============================================================
	Global FindWindowTimeout    As Integer	'Default Window Search time
	Global FindErrorTimeout     As Integer  'Error Window Search time
	Global MainAppWindow        As Long     'Handle for Main Application Window
	Global WarningWindow        As Long		'Window Handle variable
	Global ErrorWindow          As Long		'Window Handle variable
	Global DialogWindow         As Long		'Window Handle variable
	Global TheCurrentWindow     As Long		'Window Handle variable

	'============================================================
	' Initialize Global variables
	'============================================================
	FindWindowTimeout = 10        ' Set Find Window Timout Interval
	FindErrorTimeout  =  5        ' Set Find Error Window Timout Interval


The 3 sections of code above are related. Which I will try to explain with the
following VT functions WFndWnd()/WFndWndC():


    MainAppWindow = WFndWndC("CaptionText","ClassText",FNDWINDFLGS,(FindWindowTimeout))


WFndWndC()s job is to find a designated window and give it focus.  It is a very
important function and is used often in our programs.

We know we're going to use this function all of the time so we may as well
create the variables for its parameters right off the bat.


If WFndWndC() is successful it returns a LONG which is the HANDLE of the window 
it found.  So I've created some descriptive variables to contain that value.


	Global MainAppWindow        As Long     'Handle for Main Application Window
	Global WarningWindow        As Long		'Window Handle variable
	Global ErrorWindow          As Long		'Window Handle variable
	Global DialogWindow         As Long		'Window Handle variable
	Global TheCurrentWindow     As Long		'Window Handle variable


The reason for this approach is that we will eventually have standard 
paragraphs of code using WFndWndC() and this will make that approach much easier.


WFndWndC() searches for the desired window X number of times.

	FindWindowTimeout = 10        ' Set Find Window Timout Interval
	FindErrorTimeout  =  5        ' Set Find Error Window Timout Interval


I've found that for most windows searching 10 times is plenty and shorten it
to 5 for others.


In our example we are searching for a window 10 times.


	MainAppWindow = WFndWndC("CaptionText","ClassText",FNDWINDFLGS,(FindWindowTimeout))


	Which is the same as:

	For i = 1 To 10

		Search for window
		Sleep 1
		If window found then
			EXIT FOR
		END IF	

	Next


It is important to note that 10 is the MAXIMUM number of attempts.  If it 
finds it on the second try it quits looking.  So when we're looking for the 
main window of an application when it first opens don't worry about using a 
bigger number than 10.   You could set the number to 20 or 30, if it finds
it on number 2 then it's done.


Our	FNDWINDFLGS variable is harder to explain.


	Const FNDWINDFLGS = FW_FOCUS Or FW_PART Or FW_HIDDENOK


Look up WFndWnd in online help:

	flags
	
	Specifies the type of search to perform, and whether or not the window 
	gets the focus if found. If you omit this parameter, its value is assumed 
	to be FW_DEFAULT.


By creating our own flag the way we did, we are saying:

		 FW_DEFAULT +  FW_FOCUS	+ FW_PART + FW_HIDDENOK

	Which means look for the window using all of the default values 
	
	AND give it focus
	AND partial match is OK
	AND hidden windows are OK


Read the online help for this and experiment with the flag values.  I've had
good luck with the settings I'm showing you.



SetDeadLockTimeout(500000)    ' Set Deadlock Timeout Interval


	Here is the explanation from online help:

	Specifies the length of time that Visual Test will wait 
	before generating a "Possible deadlock condition" error.
	
	Syntax
	SetDeadlockTimeout ( msec& )
  
	msec&
	A value representing the number of milliseconds for the timeout. 
	The default setting is 300000 (5 minutes).


The reason for setting this value is that in the past I've had processes that
took more than 5 minutes to complete.  VT would get tricked and think that a 
deadlock had occurred, where in reality there just wasn't any keyboard activity.

If VT thinks there is a deadlock it will quit.  It used to be that the actual 
time it would wait was machine dependent, at least that's the way it seemed to
me.  The bottom line is that by setting this value, my deadlock problems went
away.


The rest of GenUtil.INC are the same functions and procedures that are in 
WBImport.mst.

Open up and examine GenUtil.INC.


GenAPI.INC
-----------
This is our replacement for WINAPI.INC.  WINAPI.INC contains about 9000 lines 
of code.  GenAPI.INC is MUCH smaller.  It's a pretty substantial savings.

As we progress this file will grow but I doubt your API library file will ever
near 9000 lines.  I hope that you understand that the libraries you include
make the size of your compiled program larger. 

How the API works is beyond my scope and there are books you can read about it.

My explanations of the API functions that I use will be BRIEF.  Hopefully the
comments within the functions will be adequate.  Maybe someone would like to
volunteer some explantions of the API?

The structure of GenAPI.INC is identical to GenUtil.INC so there should be
nothing left to explain.

Open up and examine GenAPI.INC.


In Review
----------

1. We have 2 projects
1. We have an example of a program.
2. We've had a taste of proceduralized programming.
3. We have 2 Templates.
4. We have the start of a library.


We now have an excellent foundation from which to continue and have covered 
many of the basic concepts of VT programming.

In our next section we will continue on and learn how to manipulate Windows
objects, which is what gives VT its unique flavor.

Before we do that, let's read about pointers.


Chapter 5.  Pointers
----------
The following chapter was written by:

Yuri Serebriany
39 Hollywood Ave
Bondi Junction NSW 2022
Australia
Email yuris@spectra.com.au

Thanks from all of us Yuri, you did a great job!


Pointers
--------

Please skip this chapter if you already familiar with pointers.

Sometimes people call Visual Test "Visual Basic with pointers".  
I think they have a good reason for that.

In order to have more fun (and comprehension) I would recommend you to copy and 
to run all small scripts from this chapter.


POINTER is a data type that can contain the address of another data type such 
as a variable, or of a procedure.

Putting it simple - a pointer is an address of another variable or procedure.  
It "points" to the starting cell in the memory where the value of variable or 
the built code of procedure is stored in run time.

The actual addresses are not known when we are writing our code.

Everything has own address in memory in run time - constants, variables, code 
instructions.

Manipulation of addresses is actually taking place when code is executed.  
There are no variable names or procedures names there, only addresses or 
offsets from the start of a memory segment. 
 
We can see it in machine code: instructions are using addresses of variables 
and each instruction also has an address in memory.  
For middle-sized program, code and data reside in the different segments of 
memory.

The declaration:
'Dim My_Variable As Integer'
tells compiler to reserve 2 bytes (VT Integers have 2 bytes) at some address in 
memory to store My_Variable.

For higher level languages like VT we don't need to know the actual addresses 
but we should know how to use pointers.

A pointer is a variable itself.  We should declare and initialize it.

We declare a pointer as a pointer to a particular data type to let the compiler 
know what kind of information to expect at this address.

To initialize pointer means assign to a pointer variable the address of the 
other variable, procedure or the direct memory address.  
VarPtr() function and ALLOCATE statement are for this purpose in VT.


Pointers to Variables
---------------------

Here is a simple example:

Dim myInt As Integer
Dim pmyInt As Pointer to Integer

pmyInt = VarPtr(myInt)
myInt = 100
Print myInt, pmyInt
End

Run this small script and the output should be something like: 
"100 	29FF030"

Naturally, "100" is the value in the variable myInt and "29FF030" is a 
hexadecimal address in memory where the variable named "myInt" is stored this 
time.

Note it is very important to initialize pointers, preferably straight after the 
declaration. 

Using an uninitialized pointer could have disastrous consequences.  
VT performs a basic pointer validation and displays an error message "Memory 
access violation" in runtime on attempting to use an uninitialized pointer.  
Be careful!

You can assign value to the variable using its address, i.e. pointer.  Here is 
an example.  Note the use of square brackets ([ ]) to 'dereference' the pointer. 
Dereference means access the value at the address stored in the pointer 
variable.

Dim myInt As Integer
Dim pmyInt As Pointer to Integer

pmyInt = VarPtr(myInt)
myInt = 100
Print myInt, pmyInt
pmyInt[0] = 50
Print myInt, pmyInt
End

Run this script and the output should be something like: "50	29FF030"

"pmyInt[0]" means: value of starting element (number 0) of specified type 
(Integer) at the address pmyInt.  In this case we have only one element.  

In the case of an array there could be many elements in memory starting 
at this address.

Pointers to Arrays
------------------

Dim myIntArray(10) as Integer, iCount As Integer
Dim pIntArray As Pointer to Integer

pIntArray = VarPtr(myIntArray(0))

For iCount = 0 To 9
	pIntArray[iCount] = iCount
	Print myIntArray(iCount);
Next iCount
End

Expected output: "0	1	2 ...8	  9"

Note that the index starts from 0 and goes to 9 (i.e. 10 elements in the 
array).

"pIntArray = VarPtr(myIntArray(0))" means that we assign the address of the 
starting element (element 0) of myIntArray to pointer pIntArray.

Note also a difference in syntax: 
for array element we use "( )" and for pointer notation "[ ]".

Pointers to User-Defined Types
------------------------------

Type MyType
	aLong As Long
	aInt As Integer
	aString As String*9
End Type

Dim myVar As MyType
Dim pmyVar As Pointer to MyType

pmyVar = VarPtr(myVar)

pmyVar[0].aLong = 9999
pmyVar->aInt = 99
pmyVar[0].aString = "Test"

Print myVar.aLong, myVar.aInt, myVar.aString
END

Expected output: "9999 	 99 	Test"

Note the two possible syntax's: "pmyVar[0].aLong" and "pmyVar->aLong" are 
equivalent.  

I personally prefer the second one because it's compact and reflect the word 
"pointer" but it's a matter of programming taste.

The variable of pointer type could also be a member of a user-defined type.  
Of course, this pointer should also be initialized.

User defined data type that includes pointer to its data type:
Type MyType
	aLong As Long
	aInt As Integer
	aString As String*9
	pmyType As Pointer to MyType
End Type
allows us to implement, for e.g., Linked List storage method.

Combining the two last examples you can use pointers to arrays of user-defined 
types.

Procedures: Passing and Returning Pointers
------------------------------------------

Basically, it's nothing fancy here.  See example below.

Dim myStrArray(3) As String
Dim pmyStrArray As Pointer to String
Declare Function myNewFunction(StrArray() As String) As Pointer to String
Declare Sub myNewSub(p As Pointer to String) 

pmyStrArray = myNewFunction(myStrArray)
myNewSub(pmyStrArray)

Print myStrArray(0) + myStrArray(1) + myStrArray(2)

Function myNewFunction(StrArray() As String) As Pointer to String
	Dim pArray As Pointer to String
	pArray = VarPtr(StrArray(0))
	myNewFunction = pArray
End Function

Sub myNewSub(p As Pointer to String)
	p[0] = "Hi friends, "
	p[1] = "VT is easy, "
	p[2] = "just get used to it"
End Sub

End

Firstly, we are passing an array to myNewFunction() and returning the pointer 
to that array.
Secondly, we are passing the pointer to the array to a sub and initialising 
array with values.
Thirdly, we print out the array elements.

The interesting thing here is the syntax of passing arrays to a procedure:
"ArrayName() As DataType" in declaration
"ArrayName" without parentheses (!) in the actual call

Pointers to Procedures
----------------------

Generally speaking pointers to procedures are used in order to pass the 
procedure as a parameter to other procedure.  
But there is no limit, you can use pointers to procedures however you want. 

Here is an example:

Declare Sub mySub(sMsg As String)
Dim pmySub As Pointer to Sub(sStr As String)

pmySub = VarPtr(mySub)

pmySub("Hello")		'calling sub via pointer
mySub("VT lovers")	'calling sub itself

Sub mySub(sMsg As String)
	Print sMsg
End Sub

END

We can use a pointer to a procedure in exactly same way as the procedure 
itself.  
Note that you also should declare a pointer to a procedure in a very similar 
manner to the declaration of the procedure itself.

The only possibility for passing a procedure as a parameter to another procedure 
is via pointers.

Declare Sub myNewSub(p As Pointer to Function (sStr As String) As Integer,
sMyStr As String)
Declare Function myFunction(sMsg As String) As Integer
Dim pmyFunction As Pointer to Function (sStr As String) As Integer

pmyFunction = VarPtr(myFunction)

myNewSub(pmyFunction, "Press any button")

Function myFunction(sMsg As String) As Integer
Dim iRet As Integer
	iRet = MsgBox(sMsg, MB_OKCANCEL, "This is a test")
	myFunction = iRet
End Function

'passing a pointer to function as a parameter to the procedure

Sub myNewSub(pFn as Pointer to Function (sStr As String) As Integer, sMyStr As 
String)
	If pFn(sMyStr) = IDOK Then
		Print "OK button pressed"
	Else
		Print "CANCEL button pressed"
	EndIf
End Sub

END

Cool, isn't it.  Note the boring long declaration of a pointer to a function 
we use every time.  You can do nothing about it, these are the rules of the game.

Memory Allocation
-----------------

Another important usage of pointers is for memory allocation.  There is the 
ALLOCATE statement for this in VT.  It allocates a buffer somewhere in memory 
and assigns its starting address to a pointer.  
Example below shows how to allocate (or reserve), deallocate (free) and 
reallocate (reuse) memory.

Const ItemNumber = 10
Dim pmyLong As Pointer to Long
Dim pmyShort As Pointer to Short
Dim iCount As Integer

Allocate pmyLong, ItemNumber

For iCount = 0 To ItemNumber - 1
	pmyLong[iCount] = iCount
	Print pmyLong[iCount];
Next iCount
Print	
Reallocate pmyLong, 2*ItemNumber

For iCount = ItemNumber To 2*ItemNumber-1
	pmyLong[iCount] = 10*iCount
Next iCount

For iCount = 0 To 2*ItemNumber - 1
	Print pmyLong[iCount];
Next iCount	
Print
pmyShort = Cptr(pmyLong)	'pointer casting
Reallocate pmyShort, ItemNumber
For iCount = 0 To ItemNumber - 1
	Print pmyShort[iCount];
Next iCount	

Deallocate pmyLong
END

Firstly, we allocate memory, you could think about it as getting a dynamic 
array of type long with ItemNumber elements, each 4 bytes long.  
Secondly, we add some more memory, old values stored in memory are preserved.  
Thirdly, we 'cast' pointer from type of long to type of short.  Now each 2 bytes 
of this memory buffer are treated as short values.  
And finally we free (deallocate) memory.  Pointers pmyLong and pmyShort "point" 
to the same memory cell; i.e. they contain the same memory address.

In case of allocating memory for user defined types each allocated element will 
be padded to a word boundary.

Type MyType
	aLong As Long		'4 bytes
	aInt As Integer		'2 bytes
	aString As String*9	'9 bytes
End Type
Dim pMytype As Pointer to MyType
Allocate pMyType, 10

16 bytes will be allocated for each of the 10 elements.

Warning. Don't forget to deallocate memory.  Always start pointer index with 0.

Pointers to Pointers
--------------------

As you can recall a pointer is a variable itself.  All variables have their 
memory addresses.  That's why we can use pointers to this type of variables. 
 
Pointer containing an address of another pointer is a pointer to a pointer.

Use this thing carefully. Just to show how powerful pointers are here are some 
examples:

Dim myString as String
Dim pmyString As Pointer to String
Dim ppmyString As Pointer to Pointer to String

pmyString = VarPtr(myString)
ppmyString = VarPtr(pmyString)

ppmyString[0][0] = "This is a test"
Print myString, pmyString[0], ppmyString[0][0]
Print ppmyString[0], pmyString
End

Have a look on our printout.  Think about it for a wile, it's important for 
understanding of 'dereferencing'.  

"ppmyString[0]" is equivalent to "pmyString" and "ppmyString[0][0]" to 
"myString".

Don't forget to initialize all pointers!

Another example:

Const ItemsToAllocate = 12

Dim i As Integer
Dim pmyInteger As Pointer to Integer
Dim ppmyInteger As Pointer to Pointer to Integer

ppmyInteger = VarPtr(pmyInteger)

Allocate pmyInteger, ItemsToAllocate

For i=0 To ItemsToAllocate-1
	ppmyInteger[0][i] = i+1
	Print ppmyInteger[0][i];
Next i

Deallocate 	pmyInteger

End

ALLOCATE statement initialize pmyInteger pointer.

*************************************************

That's all about it!

For more detailed information and unsimplified definitions I refer you to any 
book about C/C++.
I think that a good source is for e.g.
"Microsoft C. Programming for the PC"
Robert Lafore
ISBN  0-672-22738-X


Additional examples of pointers - Pointers.mst.
------------------------------------------------

The following program is located in the WBImport Project
and contains additional examples of pointers:

 Pointers.mst
 Arthur Jaarsma
 Digital Equipment
 Littleton, Ma.
 JAARSMA@sqeez.lkg.dec.com


Thanks Arthur we appreciate your help!


==========
Section 2:  Windows Programming
==========

Example programs that follow are located in the WnObjPrg Project.



Chapter 6.  Starting and Stopping an application
----------

The Explorer
------------

I will use the Explorer.exe as an example because I assume everyone has it and
it contains many common objects to demonstrate.

The first step is to decide what objects we wish to manipulate/test.  This 
would be a design spec or test case.  My design spec for this project will not
be elaborate.

	Open Explorer.exe
	Open the View Menu
		Select all items on the View Menu
		Select all items of the Options Menu of the View Menu
		Options Menu
			Test all the objects found
	Close Explorer.exe


Some of the types of objects found within Explorer.exe:

	Windows
	Menus
	SysTreeView32
	SysListView32
	SysTabControl32
	Button
	ListBox

This should give us plenty of windows and objects to play with.  So armed with
our detailed design spec:

We will start with the Template.mst program that we created earlier and from it
create a new program TstExp01.mst.  Inside of it we will create subroutines to
test portions of the Explorer, testing each subroutine thoroughly as we 
progress.

When we have created all of our subroutines we will then create SCENARIOS to 
call them.

Our first 2 subroutines will be StartExplorer() and CloseExplorer().

Our program will need to know the path and name to Explorer.exe in order to run
it.  So I updated WBImport.INI with the following variable.

	; Explorer Path and name
	ExplorerName = C:\WINDOWS\EXPLORER.EXE

If the Explorer.exe is located in a different directory then you must correct
your copy of the INI file.

In addition the following code must be added to the SetDefaults function in the
GenUtil.inc file.

	ExplorerName = ReadIni(TheIniFile,SectionName,"ExplorerName")
	' Test value returned
	If ExplorerName = "NotFnd" Then
		SetDefaults = False
		LogWrite("1     Can't find ExplorerName in: " + TheIniFile+"!",LogPath,CritErrPath)
		Exit Function
	End If

Finally we nominate	ExplorerName as a GLOBAL variable in our program.

'============================================================
' Program Variables
'============================================================
Global ExplorerName As String



Remember if you run these programs from within Developers Studio you must make 
sure the the project command line is correct.



Starting and Closing Explorer
------------------------------


Now we're ready to create the StartExplorer and CloseExplorer functions.


'*************************************************************************
' Function StartExplorer
' Attempts to Start Explorer.Exe
'*************************************************************************
Function StartExplorer() As Integer

 	Dim ReturnInteger As Integer

	StartExplorer = True

	'Check Application path
    If Exists(ExplorerName) Then
		' Use API to start the program
		ReturnInteger = StartTheProgram(ExplorerName, NULL)
		If ReturnInteger = False Then
			StartExplorer = False
			LogWrite("1     Can't Start "+ExplorerName+"!",LogPath,CritErrPath)
			Exit Function
		End If
    Else
		StartExplorer = False
        ' Write error message
        LogWrite("1     Can't Find "+ExplorerName+"!",LogPath,CritErrPath)
		Exit Function
	End If

	' Close	StatusBox if open
	StatusBox Close

    ' Check for existence of Application and remember main window handle
    MainAppWindow = WFndWndC("Exploring","ExploreWClass",FNDWINDFLGS Or FW_RESTORE,(FindWindowTimeout))
	If MainAppWindow = 0 Then
		StartExplorer = False
       ' Write error message
		LogWrite("1     Can't find "+ExplorerName+" Main Window!",LogPath,CritErrPath)
		Exit Function
	Else
		LogWrite("0     "+ExplorerName+" Started successfully.",LogPath,CritErrPath)

		' Use API to check validity of Window Handle - that it exists
		LogWrite("0     Checking for existence of Main Explorer Window.",LogPath,CritErrPath)
		If IsWindow(MainAppWindow) <> 0 Then
			' Make sure window is enabled
			If IsWindowEnabled(MainAppWindow) <> 0 Then
				LogWrite("0     "+ExplorerName+" Main Window exists.",LogPath,CritErrPath)
				' Set Main Explorer window as active
				WSetActWnd(MainAppWindow)
				' Make sure window is visible and restore size
				If WIsVisible(MainAppWindow) = 0 Then
				   WMaxWnd(MainAppWindow)
				   WResWnd(MainAppWindow)
				End If
			Else
				LogWrite("0     "+ExplorerName+" Window handle: "+Trim(Str$(MainAppWindow))+" exists but is not enabled.",LogPath,CritErrPath)
			End If
		Else
			LogWrite("0     "+ExplorerName+" Window handle: "+Trim(Str$(MainAppWindow))+" is closed.",LogPath,CritErrPath)
		End If

		' Test for system menu
		If Not WSysMenuExists(MainAppWindow) Then
			StartExplorer = False
			LogWrite("1     Can't find "+ExplorerName+" System Menu!",LogPath,CritErrPath)
			Exit Function
		Else
			LogWrite("0     Found "+ExplorerName+" System Menu.",LogPath,CritErrPath)
		End If
    End If

End Function
'*************************************************************************
' Function CloseExplorer
' Attempts to Close Explorer.Exe
'*************************************************************************
Function CloseExplorer() As Integer

	Dim ReturnInteger As Integer
	Dim DialogInteger  As Integer

	CloseExplorer = True

	' Test for Main Application Window Handle
	If MainAppWindow = 0 Then
        MainAppWindow = WFndWndC("Exploring","ExploreWClass",FNDWINDFLGS Or FW_RESTORE,(FindWindowTimeout))
	End If

	' Test again for Main Application Window Handle
	If MainAppWindow = 0 Then
		' Write error message
		LogWrite("1     Can't find "+ExplorerName+" Main Window to Exit!",LogPath,CritErrPath)
		' Try to Terminate Application
		ReturnInteger = StopTheProgram
		If ReturnInteger <> False Then
			LogWrite("0     Terminated Application successfully!",LogPath,CritErrPath)
			Exit Function
		Else
			CloseExplorer = False
			LogWrite("1     Can't Terminate Application!",LogPath,CritErrPath)
			Exit Function
		End if
	Else
		' Use API to check validity of Window Handle - that it exists
		LogWrite("0     Checking for existence of Main "+ExplorerName+" Window.",LogPath,CritErrPath)
		If IsWindow(MainAppWindow) <> 0 Then
			' Make sure window is enabled
			If IsWindowEnabled(MainAppWindow) <> 0 Then
				LogWrite("0     Main "+ExplorerName+" Window exists.",LogPath,CritErrPath)
				' Set Main Explorer window as active
				WSetActWnd(MainAppWindow)
				' Make sure window is visible and restore size
				If WIsVisible(MainAppWindow) = 0 Then
				   WMaxWnd(MainAppWindow)
				   WResWnd(MainAppWindow)
				End If
			Else
				LogWrite("0     "+ExplorerName+" Window handle: "+Trim(Str$(MainAppWindow))+" exists but is not enabled.",LogPath,CritErrPath)
			End If
		Else
			LogWrite("0     "+ExplorerName+" Window handle: "+Trim(Str$(MainAppWindow))+" is closed.",LogPath,CritErrPath)
		End If
	End If

	' Select File\Close from menu to exit
	ReturnInteger = SelectMenuItem(MainAppWindow,"&File\&Close","File\Close")

	' Test Return Value
	If ReturnInteger = False Then
		' Try to Terminate Application
		ReturnInteger = StopTheProgram
		If ReturnInteger <> False Then
			LogWrite("0     Terminated Application successfully!",LogPath,CritErrPath)
			Exit Function
		Else
			CloseExplorer = False
			LogWrite("1     Can't Terminate "+ExplorerName+" Application!",LogPath,CritErrPath)
			Exit Function
		End If
	End If

	' Reset	MainAppWindow to zero
	MainAppWindow = 0

End Function
'*************************************************************************


Let's take a look at some of the code from the above functions.


	'Check Application path
    If Exists(ExplorerName) Then
		' Use API to start the program
		ReturnInteger = StartTheProgram(ExplorerName, NULL)
		If ReturnInteger = False Then
			StartExplorer = False
			LogWrite("1     Can't Start "+ExplorerName+"!",LogPath,CritErrPath)
			Exit Function
		End If
    Else
		StartExplorer = False
        ' Write error message
        LogWrite("1     Can't Find "+ExplorerName+"!",LogPath,CritErrPath)
		Exit Function
	End If

The first paragraph uses the EXISTS function to make sure that the path and 
name to the Explorer.exe is correct.  If this function errors make sure your
INI file variables are correct.


	' Check for existence of Application and remember main window handle
	MainAppWindow = WFndWndC("Exploring","ExploreWClass", FNDWINDFLGS Or 
	FW_RESTORE,(FindWindowTimeout))


We then retrieve the HANDLE for the Explorer window.  Every window has a 
unique number identifying it.  This number is assigned when the window is 
opened and is that windows identifier as long as that window "lives".

Once you have identified a windows handle you can refer to it as many times
as you like within the program as long as that window "lives".

The handle for our Explorer program is obviously important so we created a
GLOBAL variable	MainAppWindow to remember it.

By retrieving this handle with the WFndWndC function we have given that window
FOCUS.


	If IsWindow(MainAppWindow) <> 0 Then
		' Make sure window is enabled
		If IsWindowEnabled(MainAppWindow) <> 0 Then
			LogWrite("0     "+ExplorerName+" Main Window exists.",LogPath,CritErrPath)
			' Set Main Explorer window as active
			WSetActWnd(MainAppWindow)
			' Make sure window is visible and restore size
			If WIsVisible(MainAppWindow) = 0 Then
			   WMaxWnd(MainAppWindow)
			   WResWnd(MainAppWindow)
			End If


The above code checks to make sure that our Explorer window is valid and 
visible on the screen.  For instance the Explorer could have already been 
started before we launched our test program but not visible because it was
iconized on the STATUSBAR.

		' Use API to start the program
		ReturnInteger = StartTheProgram(ExplorerName, NULL)

		' Try to Terminate Application
		ReturnInteger = StopTheProgram


Instead of using the RUN command to start the Explorer I've used an API call.

The reason for this is that if something goes wrong and I can't shut down
Explorer with its menu  i.e.  File\Close, I can KILL the program if I know 
the process handle for the application.


*** Please read the following KNOWLEDGE BASE ARTICLE in your online help:
	---------------------------------------------------------------------

	"How to Wait a Finite Time for a Run Process to Terminate"


Every program that you launch has an internal ID number.  If you know that
number you can TERMINATE it.  Now before you get crazy Arnold, this is a last
resort.  Always try to close the application via its menu system first.


The following code is located in the GenAPI.inc:


	' The PROCESS_INFORMATION structure is used by the CreateProcess Function to
	' return information about the newly created process. The two handles that
	' are returned in this structure are open handles to the new process and its
	' primary thread. 
	Type PROCESS_INFORMATION
		hProcess        As Long
		hThread         As Long
		dwProcessId     As Long
		dwThreadId      As Long
	End Type

	Global pi           As PROCESS_INFORMATION


What this means is that: 

	pi.hProcess

is the Process handle of the application and internally you can refer to this 
handle.

The TRICK here is our StartTheProgram() function returns Explorer's process 
handle within the PROCESS_INFORMATION structure. whereas VT's RUN command 
doesn't.

By knowing this value we can Terminate the program if necessary with our	
StopTheProgram function.

		' Try to Terminate Application
		ReturnInteger = StopTheProgram


(Remember, I told you to buy Daniel Appleman's book on the API.)


You will find all of the API code in the GenAPI.inc library.



Guide for spawning DOS commands from a VT script
------------------------------------------------

The following is another contribution from Arthur, thanks again!


Here is another example of using the CREATE PROCESS API function to start an
application only this time it's DOS.

	Subj:	 Guide for spawning DOS commands from a VT script
	Date:	97-08-29 13:08:52 EDT
	From:	JAARSMA@sqeez.lkg.dec.com
	To:	mt_info@eskimo.com


The following guide describes the problems associated with spawning DOS
commands from within a VT script.  A sample VT routine that solves these
problems is appended to the end of the guide. Because spawning to DOS is
probably a common practice among test script developers I am sharing my
experiences with this group.

Hope this guide is useful ...Arthur


Recently I had a need to spawn a DOS command from a VT script. What
seemed like a simple operation turned out to be not so simple. Listed
below are the problems that I had to overcome.

1) The DOS command line is different between Win95 and NT clients. Win95
clients require that command.com be specified in the command line. NT
clients can not have command.com specified in the command line.

2) There is no direct way of verifying the DOS command successfully
completed.

3) The Shell command works most of the time but has the following side
affects, causing your VT script to fail.

- If the DOS command hangs, common occurrence when doing stuff over the
network, the test scrip hangs.

- The shell command creates a temporary DOS box window. This window
looks ugly when the script is running and may cause problems with window
focus of the application under test, causing the script to fail.

4) Using Run command with the SW_HIDE option solves the temporary DOS
box window problem but has the following side affects.

- Unlike the shell command the run command returns immediate control
back to the test script. Because of this there is no direct way of
controlling synchronization between the DOS command and test script.
Worse still if the DOS command hangs the client needs to be rebooted to
recover.

The following routine was my solution to the previous problems. This
routine uses the window CreateProcess and TerminateProcess API's to
spawn the DOS command. This routine assumes the global variable
OSPlatform has been previously defined/set to contain the current
operation system.   

'****************************************************************************
'****************************** SQESpawnCmd *********************************
'****************************************************************************
' 
' This routine spawns a DOS command and outputs the results to c:\temp.txt

' This file is then checked for the expected results. Note: The check is not
' case sensitive and only the first 2048 charaters are checked.
'
' TimeOut = Maximum number of seconds to wait for comand completion.
'
' Note:
'           This routine checks every second for command completion up to the
'           TimeOut value.
'
' Returns - True for success, False for failure
' 
'*****************************************************************************
Function SQESpawnCmd(Command$, ExpectedSubstring$, TimeOut&) as Long

Dim si As STARTUPINFO
Dim pi As PROCESS_INFORMATION
Dim Handle as Long
Dim ReadBuf as String
Dim ret as Long
Dim x as Long

  si.dwFlags=STARTF_USESHOWWINDOW 'setup for create process to hide the	DOS window
  si.wShowWindow = SW_HIDE

  '
  ' spawn the DOS command
  '
  If Exists("C:\Temp.txt") = -1 Then Kill "C:\Temp.txt"  'start with clean output file
  
  If (OSPlatform = Win95_Platform) Then	 ' 95 does it diffrently than NT
    ret = CreateProcess( Null, Environ$("windir") + "\command.com /c " + Command + " > C:\Temp.txt", NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, 0, NULL, si, pi )
    If ret = False Then 
      ret = GetLastError
	  Log_Error("Spawning DOS command " + Command + " > C:\temp.txt")
	  Log_Status("Returned " + SQEErrorLookup(ret))
	  SQESpawnCmd = False
	  Exit Function
    End If
  Else
    ret = CreateProcess( Null,Command + " > C:\Temp.txt", NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, 0, NULL, si, pi )
    If ret = False Then 
      ret = GetLastError
	  Log_Error("Spawning DOS command " + Command + " > C:\temp.txt")
	  Log_Status("Returned " + SQEErrorLookup(ret))
	  SQESpawnCmd = False
	  Exit Function
    End If
  End If
  
  '
  ' wait for command completion by checking the spawn output file
  '
  Handle = INVALID_HANDLE_VALUE 
  x = 1
  Do While ((x < TimeOut) and (Handle = INVALID_HANDLE_VALUE)) 
	 'open results file
    Handle = CreateFile("C:\temp.txt",GENERIC_READ,0,NULL,Open_Existing,0,NULL)
    x = x + 1
    sleep 1
  Loop

  If Handle = INVALID_HANDLE_VALUE Then
	Log_Error("Timeout condition spawning the DOS command - " + Command + "	> C:\temp.txt") 
	Log_Status("Output verification file C:\Temp.txt still in use after" + Str$(TimeOut) + " seconds")
	ret = TerminateProcess(pi.hprocess,0)  'kill hung process to unlock any files it uses
	sleep 60                               'let things get cleaned up and files unlocked
	SQESpawnCmd = False
	Exit Function
  End If

  ReadBuf = string$(2048,32) 'set buffer length for ReadFile operation
           

  ret = ReadFile(Handle,ReadBuf,len(ReadBuf),VarPtr(x),NULL) 'read results file
  If ret = False Then
	ret = GetLastError
 	Log_Error("Spawning DOS command -  Unable to read results file C:\Temp.txt")
	Log_Status("Returned " + SQEErrorLookup(ret))
    ret = CloseHandle(Handle)
	SQESpawnCmd = False
	Exit Function
  End If

  ret = CloseHandle(Handle) 'close the results file
  If ret = False Then
    ret = GetLastError
 	Log_Error("Spawning DOS command - Unable to close results file C:\Temp.txt")
	Log_Status("Returned " + SQEErrorLookup(ret))
	SQESpawnCmd = False
    Exit Function
  End If

  '
  ' verify the results
  '
  If Instr(1, Ucase(ReadBuf), UCase(ExpectedSubstring)) = 0 Then
	Log_Error("Spawning DOS command: " + Command + " > C:\Temp.txt")
	Log_Status("Expected substring: " + ExpectedSubstring + CRLF)
    Log_Status("Returned:" + CRLF)
	Log_Status(ReadBuf)
	SQESpawnCmd = False
    Exit Function
  End If

  SQESpawnCmd = True

End Function



Chapter 7. 	Manipulating Objects
-----------

Our next project (WnObjPrg) will involve manipulating Windows Objects.

=====================
FOCUS - FOCUS - FOCUS
=====================

There are always questions on the board asking why a certain section of code
doesn't work all of the time.  Sometimes it does, sometimes it doesn't.  Many
times it's because the program doesn't put focus on the intended object.
In order to manipulate an object you must have it's FOCUS.

If you TAB on an object the result of the tab keystroke will put you somewhere
else on the screen.  Where that somewhere else is, now IT has the focus.  So 
the focus can change with every keystroke.

Everytime you touch a button on the screen a program tells the application to
do something.  The result can place focus elsewhere. 

To be safe, assume that every keystoke will cause you to loose focus.

	1. You must ALWAYS put focus on the window containing the object you wish 
	to manipulate.

		The WFndWnd() or WFndWndC() VT function is the typical way of putting 
		focus on a window.

	2. You must always put focus on the object you wish to manipulate.

		W*Find, W*Exist and W*Enabled functions are good ways to put 
		focus on an object.

		i.e.

		WButtonFind ( control$ [ , timeout& ] )
		WCheckFind ( control$ [ , timeout& ] )
		WEditFind ( control$ [ , timeout& ] )

		WButtonExists ( control$ [ , timeout& ] )
		WCheckExists ( control$ [ , timeout& ] )
		WEditExists ( control$ [ , timeout& ] )

		WButtonEnabled ( control$ [ , timeout& ] )
		WCheckEnabled ( control$ [ , timeout& ] )
		WEditEnabled ( control$ [ , timeout& ] )

		Etc.


----------------------
' Manipulating Objects
----------------------

Let the fun begin!

Or first object manipulation function is called by the CloseExplorer() 
function to select "\File\Close" from the Explorer menu.


	' Select File\Close from menu to exit
	ReturnInteger = SelectMenuItem(MainAppWindow,"&File\&Close","File\Close")


'*************************************************************************
' Function SelectMenuItem()
'
' Parameters:
'
' WindowHandle& -   The Handle of the window containing the menu
' MenuItem$		-   The Menu item to select i.e. &File\E&xit 
' MenuItemName$	-   Descriptive name of menu item i.e. File\Exit      
'
'
'*************************************************************************
Function SelectMenuItem(WindowHandle&,MenuItem$,MenuItemName$) As Integer

	SelectMenuItem = True

	' Search for Sytem Menu in nominated window
	If WSysMenuExists(WindowHandle) Then
		Sleep .25
		LogWrite("0     Found System Menu.",LogPath,CritErrPath)
		WSysMenu(WindowHandle)
		Sleep .25
		LogWrite("0     Selected System Menu.",LogPath,CritErrPath)
		' Select Insert Contact/Account
		If WMenuExists(MenuItem,(FindWindowTimeout)) Then
			Sleep .25
			If WMenuEnabled(MenuItem,(FindWindowTimeout)) Then
				Sleep .25
				WMenuSelect(MenuItem)
				Sleep .25
				LogWrite("0     Selected Menu item: "+MenuItemName+".",LogPath,CritErrPath)
			Else
				SelectMenuItem = False
				' Write error message
				LogWrite("1     "+MenuItemName+" Bar isn't enabled!",LogPath,CritErrPath)
				Exit Function
			End If
		Else
			SelectMenuItem = False
			' Write error message
			LogWrite("1     Can't find "+MenuItemName+" Bar on System Menu!",LogPath,CritErrPath)
			Exit Function
		End If
	Else
		SelectMenuItem = False
		' Write error message
		LogWrite("1     Can't find System Menu to "+MenuItemName+"!",LogPath,CritErrPath)
		Exit Function
	End If

End Function
'*************************************************************************



Examining our "Wrapper" functions
---------------------------------

First of all, you will see some SLEEP statments in the wrapper functions.
This is to slow things down so the actions will be visible on the screen.


Our	SelectMenuItem() function calls 5 VT functions:

	WSysMenuExists - Does the Explorer menu exist?
	WSysMenu	   - Select the Explorer menu
	WMenuExists	   - Does File\Close exist on the menu?
	WMenuEnabled   - Is File\Close enabled?
	WMenuSelect    - Select File\Close


Now this may seem crazy to write all that code to select one item from the 
menu.  

But...

		1. We have all the error checking code within the function.
		2. It's generic, transportable can be used on any menu.
		3. We only have to write the code once.



Let's compare SelectMenuItem() to another function ClickButton() which will
be used later.


'*************************************************************************
' Function ClickButton
'
' Parameters:
'
' ClassType$  	From WInfo i.e.	  Button
' OrdValue%		From WInfo i.e.	  1
' ButtonName$	Button Caption i.e. OK
'
' Syntax Example:
' Click OK
' ReturnInteger=ClickButton("Button",1,"OK")
'
' Returns True/False Integer
'
'*************************************************************************
Function ClickButton(ClassType$,BtnOrdValue%,ButtonName$) As Integer

	Dim ObjectHandle As Long

	ClickButton = True

	' Set the Button Class
	Sleep 1
	WButtonSetClass(ClassType)

	' Click Button
	ObjectHandle = WButtonFind(_ord((BtnOrdValue)),(FindWindowTimeout))
	If ObjectHandle <> 0 Then
		If WButtonExists(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
			If WButtonEnabled(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
				WButtonClick(_hWnd(ObjectHandle),(FindWindowTimeout))
				LogWrite("0     Clicked "+ButtonName+".",LogPath,CritErrPath)
			Else
				ClickButton = False
				' Write error message
				LogWrite("1     "+ButtonName+" Button isn't enabled!",LogPath,CritErrPath)
				Exit Function
			End If
		Else
			ClickButton = False
			' Write error message
			LogWrite("1     "+ButtonName+" Button doesn't exist!",LogPath,CritErrPath)
			Exit Function
		End If
	Else
		ClickButton = False
		' Write error message
		LogWrite("1     Can't Find "+ButtonName+" Button!",LogPath,CritErrPath)
		Exit Function
	End If

End Function
'*************************************************************************


Here are the VT functions called by ClickButton()

	WButtonSetClass  - Set the button class
	WButtonFind      - Get the handle for the button
	WButtonExists    - Make sure button exists
	WButtonEnabled   - Make sure button is enabled
	WButtonClick     - Click the button


Here's another wrapper:


'*************************************************************************
' Function EnterEditBox
'
' Parameters:
'
' ClassType$    From WInfo i.e.	  Edit
' OrdValue%		From WInfo i.e.	  1
' FieldName$	Descriptive Field Name
' TextToEnter$  Text to enter in Edit Box
'
' Returns True/False Integer
'
' Syntax Example:
' Enter Address1
' ReturnInteger=EnterEditBox("Edit",1,"Address1","123 Main Street")
'
'*************************************************************************
Function EnterEditBox(ClassType$,EditOrdValue%,FieldName$,TextToEnter$) As Integer

	Dim ObjectHandle As Long

	EnterEditBox = True

	' Set the Edit Class
	Sleep .5
	WEditSetClass(ClassType)

	' Enter New Type
	ObjectHandle = WEditFind(_ord((EditOrdValue)),(FindWindowTimeout))
	If ObjectHandle <> 0 Then
		If WEditExists(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
			If WEditEnabled(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
				WEditSetText(_hWnd(ObjectHandle),TextToEnter,(FindWindowTimeout))
				LogWrite("0     Entered New "+FieldName+": "+ TextToEnter+".",LogPath,CritErrPath)
			Else
				EnterEditBox = False
				' Write error message
				LogWrite("1     "+FieldName+" Edit isn't enabled!",LogPath,CritErrPath)
				Exit Function
			End If
		Else
			EnterEditBox = False
			' Write error message
			LogWrite("1     "+FieldName+" Edit doesn't exist!",LogPath,CritErrPath)
			Exit Function
		End If
	Else
		EnterEditBox = False
		' Write error message
		LogWrite("1     Can't Find "+FieldName+" Edit!",LogPath,CritErrPath)
		Exit Function
	End If

End Function
'*************************************************************************



To ReCap:
---------


Here are the VT functions called by SelectMenuItem()

	WSysMenuExists - Does the Explorer menu exist?
	WSysMenu	   - Select the Explorer menu
	WMenuExists	   - Does File\Close exist on the menu?
	WMenuEnabled   - Is File\Close enabled?
	WMenuSelect    - Select File\Close

Here are the VT functions called by ClickButton()

	WButtonSetClass  - Set the button class
	WButtonFind      - Get the handle for the button
	WButtonExists    - Make sure button exists
	WButtonEnabled   - Make sure button is enabled
	WButtonClick     - Click the button

Here are the VT functions called by EnterEditBox()

	WEditSetClass  - Set the Edit class
	WEditFind      - Get the handle for the Edit
	WEditExists    - Make sure Edit exists
	WEditEnabled   - Make sure Edit is enabled
	WEditSetText   - Enter text into Edit


If you examine all of the W* functions that manipulate objects you should 
discover a pattern.

ClickButton() is in effect identical to EnterEditBox(), SelectMenuItem() is 
different but still has 2 of the same functions	W*Exists and W*Enabled.

This should occur to you that this is not a coincidence.  There is a definite
order to the VT W* commands.

This was a huge revelation to me.

	1.  This MUST be the way to do this, or the functions wouldn't have such
	an orderly pattern.

	2.  It makes creating wrapper functions extremely easy.

Believe it or not, I worked with VT for over 2 years before I realized this.

So, there is no reason not to do it this way.  It makes so much sense.  Even 
though it takes a little more effort up front, your development time afterwards
is decreased immensely.

Once you have created a few routines to manipulate objects it becomes a
matter of cut and paste.  You can use the ClickButton() routine as a
template to create the EnterEditBox() routine.

In a day or two you can create a library of routines to manipulate most
Windows objects.

Or everytime you have to test a new object you create a wrapper from one that
has the same characteristics and works about the same.


Let's take a closer look at how these functions work.

	WButtonSetClass()  - Set the button class


A typical button class is "Button".  However your developers may have created
their own classes and you may have a button class with some other name 
i.e. "TButton".  

A good rule of thumb:  If it looks like a duck and quacks like a duck, it's
                       probably a duck.

What I mean is there are no TBUTTON functions.  But there ARE Button functions.

So if we have an object with a class name of TButton and it looks like a Button
then we probably want to use WButton* commands.

You can determine an objects CLASS name with the VT WInfo Tool.

In the Explorer app you can find Check Boxes and Radio Buttons that have the 
class name "Button". 

To "check" a Check Box you would want to use WCheck* functions instead of 
WButton* functions.

So to use WCheck* functions with an object with a class of "Button", you must 
first set the Check Box class to "Button".

	WCheckSetClass("Button")

Having done so you can use the rest of the WCheck* functions.

-Or-

If we have a Button with the Class name TButton then we change the Button Class 
to TButton.

	WButtonSetClass("TButton")


Back to WButton* functions.

Having set the class, the next step is to retrieve the Button's Window HANDLE.


	ObjectHandle = WButtonFind(_ord((BtnOrdValue)),(FindWindowTimeout))


To do this we pass to WButtonFind the ORDINAL value of the button which we
determined with the WInfo tool.


						Ordinal or ID?
						--------------

Somehow we've got to identify the object that we wish to manipulate.  There
may be 10 buttons on the screen, which one do we want to click?	 

From online help:

WButtonFind ( control$ [ , timeout& ] )
  
control$
String that identifies the button. Controls can be identified by their: 

	index position 
	ID value 
	hwnd 
	or the case-insensitive caption associated with the control.


	1. hwnd           - We don't know that yet so this is no good.
	2. caption        - don't always have one and it's kind of "messy". 
						(technical terminology) meaning it could change.

	Which leaves us with:
						
	3. index/ordinal  - numerical position.  i.e. 1 of 10. Sometimes relates 
	                    to Tab order.

	4. ID             - unique numerical identifier	similiar to and MAY
						the same as the HANDLE.


BOTH Ordinal and ID work well.

I'm not going to say ORDINAL is better than ID or vice versa.  It can 
depend on the application.  There are pros and cons to each.  Examine your
application's objects with the WInfo tool.  Is there some pattern or reason 
one looks better than the other?  Try to stick with	whatever you decide.  
If all of your wrappers use a consistent value, life is easy.
If you change your mind later however, it's only a matter of search and 
replace.

Just remember control$ is a STRING.  Ordinals, indexes, IDs and handles are
numbers.  So they must always be converted to a string.  VT makes this easy
for us by doing the following:


	Ordinal:
	--------

		ObjectHandle = WButtonFind(_ord((BtnOrdValue)),(FindWindowTimeout))

		_ord((BtnOrdValue))

	Which is the same as:

		The String "@number"

		i.e  _ord(1)  or "@1"

	ID:
	------

		ObjectHandle = WButtonFind(_id((BtnOrdValue)),(FindWindowTimeout))

		_id((BtnOrdValue))

	Which is the same as:

		The string "#number"

		i.e  _id(1)  or "#1"


Once we have "found" the object with the W*Find function we know its HANDLE.
The handle is a LONG number that UNIQUELY identifies the object.  From here on out
we can use this handle to identify the object.


	If ObjectHandle <> 0 Then
		If WButtonExists(_hWnd(ObjectHandle),(FindWindowTimeout)) Then


	_hWnd:
	------

	We now refer to the object this way:

		_hWnd(ObjectHandle)

	Which is the same as:

		The String "=number"

			i.e  _hWnd(-38948)  or "=-38948"   (plus or minus /not equal to zero)


We direct the remaining WButton* functions at this HANDLE checking to
make sure the button EXISTS and then to make sure the button is ENABLED.

There are a couple of good reasons for finding the HANDLE:

		1. By searching for an objects handle we verify its existence

		2. It works out great in our template design.
			i.e. it is easy to cut and paste, search and replace.

		3. It uniquely identifies the object.


So forget the safety factor for a minute.  If you really examine our wrapper
functions you will see how much easy it is to make duplicate wrappers by
using the handle approach.  It gives great cut and paste.

Again, it's so sensible.


	If WButtonEnabled(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
		WButtonClick(_hWnd(ObjectHandle),(FindWindowTimeout))


Our Button may have a handle and may exist, BUT it could be DISABLED! If we
try to click a button that is disabled we will get an error, which means a
dialog box that we have to account for.  Bummer man.  By testing to make sure
that it is enabled we avoid a possible system error.


--So--

	If we successfully 

		Set the class,
		found the Handle,
		made sure the object exists,
		and made sure the button is enabled, then

	we click it!


Now all of this gets back to FOCUS.  By carefully going through all of the 
steps above we know that we have focus and are directing our commands to the
proper object.  There is NO GUESSWORK.

By creating subroutines or wrappers for our VT functions we can create 
BULLETPROOF code.  

If you recall this is one of the goals in our design.

	Modular program design
	----------------------

		3. The subroutine should be completely debugged before being
		   integrated into a program.


So we create these wrappers and completely debug them.  Once that is done we
can use them over and over again.  If something goes wrong it will be reported
in our error logs because we have built in all of the error checking and 
reporting.  Finding errors becomes easy because we know our object 
manipulation routines are error free.  We need only check our error logs.


Some people will object to the idea of creating wrapper functions to do object
manipulation.  There may be some good reasons not to do it this way.

Try to see this as a concept rather than a strict rule.  I see similiar 
questions on the board all of the time.


	What do I do when the class changes?
	Why does my program sometimes click the button and then doesn't?
	Why does my program work on one machine and not another?


I should keep a file of questions that relate to this problem so that I could
give better examples.  It comes down to this.  Are your programs erratic?  Do
some of the objects seem to work erratically?  Try some of the steps I showed
you or a wrapper function on an object that you're having a tough time with.

If you can see the relationship of the W* functions and how they may be able
to fit together, then I've met my goal.

One final point, the wrappers have built in logging and error checking.  That 
is a huge plus.

You will find all of the above object manipulation functions within the 
GenUtil.inc library.


PLAY statement revisited
------------------------

Sometimes we can't use VT W* commands to manipulate an object.  In that case
we can use the PLAY command which simulates the actions of a keyboard or mouse.


	Syntax
	PLAY [ hwnd&, ] string$
  
	hwnd&
	Mouse coordinates are relative to a windows hwnd 
	(value used to identify a window). If hwnd is omitted, the mouse 
	coordinates are absolute (screen) coordinates.

	string$
	String expression sent to the active window. The string can contain 
	sequences 	of key text, action codes, and characters with special 
	meaning to the PLAY statement.



	Standard Objects
	----------------


The following functions are examples of clicking on an object:

		ClickGrid()
		ClickOption()
		DblClickButton()
		DblClkEditBox()

and are located in GenUtil.inc.  


ClickOption() uses the VT command:

		WOptionClick(_hWnd(ObjectHandle),(FindWindowTimeout))


The remaining three functions use the PLAY command.


Here is sample from DblClickButton():


ObjectHandle = WButtonFind(_ord((BtnOrdValue)),(FindWindowTimeout))
If ObjectHandle <> 0 Then
	If WButtonExists(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
		If WButtonEnabled(_hWnd(ObjectHandle),(FindWindowTimeout)) Then
			' Double click on nominated field
			Play ObjectHandle,"{DBLCLICK}"
			LogWrite("0     Double Clicked on: "+ButtonName+".",LogPath,CritErrPath)
		Else
			DblClickButton = False
			' Write error message
			LogWrite("1     "+ButtonName+" Button isn't enabled!",LogPath,CritErrPath)
			Exit Function
		End If
	Else
		DblClickButton = False
		' Write error message
		LogWrite("1     "+ButtonName+" Button doesn't exist!",LogPath,CritErrPath)
		Exit Function
	End If


The DblClickButton() function uses the same approach as the wrappers previously 
discussed.  There are times that even though we are manipulating a standard 
object that allows the W* commands, we must for some reason use the play 
command.  If this is the case then I would recommend that you use the W* 
commands to precede it.


			Play ObjectHandle,"{DBLCLICK}"


When you play without specifying XY coordinates it clicks in the center of the 
object.


	Grids - Non standard Objects
	-----------------------------

A GRID is not a standard control.  There are no WGrid* commands.  Here we need 
to click to a XY coordinate location.

	' Find Grid window
    GridHandle = WFndWndC(GridCaption,GridClass,FNDWINDFLGS,(FindWindowTimeout))
	If GridHandle Then

		LogWrite("0     Found Grid "+ GridName+".",LogPath,CritErrPath)

		' Click Grid
		Play GridHandle,"{CLICK "+Str$(GRight)+","+Str$(GDown)+" }"
		LogWrite("0     Clicked on: "+GridName+".",LogPath,CritErrPath)


Note that the XY coordinate needs to be translated from a number to a string.

See the Function ClickGrid() located in GenUtil.inc


	Painted Controls - Non standard Objects
	---------------------------------------

Painted Controls are not standard controls. They are a picture of a control.
ClsNtPad.MST demonstrates a way to play a click command to a painted control.

Examine ClsNtPad.MST and take a close look at the function: 

	ClickCorrectionFactor()

This function demonstrates WGetWndPosSiz() to apply a correction factor to 
the play statement.



Testing Custom Controls with OLE Control Procedures (MSTEST.OCX)
----------------------------------------------------------------

From online help:

There are a few conditions that must be met before you can use Visual Test to 
test an application containing OLE controls:

	Visual Test provides a special OLE control (MSTEST.OCX). This special 
    control must be included on any forms containing OLE controls you plan 
	to manipulate or test with the OLE control procedures. Although the control
	is visable at design time, it will not be seen at run time. After testing 
	is completed, you can remove the control from your forms or you can include
	this redistributable control when you ship the application.

	Any container which holds an OLE control you plan to test must support the 
    IOleContainer interface. If not, Visual Test will not be able to locate 
	other OLE controls on the form. If you try to use an OLE control procedure
	on an OLE control that does not support this interface, a run-time error 
	will be generated.

	The IOleContainer interface must also support the EnumObjects method. 
    This method enables Visual Test to locate and communicate with OLE control 
	objects. If the container does not support this method and you attempt to 
	use an OLE control procedure on an OLE control, a run-time error will be
	generated.
  

Grids, Calendars and Flex Arrays are examples of custom controls.  If there are
no VT W* commands to manipulate a control you have 2 choices, Play commands or
VT OLE Control Procedures.  

VT OLE commands will only work if the MSTEST.OCX is put on the form at design 
time by the developer.  The MSTEST.OCX will only work if the above conditions 
are met.

So assuming that all of the above is true.  The following OLE Control Procedures
can be used to manipulate the control.

VT OLE Control Procedures:

		OleCtlClick
		OleCtlDblClk
		OleCtlDragTo
		OleCtlExists
		OleCtlFind
		OleCtlGetClass
		OleCtlGetProperty
		OleCtlHasProperty
		OleCtlHitTest
		OleCtlPpropertyList
		OleCtlMoveTo
		OleCtlSet
		OleCtlSetClass

If you are unfamiliar with this and have custom controls to test I would ask 
the developer to create a small test app that contains the control and 
MSTEST.OCX.  In addition, the control should be loaded with test data.  When
you have mastered this test app and know it all works, move on to the real thing.


'**********************************************************
'  GetCrlProperties() - Get Properties from custom control
'
'  Parameters:
'
'  ControlName$  - Name assigned by developer to control
'
'**********************************************************
Function GetCrlProperties(ControlName$) As Integer

	GetCrlProperties = True

	' Make sure ControlName variable is correct
	If Left$(ControlName,1) <> "#" Then
		ControlName = "#"+ControlName
	End If

	' Make sure control exists
	If OleCtlExists(ControlName,(FindWindowTimeout)) Then

		Dim NumberOfProperties As Integer
		Dim ArrayCount As Integer

		LogWrite("*****************************",LogPath,CritErrPath)

		Dim PropertyList(1 To 100) As OLECTL_PROPERTY

		' Determine number of properties in control
		NumberOfProperties = OleCtlPropertyList(ControlName,PropertyList,(FindWindowTimeout))

		' Print each property
		For ArrayCount = 1 To NumberOfProperties
			LogEnter (PropertyList(ArrayCount).PropName +" "+PropertyList(ArrayCount).PropValue,LogPath,CritErrPath)
		Next

		LogWrite("*****************************",LogPath,CritErrPath)

	Else
		LogWrite("1     Can't Find OLE Control "+ControlName+"!",LogPath,CritErrPath)
		GetCrlProperties = False
		Exit Function
	End If

End Function
'*************************************************************


The above function will print the controls properties to the log file.  With 
this list you now know what properties in the control you may use.

The Function ReadvsFlexArray() located in GenUtil.inc is an example of 
manipulating a custom control with the OleCtlGetProperty and OleCtlSetProperty
commands.



Chapter 8.	TstExp01.mst   Test Explorer 101
----------

Let's have our way with Explorer.

I've put several object manipulation subroutines in the GenUtil.inc include 
file and in addition put our Start and Close API functions in the 
GenAPI.inc file.

To begin with let's just write a program to Start and Close Explorer.exe.  Its
name is:  TstExp01.mst


Here is the code in TstExp01.mst to Start and Close Explorer.exe which calls our
StartTheProgram() and CloseExplorer().


	ReturnInteger = StartExplorer()
	' Test Return Value
	If ReturnInteger = False Then
		LogError("Error Starting Explorer!",0,LogPath,CritErrPath)
	End If

	ReturnInteger = CloseExplorer()
	' Test Return Value
	If ReturnInteger = False Then
		LogWrite("1     Error Closing Explorer!",LogPath,CritErrPath)
	End If


If successful our ERROR.LOG will look something like this:

8/4/97 3:42:27 PM  0   TstExp01.PCD STARTED. Rev 1.0
8/4/97 3:42:29 PM  0     Checking for existence of Main C:\WINDOWS\EXPLORER.EXE Window.
8/4/97 3:42:29 PM  0     Main C:\WINDOWS\EXPLORER.EXE Window exists.
8/4/97 3:42:29 PM  0     Found System Menu.
8/4/97 3:42:29 PM  0     Selected System Menu.
8/4/97 3:42:32 PM  0     Selected Menu item: File\Close.
8/4/97 3:42:32 PM  0   TstExp01.PCD ENDED.
8/4/97 3:42:34 PM  


Errors may look like this:

8/4/97 3:42:05 PM  0   TstExp01.PCD STARTED. Rev 1.0
8/4/97 3:42:16 PM  1     Can't find C:\WINDOWS\EXPLORER.EXE Main Window to Exit!
8/4/97 3:42:16 PM  1     Can't Terminate Application!
8/4/97 3:42:16 PM  1 ********************************************************
8/4/97 3:42:16 PM  1 ***               FATAL ERROR!
8/4/97 3:42:16 PM  1 *** C:\WBImport\TstExp01.PCD Could Not Continue!
8/4/97 3:42:16 PM  1 *** Error Closing Explorer!
8/4/97 3:42:16 PM  1 *** Current Window Information Follows: 
8/4/97 3:42:16 PM  1 *** Window Handle:          2188 
8/4/97 3:42:16 PM  1 *** Window ID:              0 
8/4/97 3:42:16 PM  1 *** Visible:               TRUE
8/4/97 3:42:16 PM  1 *** Enabled:               TRUE
8/4/97 3:42:16 PM  1 *** Window Caption:        Microsoft Developer Studio [run] - WBImport - [TstExp01.MST *]
8/4/97 3:42:16 PM  1 *** Window Classname:      Afx:400000:8:151e:0:512f
8/4/97 3:42:16 PM  1 *** Name of Owning Module: MSDEV.EXE
8/4/97 3:42:16 PM  1 *** Associated Label:      
8/4/97 3:42:16 PM  1 ********************************************************
8/4/97 3:42:21 PM  0   TstExp01.PCD ENDED.


If you have errors check to make sure your INI file is correct and you have 
pathed to it correctly in your command line.



Chapter 9.	TstExp02.mst  Test Explorer 102
----------

*** WARNING Will Robinson! ***


Expanding and Collapsing the TreeView in Explorer is kind of fun but if you 
have lots of directories and subdirectories, this program could take a while.

Start it and go get a cup of coffee or maybe lunch.

The reason that I'm including these 2 particular functions is that they are a
bit more complex than ClickButton() for instance.  As the TreeView object 
contains a LIST of directories and subdirectories the function to read the
tree must contain a LOOP.


ReturnInteger = StartExplorer()
' Test Return Value
If ReturnInteger = False Then
	LogError("Error Starting Explorer!",0,LogPath,CritErrPath)
End If

ReturnInteger = ExpandTree("SysTreeView32",1)
' Test Return Value
If ReturnInteger = False Then
	LogWrite("1     Error Expanding Explorer Tree!",LogPath,CritErrPath)
End If

ReturnInteger = CollapseTree("SysTreeView32",1)
' Test Return Value
If ReturnInteger = False Then
	LogWrite("1     Error Collapsing Explorer Tree!",LogPath,CritErrPath)
End If

ReturnInteger = CloseExplorer
' Test Return Value
If ReturnInteger = False Then
	LogWrite("1     Error Closing Explorer!",LogPath,CritErrPath)
End If


'*************************************************************************
' Function Function ExpandTree(ClassName$,ViewOrdinal%) As Integer
'
' Parameters: 
'
' ClassName$   - Class Name of Tree from WInfo
' TreeOrdinal% - Ordinal Value of Tree from WInfo
'
' Returns True/False
'
' Expands Tree and writes path names to Error.log
'
'*************************************************************************
Function ExpandTree(ClassName$,TreeOrdinal%) As Integer

	Dim TreeHandle     As Long
	Dim TreeCount      As Integer
	Dim TreeCounter    As Integer
	Dim TreeString     As String
	Dim TreeItemsExist As Integer

	ExpandTree = True
    
	' Set the Tree Class
	Sleep .5
	WTreeSetClass(ClassName)

	' Get handle for Tree Control
	TreeHandle = WTreeFind(_ord((TreeOrdinal)),(FindWindowTimeout))
	If TreeHandle <> 0 Then
		' Find Tree Control
		If WTreeExists(_hWnd(TreeHandle),(FindWindowTimeout)) Then
			' Make sure Tree is enabled
			If WTreeEnabled(_hWnd(TreeHandle),(FindWindowTimeout)) Then
				' Count Tree Items
				TreeCount = WTreeCount(_hWnd(TreeHandle),(FindWindowTimeout))
				' Loop through Tree	Items
				If TreeCount > 0 Then
					' Initialize variable
					TreeItemsExist = True
					LogWrite("0     ==============",LogPath,CritErrPath)
					LogWrite("0     Expanding Tree",LogPath,CritErrPath)
					LogWrite("0     ==============",LogPath,CritErrPath)
					' Expand Tree View and write items to Log File
					Do
						' Increment Counter
						TreeCounter = TreeCounter + 1
						' Test for Tree Item
						If WTreeItemExists(_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout))	Then
							' Ignore Floppy CDROM Drives and Network directory
							If Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"A:" ) = 0 _
							And Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"D:" ) = 0 _
							And Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"Network" ) = 0 Then
								Sleep .15
								' Expand Tree Item
								If Not WTreeItemExpanded(_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)) Then
									WTreeItemExpand(_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout))
									' Click Tree Item
									WTreeItemClk(_hwnd((TreeHandle)),_idx((TreeCounter)),VK_LBUTTON,(FindWindowTimeout))
								End If
							End If
							' Get Text of Tree Item Path
							TreeString = TreeItemPath(_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout))
							' Write Tree Items to Log File
							LogWrite("0     "+TreeString,LogPath,CritErrPath)
						Else
							' All done exit loop
							LogWrite("0     Tree Item Count: "+ Trim(Str$(TreeCounter-1))+".",LogPath,CritErrPath)
							TreeItemsExist = False
						End If
					Loop While TreeItemsExist
				Else
					ExpandTree = False
					LogWrite("1     No Items found in Tree!",LogPath,CritErrPath)
					Exit Function
				End If
			Else
				ExpandTree = False
				LogWrite("1     Tree Control isn't enabled!",LogPath,CritErrPath)
				Exit Function
			End If
		Else
			ExpandTree = False
			LogWrite("1     Tree Control doesn't exist!",LogPath,CritErrPath)
			Exit Function
		End If
	Else
		ExpandTree = False
		LogWrite("1     Can't Find Tree Control!",LogPath,CritErrPath)
		Exit Function
	End If

End Function
'*************************************************************************


The ExpandTree() function uses a DO WHILE LOOP to loop through the TreeView 
until it runs out of subdirectories.

Note this section to avoid some of the Tree Subdirectories that I don't like.

' Ignore Floppy CDROM Drives and Network directory
If Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"A:" ) = 0 _
And Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"D:" ) = 0 _
And Instr(1,TreeItemText (_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout)),"Network" ) = 0 Then


Hopefully, you will see that in reality this wrapper function is more of the 
same stuff you've seen before, just more of it.


	' Make sure Tree is enabled
	If WTreeEnabled(_hWnd(TreeHandle),(FindWindowTimeout)) Then
		' Count Tree Items
		TreeCount = WTreeCount(_hWnd(TreeHandle),(FindWindowTimeout))
		' Loop through Tree	Items
		If TreeCount > 0 Then
			' Initialize variable
			TreeItemsExist = True
			LogWrite("0     ==============",LogPath,CritErrPath)
			LogWrite("0     Expanding Tree",LogPath,CritErrPath)
			LogWrite("0     ==============",LogPath,CritErrPath)
			' Expand Tree View and write items to Log File
			Do

I first make sure the Tree is enabled and then get a count of its items.  
If it actually exists and there are items then I go into the DO LOOP.


	Loop While TreeItemsExist

	If WTreeItemExists(_hwnd((TreeHandle)),_idx((TreeCounter)),(FindWindowTimeout))	Then

	Else
		' All done exit loop
		LogWrite("0     Tree Item Count: "+ Trim(Str$(TreeCounter-1))+".",LogPath,CritErrPath)
		TreeItemsExist = False
	End If


While we're in the Loop the function tests to see if there are more items.  
When it runs out it sets the TreeItemsExist to False which exits the loop.


CollapseTree() uses a FOR NEXT LOOP and steps through the TreeView from
BOTTOM - UP using the STEP -1 parameter.

It counts the number of items in the Tree with WTreeCount()


	' Count Tree Items
	TreeCount = WTreeCount(_hWnd(TreeHandle),(FindWindowTimeout))
	' Loop through Tree	Items
	If TreeCount > 0 Then
		LogWrite("0     ===============",LogPath,CritErrPath)
		LogWrite("0     Collapsing Tree",LogPath,CritErrPath)
		LogWrite("0     ===============",LogPath,CritErrPath)
		For TreeCounter = TreeCount To 1 Step -1


So CollapseTree() is an example of how to loop through something backwards.


Chapter 10.	TstExp03.mst  Test Explorer 103
----------

Back to our original specification:


	1.	Open Explorer.exe
	2.	Open the View Menu
	3.		Select all items on the View Menu
	4.		Select all items of the Options Menu of the View Menu
	5.		Options Menu
	6.			Test all the objects found
	7.	Close Explorer.exe


We already have code for 1,2,3,4 and 7.  Cool.


Oh, No look at number 6 all the stuff in the Options Menu!

	1st Tab "View"			2nd Tab "File Types"

	Window					Window
	SysTabControl32			SysTabControl32
	Option Buttons			SysListView32
	ListBox					Button
	CheckBox

Of course, I'm putting myself through this pain deliberately on your behalf.


So we will need code for:

	SysTabControl32:
	ReturnInteger = SelectTabItem("SysTabControl32",1,"View")

	Option Buttons:
	ReturnInteger = ClickOption("Button",1,"Show All Files")

	ListBox:
	ReturnInteger = GetListItems("ListBox",1)

	CheckBox:
	ReturnInteger = SelectCheckBox("Button",1,"Display","Check")

	SysListView32:
	ReturnInteger = SelectViewItem("SysListView32",1,"Help File")

	Button:
	ReturnInteger = ClickButton("Button",1,"OK")

	Edit:
	ReturnString = GetEditText("Edit",1,"Description of Type")


Here is a quick test case outline:

1.  Create TestViewMenu() function

	a. Select menu option View\Toolbar
	b. Select menu option View\Status Bar
	c. Select menu option View\Large Icons
	d. Select menu option View\Small Icons
	e. Select menu option View\List
	f. Select menu option View\Details
	g. Select menu option View\Arrange Icons
	h. Select menu option View\Line up Icons
	i. Select menu option View\Refresh
	j. Select menu option View\Options


2.  Create a TestViewOptions function.

	a. Select menu option View\Options
	b. Select Tab "View"
		1. Show all files option button
		2. Hide files of these types option button
		3. Read the list box
		4. Check Display the full MS-DOS &path in the title bar
		5. Check Hide MS-DOS file &extensions for file types that are 
		registered
		6. Check Include &description bar for right and left panes
	c. Select Tab "File Types"
		1. Select item from list view
		2. Retrieve Text from an Edit
		3. Click Cancel


I heard a good one on TV the other night:

	"Marines don't plan, they IMPROVISE!"

So if I see anything along the way that looks fun I'll just Marine it.


==========
Section 3:  Running Your Project
==========


Chapter 11.	TstExp04.mst  Test Explorer 104
----------

By now you should either be an expert at creating subroutines or just sick of
staring at my code.  In any event, it's now time to start putting it together 
in a more final version.


Microsoft Developer Studio
--------------------------
In order to utilize the Suite Manager we must take a look at organizing a 
project in the Developers Studio.

A project is organized in Developer Studio in a Tree fashion.


	Very simple directory design used by WnObjPrg:

	- WnObjPrg
		RunPrg.mst
		Template.mst
		TstExp01.mst
		TstExp02.mst
		TstExp03.mst
		TstExp04.mst
		TstExp05.mst
		Book.txt
		Etc.
		
	More Complex design example used by WbSMProject:
	
	- WbSMProject
		ExpStart.mst
		- Explorer Menu Test
			TstMnTst.mst
		- Log Files
			CRITERR.LOG
			ERROR.LOG
			Status.log
		ExpClose.mst
		- Include Folder
			GenAPI.inc
			GenUtil.inc
			TDeclare.inc
		Book.txt

The point being that you can create subdirectories to separate groups of 
programs.  For instance you could create a folder to keep regression tests
and another folder to keep load tests.

	- WbSMProject
		ExpStart.mst
		- Explorer Menu Tests
			TstFlMnu.mst
			TstEdMnu.mst
			TstVwMnu.mst
			TstTlMnu.mst
			TstHpMnu.mst
		- Regression Tests
			TstExp01.mst
			TstExp02.mst
			TstExp03.mst
			TstExp04.mst
		- Load Tests
			TstExp05.mst
			TstExp06.mst
			TstExp07.mst
			TstExp08.mst
		- Log Files
			CRITERR.LOG
			ERROR.LOG
			Status.log
		ExpClose.mst
		- Include Folder
			GenAPI.inc
			GenUtil.inc
			TDeclare.inc
		Book.txt


	Creating the Directory Structure
	--------------------------------

  The TestExplorer example is built by creating a new workspace named 
  TestExplorer.  Once that project appears in the TestView window we
  can RIGHT CLICK on it.  By selecting NEW...  the "New Test Project Item"
  dialog appears.

  Create a Folder Entry Script (ExpEntry.mst) and a Folder Exit Script 
  (ExpExit.mst).  The test programs	that fall between can be added to to the
  project from the IMPORT... popup menu.
  
  		TstFlMnu.mst
		TstEdMnu.mst
		TstVwMnu.mst
		TstTlMnu.mst
		TstHpMnu.mst

  The Include subdirectory is created by selecting the INCLUDE FOLDER in the
  the "New Test Project Item" dialog.

	Setting the Order
	-----------------

  The order that the test cases are run is set by Right Clicking on the
  TestExplorer project and selecting PROPERTIES. The Project Properties dialog
  will appear.  Select the ORDER TAB and arrage the programs in the order you 
  wish to have them run.


In the above example am MST file is created for testing EACH menu option for 
the Explorer.

There is debate as to the level of MST detail necessary.

		ExpEntry.mst
		TstFlMnu.mst
		TstEdMnu.mst
		TstVwMnu.mst
		TstTlMnu.mst
		TstHpMnu.mst
		ExpExit.mst

			-or-

		TestExplorer.mst?


It is certainly possible to combine the 7 programs into one test case.  The way 
we are coding it now each one of the MSTs would be calling 1 function each.  So
it would be very easy to combine all functions/scenarios into one test case.

This is a matter of personal preference there are good arguments both ways.  
There are a few things to consider.

	1. Each mst/scenario/function has to start and return to a known state.

	For instance TstFlMnu  "Test File Menu".  How does it know that the 
	Explorer application has started?  Before it does the File Menu test it 
	must first test to make sure the application is open.

	When it is done testing the File Menu it must put the application back to
	a KNOWN STATE.

	TstEdMnu "Test Edit Menu" has to test for the Explorer application all over
	again and put it back the way it was again.

	In the single program version much of this duplication is eliminated.

	2. Is is necessary to share memory variables?  Are there dependencies?

	If you have separate MSTs do the programs need to share things?  If some
	Global memory variable needs to be set that all the programs need to know
	how will you deal with it?

	You will have to update text files such as INI files or the Registry so 
	that the programs can read the status of Global memory variables.

	3. Include files.

	How do you share common include files?  A common directory may need to be 
	set up or use the \MSDEV\INCLUDE directory.  The downside is that this 
	directory may not be visible in your Test View window.


So in breaking up your project into separate programs remember that they are 
SEPARATE.  They must be able to operate independently and alone as well as the
third on the list.

If you have dependency problems then you may want to combine programs or have
fewer test cases.  Naturally combinations should be related events.  

We wouldn't put test the Explorer menu and test the notepad edit button in 
one test case.

Any directories/subdirectories created in Developer Studio are created on your
hard disk.  This is not virtual reality.  The project must exist in contiguous
subdirectories.  You can't have files spread out over the network and include
them in your project.

If you intend to use the Suite Manager to run your project's programs then you
must understand project organization.


Suite Manager
-------------

VT comes with a tool called the Suite Manager which provides:

	1.  Running Projects/Suites
	2.  Logging

You can create Suites by selecting a list of MSTs to run from a project and 
designate their order.  Or you can run projects and set the order to run the
MSTs in the project.  In order to create a Suite you must have a Project.  A
Suite is a subset of a project.

In addition, by creating subdirectories in your project you can run just a 
subdirectory, i.e. your regression tests, by just selecting that subdirectory
in Suite Manager.

You can choose to Log to a:

	1.  Text file
	2.  Database
	3.  View Port
	4.  No Logging

In the Suite Manger's View\Options menu you must set the various options for
the project.  If you have not done so, please take a look at this:

	In Developer Studio:

		Test\Suite Manager

	In Suite Manager:

		View\Opions
		Select the Logging Tab

		File Name:       c:\WBImport\Status.log
		Option:          Flat File
		Detail Level:    3
		Product Version: 1.0.0.1
		Machine Name:    RICHSWKS (This is my machines name put in YOURS)
		Language: 	     U.S. English


An example of a Suite Manager organizational approach could be:

Suite 1

Test Case A
	Scenario 1
	Scenario 2
	Scenario 3
Test Case B
	Scenario 1
	Scenario 2
	Scenario 3
Test Case C
	Scenario 1
	Scenario 2
	Scenario 3

Test Cases are the same as MSTs/PCDs.
Scenarios  are similiar to SUBs/Procedures.

Scenario Organization Example 1:
--------------------------------

	This is the Suite Manager Organizational Approach
	--------------------------------------------------

	Scenario 1

	Scenario Cleanup

	End Scenario


	Scenario 2

	Scenario Cleanup

	End Scenario



Scenario Organization Example 2:
--------------------------------

	This is the Program Driver Organizational Approach
	--------------------------------------------------

	LogBeginSuite 1

		LogBeginCase A
			LogBeginScenario 1
			LogEndScenario   1
			LogBeginScenario 2
			LogEndScenario   2
			LogBeginScenario 3
			LogEndScenario   3
		LogEndCase

		LogBeginCase B
			LogBeginScenario 1
			LogEndScenario   1
			LogBeginScenario 2
			LogEndScenario   2
			LogBeginScenario 3
			LogEndScenario   3
		LogEndCase

		LogBeginCase C
			LogBeginScenario 1
			LogEndScenario   1
			LogBeginScenario 2
			LogEndScenario   2
			LogBeginScenario 3
			LogEndScenario   3
		LogEndCase

	LogEndSuite


The difference is who turns on the logging engine?  In example 1 the logging
engine is turned on by the Suite Manager. 

The Program Driver must launch the logging engine manually so to speak and
utilizes a different set of logging commands:

	LogBeginSuite    - launches VT's logging Engine.
	LogBeginCase     - writes the Case name
	LogBeginScenario - writes the Scenario names 
	LogEndSuite      - Ends the suite.

It should be noted that if your Program Driver should fail and not execute the
LogEndSuite command the logging engine will remain turned on.  From there after
logging will not work you will get error messages stating that the logging 
engine can't be started up again. 

8/15/97 4:06:36 PM  62  ********************************************************
8/15/97 4:06:36 PM  62  *** MS-TEST Reports the following ERROR:
8/15/97 4:06:36 PM  62  *** Suite already started online: 179
8/15/97 4:06:36 PM  62  *** In Program: C:\WBImport\RunPrg.mst
8/15/97 4:06:36 PM  62  *** This may cause unexpected results.
8/15/97 4:06:36 PM  62  *** Attempting to end RunPrg.PCD!
8/15/97 4:06:36 PM  62  ********************************************************


If the logging engine works at the beginning of the day and then mysteriously 
quits, this may be your problem.  You have to shut down VT and start it again.


******** ALWAYS SAVE YOUR WORK BEFORE RUNNING IT! *********
		 If it explodes, all new work is gone --> !


Compatibility issues
--------------------
Example 2 will work in the Suite Manager.  Example 1 won't work for the Program
Driver.

Example 1

	Scenario 1
	Scenario Cleanup
	End Scenario

Example 2

	LogBeginScenario 1
	LogEndScenario (LogResultPass)
		-or-
	LogEndScenario (LogResultFail)


Suite Manager or Program Driver?
--------------------------------

The question then is whether or not you need to use a Program Driver as opposed 
to using the Suite Manager.

A major issue as far as I'm concerned is whether or not NETWORK TESTING is 
important.

Do you have the requirement to run your tests on numerous workstations 
simultaneously?

Imagine running the Suite Manager on 200 workstations to do load testing.  
Let's say no problem have that all worked out and I can make it all run on 
command.

How about setting the options within Suite Manager on all those stations?

		File Name:       c:\WBImport\Status.log
		Option:          Flat File
		Detail Level:    3
		Product Version: 1.0.0.1
		Machine Name:    RICHSWKS (This is my machines name put in YOURS)
		Language: 	     U.S. English

Do we want 200 Status logs to reflect the correct information?  If so you have
to set the options in Suite Manager on 200 stations.

Where are all of the project files/MSTs located?  Can all the programs reside
on one Host machine or do they have to be loaded on all 200 so the Suite Manager
can find them?

I see some problems here.  

So if you require the flexibility of a program driver then you must write your 
scenarios accordingly.


SCENARIO CLEANUP
----------------

	Scenario 1
	Scenario Cleanup
	End Scenario

A scenario cleanup puts the program back to a known state.

If a scenario FAILS it will go to the Scenario Cleanup section if one exists 
and execute whatever commands it finds.

	Scenario "Test"

		Dim ReturnInteger As Integer

		ReturnInteger = TestFunction()

		If ReturnInteger = False Then
			FAIL
		End If
			
	Scenario Cleanup
	
		If FAILED Then
			END
		End If
	
	End Scenario


The	Scenario Cleanup statement is not magical and the same result could be
achieved in other ways.

The important thing is the concept.

Scenario2 may depend on the results of Scenario1.  For instance Scenario1 may
open the application.  If it fails there is no point in continuing.

The idea of CLEANUP is to put the application back to a known state so that the
next scenario can continue properly or perhaps not run at all.

Putting the application back to a known state may be as simple as clicking the
cancel button.  What if your scenario accesses several dialogs or menus?  If it
fails there may be numerous possiblities as to what must be dismissed to get 
back to the known state.

Let's say that the scenario puts up 10 dialogs and somewhere along the line the
scenario fails.  The result is probably a dialog still sitting on the screen 
waiting for someone to click the cancel button.

Let's say each of the 10 dialogs has 3 buttons.  So our cancel button could be 
one of 30.

Cleanup can be a real drag and I can think of several options how to deal with 
it.  

	1. Create a close program that contains every possible dialog window.
	Look for each of these windows and if found close it.

	2. Create an array.  As you go along add each window encountered to the 
	array.  Take it out of the array when the window is dismissed.  When 
	failure occurs dismiss whatever is in the array.

	3. Deal with each window or object as it happens.  Instead of using a 
	cleanup section make it the responsibility of the called function to 
	cleanup after itself.

	4. Some other way?


I wish I could give you a magic program and say "Run this in your cleanup."

My preference is to do the cleanup at the lowest level, i.e. within the
function call.  The function should return the application to a known state
upon error.  At least try!

If you have the magic code or idea, let me know.  We'll add it in right here.


Scenarios
---------
Scenarios are similiar organizationally to SUBS/procedures but are mechanically
different.

The main difference is that you can't "CALL" them, they run in what ever order
you place them in your program.  As a result you can't put a scenario in a 
library.  This to me is a huge drawback, but I'll show you how I get around it.


So again there are 2 benefits (As I see it) to the Suite Manager:

	1.  Running a list of programs
	2.  Suite Manager Logging


As we have already created all of our own logging functions, Suite Manager 
Logging is somewhat redundant, but who can get enough of this stuff?

For now, all we have to worry about is how to create a Scenario.

Here is a sample of a Scenario that uses the StartExplorer() function we 
created earlier. 

As I'm heading in the direction of a program driver, my scenarios will be 
written in that structure/format.


'************************************************
LogBeginScenario ("Start Explorer Application")

	ReturnInteger = True

	' Write message to log file
	LogWrite("0    =======================================================",LogPath,CritErrPath)
	LogWrite("0    Scenario Start Explorer Application",LogPath,CritErrPath)
	LogWrite("0    =======================================================",LogPath,CritErrPath)

	' Test for Main Application Window Handle
	If MainAppWindow = 0 Then

		MainAppWindow = WFndWndC("Exploring","ExploreWClass", FNDWINDFLGS Or FW_RESTORE,(FindWindowTimeout))

		' Test Again for Main Application Window Handle
		If MainAppWindow = 0 Then

	        'Run StartExplorer procedure
	        ReturnInteger = StartExplorer()

			If ReturnInteger = False Then

				LogWrite("1     Can't start "+ExplorerName+"!",LogPath,CritErrPath)

				'Run CloseExplorer procedure
				ReturnInteger = CloseExplorer()

				' Test ReturnInteger
				If ReturnInteger = True Then
					LogWrite("0     Successfully closed "+ExplorerName+".",LogPath,CritErrPath)
				Else
					LogWrite("1     Error closing "+ExplorerName+"!",LogPath,CritErrPath)
				End If

				' End program
				LogClose(LogPath,CritErrPath)
				LogWrite("",LogPath,CritErrPath)
				End

			End If
		Else
			' Make sure window is Restored
			WResWnd (MainAppWindow)
		End If
	End If

	' Write message to log file
	If MainAppWindow <> 0 Then
		StatusBox Close
		LogWrite("0    Successfully started "+ExplorerName+".",LogPath,CritErrPath)
	End If

	If ReturnInteger = True Then
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultPass)
	Else
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultFail)
	End If
'************************************************
' Scenario End
'************************************************

As I said we can't "CALL" a Scenario.  Obviously we CAN call a function.
So my approach is to create functions and call them with the Scenarios.

Piece of cake.

OK, so what's new about this?

The reality is we've added the following commands:

	LogBeginScenario()
	LogEndScenario()

i.e.

	LogBeginScenario ("Start Explorer Application")

	If ReturnInteger = True Then
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultPass)
	Else
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultFail)
	End If


By doing so, we are now writing to the Suite Manager's Log file the fact that
we've started and ended a Scenario.  We've given this Scenario a name and said
that it PASSED or FAILED.

If we run TstExp04.mst from Developer Studio, the logged results will appear 
in the ViewPort window.

	  - OR -

If we run TstExp04.mst from the Suite Manager and set the options in 
it as we discussed earlier, the results will be written to the nominated log 
file. i.e. Status.Log

This is a portion of the resulting log file which I have nominated in the Suite
Manager as "Status.Log":

[Start Suite Header]
[Suite Name]         WBImport
[Machine]            RICHSWKS
[Start Time]         08/05/1997 13:27:52
[End Suite Header]

  [Start Case Header]
  [Case Name]         C:\WBImport\TstExp04.MST
  [Product Version]   1.0.0.1
  [Language]          U.S. English
  [Machine]           RICHSWKS
  [Start Time]        08/05/1997 13:27:53
  [End Case Header]

    [Start Scenario] 
    [Name]           Start Explorer Application
    [Test Location]  C:\WBImport\TstExp04.MST (72)
    [Start Time]     08/05/1997 13:27:54

    [Detail 0]           =======================================================
    [Detail 0]           Scenario Start Explorer Application
    [Detail 0]           =======================================================
    [Detail 0]            C:\WINDOWS\EXPLORER.EXE Started successfully.
    [Detail 0]            Checking for existence of Main Explorer Window.
    [Detail 0]            C:\WINDOWS\EXPLORER.EXE Main Window exists.
    [Detail 0]            Found C:\WINDOWS\EXPLORER.EXE System Menu.
    [Detail 0]           Successfully started C:\WINDOWS\EXPLORER.EXE.
    [Detail 0]           Scenario Complete.
    [Result]         PASS
    [Elapsed Time]   11.484
    [End Scenario]


Here is the corresponding section from our Error.log:


8/5/97 1:27:54 PM  0   TstExp04.PCD STARTED. Rev 1.0
8/5/97 1:27:54 PM  0    =======================================================
8/5/97 1:27:54 PM  0    Scenario Start Explorer Application
8/5/97 1:27:54 PM  0    =======================================================
8/5/97 1:28:05 PM  0     C:\WINDOWS\EXPLORER.EXE Started successfully.
8/5/97 1:28:05 PM  0     Checking for existence of Main Explorer Window.
8/5/97 1:28:05 PM  0     C:\WINDOWS\EXPLORER.EXE Main Window exists.
8/5/97 1:28:05 PM  0     Found C:\WINDOWS\EXPLORER.EXE System Menu.
8/5/97 1:28:05 PM  0    Successfully started C:\WINDOWS\EXPLORER.EXE.
8/5/97 1:28:05 PM  0    Scenario Complete.


So the information is essentially the same, the format is different.

If you can remember back that far, you may recall this section of code from our
LogWrite() function:


		Case "0"
			Log #0,+LogMessage
		Case "1"
			Log #1,+LogMessage
			' Write Error message to Critical Error Log
			LogFH=FREEFILE 
			Open CritErrPath for Append As #LogFH
		    Print #LogFH, Date$ + " " + Time$ + "  "+ Message
			Close #LogFH


Note the above command:

	Log #0,+LogMessage


The VT LOG function writes messages to the Suite Manager log file.  This of 
course, is a separate function from LogBeginScenario and LogEndScenario.

The	VT LOG function allows us to write whatever messages we want to the Suite
Manager log file.

By using the CASE structure in the LogWrite() function, severity levels are 
reported.

So wherever we use our LogWrite() functions detail such as this will be written
to our Suite Manager log file:

	Status Log
	----------

	[Detail 0]               Found System Menu.
	[Detail 0]               Selected System Menu.
	[Detail 0]               Selected Menu item: View\Toolbar.
	[Detail 0]               Found System Menu.
	[Detail 0]               Selected System Menu.
	[Detail 0]               Selected Menu item: View\Status Bar.
	[Detail 0]               Found System Menu.

	Error.log
	---------

	8/5/97 1:11:13 PM  0     Found System Menu.
	8/5/97 1:11:13 PM  0     Selected System Menu.
	8/5/97 1:11:15 PM  0     Selected Menu item: View\Toolbar.
	8/5/97 1:11:15 PM  0     Found System Menu.
	8/5/97 1:11:16 PM  0     Selected System Menu.
	8/5/97 1:11:18 PM  0     Selected Menu item: View\Status Bar.
	8/5/97 1:11:18 PM  0     Found System Menu.


By putting appropriate LOG commands in all of our logging functions such as
LogWrite() logging to the Suite Manager log file is an automatic by product.

So TstExp04.mst has only the 1 Scenario and is followed by the remaining 
functions that we created earlier and used in TstExp03.mst.

That of course is too show the differences between the 2 approaches and that
we can have Scenarios and Functions coexisting peacefully within the same
program.

They both work essentially the same with the exception of additional logging to
the Suite Manager by our 1 Scenario.

So what's all the fuss about?  
-----------------------------

The concept here to me is what is important.  It should be starting to become 
obvious to you that we can duplicate all of the functionality of the Suite 
Manager if we try hard enough.

The point to me is the structured approach, a method, a way of organizing our
programs.  Playing on the same sheet of music as everyone else.  Or perhaps a
practical approach, our managers WANT us to use the Suite Manager because it's
there.  STANDARDIZATION.

So whether you use home grown programs or the Suite Manager we'll try to keep
its structure.

OK, so much for philosophy, let's create Scenarios for the rest of it.


Chapter 12.	TstExp05mst  Test Explorer 105
----------

Here's our second Scenario:


Old way:

ReturnInteger = TestViewMenu()
' Test Return Value
If ReturnInteger = False Then
	LogWrite("1     Error Testing View Menu!",LogPath,CritErrPath)
End If

New way:

'************************************************
LogBeginScenario ("Test Explorer View Menu")

	' Write message to log file
	LogWrite("0    =======================================================",LogPath,CritErrPath)
	LogWrite("0    Test Explorer View Menu"+,LogPath,CritErrPath)
	LogWrite("0    =======================================================",LogPath,CritErrPath)

	' Run Function to Test Explorer View Menu
    ReturnInteger = TestViewMenu()

	' Test ReturnInteger
	If ReturnInteger = True Then
		' Write message to log file
		LogWrite("0     Successfully Tested Explorer View Menu.",LogPath,CritErrPath)
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultPass)
	Else
		' Write message to log file
		LogWrite("1     Error Testing Explorer View Menu!",LogPath,CritErrPath)
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		LogEndScenario (LogResultFail)
	End If
'************************************************
' Scenario End
'************************************************


Please examine the STATUS.LOG and the ERROR.LOG for results.

(That is if you haven't destroyed them by now.)


If you take a look at TstExp05mst, you will see that we now have 4 Scenarios.

Like alot of programming, it was all in the cut and paste.

If you noticed,  TstExp03mst, TstExp04mst, and TstExp05mst all contain the 
following functions:

	Declare Function TestViewMenu() As Integer
	Declare Function TestViewOptions() As Integer

Normally I would put them into our GenUtil.inc library with the rest of the 
gang.

I left them in the MSTs, so you could perhaps reference them a bit easier and
2 to demonstrate that you can have functions in both places.  You just can't
have the same function in both places at the same time.

My rule of thumb is that if the subroutine is specific to that MST I may leave
it there.  If I call it from more than one MST then I put it in the library.
I guess I broke the rules.

Thats it!  Scenarios are easy.

One other thing.  I wouldn't put too many Scenarios in a TestCase MST/PCD.
I don't know if there is any actual physical limitation, but I would recommend
no more than 10.  This is for administration/maintenance purposes.  

If your program is that big 20,30,50 Scenarios, trust me something is wrong.  
Break up into more Test Cases.


Chapter 13.	A Suite Manager Project.
----------

Please open Project Workspace WbSMProject.


	- WbSMProject
		ExpStart.mst
		- Explorer Menu Test
			TstMnTst.mst
		- Log Files
			CRITERR.LOG
			ERROR.LOG
			Status.log
		ExpClose.mst
		- Include Folder
			GenAPI.inc
			GenUtil.inc
			TDeclare.inc
		Book.txt


I wrote this project to demonstrate the directory and scenario structure of 
for a the more traditional Suite Manager approach.

The reason for breaking the project up into subdirectories is because that is
the easiest way to run them in Suite Manager in groups.  If you need to 
separate groups of programs, this is how you do it.

Within Suite Manager you can choose to run the entire project or select the
various subdirectories.

Please examine the 3 MSTs and compare their scenarios with the structures
we've been describing. 


ExpMnTst.MST
------------

'************************************************
Scenario ("Start Explorer Application")

	ReturnInteger = True

	' Write message to log file
	LogWrite("0    =======================================================",LogPath,CritErrPath)
	LogWrite("0    Scenario Start Explorer Application",LogPath,CritErrPath)
	LogWrite("0    =======================================================",LogPath,CritErrPath)

	' Test for Main Application Window Handle
	If MainAppWindow = 0 Then

		MainAppWindow = WFndWndC("Exploring","ExploreWClass", FNDWINDFLGS Or FW_RESTORE,(FindWindowTimeout))

		' Test Again for Main Application Window Handle
		If MainAppWindow = 0 Then

	        'Run StartExplorer procedure
	        ReturnInteger = StartExplorer()

			If ReturnInteger = False Then

				LogWrite("1     Can't start "+ExplorerName+"!",LogPath,CritErrPath)

				'Run CloseExplorer procedure
				ReturnInteger = CloseExplorer()

				' Test ReturnInteger
				If ReturnInteger = True Then
					LogWrite("0     Successfully closed "+ExplorerName+".",LogPath,CritErrPath)
				Else
					LogWrite("1     Error closing "+ExplorerName+"!",LogPath,CritErrPath)
				End If

				' End program
				LogClose(LogPath,CritErrPath)
				LogWrite("",LogPath,CritErrPath)
				End

			End If
		Else
			' Make sure window is Restored
			WResWnd (MainAppWindow)
		End If
	End If

	' Write message to log file
	If MainAppWindow <> 0 Then
		StatusBox Close
		LogWrite("0    Successfully started "+ExplorerName+".",LogPath,CritErrPath)
	End If

	If ReturnInteger = True Then
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
	Else
		' Write message to log file
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		Fail
	End If

Scenario Cleanup

	' End program upon failure
	If Failed Then
		LogClose(LogPath,CritErrPath)
		LogWrite("",LogPath,CritErrPath)
		End
	End If

End Scenario
'************************************************
' Scenario End
'************************************************
Scenario ("Test Explorer View Menu")

	' Write message to log file
	LogWrite("0    =======================================================",LogPath,CritErrPath)
	LogWrite("0    Test Explorer View Menu",LogPath,CritErrPath)
	LogWrite("0    =======================================================",LogPath,CritErrPath)

	' Run Function to Test Explorer View Menu
    ReturnInteger = TestViewMenu()

	' Test ReturnInteger
	If ReturnInteger = True Then
		' Write message to log file
		LogWrite("0     Successfully Tested Explorer View Menu.",LogPath,CritErrPath)
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
	Else
		' Write message to log file
		LogWrite("1     Error Testing Explorer View Menu!",LogPath,CritErrPath)
		LogWrite("0    Scenario Complete.",LogPath,CritErrPath)
		Fail
	End If

End Scenario
'************************************************
' Scenario End
'************************************************


We discussed the traditional Scenario structure previously:

Scenario
Scenario Cleanup
End Scenario


Notice that in ExpMnTst.Mst I first run:
	Scenario "Start Explorer Application" 

prior to:
	Scenario "Test Explorer View Menu"

This is because we must first test to make sure the Explorer is open before
testing its menu.

If the program can't start Explorer it generates a FAIL which sends
the program to the SCENARIO CLEANUP.


This project demonstrates a more complex tree structure.

	We can have an mst to: 

		Start Explorer
		Close Explorer

	and an mst to test each menu item:

		TstExpView			I was lazy and did one program to do
		TstExpEdit			all of this	-
		TstExpHelp			ExpMnTst.mst.
		Etc.


What if we want to externally run Suite Manager and have it automatically
run our project via a RUN command, DOS shell or ICON?

The Suite Manager is a PCD and subject to the same rules of passing a command
line as are in our own programs.

So if we use an external command, the Suite Manager must parse the command line
the same way we did.

Here is the best explanation of how to run Suite Manager externally that I can
find in online help:

				Visual Test 4.0 Books Online
				Utilities User's Guide
				Suite Manager
				Running Suite Manager from the Command Line
				Using COMMAND$
				/EXIT

	In the following example, the batch file starts Suite Manager and runs a 
	suite. Then when the suite finishes executing, Suite Manager terminates. 
	The batch file can then continue performing other operations, such as set
	up for another suite to run.
  
	mtrun sm /T project.vtp /C "/GO /EXIT"
  
	Notice that the options must be placed inside double quotation marks 
	following the /C (COMMAND$) option. The options are case insensitive 
	and the forward slash is interchangeable with a hyphen option, as in:
  
	mtrun sm -T project.vtp -C "-GO -EXIT"



Suite Manager Command Line Translation:
---------------------------------------

What's the CHR charater for double quotes Rich?  34!


Within an MST:

RUN	"C:\MSDEV\BIN\MTRUN.EXE C:\MSDEV\SUITEMAN\SM.PCD" +	" /T C:\WBIMPORT\WBSMPROJECT.vtp /C "+Chr(34)+"/GO /EXIT"+Chr(34)

	-or-

RunSMString  = "C:\MSDEV\BIN\MTRUN.EXE C:\MSDEV\SUITEMAN\SM.PCD"
OptionString = " /T C:\WBIMPORT\WBSMPROJECT.VTP /C "+Chr(34)+"/GO /EXIT"+Chr(34)

RUN RunSMString + OptionString


Hint:	Make sure you have spaces " /T C:\WBIMPORT\WBSMPROJECT.VTP /C "+  
		in front of and after the /T and /C .


From a Shortcut Icon:

C:\MSDEV\BIN\MTRUN.EXE C:\MSDEV\SUITEMAN\SM.PCD /T C:\WBIMPORT\WBSMPROJECT.VTP /C "/GO /EXIT"


The Suite Manager program has to parse the string following /C and is
looking for specific words.  

I don't think our INI file path and name is part of the plan.  So we must 
instead put all of our variables in an INC file.  So all of the variables that
were in our INI file now reside in \WBIMPORT\Include Folder\GenUtil.inc.

Because we have another set of INC files in the \WBIMPORT directory, VT may get
confused and not know which INC file to use.


Notice that \WBIMPORT\Include Folder\TDeclare.inc provides specific paths.

	'$HEADER START 'TDeclare.HDR'
	'$INCLUDE 'MSTEST.INC'
	'$INCLUDE 'C:\WBIMPORT\INCLUDE FOLDER\GenUtil.INC'
	'$INCLUDE 'C:\WBIMPORT\INCLUDE FOLDER\GenAPI.INC'
	'$HEADER STOP


Within our MSTs we nominate the path to TDECLARE.INC.  i.e. ExpMnTst.MST:

	'=============================================
	' Metacommand declarations follow
	'=============================================
	'$INCLUDE 'C:\WBIMPORT\INCLUDE FOLDER\TDECLARE.INC'



Trying to explain the relationships between the Developer Studio and the Suite
Manager is obviously difficult.  I hope the examples presented provide some
useful insight.  Get in there and play around.  



Chapter 14.	Program Driver to replace the Suite Manager.
----------

As discussed previously I see 2 benefits to the Suite Manager:

	1.  Running a list of programs
	2.  Suite Manager Logging


The Suite Manager is a TOOL.  I think it's nice because it provides an 
interface for VT to run suites of programming and provides choices for
logging.  It makes it fairly easy to accomplish those goals.

The downside is that it is resource hungry and inflexible. How about trying to 
use it to do network load tests?

Let's say that you would like to run a suite of tests on 20 networked machines
simultaneously.  If you wanted to use Suite Manager I would assume that you 
would have to install it on 20 machines.

What if you want to run programs that are not in contiguous directories?

The point is that the Suite Manager has its place but there may be times that
something else with more flexibility is required.

If that is the case, we can duplicate the Suite Manger's functionality with 
our own program, after all Suite Manager is written in VT.


Here's our SPEC:

	1. Easy to use
	2. Has all the Suite Manager Logging Functions
	3. Can run a designated list of programs
	4. As transportable as any VT program
	5. Consume a minimal amount of resources


Custom Logging Functions
------------------------

Let's start out with the most difficult part.  VT provides all of the functions
necessary to duplicate the Suite Managers logging functions.  It's only a matter
of figuring out how to use them.


Setting our Options
-------------------

In Suite Manager:
-----------------

	View\Opions
	Select the Logging Tab

	File Name:       c:\WBImport\Status.log
	Option:          Flat File
	Detail Level:    3
	Product Version: 1.0.0.1
	Machine Name:    RICHSWKS (This is my machines name put in YOURS)
	Language: 	     U.S. English



In RunPrg.mst:
--------------

	In our GenUtil.inc we create the following GLOBAL variables:

		Global SMLogPath            As String    ' Status.log
		Global TheProductVersion    As String    ' Product Version
		Global TheMachineName       As String    ' Machine name

	This is because down the road we may wish to use these variables in programs
	besides RunPrg.mst


	In out WBImport.ini file we add the following variable:

	; Suite Manager Log Path
	SMLogPath = C:\WBIMPORT\STATUS.LOG


	And put this section of code in our SetDefaults() procedure:

 	SMLogPath = ReadIni(TheIniFile,SectionName,"SMLogPath")
	' Test value returned
	If SMLogPath = "NotFnd" Then
		SetDefaults = False
		LogWrite("1     Can't find SMLogPath in: " + TheIniFile+"!",LogPath,CritErrPath)
		Exit Function
	End If


	Having done that we create or initialize the remaining variables:

	Const TheLogOutputType   = LogTypeFile  ' Flat File
	Const TheMinDetailLevel	 =	3	  ' Severity level
	TheProductVersion        = sFileVersion(ExplorerName) ' Product Version
	TheMachineName           = GetMyComputerName  ' Machine name
	Const TheLanguage        = "US English"	' Language



Let's examine these variables one by one.


TheLogOutputType
----------------

	Const TheLogOutputType   = LogTypeFile  ' Flat File
	' Const TheLogOutputType   = LogTypeDatabase	    ' Database
	' Const TheLogOutputType   = LogTypeViewPort	    ' Viewport
	' Const TheLogOutputType   = LogTypeNoLogging	' No Logging


The TheLogOutputType variable defines how are output will be done.  I've set it
up to log to a Flat File (Text).

I've remarked out the other choices for you.  You can only choose one.  This is
to make it easier if you change your mind later.

The following variables are are defined within VT:

		LogTypeFile
		LogTypeDatabase
		LogTypeViewPort
		LogTypeNoLogging

Somewhere internally within VT those words are defined, if you do a query 
search in help it will explain in more detail.   The point is that these are 
your only choices.


TheMinDetailLevel
-----------------

	Const TheMinDetailLevel	 =	3	  ' Severity level

You can use whatever severity level you wish.  3 just seems like a good number 
to me.  We discussed severity levels previously


TheLanguage
-----------
This is pretty meaningless to me, I always put in U.S. English.  If you use a
different language it's OK with me.


TheMachineName
--------------

	TheMachineName = GetMyComputerName  ' Machine name

To get our workstations name we need to use an API call GetComputerName().
I've included a wrapper GetMyComputerName() which I've placed in the GenAPI.inc.


TheProductVersion
-----------------

	TheProductVersion = sFileVersion(ExplorerName) ' Product Version

To get the Version Number of the application being tested we need to use a few 
API calls.  I've included a wrapper sFileVersion() which I've placed in the 
GenAPI.inc.  You need only pass the path and file name to receive that file's 
version number.

sFileVersion() is a very handy function, thanks to all the smart folks on the
Eskimo group for helping me figure it out.  Don't ask me to explain it, look
at the code, scratch your head, maybe it'll come to you.


TrapLog()
---------

Our next step is to create a pointer to our TrapLog() procedure.

	' Create pointer to procedure to trap events in
	' Custom Logging Features
	Global FunctionPointer As Pointer To Sub (Action As Long, Text As String)
	FunctionPointer = VarPtr(TrapLog)

The TrapLog() procedure is well documented inside of RunPrg.mst.  I've never
figured out any real useful purpose for it.  It's mentioned in the documentation
about the Custom Logging Features, so I include it for reference.  There it is.
Play with it.  If you come up with a good reason for it let me know.



LOGINITSTRUCT
-------------

All of the information that we've collected is now entered into a structure.

	' Construct structure for Custom Logging Features
	' The variables on the right side of the equation 
	' are defined as variables above.
	Global Lis As LOGINITSTRUCT
		Lis.LogOutputType	  = TheLogOutputType
		Lis.MinDetailLevel	  =	TheMinDetailLevel
		Lis.ProductVersion	  =	TheProductVersion
		Lis.MachineName		  =	TheMachineName
		Lis.Language		  =	TheLanguage
		Lis.LogLocation		  =	SMLogPath
		Lis.NotificationProc  = FunctionPointer


From the the online help article:  "Initializing the Logging Engine"

"The predefined LOGINITSTRUCT structure is used to initialize the logging 
engine. It contains information about where and how logging is done." 

So VT defines this structure specifically and what we do is "fill in the 
blanks".

If our code works properly that information will be reported in our Suite
Manager log something like this:


	[Start Suite Header]
	[Suite Name]         WBImport
	[Machine]            RICHSWKS
	[Start Time]         08/05/1997 13:27:52
	[End Suite Header]

	  [Start Case Header]
	  [Case Name]         C:\WBImport\TstExp04.MST
	  [Product Version]   1.0.0.1
	  [Language]          U.S. English
	  [Machine]           RICHSWKS
	  [Start Time]        08/05/1997 13:27:53
	  [End Case Header]



Reviewing Setting our Options
------------------------------

The instructions for setting the options in Suite Manager are quite a bit 
simplier than all of the programming we just went through.

We just duplicated much of what has to go on behind the scenes of the Suite
Manager.

Be doing so we've actually "one-upped" it a bit by AUTOMATING getting our 
computers name and the version number of the application that we're testing.

This automation aspect is important in our network environment.  If we're
dealing with 200 machines, our program will figure out the workstation's name 
and the product version.

Our error logging can now write the specific workstation's name to the report
and all errors tied to that machine.


The Configuration File
----------------------
Our Specification says that we must be able to run a designated list of 
programs in a specified order.

There are a number of ways to do this.  We could establish a directory and
create a FILELIST array.  i.e. RUN everything in this directory that ends with
.MST.

The problem with this approach is trying to get the list in the order that you
want.  You'd have worry about the names of all the files so that they would be
in the right order.  You may have to add and subtract files from the directory.

We could build an interface that the user could drag the desired files or click
on them to create a list.  Coding interfaces with VT is a real pain.  In my 
opinion if you want to create an interface, use Visual Basic.

Creating an interface with Visual Test is not a topic I would like to write 
about.

To me, using a configuration file is the easiest way.  You can edit it with any
text editor and it is easy to explain.


Here's the pertinent portion of RunPrg.cfg:

**********************************************************************************
* Program Path and Name to Run                                          Sort Order
*---------------------------------------------------------------------------------
* Column  (1 to 77)                                                        (78-79)
*================================================================================*
*<--------------- 1  to  77 ---------------------------------------------->|<--> *
*================================================================================*
*example file and path names follow ---> look here for sort order
c:\WBIMPORT\TstExp01.pcd /C C:\WBIMPORT\WBImport.INI                         1 
c:\WBIMPORT\TstExp03.pcd /C C:\WBIMPORT\WBImport.INI                         2
c:\WBIMPORT\TstExp04.pcd /C C:\WBIMPORT\WBImport.INI                         3
c:\WBIMPORT\TstExp05.pcd /C C:\WBIMPORT\WBImport.INI                         4
********************************************************************************
* End Here. Do Not Create Empty lines above or below! *
********************************************************************************

Tell people the name and location of the file, show it to them a couple of times
and that's about it.  Within the configuration file you can write whatever 
instructions you like.


Where is the configuration file?
--------------------------------

We have several options how to tell our program the path and name of our config
file.  We could put it into our GenUtil.inc as a CONSTANT.  We could read it out
of our INI file.

We need more flexibility because the location and even name of the config file
could change.

My solution is to include the path and name in the command line that calls our
RunPrg.mst


'  Syntax: \FullPATH\mtrun.exe \FullPATH\RunPrg.pcd /C "TheIniFile,TheCfgFile"
'
'  Command Line Parameters:
'
'  TheIniFile - The path and name of the RunPrg.INI
'  TheCfgFile - The path and name of the RunPrg.CFG
'  A comma must separate the command line parameters

We are already parsing out the path and name of the INI file, so it's just a 
matter of adding a few more lines of code to get the config file as well.

I created a new CheckRPCommandLine() from the CheckCommandLine function 
and put it inside RunPrg.mst.  You may wish to check how it differs from
the original.


Reading the configuration file
------------------------------
Now that we know the name and location of our config file we must read it.

Let's assume that you're convinced I'm right (you have no choice anyway).  The 
question is how do we read the configuration file and tell our program what to
do?

If you could follow lession one when we were doing all the string manipulation
stuff and low level file operations, this should be a snap.  I was setting you
up.

Our answer is that we read the configuration file line by line and put
the information into an array.


The first line of the configuration file has a validation word: "PROGRAMS"

	PROGRAMS
	********************************************************
	* File Name: RunPrg.CFG
	*
	**********************************************************************
	* Richard J. Wartner
	* 7610 North Via De Manana
	* Scottsdale, Arizona 85258-3517
	* RWartner@aol.com
	* August 1997
	**********************************************************************


Which is tested:

	' Test file for validation word
    If UCase$(Mid$(RdBuffer, 1, 8)) = "PROGRAMS" Then
        ' Loop through configuration file and insert into array


We also test for Asterisks, Blank Lines and Tab characters.

	' Ignore Asterisks and Blank Lines
	If Mid$(RdBuffer, 1, 1) <> "*" And Len(Trim$(RdBuffer)) > 0 Then
		' Test for Tabs which will screw us up
		If Instr(1,RdBuffer,Chr$(8)) > 0	Then
			LogWrite ("1   Tab char found in Configuration File online: "
			+Trim$(Str$(LineCounter))+"!",LogPath,CritErrPath)
			ReadCfgFile = 0
			Close #fh
		Exit Function


Put the program name and sort order into a temporarty array:

	' Resize array to hold next record
	ReDim Preserve TmpDriver(1 To i) As TempArray
	' Parse buffer into array fields
	TmpDriver(i).PrgName = Trim$(Mid$(RdBuffer, 1,77))
    TmpDriver(i).Order   = Trim$(Mid$(RdBuffer,78, 2))


Running the Programs
--------------------

	We sort the programs to run by taking then from our temporary array
	and putting them into a new array:

	' Resize the array to number of configuration file records
	ReDim PrgDriver(1 To TotalFileSpec) As PrgNameArray

	' Create Final Array in desired Sort Order as specified
	' in RunPrg.Cfg Order Column.
	' There is no inherent function to sort the array
	' so we have to take the records from the temp array
	' and put them into a final array in proper order.
	For RecordCounter = 1 To TotalFileSpec
		OrderNumber = Val(TmpDriver(RecordCounter).Order)
		PrgDriver(OrderNumber).PrgName = TmpDriver(RecordCounter).PrgName
	Next

So if you've sat up nights wondering how to sort an array in VT, there you go.


We then strip out command line parameters:

	' Run the programs in sorted order
	For RecordCounter = 1 To TotalFileSpec

		' Test for command line parameters
		StringCounter =  InStr(1, Trim$(PrgDriver(RecordCounter).PrgName),"/C")

		' Remove command line parameters for pathstring
		If StringCounter > 0 Then
			PathString = Trim(Left(Trim$(PrgDriver(RecordCounter).PrgName),StringCounter-1))
		Else
			PathString = Trim$(PrgDriver(RecordCounter).PrgName)
		End If

That is so that later we can check to make sure the program to run exists:

	' Test to make sure file exists
	If Exists(PathString) Then


The program is run with the appropriate command based upon its extension.

	' Test to make sure file exists
	If Exists(PathString) Then

		'Write to Status Log
		LogBeginCase(Trim$(PrgDriver(RecordCounter).PrgName))

		'Run program
		Run MTRunExePath+" "+Trim$(PrgDriver(RecordCounter).PrgName)

		'Write to Status Log
		LogEndCase
	Else
		LogWrite("1     "+PathString+ " Not Found!",LogPath,CritErrPath)
		RunThePrograms = False
		Exit For
	End If

Notice the usage of LogBeginCase and LogEndCase which tell the logging 
engine that a test case has started and stopped.


Most of the code within RunPrg.mst is similiar to what we've been doing.  Only
a few new concepts.  So I didn't review all the code in detail.


Here is the command line syntax to run our program driver:

C:\MSDEV\BIN\MSTRUN.EXE C:\WBIMPORT\RUNPRG.PCD /C C:\WBIMPORT\WBIMPORT.INI,C:\WBIMPORT\RUNPRG.CFG


So now you have a choice between running your test cases within the Suite
Manager or using a program driver.

There may be valid reasons for making that choice or personal preference.  At
least you have an option.  You decide.



Chapter 15.	Network Distribution.
-----------

Please open the NetDist project.

The NetDist Project enables automated testing at multiple workstations
simultaneously, with the primary goal to facilitate load/stress testing.

Our Test Manager is so enamoured by blinking screens he's decided hire a 
test lab in Texas with 250 workstations.  It's months away but the spec is now 
changed to include running our programs on every machine available, all at the
same time.

Somewhere I read, or someone told me that Visual Test Network Distribution 
could run/control up to 250 machines.  That could be wrong, I don't know.  
I've had it	up to about 15 and VT worked fine.  The project that was going 
to hire the test lab got called off before we got there.

The test could be as simple as starting the application simultaneously 
at increasing numbers of workstations to see if it falls over.  There are a 
number of tests that can be done to test memory useage, network and database
performance or whatever else you can think of.

It might be a good idea to ask the developers what areas in the application 
will test performance.  For instance, when does it write to the database?

That isn't our problem here, we already have scripts to run, the question is
how do we run them everywhere?

Program Driver
--------------

We already have a program driver, so the basic idea will be to launch it at
several workstations.  A good feature of this concept is that we can launch
the program driver manually at several workstations already. 

The reason I mention this is that there is a question of how programs should
be launched in the Net Dist application.  You could eliminate the program driver
by having the Client Workstation contain the program driver instead.

		A.						B.

		NetStat					NetStat
		   |					   |
		   V					   V
		RunPrg					TestScript
		   |
		   V
		TestScript


There are several ways of doing this.  I prefer A.  This way you don't have to
depend on the Net Dist application.



The project utilizes 2 configuration files:
-------------------------------------------------------
Host.Cfg:    The name of program sent to Workstations to run.
RunPrg.Cfg:  The list of programs for the Workstations (RunPrg) to run.


The project utilizes 1 additional INI file:
-------------------------------------------------------
Timer.INI contains the following variables:


[Timer]

;StartTime Syntax: HH:MM:SS AM/PM  i.e  08:15:05 AM
;==================================================
StartTime=04:53:24 PM

;StartDate Syntax: MM/DD/YY
;==================================================
StartDate=04/22/97

TimerTrigger=\\TestDirector\NETTEST\Timer.TRG
ProgramToRun=\\TestDirector\NETTEST\NetStat.PCD



Project Programs:
-------------------------------------------------------

StartHst.Pcd
-------------
StartHst.Pcd resides on the Host Workstation or File Server
It updates Timer.INI and starts NetHost.Mst.
This program is used as a convenience.

NetHost.Pcd
------------
NetHost.Pcd resides on the Host Workstation or File Server
to communicate commands to the workstations.

NetStat.Pcd
------------
NetStat.Pcd is run from the Client Workstations to communicate
with the Host and on command initiates the desired test programs.

Timer.Pcd
-------------
Timer.Pcd is run from the Client Workstations to start NetStat.Mst
at the desired time.

Timer.Pcd reads the Timer.INI for instructions.
A start time and a trigger file are necessary for it to launch programs.

RunPrg.Pcd
-----------
RunPrg.Pcd is run from the Client Workstations to launch the desired
test programs.

RunPrg.Pcd reads RunPrg.cfg for the list of programs to be run.


========================
Project Diagram:
========================

            StartHst			 <-- StartHst updates the Timer.INI and launches
			   |                     NetHost.mst and clicks NetHost's buttons.
			   |
			   |
			   |
		  Host (TestDirector Workstation)				
               |
               |			     <-- Host reads configuration file for program
               |                     path and name to send to Stations.
               |
		       |                 <-- Host and Stations make communication link.
               |                     Host tells Stations what program to run.
               |                     When all are connected Host sends command
		       |                     to all Stations to run test programs.
               v
     -------------------
	  |         	  |
	  |         	  |
	Timer           Timer        <-- Timer reads INI file for Instructions.
	  |		          |              Timer launches Station communication program
	  |	              |              at desired time to link with Host.
      v               v
  Workstation1      Workstation2 <-- When Station receives command from Host
	  |	 		      |              Station launches RunPrg.
	  |			      |	   
      v               v   
    RunPrg          RunPrg       <-- RunPrg reads RunPrg.Cfg for list of programs.
      |               |              RunPrg runs test programs.
	  |               |	   
      v               v
  Test Programs   Test Programs  <-- Test scripts are run. 




Pathing Problems
----------------
Because we're in a network environment, we must rely on UNC names for the 
location of files.

Our file names and directory paths are nominated as variables in our INI, INC
and CFG files.  If these paths are not set properly the programs obviously will
not be able to find them and you will get errors.

The concept here is that all of the important files are to be located on one
computer, typically the Host.  This makes maintenance and support much easier.
There is no reason to copy your programs for instance on all of the client
workstations.  

If your paths are set properly, the only files that need to be copied to the 
workstations are:

		MSTEST40.DLL
		MSVCRT40.DLL


They should be copied to the \WINDOWS\SYSTEM directory or a directory that is 
in the workstation's path statement.


So what path names do we have to worry about?

	1. MTRUN.EXE, MT.EXE
	2. All PCDs to be run at our client workstations
	3. INI and CFG files
	4. Centralized logging directory


All of the above can/should be located on one Host computer.  

The centralized logging directory is a directory on the host computer where 
the client workstations either send or write logging files to.  This may be in 
addition to any files that a workstation may maintain on its local drive.

For instance, we may have 20 clients in the test.  Each writes an error.log
locally.  At the end of the test a copy of the individual logs are sent to the 
centralized logging directory.  This of course makes it easier to examine them.

The only problem is that the error logs must be given unique names or you will
end up with only one.
					 
Statistics logs such as timings, memory useage and pass/fail may be kept as one
file each on the host.  Each workstation could append data to the files.


Share Names
------------
The method I use is assigning share names to directories on the host.

	1.  Assign a name to the Host Computer
	2.  Assign share names to important directories on the Host Computer

Let's assume that our Host computer's name is "TestDirector".  It's path name
would then be \\TestDirector.

Here is a list of directories and sample share names:

	Directory on Host computer - Share Name - Useage
	==========================   ==========   ========
	C:\WBIMPORT\CTLOGDIR\		 CtLogDir	  Centralized Logging Directory
	C:\WBIMPORT\				 NetTest	  Where VT scripts are kept
	C:\MSDEV\BIN\				 MsDevBin     Visual Test (MTRUN.EXE) location


Sample syntax examples using share names:
-----------------------------------------

RunPrg.pcd:

\\TestDirector\MsDevBin\mtrun.exe \\TestDirector\NetTest\RunPrg.pcd _
\\TestDirector\NetTest\WBImport.ini,\\TestDirector\NetTest\RunPrg.cfg

NetStat.pcd:

\\TestDirector\MsDevBin\mtrun.exe \\TestDirector\NetTest\NetStat.pcd _
\\TestDirector\NetTest\WBImport.ini

RunPrg.cfg:

**********************************************************************************
* Program Path and Name to Run                                          Sort Order
*---------------------------------------------------------------------------------
* Column  (1 to 77)                                                        (78-79)
*================================================================================*
*<--------------- 1  to  77 ---------------------------------------------->|<--> *
*================================================================================*
*example file and path names follow ---> look here for sort order
\\TestDirector\NetTest\TstExp01.PCD /C \\TestDirector\NetTest\WBIMPORT.INI   1 
\\TestDirector\NetTest\TstExp03.PCD /C \\TestDirector\NetTest\WBIMPORT.INI   2
\\TestDirector\NetTest\TstExp04.PCD /C \\TestDirector\NetTest\WBIMPORT.INI   3
\\TestDirector\NetTest\TstExp05.PCD /C \\TestDirector\NetTest\WBIMPORT.INI   4
********************************************************************************
* End Here. Do Not Create Empty lines above or below! *
********************************************************************************

Timer.ini:

This is the path and name of your trigger file
==================================================
TimerTrigger=\\TestDirector\NetTest\Timer.TRG

This is the path and name of the program you want the timer to run
==================================================
ProgramToRun=\\TestDirector\NetTest\NetStat.PCD


So the idea is that any path that is referenced by a client workstation uses 
its network name.  Any file that is used to retrieve these path variables
must be changed accordingly.



Assigning Network path variables
---------------------------------
All of the pathing variables are retrieved from the WBImport.ini file but the
paths within the [DEFAULT] sections are all for a local drive.

The [NetDist] section was created for Network Distribution variables.

SetNDDefaults() was created to retrieve variables from this section and is 
located in GenUtil.inc


Network Distribution Variables
------------------------------
A number of global variables including the following, were created and placed
in the GenUtil.inc files.  It would be possible to create a separate INC file
for them if you wish.

'============================================================
' Network Distribution Variables
'============================================================
Global MainComputerName     As String   'Name assigned to Host Computer 
Global ProjectDirPath		As String	'Dir where scripts are located
Global RunPrgCfgFile        As String	'RunPrg CFG File Path and Name
Global TimerIniPath 		As String   'Timer INI Path and Name
Global NetHostPCDPath		As String	'NetHost.pcd Path and Name
Global NetHostCfgFile		As String	'NetHost Config File Path and Name
Global CopyLogDir			As String	'Dir for Remotes to copy log files 
Global TimeSyncServer		As String   'Computer to sync net time with
Global intUseStationCount   As Integer  'True/False variable
Global intfActive           As Integer  'Loop variable
Global intHostSleepTime		As Integer  'Amount of time to sleep
Global intNetDist           As Integer	'Are we in Network Load Test
Global intStationID         As Integer	'WorkStation ID given by Host

Etc.


StartHst.pcd
------------
StartHst.pcd is used as a convenience by automating NetHost.pcd at the Host 
computer.  It updates Timer.INI with today's date and a time two minutes from 
now.

It then runs NetHost.pcd and answers its dialogs with default answers.

Timer.pcd
---------
A gut level question is if I have 200 client workstations, how do I start them 
all, how to I tell them what do do, etc?

Timer.pcd reads the Timer.INI file for those instructions.  Timer.pcd is meant 
to be launched from a startup mechanism, such as the startup directory or from 
an icon manually.

In a testroom with 20 client workstations, it is fairly easy to click on 20 
Timer shortcut icons.  2 Minutes later the Timer simultaneously launch the 
desired program at all the workstations.

If StartHst.pcd is used at the Host the Timer.ini file variables will be set
properly.

So first click on StartHst then click on all the Timers.

As a fail-safe mechanism Timer.pcd requires a Trigger file to enable it.  The
trigger file is a text file specifically named and located as a check.  This is 
so the Timer could be set to go off every day but can be turned off/on by 
adding, renaming or removing the trigger file.



NetHost and NetStat.pcd
-----------------------
NetHost.pcd is the controlling mechanism at the Host workstation.

NetHost and NetStat establish contact on network, when contact is made NetHost
sends commands to NetStat that result in test program being run at the client
workstations.

The essential problem with NetHost and NetStat are CALLBACK FUNCTIONS.	
Pointers to procedures are known as CALLBACKS. 

	The following example shows how you can use a callback in your Test language 
	code:
  
	DIM funcptr AS POINTER TO FUNCTION (p1 AS INTEGER, p2 AS STRING) AS LONG

	funcptr = varptr(MyFunc)
	PRINT funcptr(1,"Hello")
	END

	FUNCTION MyFunc(x AS INTEGER, y AS STRING) AS LONG
		  MyFunc = x + len(y)
	END FUNCTION 

  
Using callback procedures NetHost and NetStat each establish a subroutine in 
memory and tell each other the location.  It is through this mechanism they 
are able to communicate.


	Host Message Handler function:

Declare Sub HostMsgHandler(Msg%,HostID&,StationID%,lDataType&,pData As Variant)


	Client Message Handler function:

Declare Sub StationMsgHandler(Msg%,HostID&,StationID%,lDataType&,pData As Variant)



The problem with this is that you never know when they are going to talk to
each other.  You cannot for instance, step through the program and debug the
callback routine.

The Host says "I'm here",  your 200 client workstations reply when they're 
ready.

Controlling the callback routines to work within a predictable sequence of 
events is the challenge.

A method of controlling this is by using LOOPs at the Host and Client.


At the Host
-----------

	 ' Establish Host system
	HostID = TalkCreateHost(HOST, VarPtr(HostMsgHandler))

	If HostID <> 0 Then
		LogWrite("0     Host established.",LogPath,CritErrPath)
		StatusBox "Host established - Start all Station Programs."
		' Wait for stations to connect
		Sleep 30
		StatusBox Close
	Else
		LogError("Can't establish Host in: "+CurPrgName+"!",0,LogPath,CritErrPath)
		LogClose(LogPath,CritErrPath)
		LogWrite("",LogPath,CritErrPath)
		End
	End If

	' Enter loop until escape is pressed or timeout value lapsed
	intfActive = False
	Do Until intfActive
		Sleep .1
		' False means not using Station Count - Timeout value instead
		' This is set in INI file
		If intUseStationCount = False Then
			LogWrite("0     Using AutoRun.",LogPath,CritErrPath)
			AutoRun
		End If
	Loop


At the Client
-------------

	' Connect station to Host
	HostID = TalkCallHost(Host, VarPtr(StationMsgHandler))

	If HostID <> 0 Then
		LogWrite("0     Successfully Connected to Host.",LogPath,CritErrPath)
		StatusBox "Successfully Connected to Host."
	Else
		StatusBox "Error connecting to Host!"
		LogError("Error connecting to Host!",0,LogPath,CritErrPath)
		LogClose(LogPath,CritErrPath)
		LogWrite("",LogPath,CritErrPath)
	End If

	' Loop until run instructions are received from Host
	intRunWait = False
	Do Until intRunWait 
		StatusBox "Waiting for Run Command from Host."
		Sleep 2
	Loop



While both programs are waiting in the loop they continue to communicate 
with each other via their respective message handler routines.

The loops prevent both programs from going on to the SYNC command.

The client comes out of the loop after it receives Data (the filename to run).
It then	does TalkWaitSync() and times-out waiting for a reply.

When all the clients check in with the TalkWaitSync() messages the Host sends
the Sync message TalkReleaseSync().

The TalkWaitSync() function has a time-out parameter so it in effect is another
loop causing the Client to wait for the sync message as a response.
																   								   
When the clients receive the sync message, they run the Program Driver and 
everybody hangs up.


							Sequence of Events:
							-------------------

	Host											Client
	----											------
	1. TalkCreateHost()				------> 		TalkCallHost()
	intfActive = False								
	Do Until intfActive								


	HostMsgHandler()				<------ 		2. TalkCallHost()
													intRunWait = False
													Do Until intRunWait

	3. Case TALK_CONNECT
		SendDataToStation()
			TalkSendData()			------>			StationMsgHandler()
													Case TALK_DATA
													intRunWait = TRUE


	HostMsgHandler()				<------			4. TalkWaitSync()


	5. Case TALK_SYNC
			MsgBox "All Stations Ready"
				TalkReleaseSync()	------> 		TalkWaitSync()
				HangupHost()						RunData
													TalkHangUp()

													
	ReceiveDataFromStation()		<------			6. TalkCallHost()
													TalkSendData()
													"FileToRun ran 
													successfully."


	7. When all stations report in END program
	
					TalkHangUp()    ------>			StationMsgHandler()
													8. Case TALK_HANGUP
														TalkHangUp()
														End





 So there you have it.  I hope it works.  If not we're the Test Team.


--------------------------------
Section 4:  More Windows programming
--------------------------------


Chapter 16.	Comparing Bitmaps
----------

There were some recent questions about using the bitmap compare functions.

The first step is capturing a region of the screen with the VT Screen Utility
of a designated size.

You then can "walk" a box (RECT) of the same dimensions around the screen. 
Each iteration you take a picture of the screen and compare it to the saved 
comparison image.

This is accomplished with an inner and outer loop.
1 for rows and the other for columns.

	RowEnd  = 10
	ColEnd  =  4

	For RowCounter = 1 To RowEnd
		For ColCounter = 1 To ColEnd
		Next
	Next

The loop counters increment the RECT changing its boundaries or position on the
screen.

In my example I had to "walk" around in a grid.  My comparison image was a 
blank image (of the proper color) with the same dimensions as the grid cells.  
My situation was True/False (blank or not).  So I would take a picture of each 
cell then compare it to the blank image.

The client insisted on pictures within the cells (invisible to Visual Test but 
aesthetically pleasing) and that's why I had to use screen comparison.

One challenge was that the width of the columns changed.  i.e.  2 and 4 were 
wider than 1 and 3 or something like that.   So I needed 2 comparison files 
(1 for each width) And then of course the developers would fiddle with the 
control every other week and throw everything off by a couple pictels.


' ==========================================================
' Bitmap Image Comparison program variables
' ==========================================================
' Initialize Image locations and names
Const OldImageNameBlank1 = "Res1B"     ' Blank Width 54
Const OldImageNameBlank2 = "Res2B"	   ' Blank Width 53

Global OldImageFileName As String
OldImageFileName = ProjectDirPath+"ResX.SCN"

Global NewImageFileName As String
NewImageFileName = ProjectDirPath+"ResXNew.SCN"

' Create Array of Bitmap Image Names for Room Types
Global ImageNameArray (4) As String

ImageNameArray(1) = "KNS"
ImageNameArray(2) = "KS"
ImageNameArray(3) = "DDNS"
ImageNameArray(4) = "DDS"

' Dimension Region coordinate variable
Global CurScnReg  As RECT

' Current Dimensions of Selection Bitmap
CurScnReg.wLeft   =	524	  ' Left   Boundary
CurScnReg.wTop    =	405	  ' Top    Boundary
CurScnReg.wRight  =	578	  ' Right  Boundary
CurScnReg.wBottom =	420	  ' Bottom Boundary


Declare Function CompareBitmaps(TheOldImageFileName$,TheNewImageFileName$,TheOldImageName$,TheNewImageName$,rLeft&,rTop&,rRight&,rBottom&) As Integer


'**************************************************************************
' Function CompareBitmaps
'
' Compares 2 Images
'
' Returns:
'	Integer containing True (-1) if the images are different or
'	False (0) if they are the same
'
' Parameters:
'	TheOldImageFileName - Screen File where Old Image is contained
'	TheNewImageFileName - Screen File where New Image is to be contained
'	TheOldImageName		- Name of Old Image
'	TheNewImageName		- Name of New Image
'	rLeft				- Image Left   Boundary
'	rTop				- Image Top    Boundary
'	rRight				- Image Right  Boundary
'	rBottom				- Image Bottom Boundary
'
'**************************************************************************
Function CompareBitmaps(TheOldImageFileName$,TheNewImageFileName$,TheOldImageName$,TheNewImageName$,rLeft&,rTop&,rRight&,rBottom&) As Integer

	Dim RetInteger As Integer

	' Pass in dimensions of region to retrieve bitmap
	CurScnReg.wLeft   =	rLeft
	CurScnReg.wTop    =	rTop
	CurScnReg.wRight  =	rRight
	CurScnReg.wBottom =	rBottom

	' Write coordinates to Logfile
	LogWrite("0     Retrieving Image from Coordinates:")
	LogWrite("0     Left:   "+Trim(Str(rLeft)))
	LogWrite("0     Top:    "+Trim(Str(rTop)))
	LogWrite("0     Right:  "+Trim(Str(rRight)))
	LogWrite("0     Bottom: "+Trim(Str(rBottom)))

	' Retrieve new bitmap image
	ScnCaptureScreen(TheNewImageFileName,TheNewImageName,CurScnReg,False)

	' Compare new bitmap image to Blank Bitmap image
	RetInteger = ScnCompFiles(TheOldImageFileName,TheOldImageName,TheNewImageFileName,TheNewImageName,False)
	' Returns An INTEGER containing TRUE (-1) if the images are different,
	' or FALSE (0) if they are the same.
	' If False that means it is blank and we want to check it

	' Return True (-1) or False (0)
	CompareBitmaps = RetInteger

End Function
'**************************************************************************


Here's a sample program utilizing CompareBitmaps():



'**************************************************************************
' Function SelectRoomType Finds available room type and selects it
'
' Returns True if successful
'
'**************************************************************************
Function SelectRoomType(NetTest As Integer, GuestCounter As Integer) As Integer

	Dim ReturnInteger        As Integer
	Dim ReturnVariable       As Integer
	Dim ColCounter           As Integer
	Dim RowCounter           As Integer
	Dim RowEnd               As Integer
	Dim ColEnd               As Integer
	Dim HOffSet              As Long
	Dim VOffSet              As Long
	Dim HAmount              As Integer
	Dim VAmount              As Integer
	Dim ControlHandle        As Long
	Dim EndCalendarTime      As Double
	Dim	OddColAmount         As Integer
	Dim EvenColAmount        As Integer
	Dim	TopToBottom          As Integer
	Dim	StartLeft            As Integer
	Dim	StartRight           As Integer
	Dim	StartTop             As Integer
	Dim	StartBottom          As Integer
	Dim FoundRoom            As Integer
	Dim NewReservationWindow As Long

	' Initialize ReturnInteger to False
	ReturnInteger = False

	' Search for Reservation window	to make sure we got there
	NewReservationWindow = WFndWndC(UCase$(RGSDriver(GuestCounter).FirstName) +" "+UCase$(RGSDriver(GuestCounter).LastName),ResWinClass, FNDWINDFLGS, (FindWindowTimeout))
	If NewReservationWindow = 0 Then
		LogError("Can't find Reservation window for "+UCase$(RGSDriver(GuestCounter).FirstName) +" "+UCase$(RGSDriver(GuestCounter).LastName)+"!",1)
		ReturnInteger = False
		SelectRoomType = ReturnInteger
		Exit Function
	End If

	' Test to make sure Image File exists
	If Not Exists(OldImageFileName) Then
		LogWrite("1     "+OldImageFileName+" doesn't exist!")
		ReturnInteger = False
		SelectRoomType = ReturnInteger
		Exit Function
	End If

	' Check Screen Resolution
	ReturnInteger = CheckTheResolution(800,600)

	If ReturnInteger = False Then
		LogWrite("1     Resolution Error - screen is not 800x600!")
		SelectRoomType = ReturnInteger
		Exit Function
	End If

	' Determine number of Rows in Grid
	If OleCtlExists(ResCusGrdTable,(FindErrorTimeout)) Then
		If OleCtlHasProperty(ResCusGrdTable,"Rows",(FindErrorTimeout)) Then
			RowEnd = OleCtlGetProperty(ResCusGrdTable,"Rows",(FindErrorTimeout))-1
			' Test RowEnd value
			If RowEnd = 0 Then
				LogWrite("1     Number of Rows returned as ZERO!")
				ReturnInteger = True
				SelectRoomType = False
				Exit Function
			End If
		Else
			LogError("Can't find "+ResCusGrdTable+" number of Rows!",1)
			ReturnInteger = True
			SelectRoomType = False
			Exit Function
		End If
	Else
		LogError("Can't find "+ResCusGrdTable+"!",1)
		ReturnInteger = True
		SelectRoomType = False
		Exit Function
	End If

	' ----------------------------------------------------------------------
	' Room Selection Grid NOTES:
	' ----------------------------------------------------------------------

	' column		1	2	3	4
	' Room Type   	KNS KS DDNS DDS
	' ------		--- --- --- ---
	' Left			512 566	619	673		  Sample RECT Coordinates
	' Top			379	379	379	379		  
	' Right			566	619	673	726
	' Bottom		394	394 394	394
	'
	' Col 1 Spacing = 54 (566-512)		  EvenColAmount
	' Col 2 Spacing = 53 (619-566)		  OddColAmount
	' Col 3 Spacing = 54 (673-619)
	' Col 4 Spacing = 53 (726-673)
	' Space from Top to Bottom = 15	(394-379) TopToBottom Amount

	' If the developers change the width of the Bitmap Image
	' the Screen file (OldImageFileName "ResX.SCN") must be changed accordingly.

	' OldImageNameBlank1 "Res1B" and OldImageNameBlank2 "Res2B" widths
	' must be adjusted.  These images are saved as screen files not as bitmaps.

	' The Visual Test Screen Utility program is used to make these
	' adjustments.

	' We compare to the blank image so..
	' ADJUST THE BLANKS (Res1B & Res2B)!
	' ----------------------------------------------------------------------
 
	' Initialize Horizontal and Vertical increment values

	' These values are added to move right and down through columns and rows
	OddColAmount  = 54
	EvenColAmount = 53		  ' See Table above for how amounts are derived
	TopToBottom   = 15

	' These values are additionally added to click within middle of space
	HAmount	 = 20
	VAmount  = 8

	' Remember beginning RECT values to reset when complete
	StartLeft   = CurScnReg.wLeft 
	StartRight  = CurScnReg.wRight
	StartTop    = CurScnReg.wTop 
	StartBottom = CurScnReg.wBottom

	' Compare Bitmap Images and find first Blank (available room)
	For RowCounter = 1 To RowEnd

		For ColCounter = 1 To 4

			If ColCounter = 1 Then
				' Use starting position
				If RowCounter = 1 Then
					' Allow for difference in column width
					If ColCounter = 1 Or ColCounter = 3 Then
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank1,ImageNameArray(ColCounter),CurScnReg.wLeft,CurScnReg.wTop,CurScnReg.wRight,CurScnReg.wBottom)
					Else		
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank2,ImageNameArray(ColCounter),CurScnReg.wLeft,CurScnReg.wTop,CurScnReg.wRight,CurScnReg.wBottom)
					End If
								
					If ReturnInteger = False Then
						FoundRoom = True
						LogWrite("0     Found Available Room Type: "+ImageNameArray(ColCounter)+".")
						' Change horizontal and vertical offsets to middle of space
						HOffSet = CurScnReg.wLeft + HAmount
						VOffSet = CurScnReg.wTop  + VAmount
						' Mouse Click in Blank space
						Play "{Click "+ Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet))+" }"
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Clicked Coordinates: "+Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet)))
						Exit For
					Else
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Room Type: "+ImageNameArray(ColCounter)+" not available.")
						LogWrite("0     ************************")
					End If
				' Rows > 1
				ElseIf FoundRoom = False Then
					' Deal with different widths of columns
					If ColCounter = 1 Or ColCounter = 3 Then
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank1,ImageNameArray(ColCounter),CurScnReg.wLeft,CurScnReg.wTop,CurScnReg.wRight,CurScnReg.wBottom)
					Else		
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank2,ImageNameArray(ColCounter),CurScnReg.wLeft,CurScnReg.wTop,CurScnReg.wRight,CurScnReg.wBottom)
					End If			
					If ReturnInteger = False Then
						FoundRoom = True
						LogWrite("0     Found Available Room Type: "+ImageNameArray(ColCounter)+".")
						' Change horizontal and vertical offsets to middle of space
						HOffSet = CurScnReg.wLeft + HAmount
						VOffSet = CurScnReg.wTop  + VAmount
						' Mouse Click in Blank space
						Play "{Click "+ Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet))+" }"
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Clicked Coordinates: "+Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet)))
						Exit For
					Else
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Room Type: "+ImageNameArray(ColCounter)+" not available.")
						LogWrite("0     ************************")
					End If
				End If
			
			' Columns 2-4
			ElseIf FoundRoom = False Then
				' Row 1
				If RowCounter = 1 Then
					' Deal with different widths of columns
					If ColCounter = 3 Then
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank1,ImageNameArray(ColCounter),CurScnReg.wLeft+OddColAmount,CurScnReg.wTop,CurScnReg.wRight+OddColAmount,CurScnReg.wBottom)
					ElseIf ColCounter = 2 Or ColCounter = 4	Then	
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank2,ImageNameArray(ColCounter),CurScnReg.wLeft+EvenColAmount,CurScnReg.wTop,CurScnReg.wRight+EvenColAmount,CurScnReg.wBottom)
					End If			
					If ReturnInteger = False Then
						FoundRoom = True
						LogWrite("0     Found Available Room Type: "+ImageNameArray(ColCounter)+".")
						' Change horizontal and vertical offsets to middle of space
						HOffSet = CurScnReg.wLeft + HAmount
						VOffSet = CurScnReg.wTop  + VAmount
						' Mouse Click in Blank space
						Play "{Click "+ Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet))+" }"
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Clicked Coordinates: "+Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet)))
						Exit For
					Else
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Room Type: "+ImageNameArray(ColCounter)+" not available.")
						LogWrite("0     ************************")
					End If
				' Rows > 1
				ElseIf FoundRoom = False Then
					' Deal with different widths of columns
					If ColCounter = 1 Or ColCounter = 3 Then
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank1,ImageNameArray(ColCounter),CurScnReg.wLeft+OddColAmount,CurScnReg.wTop,CurScnReg.wRight+OddColAmount,CurScnReg.wBottom)
					Else		
						ReturnInteger = CompareBitmaps(OldImageFileName,NewImageFileName,OldImageNameBlank2,ImageNameArray(ColCounter),CurScnReg.wLeft+EvenColAmount,CurScnReg.wTop,CurScnReg.wRight+EvenColAmount,CurScnReg.wBottom)
					End If			
					If ReturnInteger = False Then
						FoundRoom = True
						LogWrite("0     Found Available Room Type: "+ImageNameArray(ColCounter)+".")
						' Change horizontal and vertical offsets to middle of space
						HOffSet = CurScnReg.wLeft + HAmount
						VOffSet = CurScnReg.wTop  + VAmount
						' Mouse Click in Blank space
						Play "{Click "+ Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet))+" }"
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Clicked Coordinates: "+Str$(Cint(HOffSet)) + "," +Str$(Cint(VOffSet)))
						Exit For
					Else
						LogWrite("0     Row: "+Trim(Str(RowCounter))+" Col: "+Trim(Str(ColCounter)))
						LogWrite("0     Room Type: "+ImageNameArray(ColCounter)+" not available.")
						LogWrite("0     ************************")
					End If
				End If
			End If
		Next
	
		' Reset RECT values for each row
		CurScnReg.wLeft	  = StartLeft
		CurScnReg.wRight  = StartRight
		CurScnReg.wTop	  = CurScnReg.wTop	  + TopToBottom
		CurScnReg.wBottom = CurScnReg.wBottom + TopToBottom
	
	Next

	' Determine ReturnValue
	If ReturnInteger = False Then
		ReturnVariable = True
	Else
		ReturnVariable = False
	End If

	' Test if we found room
	If FoundRoom = False Then
		ReturnVariable = False
		LogError("Error Selecting Room Type!",1)
	Else
		ReturnVariable = True
	End If


End Function
'**************************************************************************



Chapter 17.	 DIPA (Direct In-Process Access)
----------

The following chapter was written by:

Jaek Smith
jaeksmith@usa.net

Thanks from all of us Jaek, this is interesting stuff!

DIPA.zip contains all the source code for this chapter.


DIPA (Direct In-Process Access)

VisualTest has quite an array of functions for handling common built in 
controls.  Sometimes a unique control can be handled by letting VisualTest 
know about it's different class name.  If these higher level control functions 
fail you can often use lower level mouse and input based functions to achieve 
the desired results.  VisualTest even provides methods to access IDispatch 
functions of OLE based controls.  Sometimes, however, you come across a control 
that you just can not get VisualTest to work with.

Such a situation occurred recently during a testing project I had been working 
on.  Our client's application was being built using a few third party 
components.  One of these was a tree control which would not respond to 
VisualTest's WTree* functions (even after trying to add it's class using 
WTreeSetClass).  At first I toyed with the idea of writing some functions to 
control the mouse directly (moving and clicking).  This worked for simple tests 
where we knew the layout of the data that would be present, however it soon 
became apparent that this would not be enough.  We needed to be able to find 
out structural information about the data in the control.  We were also going 
to need to be able to query the contents (lines of text) of the control.  
Basically, we needed to be able to access the control's API.

Now, VisualTest allows you to declare functions that exist in a DLL, thus 
allowing you to call on the DLL directly.  This allowed us to do some more 
extended work with the tree control such as querying the number of rows in 
the control.  However, because the control was in a different process space, 
the calls to query for the text of a line crashed the application with a 
memory access violation (as expected).  Thus, we needed to be able to make 
these same calls, but we needed to be able to make them in the process that 
contained the control and to be able to return the results to VisualTest.

Thinking this out more brought out the notions that this would require support 
on both the VisualTest side and on the application's side.  I would create a 
DLL that would be loaded by the application and would be called directly by 
VisualTest.  The DLL would setup a hidden window for event based communication 
and used a shared memory segment for sending the actual data.  In order to 
make the affect on the application minor, I setup the DLL to handle 
initialization immediately when it was loaded and cleanup as it was being 
unloaded.  This meant that the application only needed to do a single 
LoadLibrary call to get the DLL was setup and ready to go.  All other 
operations were done transparently to the application (since the calls were 
based on windows messages, the operations were handled when a message arrived). 
I also consulted with our client to make sure they were willing to do this 
(under our arrangement I was not to see any of their source code, so they 
added the LoadLibrary call - that was all the effort required from them).

Once I had the basic communications methods setup and working, it was time 
to make it do something.  The ultimate test was to be able to retrieve the 
text of a line of the tree control.  Since this method handled the 
communications between the two instances of the DLL, to create a seemless 
operation, two pieces need to be setup.  The first piece is the exported 
DLL function which VisualTest can declare and call.  This is the piece which 
provides a new function to be used by VisualTest scripts.  This first piece 
takes it's arguments, places them into a pre-setup area of (shared) memory 
and calls the basic communications methods to send the "operation request" 
to the instance of the DLL running within the applications address space.  
The second piece provides the implementation of the specific request.  It is 
called by the basic commuioncations methods and passed the data that was sent.  
This data is then used to make the calls needed in order to perform the action.  
Note that while the second piece is performing the action, the first piece is 
waiting for a response.  When the second piece finishes, it places any 
information that needs to be returned into the same data space used to pass 
the original arguments and then returns.  The first piece is then interprets 
any of the returned data that it needs to and returns to the caller 
(a VisualTest script).

Since the basic communication system handled all the sending, receiving, 
and returning, building the pieces to create new operations took very little 
time.  As time went on, I made the system a little more generic in order to 
handle another third-party control that the application was using.  This 
extension provided for a subcatory making it easy to provide operations within 
a specific category (where the catorgies, in this case, pertained to the 
different controls).  This whole system worked out quite nice and provided 
the only solution to working with these controls.

Now, when I built this DLL, I built it as a single monolithic unit.  Though 
it was easy to add new subcategories there were still internal dependencies 
which meant that, if anyone wanted to add support for another control, they 
not only had to write the new functions, they also had to adjust some pieces 
in the basic communications system.  Also, the DLL ended up being used to add 
project specific functions that really did not have anything to do with the 
original purpose of the DLL.  Finally, I thought, if this method were at all 
useful to others, they should be able to build support for different components 
as separate extensions.  Different people could then build extensions for 
different components and be able to use them all together.

Thus the DIPA (Direct In-Proccess Access) DLL was born.  DIPA.DLL encapsulates 
the basic communications methods as well as a simple API for connecting two 
instances (one running in a client (VisualTest), and one running in a server
(the application being tested)).  Extension DLLs can be built to provide 
different sets of functions.  They provide the two pieces mentioned above 
and call upon DIPA.DLL to handle the underlying communications.

Building DIPA Extensions

It is important to note that DIPA is not a complete solution in and of itself.  
DIPA, by itself, provides very little external functionality.  Extensions must 
be written to supply the desired external functionality.  An extension DLL is 
generally built in C or C++, but can be built in any language that provides the 
needed support.  The sample provided here is written in C++ (but should be 
easily comprehendable to a C programmer because of it's basic nature).

At this point it may be apparent that this method can require a bit of work 
and quite possibly require knowledge which not all QA test people are familiar 
with.  Generally this solution is useful when it is the only possible way or if 
those working with it are comfortable enough with the situation.  Do note that 
once an extensions is written, generally working with the given control becomes 
much easier since you then have the basic operations needed.  If there is 
another alternative for accessing a control (such as through OLE Automatic), 
that alternative may be a much better choice.

Here I present a rather contrived example (since the functionality we will 
develop can already be done in VisualTest).  It's purpose is to see how to 
build the pieces to create an Extension.  This tutorial will walk through 
the complete steps to build a DIPA Extension DLL in Visual C++ (version 5.0 was 
used to create this tutorial).  The tutorial assumes you have some familiarity 
with VisualC++ and the general workings of a C/C++ development system.

The goal is to create two functions which we can call from VisualTest.  
One function will set a controls text (for controls which understand the 
WM_SETTEXT message such as edits or comboboxes).  The other function will get a 
controls text (using WM_GETTEXT).  The DIPA directory contains a more complete 
version of this sample extension (it's creation is simplified here to focus on 
the major points).

We start out by creating a project, to do so, follow these steps in Visual C++:

* Select the "File" / "New..." menu item (the "New" dialog appears)
* Select the "Project" tab
* Click on "Win32 Dynamic-Link Library"
* Enter "SampleExt" in the "Project name" edit
* Set the location as desired (you may wish to put it in the DIPA direct as 
  done here)
* Press the "OK" button

Next we need to add files to the project.  First we'll add a header file and 
specify the API (functions) we wish to create:

* Select the "Project" / "Add to Project" / "New..." menu item (the "New" 
  dialog appears)
* Select the "Files" tab
* Click on "C/C++ Source File"
* Enter "SampleExt.cpp" into the "File name" edit
* Press the "OK" button

A DIPA Extension always calls DIPA, so we need to be able to access the 
DIPA function specifications as well as the DIPA static library (DIPA.lib).  
We will also use some Std C/C++ functions:

* Add the following line to the file (adjust the path to match your directory 
  structure)
		#include "../DEV/DIPA.h"
		#include <string.h>
* Select the "Project" / "Settings..." menu item (the "Project Settings" dialog 
  appears)
* Set the "Settings For:" combo box to "All Configurations"
* Select the "Link" tab
* Add "../DEV/DIPA.lib" to the list of libraries in the "Object / library 
  modules" edit (adjust the path to match your directory structure)

The data that we need to transfer for our two functions includes a window 
handle (to specify which control to operate on) and a text buffer (to store 
or retrievetext in).  Since we are implementing two separate functions, we 
create two IDs to easily differentiate between the two.  Also, we need to 
keep track of the extension ID that DIPA gives us for communicating who we are:

* Add the following code to declare a structure to hold the transfer data:
		struct MessageData {
		    HWND hWnd;
		    char Text[1024];
		};
* Add the following enumeration to declare the two message IDs:
		enum MessageID {
		    MID_SetText,
		    MID_GetText
		};
* Add the following global variable to keep track of our extension ID:
		int g_ExtensionID = 0;

We will now implement the server side functions.  These are the functions 
which execute on the server side (in the application being tested).  One 
function will send a WM_SETTEXT message, passing the window handle and text 
passed in the data transfer area.  The second functin will send a WM_GETTEXT 
message, passing the same information as well as the buffer size.  Also, we 
create a DIPA Extension Handler Function.  This is the function that we 
register with DIPA.  When DIPA gets a message to be handled by this extension, 
it will call this functions.  We use the message IDs declared above to 
identify which operation to perform:

* Add the following code to implement the server side of the set text operation:
		int SetText (MessageData *pData)
		{
		    int retval = SendMessage(pData->hWnd, WM_SETTEXT,
		                             0, LPARAM(pData->Text));
		    return (retval != 0) ? 1 : 0;
		}
* Add the following code to implement the server side of the get text operation:
		int GetText (MessageData *pData)
		{
		    int retval = SendMessage(pData->hWnd, WM_GETTEXT,
		                             sizeof(pData->Text), LPARAM(pData->Text));
		    return (retval >= 0) ? 1 : 0;
		}
* Add the following code to implement the DIPA Extension Handler Functions:
		int __stdcall SampleExtensionHandlerFcn (int MessageID, BYTE *pData)
		{
		    MessageData *pMData = (MessageData *) pData;

		    switch (MessageID) {
		        case MID_SetText: return SetText(pMData);
		        case MID_GetText: return GetText(pMData);
		        default: return 0;
		    }
		}
Note that SampleExtensionHandlerFcn is type equivalent to the 
DIPAExtensionHandlerFcn type declared in "DIPA.h".

Now we need to implement the client side API (the functions that we call from 
VisualTest).  We create two functions to directly match the functionality we 
are trying to implement.  The functions take their arguments, place them in 
the transfer buffer, and use DIPA to make the operating requrest:

* Add the following code to implement the client side of the set text operation:
		extern "C" __declspec(dllexport)
		BOOL __stdcall SESetText (HWND hWnd, char *pText)
		{
		    MessageData *pMData = (MessageData *) DIPAGetSubOpDataBuf();
		    pMData->hWnd = hWnd;
		    strcpy(pMData->Text, pText);

		    return DIPASendSubOpMessage(g_ExtensionID, MID_SetText) != 0;
		}
* Add the following code to implement the client side of the get text operation:
		extern "C" __declspec(dllexport)
		BOOL __stdcall SEGetText (HWND hWnd, char *pTextBuf, int BufSize)
		{
		    DIPAGetSubOpDataBuf();
		    MessageData *pMData = (MessageData *) DIPAGetSubOpDataBuf();
		    pMData->hWnd = hWnd;

		    if (DIPASendSubOpMessage(g_ExtensionID, MID_GetText) == 0)
		        return FALSE;

		    int Len = strlen(pTextBuf);
		    if (Len >= BufSize)
		        return FALSE;

		    strcpy(pTextBuf, pMData->Text);
		    return TRUE;
		}

When using the __stdcall compiler directive (which we need for compatibility 
with VisualTest) with Visual C++, you end up with mangled names (even though 
the extern "C" call is present - this is just how Visual C++ seems to do 
things).  The mangling method is published, but is still a pain to work with.  
The way to stop Visual C++ from mangling the names is to build a .def file 
with an EXPORTS section that provides names to use for each functions:

* Select the "Project" / "Add to Project" / "New..." menu item (the "New" 
  dialog appears)
* Select the "Files" tab
* Click on "Text File"
* Enter "SampleExt.def" into the "File name" edit
* Press the "OK" button
* Enter the following text in the file:
		EXPORTS
		    SESetText=SESetText
		    SEGetText=SEGetText
This will take care of the name mangling and make life much easier.

This implements both pieces (sides) of the functionality needed to make the 
operations work.  To make these pieces available through DIPA, we need to 
register the extension function with DIPA when the DLL loads.  Note that 
when we register the extension function we also tell DIPA the maximum data 
transfer space that will be needed.  We also unregister the extension when 
the DLL unloads:

* Add the following code to handle registration of the extension function:
		BOOL WINAPI DllMain (HINSTANCE hInstance, DWORD Reason, LPVOID Reserved)
		{
		    switch (Reason) {
		        case DLL_PROCESS_ATTACH:
		            // Perform setup emmediately
		            g_ExtensionID = DIPAAttachExtension("SampleExt.DLL", 				                                                SampleExtensionHandlerFcn,
		                                                sizeof(MessageData));
		            return g_ExtensionID > 0;

		        case DLL_PROCESS_DETACH:
		            // Perform cleanup
		            DIPAReleaseExtension(g_ExtensionID);
		            return TRUE;

		        default:
		            return TRUE;
		    }
		}

The VisualC++ project is complete.  Building it will yield SampleExt.DLL.  
In order to be able to use this DLL in VisualTest we need to build an 
include file which declares the API functions of the SampleExt DLL:

* In VisualTest create a new include file names "SampleExt.INC"
* Add the following code to the file which declares the SampleExt API functions:
		Declare Function SESetText lib "SampleExt.DLL" alias "SESetText" _
		    (hWnd as HWND, pText as String) as BOOL
		Declare Function SEGetText lib "SampleExt.DLL" alias "SEGetText" _
		    (hWnd as HWND, pText as String, BufSize as Long) as BOOL
* Add the following code which creates a simpler front end to the SEGetText:
		Function SEGetTextVT (hWnd as HWND, pText as String) as BOOL
		    pText = Space$(1024)
		    SEGetTextVT = SEGetText(hWnd, pText, Len(pText))
		End Function

Now the SampleExt DIPA Extension is ready to work with VisualTest.

Using DIPA And DIPA Extensions

Since both the VisualTest and the application are going to load the DLLs 
(DIPA and any extensions being used), these DLLs must be in the search path 
for VisualTest and the application.  Usually the easiest method is to put 
the DLLs an a directory that is in the system file search path.

The application needs only to load DIPA.DLL (DIPA will load other DLLs when 
the client side requests them).  This is done by inserting the following 
statement somewhere in the initialization phase of the application:

	LoadLibrary("DIPA.DLL");

VisualTest will automatically load a library if it is referenced in any 
declarations.  Thus, when a script includes (directly or indirectly) DIPA.INC, 
VisualTest will load DIPA.DLL.  When a script includes SampleExt.INC, 
VisualTest will load SampleExt.DLL, and, because SampleExt calls upon DIPA 
during initialization, it will load DIPA (if not already loaded).  This step 
is automatically done for us.

In order to use any functions from a DIPA Extension, we first have to get the 
DIPA instance in VisualTes to connect to the DIPA instance in the application 
being tested.  DIPAConnect is used to get the two instances connected.  
DIPAConnect requires a process ID.  DIPAConnectByHwnd (defined in DIPA.INC) 
provides an easier way, needing only the handle of a window from the test 
application.  Once a connection is made, then any of the extension functions 
can be called.

The following code shows how to use DIPA with the TestServer example provided 
in the DIPA directory:

	'$INCLUDE 'DIPA.INC'
	'$INCLUDE 'SampleExt.INC'

	Dim hWnd as Long
	Dim hWndCtrl as Long
	Dim TextBuf as String

	ViewPort Clear

	hWnd = WFndWnd("Test Server Application", FW_DEFAULT or FW_FOCUS)
	If hWnd <> NULL Then
	    If DIPAConnectByHwnd(hWnd) Then
	        hWndCtrl = WEditFind(_ord(1))
	        If hWndCtrl <> NULL Then
	            SESetText(hWndCtrl, "Test Text")
	            SEGetTextVT(hWndCtrl, TextBuf)
	            If TextBuf = "Test Text" Then
	                MsgBox "Test Succeeded"
	            End If
	        Else
	            MsgBox "Unable to find edit control"
	        End If
	        DIPADisconnect()
	    End If
	Else
	    MsgBox "Unable to find window"
	End If

Note that you do not need to explicitly disconnect.  This will happen 
automatically when the DLL is unloaded.  Also, it is not a problem to 
call DIPAConnect more than once in a row (a second connection automatically 
disconnects the first connection).  In one set of test scripts I called the 
connect function in a global piece of code and also in a startup function to 
make sure that the connection was made whether the application was already 
running or had to be started up.

Misc Info

Though this document did not discuss the implementation of DIPA's basic 
communications methods, the complete source code is included.

If you have any questions, comments, or suggestions regarding DIPA or 
DIPA Extensions, feel free to contact me at the email address below.

Jaek Smith
jaeksmith@usa.net

DIPA.DLL Version 1.0.0 Revision 19980427
DIPA Document Revision 19980427
