A custom data diagnostic adapter for Visual Studio Coded UI Test using Windows PSR

Mathew Aniyan MSFT

 

This is a guest blog from the Windows Azure AppFabric team. They are using Visual Studio Coded UI Test and want to share an exciting idea for generating rich troubleshooting logs.

Hello, my name is Jerrin Elanjikal and I’m a test developer with the Windows Azure AppFabric product group at Microsoft IDC. My team develops automated UI test frameworks and tools to ensure the quality of our user interface components. We deal a unique set of technical challenges around UI test automation and debugging, which is very different from both API test automation and manual UI testing.

 

Our product has multiple user interfaces including a Silverlight web portal, Visual Studio designer surfaces, WPF dialogs and a few legacy WinForms dialogs. We target 85% code coverage through automated UI test cases. And most of our UI automation is done using Coded UI, along with a mix of internal frameworks for specialized functions.

 

When one of these test fail and you have to debug or reproduce the failure, we run into a few common pain points that are unique to UI test automation: 

1. The average test takes a lot more time to run as compared to your typical API unit test. Our tests on average take about 10 minutes each.

2. And while a UI test is running, you cannot use your computer for anything else that involves user interaction. This means that you either keep a dedicated second machine for debugging UI test failures, or just sit back and watch.

3. UI tests can fail randomly due to a number of external reasons – a pop up thrown by another application, someone accidently logging into the machine, flaky internal test frameworks and the list goes on. It’s almost impossible to reproduce or debug such failures on your development machine.

 

The standard solution to these problems involves diagnostic logs and video recording of the test automation, so that you can debug without having to re-run the test. Often logs alone are not sufficient. Video recordings take up a lot of disk space, typically a few MB per test case. And you spend a lot of time watching the video and looking out for something interesting to happen.

 

Fortunately, there is a better alternative. The ‘Problem Steps Recorder’ is an awesome little tool that comes built in with Windows 7 and later. PSR was originally intended to help customers explain their problem scenario to tech support. What it does is take screenshots of each UI action and generate a zipped html report with images and descriptions. Looking at a PSR report is like watching the highlights of a match – short & interesting. It’s every UI developer’s dream come true!

 

So, what we did is plug PSR into our CodedUI tests by writing a custom diagnostics data adapter that essentially just invokes psr.exe with the right parameters.  This MSDN article < http://msdn.microsoft.com/en-us/library/dd286727.aspx > describes how to write and install a custom diagnostics data adapter for Visual Studio. And the command line parameters for PSR are as follows:

 

psr.exe [/start |/stop][/output <fullfilepath>] [/sc (0|1)] [/maxsc <value>]

    [/sketch (0|1)] [/slides (0|1)] [/gui (o|1)]

    [/arcetl (0|1)] [/arcxml (0|1)] [/arcmht (0|1)]

    [/stopevent <eventname>] [/maxlogsize <value>] [/recordpid <pid>]

 

/start                        :Start Recording. (Outputpath flag SHOULD be specified)

/stop                        :Stop Recording.

/sc                          :Capture screenshots for recorded steps.

/maxsc                        :Maximum number of recent screen captures.

/maxlogsize                :Maximum log file size (in MB) before wrapping occurs.

/gui                        :Display control GUI.

/arcetl                        :Include raw ETW file in archive output.

/arcxml                        :Include MHT file in archive output.

/recordpid                :Record all actions associated with given PID.

/sketch                        :Sketch UI if no screenshot was saved.

/slides                        :Create slide show HTML pages.

/output                        :Store output of record session in given path.

/stopevent                :Event to signal after output files are generated.

 

To start PSR recording at the beginning of each test case, we invoke psr.exe with the parameters /start /output <PSRFileName> /gui 0 /sc 1 /sketch 1 /maxsc 100 in the OnTestCaseStart event handler of our custom diagnostics data adapter.

 

To complete the PSR recording and generate the zipped report we invoke psr.exe /stop in the OnTestCaseEnd event handler, with an appropriate delay to complete the report file creation.

 

