WL#3312: Snoop module for Test Monkey (Windows)

Affects: GUI-Tools-3.0   —   Status: In-Progress   —   Priority: Medium

For the visual testing tool Test Monkey we need a special module on each
platform that is used as a communication channel between the Test Monkey and the
application actually under testing. For Windows this module must be written in
Delphi as our front-ends all are written in that language.
The snoop module is written as a Delphi unit, which can be included by any
Delphi project to provide the necessary snoop interface for Test Monkey.
Communication is done via sockets (TCP/IP), which requires the use of the Indy
package in the snoop module.

The snoop module runs in the same process as the tested application and must
accept connections from the TM, which will send queries to get info like
listings of widgets in windows and get info about their state. They should not
interfere with the way the application executes. In other words, the TM should
not be able
to modify the application state with it, only query it.
The Delphi unit will contain a class that listens to a specific TCP/IP port and
handles all the requests comming in from the Test Monkey. Since these requests
are entirely read commands it should be fairly easy to get the info out that is
required. In conjunction with Delphi's VCL hierarchy and runtime type info
(RTTI), plus the application's runtime structure it will be relatively easy to
get the required info.

The TCP/IP port used for communication must be given in one of two ways,
otherwise the snoop module stays inactive. Either use the MONKEY_SNOOP_PORT
environment variable or give it as a command line parameter to the application
to test. The parameter is: "-snoop_port=XXXX" in exact this form (four digit
decimal port number), no spaces around the parts of the parameter. If both forms
are given the command line gets higher priority.

The protocol used for communication follows these rules:

  - Commands are sent as text lines ending with newline (0x0A)
  - Replies are sent with a prefix of "TM:", errors with "ERROR:". Lines marked
with "ERROR:" are completed by a plain text error description after the colon.
  - Replies must end with a "TM:" (nothing else following it except for the new
line character)
  - On connection, it must send something like:
    "TM:DelphiSnoop Ready"
  - Commands and replies are case-sensitiv
  - Commands are encoded as UTF-8 allowing so Unicode identifiers and captions
in replies
  - Coordinates are always given in screen space not relative to a control's
parent window
  - Bounds are always given as left,top,width,height
  - There are no spaces around delimiters, be it colons, commas or dots
  - Reply parts which contain a colon (:) must mask this character by a
backslash because the colon is used as separator between those parts. A single
backslash must be written as double backslash (\\) to be recognised. In fact any
character can be masked by a backslash to take it literally if it has a special
meaning otherwise, e.g. new lines/carriage returns in clipboard text.
  - Widget/Controls may have no name in an application (e.g. because they are
created dynamically). In such cases the returned name is their class name
accompanied by their memory address as a hex string (without 0x prefix), e.g.:
    Important here are two attributes, which mark an entry as being in this
format: the leading capitel letter T (by convention all types and classes in
Delphi start with this letter) and the underbar. So ensure, no class type in an
application contains an underbar in its identifier (convention in Delphi is
camel case without underbar).
  - Empty captions are indicated by the NULL keyword.

Following command syntax has to be recognized:

General commands, which apply to all widgets/controls

* quit - Causes the application to terminate itself, no parameter is required
  --> quit

* toplevels - returns a list of all currently existing top-level windows:
  --> list
  TM:0x95242d0:connect_dialog:GtkWindow:SHOWN:MySQL Administrator

Top-level windows are those that have no parent window. Usually an application
only has one top level window, unless there are dialog boxes or floating
windows. The first field in the response is a pointer value of the object, the
secon is the path/name of the window, the third is the type of widget, then
follows the visibility state (SHOWN/HIDDEN) and finally the current title of the

* tree <window-spec> - returns a list of widgets (window controls) in the tree.
The window specification can be either the pointer value or the path of the window.
  --> tree connect_dialog

If a path is returned for a specifc control then its members are separated by a dot.

* props <window-spec> - returns a list of properties for the widget.
  --> props connect_dialog

Since Delphi controls have a concept of "published properties" these are used
for the props command. Subobjects are also returned by using path consisting of
the property name and the property's own properties, separated by a dot.

* info <window-spec> - returns a list of basic info for the widget:
  --> info connect_dialog
  TM:caption:MySQL Administrator

This is a fixed list of core properties that TM always needs to know and hence
asks separately for. TM:handle is the window handle of the control and might be
NULL if the widget/control is not a window control at all.

* bounds <window-spec>  - returns the out bounds for the widget
  --> bounds connect_dialog

Currently only the bounds of the control are specified as response. Others might
follow later.

