Automated Testing & Windows CE

The technique Tom presents here lets you use third-party tools to test Windows CE-based systems.


December 01, 2002
URL:http://www.drdobbs.com/tools/automated-testing-windows-ce/184405227

Dec02: Automated Testing & Windows CE

Tom is working on industrial and embedded software projects as a Director at Covansys. He can be reached at [email protected].


Software quality assurance (QA) is a key differentiator that can give a product a significant advantage over competitors. Unfortunately, the ability to test software on embedded systems has not kept pace as software and operating systems have become more complex. In this article, I present a technique you can use on Windows CE systems to drive software from a remote machine, making it possible to use conventional third-party QA tools to test Windows CE-based products.

Many of the techniques I describe here are not new. Put together, however, they provide a valuable way to test software and validate results using a standard QA tool. Figure 1 demonstrates how this technique works. In this case, the end product is a simplified client/server utility that captures screen output on the target device, displays the output on a PC running Windows 2000, then forwards mouse click and basic keyboard events back to the application running on the target machine. Both the Windows CE server utility and Windows client utility are MFC applications. In the source code accompanying this article (available electronically; see "Resource Center," page 5), the client and server communicate over TCP/IP only. Once a connection is made between the client and the server, the server running on the CE device minimizes itself so that it is not in the way of the application being tested. At this point, you can drive the CE software from the remote machine.

It is important to understand how this approach might benefit you, as well as some of the reasons it might not be suitable for your project(s). Some of the advantages of this solution include:

Some of the disadvantages of this approach include:

The advantages outweigh the disadvantages for many projects, and the ability to test embedded systems can increase significantly through this tool's use. Unfortunately, I cannot go into all of the details regarding this utility due to space limitations. Since it is more important that I discuss the basic concepts behind this approach, I will not be giving details on the communication (TCP/IP) routines. This article includes the complete source code for a simplified working version of this utility, so that you can get started right away if you find this approach useful. The source code includes a simple TCP/IP communication process that can be used as a starting point if you are unfamiliar with using the Windows Sockets API.

Building the Server Component

The server component is responsible for receiving client requests, processing requests by simulating mouse clicks and keyboard events, and capturing the image on the target device. Once a connection between the client and server is established, the server takes a snapshot of the display and sends it to the client. The server also sends a new snapshot each time a command is processed.

You start by building a simple MFC dialog-based application through the AppWizard in Embedded Visual C++. The dialog template needs only a single button to help in handshaking during the TCP/IP connection process. (See the OnConnect() method in QASrvrDlg.cpp, available electronically, for the TCP/IP details.) Once the connection is established, this window is minimized so that it no longer is in view of your running application.

Once the connection is made and server initialized, it is time to determine the window handle of the window that the server will be driving. The window handle and the window's device context are needed to take the snapshot. Ideally, this would be done by calling GetDesktopWindow() and driving the entire unit through the desktop. Unfortunately, GetDesktopWindow() is not supported under all target platforms and often simply returns NULL. My workaround for this is to simply call FindWindow() instead:

pDriverWindow = FindWindow(NULL, _T("AppWindowTitle"));

To use this, you need to replace AppWindowTitle with the results of GetWindowText() on the main window of your application. This is generally the same text you see in the title bar, but if you have the title bar hidden, you might need to use an alternative method of finding the window title. You can use Remote Spy++ to determine this and other information about your application's primary window.

The next step is to take a snapshot of the target device display and send it to the client; see Listing One. The process starts by creating a device context compatible with the display. The size of the target application window is determined. Next, a new bitmap is created using the CreateDIBSection() function. In addition to returning a bitmap handle, this API function also returns a pointer to the bitmap's storage in memory (in BGR, or reverse RGB, format). Using the BitBlt() function, the snapshot is placed into the bitmap object. There are many other useful operations that BitBlt() can perform, but in this case, SRCCOPY is used to copy the bitmap. Once BitBlt() is called, the pointer CreateDIBSection() references the bitmap snapshot of the target application. Once the bitmap is sent to the client, it is copied into a device-dependent bitmap using SetDIBits(), which accepts the data in BGR format, so no conversion is necessary.

Another interesting aspect of this approach is that the bitmap snapshot is really a snapshot of exactly what users would see while looking at a monitor. If there are windows on top of the target application window in the z-order, they appear in the bitmap. This becomes an advantage because no special processing is required to handle pop-ups that appear from the application unless they exceed the width or height of the application.

After the bitmap is displayed on the client, mouse clicks and keyboard events need to be forwarded to the server. These events are simulated on the target device so that the application thinks users have performed the specific action.

While I expected the bitmap manipulation to be the trickiest part of this solution, it turned out that simulating mouse clicks and keyboard input was more troublesome. Windows expects certain events to occur in a specific sequence. If the system state does not match the event that is being simulated, it appears as if nothing happened.