But PSR records a screenshot only when a UI action is performed. If the test fails because a UI element was not found on screen, as is often the case, PSR will not record a screenshot at that point. So we add the following code to the OnTestCaseEnd event handler to take an extra screenshot at the point of failure.

 

Playback.Initialize();

Image img = UITestControl.Desktop.CaptureImage();

Playback.Cleanup();

img.Save(ScreenshotFileName, ImageFormat.Jpeg);

 

We then attach the PSR report and the final screenshot to the test case.

 

dataSink.SendFileAsync(e.Context, ScreenshotFileName, false);

dataSink.SendFileAsync(e.Context, PSRLogFile, false);

 

The complete code for the PSR diagnostics data collector is given below:

 

using System;
using
System.Diagnostics;
using
System.Drawing;
using
System.Drawing.Imaging;
using
System.IO;
using
System.Threading;
using
System.Xml;
using
Microsoft.VisualStudio.TestTools.Common;
using
Microsoft.VisualStudio.TestTools.Execution;
using
Microsoft.VisualStudio.TestTools.UITesting;
 
namespace
PSRDataCollector
{
   
// Problem Screen Recorder diagnostic data adapter

    [
DataCollectorTypeUri("datacollector://Microsoft/PSRDataCollector/1.0")]
    [
DataCollectorFriendlyName("Problem Screen Recorder", false
)]
   
public class PSRDataCollector : DataCollector

    {
        #region Constants
 
       
private const string PSRExe = "psr.exe"
;
       
private const int
SaveDelay = 5000;
 
        #endregion
Constants
 
        #region
Fields
 
       
private DataCollectionEvents
dataEvents;
       
private DataCollectionLogger
dataLogger;
       
private DataCollectionSink
dataSink;
       
private XmlElement
configurationSettings;
       
private string
PSRLogFile;
 
        #endregion
Fields
 
        #region
DataCollector
 
       
// Required method called by the testing framework

       
public override void Initialize(XmlElement configurationElement,
                                       
DataCollectionEvents
events,
                                       
DataCollectionSink
sink,
                                       
DataCollectionLogger
logger,
                                       
DataCollectionEnvironmentContext
environmentContext)
        {
            dataEvents = events;
// The test events

            dataLogger = logger;
// The error and warning log
            dataSink = sink;    
// Saves collected data
           
// Configuration from the test settings
            configurationSettings = configurationElement;
 
           
// Register common events for the data collector
           
// Not all of the events are used in this class
            dataEvents.SessionStart +=
               
new EventHandler<SessionStartEventArgs>(OnSessionStart);
            dataEvents.SessionEnd +=
               
new EventHandler<SessionEndEventArgs
>(OnSessionEnd);
            dataEvents.TestCaseStart +=
               
new EventHandler<TestCaseStartEventArgs
>(OnTestCaseStart);
            dataEvents.TestCaseEnd +=
               
new EventHandler<TestCaseEndEventArgs
>(OnTestCaseEnd);
            dataEvents.DataRequest +=
               
new EventHandler<DataRequestEventArgs
>(OnDataRequest);
        }
 
       
protected override void Dispose(bool
disposing)
        {
           
if
(disposing)
            {
                dataEvents.SessionStart -=
                   
new EventHandler<SessionStartEventArgs
>(OnSessionStart);
                dataEvents.SessionEnd -=
                   
new EventHandler<SessionEndEventArgs
>(OnSessionEnd);
                dataEvents.TestCaseStart -=
                   
new EventHandler<TestCaseStartEventArgs
>(OnTestCaseStart);
                dataEvents.TestCaseEnd -=
                   
new EventHandler<TestCaseEndEventArgs
>(OnTestCaseEnd);
                dataEvents.DataRequest -=
                   
new EventHandler<DataRequestEventArgs
>(OnDataRequest);
            }
        }
 
        #endregion
DataCollector
 
        #region
Event Handlers
 
       
public void OnSessionStart(object sender, SessionStartEventArgs
e)
        {
           
// TODO: Provide implementation

        }
 
       
public void OnSessionEnd(object sender, SessionEndEventArgs e)
        {
           
// TODO: Provide implementation

        }
 
       
public void OnTestCaseStart(object sender, TestCaseEventArgs e)
        {
           
this.PSRLogFile = Path.Combine(Environment.CurrentDirectory, e.TestCaseName + ".psr.zip"
);
 
           
// Kill all running PSR processes

            TryKillProcess(PSRExe);
 
           
// Start PSR
           
try
            {
                InvokeProcess(PSRExe,
String.Format("/start /output "{0}" /gui 0 /sc 1 /sketch 1 /maxsc 100", this.PSRLogFile));
            }
           
catch (Exception
exp)
            {
                dataLogger.LogError(e.Context,
string.Format("Unexpected exception while trying to start PSR process : {0}"
, exp));
            }
        }
 
       
public void OnTestCaseEnd(object sender, TestCaseEndEventArgs
e)
        {
           
try

            {
               
// Save the PSR logs
                InvokeProcess(PSRExe,
@"/stop").WaitForExit(60000);
 
               
// Sleep to ensure PSR completes file creation operation

               
Thread.Sleep(SaveDelay);
 
               
if (!File.Exists(this
.PSRLogFile))
                {
                    dataLogger.LogError(e.Context,
"No user actions were recorded by PSR!"
);
                }
               
else if (e.TestOutcome != TestOutcome
.Passed)
                {
                   
string ScreenshotFileName = Path.Combine(Environment.CurrentDirectory, e.TestCaseName + ".screenshot.jpg"
);
                    CaptureScreenshot(ScreenshotFileName);
                    dataSink.SendFileAsync(e.Context, ScreenshotFileName,
false
);
 
                    dataSink.SendFileAsync(e.Context,
this.PSRLogFile, false
);
                }
               
else

                {
                   
File.Delete(this.PSRLogFile);
                }
            }
           
catch (Exception
exp)
            {
                dataLogger.LogError(e.Context,
string.Format("Unexpected exception while trying to stop PSR process : {0}"
, exp));
               
// Kill all PSR processes

                TryKillProcess(PSRExe);
            }
        }
 
       
public void OnDataRequest(object sender, DataRequestEventArgs e)
        {
           
// TODO: Provide implementation

           
// Most likely this occurs because a bug is being filed
        }
 
        #endregion Event Handlers
 
        #region
Helpers
 
       
private Process InvokeProcess(string processName, string
parameters)
        {
           
ProcessStartInfo startInfo = new ProcessStartInfo
(processName, parameters);
            startInfo.WindowStyle =
ProcessWindowStyle
.Hidden;
            startInfo.UseShellExecute =
true
;
            startInfo.ErrorDialog =
false
;
 
           
Process proc = new Process
();
            proc.StartInfo = startInfo;
            proc.Start();
 
           
return
proc;
        }
 
       
private void TryKillProcess(string
processName)
        {
           
Process[] processes = Process
.GetProcessesByName(processName);
           
foreach (Process proc in
processes)
            {
               
try
{ proc.Kill(); }
               
catch (Exception
exp) { }
            }
        }
 
       
private void CaptureScreenshot(string
ScreenshotFileName)
        {
           
Playback
.Initialize();
           
Image img = UITestControl
.Desktop.CaptureImage();
           
Playback
.Cleanup();
            img.Save(ScreenshotFileName,
ImageFormat
.Jpeg);
        }
 
        #endregion
Helpers
    }
}

 

You can download the full project from http://psr4vs.codeplex.com.

 

To install the PSR data collector into Visual Studio, download and build this project. Copy PSRDataCollector.dll to %VSINSTALLDIR%Common7IDEPrivateAssembliesDataCollectors.

 

To enable PSR data collection, open test settings from Solution Explorer in your CodedUI test and enable ‘Problem Screen Recorder’ under the ‘Data and Diagnostics’ tab.

 

image

 

Run your CodedUI test again and let it fail. Wait for the test run session to complete. Now, you will find the PSR report and final screenshot attached to the test report.

image

 

Debugging automated UI tests just got a whole lot easier!

 

0 comments

Discussion is closed.

Feedback usabilla icon