* query <x> <y> - returns the control at position x,y, in
screen coordinates

  --> query 500 600

First response parameter is a pointer to the control, second one is its
path/name and the third one its type.

* check <window-spec> - checks whether the widget exists and
returns TRUE or FALSE
  --> check MainForm.OKButton

* clickable <window-spec> - checks wether a widget exists, is visible, and
active. Returns TRUE or FALSE
  --> clickable MainForm.HiddenButotn

A widget/control is only visible if also all its parents are visible.

* waitidle - blocks until the application reaches the event loop
  --> waitidle

Although the snoop module runs in the same thread as the application's main
thread, communication is done asynchronously via a socket and a separate
communication thread. This command blocks this thread (and blocks so the calling
Test Monkey) until the application has entered its idle loop (that is, no
pending Windows messages are to be processed). This state indicates that any
previously triggered action in the application has been finished and it is safe
for the test monkey to continue execution.

* flash <window-spec> - quickly flashes the specified control
  --> flash LoginDialog.CancelButton

Flashing is done by highlighting the control. If an invalid window-spec was
given an error is returned.

* childcount <window-spec> - returns number of child controls/widgets in a given
  --> childcount Painter.Canvas

* clipboard - returns the content of the clipboard if it is just text.
  --> clipboard
  TM:Some longer text\: which might contain\n\rmultiple lines.

If anything else but text is on the clipboard then nothing is returned.

List commands (this includes list boxes, treeviews and listviews)

* list.textpos <list-spec> <column> <text> - returns the screen position of the
item with the given text, in the given column.
  --> list.textpos LoginDialog.ConnectionList 0 "Local MySQL server"

list-spec is a control/widget that comprises a list of items, possibly displayed
in a hierarchical manner (i.e. indented like in a treeview) and with one or more
columns. The result is the coordinate of an item in that control having the
given text. This item is usually not a control on its own, but just an entry in
the list widget. The returned position (given in screen coordinates) is the
upper left corner of the item, where the vertical position is relative to the
client space of the list control (taking so the current scroll state into
account) and the horizontal distance is the left border of the column in which
the item is (so there is no consideration of additional elements like
indentation, images, checkboxes and the like). If the element is currently not
visible in the client area of the list widget then it is as would it not exist
and nothing is returned.

Note: the text can be given in single or double quotes (which are stripped
before using the string). Alternativly, special characters like spaces can be
masked using a backslash.

* list.indexpos <list-spec> <index> - returns the screen position of the item at
the given index
  --> list.textpos LoginDialog.ConnectionList 12345

This command is basically the same the previous one with the exception that
instead of a text an index is given. This index indicates which item to
consider, which must exist (the index is >= 0 and < the item count) and must be
visible in the client area (that is, the control must be scrolled so that the
item is currently visible on screen).

* list.selected <list-spec> - returns a list of all selected items in the list
  --> list.selected MainForm.DirectoryListing

* list.checkstate <list-spec> - returns a list of all checked items in the list,
if the list supports check states
  --> list.checkstate MainForm.BackupList

Note: this list must be ordered by increasing index

Special commands, which are specific to certain controls only

* checkbox.checkstate <window-spec> - returns the current check state of a
single check box
  Applies only to standalone checkboxes
  --> checkstate LoginDialog.UseSSL

* radiogroup.selected <window-spec> - returns the currently selected entry in a
radio group (a group of radio buttons handled automatically as one single
  --> radiogroup.selected Options.PasswortStorage

This command also handles individiual radio buttons if they are within a common
container. In this case give the container as the window-spec. The returned
index is then the index of the selected radio button in a list of all radio
buttons in this common container, collected in creation order (zero based).

If there is no radio button in such a common container (or the radio group) or
if no button is currently selected an empty TM: line is returned.

* range.info <window-spec> - returns the current position, minimum and maximum
values of controls that display values within a range, like progress bars,
trackbars, up-down buttons, spinner, gauges etc.
  --> range.position Work.Progress

Note: :selstart: and :selend: are optional, as they are not supported by all
range controls.

* datetime.info <windo-spec> - returns date info from a date/time picker control
  --> datetime.info Options.BackupStart


Note: both :date: and :time: entries are optional but usually at least one of
both is given, depending on the control/widget which is queried.

* menus - returns all defined menus in the application
  --> menus

* menu.items <menu-spec> - returns a list of menu items in the given menu (or
sub menu)
Syntax: TM:item-id:path:class:flags:submenu-id:bounds:caption
  --> menu.items

Following flags can be returned (comma separated list), order is not important:

If none of the flags are active currently then NULL is returned instead.