Listing Two shows the details of simulating a mouse click. The positioning of the mouse click was off by an odd amount during testing. Unfortunately, I was not able to find any API that could help adjust the values dynamically. Once the coordinates of the mouse click are extracted from the string sent by the client, they are adjusted by a hardcoded amount. It may be necessary to adjust these settings depending on the screen resolution, platform, or other circumstances involving the target device.

To simulate a mouse click, you have to retrieve a pointer to the window that is immediately beneath that point using the WindowFromPoint() function. This will be the window that mouse click events are sent to. The coordinates are then translated using ScreenToClient(). If this step is missed, the coordinates will be outside the client area of the component and the component will ignore the window messages. Finally, the mouse events are sent to the component by sending WM_LBUTTONDOWN and WM_LBUTTONUP windows messages, respectively. After the events are fired, there is a short delay introduced between clicks so that all of the clicks are received properly by the target device. Currently, this utility does not handle double clicks, although it would not be difficult to add a similar double-click message that gets sent from the client to the server when appropriate.

If your Windows CE application uses standard Windows components, you probably want to send keystrokes to a text control. To do this, first determine which control has focus by calling CWnd::GetFocus(). Then send one or more WM_CHAR messages with the virtual key code of the character to the text control. For special keys, such as hot keys (created by calling the RegisterHotKey() API function) and system characters (sent using the WM_SYSCHAR windows message), you need to do some experimentation. The source code I provide sends hardcoded hot-key events directly to the target window, but this is not a generic solution.

Building the Client Component

The client component is responsible for displaying a snapshot of the target application, accepting mouse events, accepting keyboard events, and forwarding requests to the server. To reduce the load required by the server application, a new snapshot of the target is returned from the server only when requested by the client. When an area of the bitmap is clicked using the mouse, the x- and y-coordinates are queued. Similarly, when text is entered on the client, the text is also queued. The queue of events is not sent to the server until the Send button is pressed. This can be important because if the load on the server becomes too stressful, it will begin to affect the performance of the target application.

When the client receives a bitmap from the server, it must convert it into a device-dependent bitmap and draw to an appropriate device context. In my example, I created a static control on a dialog resource template, and then associated it with a class-level variable through Class Wizard. The bitmap is copied into a device context compatible with the static's controls. Listing Three shows the process of moving the bitmap from BGR format into the static device's bitmap property. This is basically the reverse process of the technique used to take the snapshot on the server.

Accepting mouse and keyboard input in the client application is standard MFC event handling. Once the event is received, it can be packaged into a command to send to the server component. In the example source code, all mouse and keyboard commands are queued until the Send button is pressed. This reduces the communication between the components that would otherwise bog down the target device.

Running the Example

If you feel this tool would be useful, the source code for a simplified version is provided with this article to help you get started. I've only tested the tool under the GEODE platform, but it uses an API that is available on most platforms.

To use the tool, modify the TARGET_APPLICATION definition in QASrvrDlg.cpp in the server application to match the title of your application's primary window. Next, compile and run the server on the target device. Compile and run the client on the machine from which you are debugging.

Enter the appropriate IP address of the target device. If your target device uses DHCP, you may need to install ipconfig.exe to the device in order to determine its IP address. If you run ipconfig.exe in a command prompt, it shows you the assigned IP address of the device. Click Connect on the server, then Connect on the client. If the connection is successful, the Send, Capture, and Disconnect buttons should become enabled. Click Capture to retrieve the bitmap from the server—you should see the bitmap appear beneath the buttons on the client. To send a mouse click, click in the bitmap area, then click the Send button.

Conclusion

After significant exploration into third-party tools and processes, I was unable to find anything that would let me automate application testing for the right price. After creating this utility, our QA group has created numerous regression and reliability tests for our Windows CE-based embedded applications. I hope that this technique is also useful for your Windows CE-based applications.

Acknowledgment

Thanks to Sudha Arulalan for contributing to the original development of this tool.

DDJ

Listing One

// Take a snapshot of the screen
CDC dc;
dc.CreateCompatibleDC(pDriverWindow->GetDC());

// Retrieve the dimensions of the application window
CRect rect;
pDriverWindow->GetWindowRect(&rect);
int nWidth = rect.Width();
int nHeight = rect.Height();
// Fill in the DIB structure
BITMAPINFO dibInfo;
dibInfo.bmiHeader.biBitCount = 24;
dibInfo.bmiHeader.biClrImportant = 0;
dibInfo.bmiHeader.biClrUsed = 0;
dibInfo.bmiHeader.biCompression = 0;
dibInfo.bmiHeader.biHeight = nHeight;
dibInfo.bmiHeader.biPlanes = 1;
dibInfo.bmiHeader.biSize = 40;
dibInfo.bmiHeader.biSizeImage = nWidth*nHeight*3;
dibInfo.bmiHeader.biWidth = nWidth;
dibInfo.bmiHeader.biXPelsPerMeter = 3780;
dibInfo.bmiHeader.biYPelsPerMeter = 3780;
dibInfo.bmiColors[0].rgbBlue = 0;

dibInfo.bmiColors[0].rgbGreen = 0;
dibInfo.bmiColors[0].rgbRed = 0;
dibInfo.bmiColors[0].rgbReserved = 0;

// Create a new device independent bitmap and retrieve a pointer to its bit 
storage -- this is the raw bitmap data that will be sent to the client
BGRBit *pBGR = NULL;
HBITMAP hNewBitmap = CreateDIBSection(dc.GetSafeHdc(), 
                (const BITMAPINFO*)&dibInfo, 
                DIB_RGB_COLORS, (void**)&pBGR, NULL, 0);
HBITMAP hOldBitmap = (HBITMAP) dc.SelectObject(hNewBitmap);

// Copy bitmap into new device context -- this also copies bitmap to DIB
dc.BitBlt(rect.left, rect.top, nWidth, nHeight, pDriverWindow->GetDC(), 
                                                   0, 0, SRCCOPY);
// Determine the buffer size of the data to be sent to the client
int bufSize = sizeof(*pBGR) * nWidth * nHeight;

Back to Article

Listing Two

// Now that we have received click event, we need to adjust positioning a 
// bit so that click is translated properly to correct location. Note that at 
// different resolutions hardcoded numbers might need adjusted (this has not 
// been tested at a resolution other than 640x480 at this time).
pt.x = _wtoi(tcXCoordinate) + 8;
pt.y = _wtoi(tcYCoordinate) + 3;

// Determine the window that is beneath the specified point.
// The mouse click event must be sent directly to this window.
pWindow = WindowFromPoint(pt);

// To simulate a mouse click, the pointer must be in the proper position.
SetCursorPos(pt.x, pt.y);

// Make sure the target window has focus
pDriverWindow->SetFocus();

//Map the screen coordinates to the client
pWindow->ScreenToClient(&pt);

// Send a message to the window indicating the button press
wParam = 0;
lParam = MAKELONG(pt.x, pt.y);
pWindow->PostMessage(WM_LBUTTONDOWN, wParam, lParam);
pWindow->PostMessage(WM_LBUTTONUP, wParam, lParam);
// If this delay does not exist between mouse clicks,
// the application misses some of the clicks.
Sleep(700);   // To give time interval b/n two mouse clicks

Back to Article

Listing Three

void CScreenDriverDlg::CopyBitmap(BGRBit *pBGR, int nWidth, int nHeight)
{
    // If there is a bitmap already stored in the module level
    // bitmap object, delete it
    if ((HBITMAP) bitmap != NULL)
        bitmap.DeleteObject();
    // Create a bitmap compatible with the screen device context
    bitmap.CreateCompatibleBitmap (this->GetDC(), nWidth, nHeight);
    // Fill out the bitmap info structure -- this should be 
    // identical to the server version
    BITMAPINFO dibInfo;
    dibInfo.bmiHeader.biBitCount = 24;
    dibInfo.bmiHeader.biClrImportant = 0;
    dibInfo.bmiHeader.biClrUsed = 0;
    dibInfo.bmiHeader.biCompression = 0;
    dibInfo.bmiHeader.biHeight = nHeight;
    dibInfo.bmiHeader.biPlanes = 1;
    dibInfo.bmiHeader.biSize = 40;
    dibInfo.bmiHeader.biSizeImage = nWidth*nHeight*3;
    dibInfo.bmiHeader.biWidth = nWidth;
    dibInfo.bmiHeader.biXPelsPerMeter = 3780;
    dibInfo.bmiHeader.biYPelsPerMeter = 3780;
    dibInfo.bmiColors[0].rgbBlue = 0;
    dibInfo.bmiColors[0].rgbGreen = 0;
    dibInfo.bmiColors[0].rgbRed = 0;
    dibInfo.bmiColors[0].rgbReserved = 0;
    // Create a temporary device context that will be used
    // to copy the bitmap
    CDC dc;
    dc.CreateCompatibleDC(this->GetDC());
    // Use the module level bitmap as the destination for the
    // device dependent bitmap
    CBitmap *pOldBitmap = dc.SelectObject(&bitmap);
    // Copy the DI bits into the device dependent bitmap
    // We use SetDIBits here instead of SetBitmapBits because it will
    // accept the data in BGR format instead of RGB format.
    SetDIBits(dc, bitmap, 0, nHeight, pBGR, &dibInfo, DIB_RGB_COLORS);
    // Select the old bitmap back into the device context to
    // avoid resource leaks
    dc.SelectObject(pOldBitmap);
    dc.DeleteDC();
    // Place the bitmap into the CStatic 
    HBITMAP hOldBitmap = m_BitmapArea.SetBitmap(bitmap);
}




Back to Article

Dec02: Automated Testing & Windows CE

Figure 1: High-level view of how this utility interacts with an application running on the target device, and QA software running on a client machine.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.