Wednesday, December 28, 2011

Selenium Grid 2 - Up and Running

WebDriver and Grid 2 WebDriver is the latest greatest API for testing browsers, and is incorporated into Selenium 2 in a variety of languages, including Python. I'm building it into my ptest framework as well, in the WebDriverLib class in the weblib.py module.

The plan is to move all the legacy Selenium 1 tests to Grid 2

Since all of my tests are in Selenium 1 and WebDriver is a completely different API (woo hoo) I have a translation layer to map the existing tests to the new API.

cd $TEST_HOME/src
python
   then, in the python shell ...

 from weblib import WebDriverLib settings = {"browser": "*iehta"}
lib = WebDriverLib(settings)
email = settings.get('email', 'auto_test@your.co.com')
password = settings.get('password', 'autotest1')
dev_url = settings.get("dev_url", "https://your.co.com")
lib.open(dev_url) # translation layer for 'get'
lib.wait_and_type("//input[@id='Email']", email) # translation layer for find element and send_keys
lib.wait_and_type("//input[@id='Password']", password)
lib.wait_and_click("//a[@class='ui-button submit']") # translation layer for find element and click it
lib.wait_for_page_to_load(30000)

 Grid 2
BASICS:
 to launch the hub locally:
    java -jar selenium-server-standalone-2.15.0.jar -role hub

to launch a node (formerly 'remote control') locally:
    java -jar selenium-server-standalone-2.15.0.jar -role node -port 5555 -hubHost localhost -hubPort 4444 -hub http://localhost:4444/grid/register

 running on my qa-jenkins machine at port 4445:
hub:
     nohup java -jar selenium-server-standalone-2.15.0.jar -role hub -port 4445 node: java -jar selenium-server-standalone-2.15.0.jar -role node -port 5555 -hubHost qa-jenkins -hubPort 4445 -hub http://qa-jenkins:4445/grid/register node

multiple firefox nodes on the remote control machine (note the different versions and executable paths specified)
     java -jar selenium-server-standalone-2.15.0.jar -role node -port 2333 -hubHost qa-jenkins -hubPort 4445 -hub http:// qa-jenkins :4445/grid/register -browser browserName=firefox,version=3.6,firefox_binary=c:\progra~2\mozill~2\firefox.exe,maxInstances=1,platform=WINDOWS
and
    java -jar selenium-server-standalone-2.15.0.jar -role node -port 2888 -hubHost  qa-jenkins  -hubPort 4445 -hub http:// qa-jenkins :4445/grid/register -browser browserName=firefox,version=8,firefox_binary=c:\progra~2\mozill~1\firefox.exe,maxInstances=1,platform=WINDOWS

for ptest config:
 -b *firefox3 will invoke the 3.6 node, and *firefox8 will invoke the 8 node ptest
example:
 python ptest.py -c webtest_config.json -t test_webdriver -w true -b *firefox8

 Internet Explorer node:
     java -jar selenium-server-standalone-2.15.0.jar -role node -hub http://qa-jenkins:4445/grid/register -browser browserName=iexplore,version=9,platform=WINDOWS -port 8000

 Google Chrome node: (note the path is to the chromedriver executable, not to an installed Chrome browser. Also, the path to the chromedriver executable must be set in the PATH environment variable)
 java -jar selenium-server-standalone-2.15.0.jar -role node -port 6000 -hubHost qa-jenkins -hubPort 4445 -hub http://qa-jenkins:4445/grid/register -browser browserName=chrome,chrome_binary=C:\chromedriver\chromedriver,maxInstances=1,platform=WINDOWS

 see http://code.google.com/p/selenium/wiki/ChromeDriver for more information

For SSL Issues with Google Chrome, initialize the WebDriver client with a chrome.switches array as below (python bindings example) ->

                    from selenium import webdriver

                    dc = webdriver.DesiredCapabilities.CHROME
                    dc["chrome.switches"] = ["--ignore-certificate-errors"]
                    driver = webdriver.Remote(str(remote_url), self.dc)

Thursday, December 8, 2011

Selenium and SSL

If you have to test https sites using Selenium on a variety of browsers, you are asking for a world of pain!  Practically every version of every browser on every platform requires either a different jar file or different arguments for both the selenium server and the remote client.

First of all, the all-important version disclaimer!
   selenium-server-standalone-2.15.0.jar
   selenium-grid-1.0.8
   Firefox 3.6  on Windows 7 and Ubuntu 11.04
   Firefox 8 on Windows7
   Chrome 15.0.874.121 on Windows 7
   Internet Explorer 9 on Windows 7

For Local Selenium Server
-------------------------------------
 java -jar selenium-server-standalone-2.15.0.jar


For Selenium RC Clients (your test app)
-----------------------------------------------------
  call selenium.start with the commandLineFlag -disable-web-security

  Python example:
    from selenium import selenium
    sel = selenium(test_host, int(test_port), self.browser, url)
    sel.start('commandLineFlags=-disable-web-security')

For Selenium Servers and Selenium Grid Remote Clients
---------------------------------------------------------------------------

For Firefox, I needed to use a profile and pass it into the startup script.
  1. start firefox from the command-line with "firefox -profileManager" and create a new profile
  2. Manually go into the site using Firefox, accept the various certificate challenges, and then quit the browser
  2. Start the selenium client using your new firefox profile. For example, as a Selenium Grid remote client:
           ant -Dport=5777 -Denvironment="*chrome" -Dhost=MY_IP
 -DhubURL=http://SELENIUM_GRID_SERVER_IP:4444 -DseleniumArgs="-firefoxProfileTemplate C:\Users\ME\FIREFOX_PROFILE_DIRECTORY_COPY\firefox_profile"  launch-remote-control

you may need to add some lines to the prefs.js file in the firefox profile. for example, if you see 403's and/or 404's being returned because of the browser looking for /favico.ico, you should add these two lines
    user_pref("browser.chrome.favicons", false);
    user_pref("browser.chrome.site_icons", false);
NOTE that this USUALLY WORKS but, in my case, it simply STOPPED WORKING on Windows 7. The -firefoxProfileTemplate argument passed in to ant as seleniumArgs is not passed along by the remote control to the firefox startup command. Here the specified profile is simply ignored:
    [java] 09:16:47.113 INFO - Preparing Firefox profile...
     [java] 09:16:49.072 INFO - Launching Firefox...
     [java] 09:16:49.074 DEBUG - Execute:Java13CommandLauncher: Executing 'C:\Program Files
(x86)\Mozilla Firefox\firefox.exe' with arguments:
     [java] '-profile'
     [java] 'C:\Users\ME\AppData\Local\Temp\customProfileDirc829f057ff9e497ea065add1ca892726'


The solution was to replace the selenium-server-standalone jar file in selenium-grid-XX/vendor with a newer one from Selenium org (2.15) - However, this broke IE which needed selenium server standaloine 2.12.0 !!!)


For Internet Explorer
    Disable popup blockers - Select Tools/Popup Blocker/Turn off pop-up blocker
    Disable IE protected mode - Untick Tools/Internet Options/Security/Enable protected mode - do this for all four zones

For Google Chrome
    Open Chrome Options
    go to 'Under the Hood'
    click on the 'Manage Certificates' button at HTTPS/SSL
    IMPORT your https server's PFX certificate and save it under Trusted Root Certification Authorities (input the password when prompted)



Wednesday, November 30, 2011

Selenium xpath arrays

Accessing xpath array elements in Selenium can be tricky! Sometimes you may have a number of elements on a page that can only be referenced with the same locator (for example, you can't rely on unique id's, only on some field they all have in common)

example:
a website has 4 buttons with 'addToCart' in onClick being the only reliable bit to check:

  ("//button[contains(@onclick,'addToCart')])

  you might think you could click on them by subscripting like this:
  sel.click("//button[contains(@onclick,'addToCart')][1]")
  sel.click("//button[contains(@onclick,'addToCart')][2]")

but no. the first one succeeds and fakes you into thinking you can access the array this way. but you can't. if you take out the [1] you get the same result. 

So what you have to do is isolate the array first, using the 'xpath=' notation, and then subscript it:

  sel.click("xpath=(//button[contains(@onclick,'addToCart')])[3]")

notice the use of 'xpath=' and the extra parens around the locator, followed by the subscript!

Wednesday, November 9, 2011

Reading Outlook Email With Python

There are several ways to read Outlook Email with Python, and I scouted around a number of blogs and websites to piece together a method that worked for me. This may not work for you, depending on how your company sets up its Outlook, but here it is, as a Python class

here is some usage ...

  outlook = OutlookLib()
  messages = outlook.get_messages('you@yourcompany.com')
  for msg in messages:
      print msg.Subject
      print msg.Body

and here is the class ...


import win32com.client

class OutlookLib:
        
    def __init__(self, settings={}):
        self.settings = settings
        
    def get_messages(self, user, folder="Inbox", match_field="all", match="all"):      
        outlook = win32com.client.Dispatch("Outlook.Application")
        myfolder = outlook.GetNamespace("MAPI").Folders[user] 
        inbox = myfolder.Folders[folder] # Inbox
        if match_field == "all" and match =="all":
            return inbox.Items
        else:
            messages = []
            for msg in inbox.Items:
                try:
                    if match_field == "Sender":
                        if msg.SenderName.find(match) >= 0:
                            messages.append(msg)
                    elif match_field == "Subject":
                        if msg.Subject.find(match) >= 0:
                            messages.append(msg)
                    elif match_field == "Body":
                        if msg.Body.find(match) >= 0:
                            messages.append(msg)
                    #print msg.To
                    #msg.Attachments
                        # a = item.Attachments.Item(i)
                        # a.FileName
                except:
                    pass
            return messages
        
    def get_body(self, msg):
        return msg.Body
    
    def get_subject(self, msg):
        return msg.Subject
    
    def get_sender(self, msg):
        return msg.SenderName
    
    def get_recipient(self, msg):
        return msg.To
    
    def get_attachments(self, msg):
        return msg.Attachments

Friday, November 4, 2011

Selenium RC and Confirmation Dialogs

This is worth noting. Sometimes in Selenium RC you will click on a button and it will bring up a confirmation dialog (Are You Sure You Want To Do This?)

Here's how to handle it in python-selenium


sel.choose_ok_on_next_confirmation() # will 'ok' the next one that comes up
sel.click('link='Delete') # your delete button
sel.get_confirmation() # absorbs the confirmation dialog

Thursday, September 15, 2011

Simplify with Django

As a legacy from a previous job, I had continued using Ruby on Rails as a web front-end to display test regression results and other data. There is a lot about Ruby on Rails that I never really understood (such as 'routes'), and the sheer number of folders and files created by a new Rails project always intimidated me. After all, I am not doing much with it, so why should I have all this extra stuff I never even seem to need? Yet, I did grow fond of Ruby itself, so I didn't mind the Rails wilderness too much, as long as I could get my little stuff to work.


In a subsequent job, I needed to learn Python, and I found the transition from Ruby to be pretty easy. There are a few Ruby-isms I missed but for the most part I was fine with Python. I still used a variant on the old Rails project, but decided to explore Django, since it's Python as well, and it's always better to simplify, if you can. What I didn't expect, though, was just how much simpler Django was going to be than Rails.


My projects, as I said, are very simple. Regression test results (and other data, such as apache benchmarks and server performance monitoring data) are stored in simply MySQL databases. I want access to these results through a browser. Most of the results are presented in HTML tables. Others are displayed in Google Charts.


in Rails I needed separate files for each database table's corresponding controllers, models and and views. I also touched the databse.yml file, the routes.rb and migrate files. I don't really have anything in the app/helpers or lib or log or public or script or test or tmp or vendor directories, yet there they are! In Rails, the views directories for each table contain index, new, show, edit and delete html files. In sum, there are more than 40 files to edit and maintain in my bare-bones Rails project.


In the Django version, there are the main 3 files (manage,py, settings.py and urls.py), and then one file for all models (models.py) and one file for all views (views.py). I have html template files for each table, so that's another 5 - for a total of 10 files. There are no unused folders, no other clutter.


And that's not all. In Rails, I had to write MySQL scripts to extract the data and Ruby code to pass the results up through the controller to the view, stuff like this:


def self.find_history
    date = (Date.today-30).to_s
    stmt = "select *
            from (select server, date
                   from ads f1
                   group by server
                   order by date desc) f1
            left join ads f2 on f1.server=f2.server and f2.date > '#{date}'"
            
     result = find_by_sql(stmt)
     @server = Array.new
     @mean1 = Array.new
     @mean2 = Array.new
     @mean3 = Array.new
     @date = Array.new
     
     result.each do | a |
        @server << a.server
        @mean1 << a.mean1
        @mean2 << a.mean2
        @mean3 << a.mean3
        p = a.date.to_s.split(" ")
        @date << p[0]
      end
      
      return result
  end
In Django, no. I just define the db in the models file:


class Benchmarks(models.Model):
    server = models.CharField(max_length=128)
    users = models.CharField(max_length=32)
    mean1 = models.CharField(max_length=32)
    mean2 = models.CharField(max_length=32)
    mean3 = models.CharField(max_length=32)
    date = models.DateTimeField()



And in the views file I use a built-in method to get the data from MySQL, then use a 'context' to pass it along to a 'template' file which will be rendered by the browser


def benchmark_index(request):
    all_entries = Regression.objects.all()
    t = loader.get_template('benchmark/index.html')
    c = Context({
        'all_entries': all_entries,
    })
    return HttpResponse(t.render(c))
The template is similar in look-and-feel to embedded Ruby. You put Python code in between {% and %} markers, and the rest is html:


table here:
tr
th>Server
th>Users
th>Mean Response Time 1
th>Mean Response Time 2
th>Mean Response Time 3
/tr

{% if all_entries %}
{% for a in all_entries %}
tr>
td>{{ a.server }}
td>{{ a.users }}
td>{{ a.mean1 }}
td>{{ a.mean2 }}
td>{{ a.mean3 }}
td>{{ a.date }}
{% endfor %}
{% endif %}

/tr>




Notice that 'all_entries' - the variable used in the template, was explicitly defined in the views file and passed into the context. Also, the urls used by Django are explicitly defined, formed by regular expressions, and stored all together in a file called urls.py:


urlpatterns = patterns('',
    # Examples:
    (r'^benchmarks/$', 'regression.views.benchmark_index'),


)



It makes it clear that the url is ROOT/benchmarks, and when you go there, you invoke the method 'benchmark_index' in the views.py file in the 'regression' application. It's all quite explicit and easy to track.


This is most of the project in a nutshell. A simple db with values easily retrieved and displayed in an HTML table in a browser.


bonus coverage: The google charts aspect was also straightforward. 1) Install the Python module: sudo easy_install -U GChartWrapper 2) embed some google chart code in your template file 3) create and pass the data to the chart from the views.py file


Building on the example above, add to the benchmark_index and template file:



def benchmark_index(request):
    all_entries = Benchmarks.objects.all()
    data = []
    max_val = 0
    for a in all_entries:
        if a.mean3 > max_val:
            max_val = a.mean3
        data.append(a.mean3)
    #print max_val
    mid_val = float(max_val) / 2
    t = loader.get_template('benchmark/index.html')
    c = Context({
        'all_entries': all_entries,
        'data': data,
        'max_val': max_val,
        'mid_val': mid_val,
    })
    return HttpResponse(t.render(c))



// in the template, below the sample code above



{% load charts %}
{% chart Line data %}
{% title 'Max Mean Response Times' 0000FF 36 %}
{% color 3072F3 %}
{% line 3 2 0 %}
{% size 600 200 %}
{% axes type xy %}
{% scale 0 max_val %}
{% marker 's' 'blue' 0 -1,5 %}%}
{% legend 'Mean Response Times' %}
{% axes range 1 0,max_val %}
{% axes label 0 %}
{% axes label 1 0 mid_val max_val %}
{% img alt=DataScaling height=200 id=img title=DataScaling %}
{% endchart %}



The result:


Nothing fancy, but there you have it. Why complicate your life? Simplify with Django and Python.

Thursday, July 21, 2011

Customizing Post-Build Email Notifications in Jenkins

Jenkins, the successor to Hudson, is a general purpose jobs-management console in Java. Lately I've been using this tool a lot to create and monitor nightly automation tests. After a job is run, I like to get an email notification, and in this notification, I want only a summary of the results, a custom-parsing of my console output. It happens that in my console output I've printed each test's results with a line that looks like this: RESULT ==> PASSED: nameOfTest or RESULT ==> FAILED: nameOfTest. All I want in the email is the list of these lines.

The default post-build email job in Jenkins is not very configurable. Fortunately there is an extensible email plugin which provides more functionality. To use it, though, you need to become at least a little familiar with something called 'jelly' - Java/XML hybrid language that apparently belongs to the Maven project.

The plugin comes with a couple of 'jelly' scripts which demonstrate a lot of stuff, including how to paste the console output into your post-build email notification. I only wanted to paste part of the output, though, and had to modify the original script. This was tricky only until I understood that within this jelly language, you call normal Java functions.

Eventually, I did have  success parsing the console output using the jelly script. I made a copy of the html.jelly included in the email-ext, found in /var/lib/jenkins/plugins/email-ext/WEB-INF/classes/hudson/plugins/emailext/templates

at the bottom of html.jelly is this section: <br />
<j:getstatic classname="hudson.model.Result" field="FAILURE" var="resultFailure">
<j:if test="${build.result==resultFailure}">


<j:foreach items="${build.getLog(100)}" var="line"></j:foreach>
<table cellpadding="0" cellspacing="0"><tbody>
<tr><td class="bg1">CONSOLE OUTPUT</td></tr>
<tr><td class="console">${line}</td></tr>
</tbody></table>
</j:if></j:getstatic>


I wanted to parse out any line containing the word 'RESULT' regardless of whether the build passed or failed, so I removed the build.result lines and added an if statement inside the forEach loop. Reading the last 100 lines of the console output (retrieved by build.getLog), I tested each line for 'RESULT' using the Java String indexOf function, and when that resulted in 'true', I printed out the line
<br />
<j:foreach items="${build.getLog(100)}" var="line">
<j:if test="${line.indexOf('RESULT')&gt;=0}">
</j:if></j:foreach>


<table cellpadding="0" cellspacing="0"><tbody>
<tr><td class="console">${line}</td></tr>
</tbody></table>
</pre>
<br />
<br />
in the Jenkins job configuration (Editable Email Notification, in the post-build options), I selected HTML output and referenced this new, modified file (I called it custom_html2.jelly)
in DEFAULT CONTENT, like this: <br />
${JELLY_SCRIPT,template="custom_html2"} <br />
<br />
On post-build, the plugin uses the custom_html2.jelly to prepare and include the parsed output into the email notification

Friday, May 27, 2011

Praystation - An Android App

My first Android app is a wee little thing that draws an arrow pointing to Mecca from wherever you are. As such, it demonstrates some use of the Locator Api as well as some Canvas drawing and rotating

To begin with, the AndroidManifest.xml file contains simply a text area with a canvas beneath it. It also requires location permissions:

<manifest android:versioncode="1" android:versionname="1.0" 

package="com.praystation" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-sdk android:minsdkversion="8"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"> <application android:icon="@drawable/arrow" android:label="@string/app_name"> <activity android:label="@string/app_name" android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"> <category android:name="android.intent.category.LAUNCHER"> & </category> </action> </intent-filter> </activity> </application></uses-permission></uses-permission></uses-sdk></manifest>

I have an 'arrow.png' file in res/drawable for the app logo as well as for the arrow used in this app itself.

The MainActivity finds the best LocationManager and gets your location (assuming you have gps enabled o your device, of course). It also takes in the fixed coordinates of Mecca and calculates the distance and bearing from where you are to where it is:

package com.praystation;

import android.app.Activity;
import java.util.List;

import android.graphics.Canvas;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity  implements LocationListener {
    private static final String TAG = "LocationDemo";
    private static final String[] S = { "Out of Service",
            "Temporarily Unavailable", "Available" };
    
    private TextView output;
    private LocationManager locationManager;
    private Location mMarkedLocation;
    private String bestProvider;
    
    public static float sBearing = 0.0f;
    public static float sDistance = 0.0f;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // Get the output UI
        output = (TextView) findViewById(R.id.output);

        // Get the location manager
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

        Criteria criteria = new Criteria();
        bestProvider = locationManager.getBestProvider(criteria, false);

        Location location = locationManager.getLastKnownLocation(bestProvider);
        printLocation(location);
        
        mMarkedLocation = new Location("Mecca");
        mMarkedLocation.setLatitude(21.4266667);
        mMarkedLocation.setLongitude(39.8261111);
        
        sDistance = location.distanceTo(mMarkedLocation) / 1000;
        output.append(String.format("\n\ndistance to Mecca: %.2f kilometers\n", sDistance));
        
        sBearing = location.bearingTo(mMarkedLocation);
        output.append(String.format("\n\nbearing to Mecca: %.2f degrees\n", sBearing));
    }

    /** Register for the updates when Activity is in foreground */
    @Override
    protected void onResume() {
        super.onResume();
        locationManager.requestLocationUpdates(bestProvider, 20000L, 1.0f, this);
    }

    /** Stop the updates when Activity is paused */
    @Override
    protected void onPause() {
        super.onPause();
        locationManager.removeUpdates(this);
    }

    public void onLocationChanged(Location location) {
        printLocation(location);
    }

    public void onProviderDisabled(String provider) {
        // let okProvider be bestProvider
        // re-register for updates
//        output.append("\n\nProvider Disabled: " + provider);
    }

    public void onProviderEnabled(String provider) {
        // is provider better than bestProvider?
        // is yes, bestProvider = provider
//        output.append("\n\nProvider Enabled: " + provider);
    }

    public void onStatusChanged(String provider, int status, Bundle extras) {
//        output.append("\n\nProvider Status Changed: " + provider + ", Status="
//                + S[status] + ", Extras=" + extras);
    }

    private void printProvider(String provider) {
        LocationProvider info = locationManager.getProvider(provider);
//        output.append(info.toString() + "\n\n");
    }

    private void printLocation(Location location) {
//        if (location == null)
//            output.append("\nLocation[unknown]\n\n");
//        else
//            output.append("\n\n" + location.toString());
    }
    
}

The Panel class does the actual drawing of the canvas, taking the arrow image and rotating it in the direction of the static calculated bearing:

package com.praystation;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class Panel extends SurfaceView implements SurfaceHolder.Callback{
    private CanvasThread canvasthread;
    
    public Panel(Context context, AttributeSet attrs) {
        super(context, attrs); 
        // TODO Auto-generated constructor stub
        getHolder().addCallback(this);
        canvasthread = new CanvasThread(getHolder(), this);
        setFocusable(true);
    }

     public Panel(Context context) {
           super(context);
            getHolder().addCallback(this);
            canvasthread = new CanvasThread(getHolder(), this);
            setFocusable(true);
        }

    @Override
    public void onDraw(Canvas canvas) {
        Log.d("ondraw", "lefutott");
        Paint paint = new Paint();
        int isRusty = 1;

        Bitmap arrow = BitmapFactory.decodeResource(getResources(),
                R.drawable.arrow2);
        canvas.drawColor(Color.BLACK);
        
        Matrix rotateMatrix = new Matrix();
        rotateMatrix.setRotate(MainActivity.sBearing, canvas.getWidth()/2, canvas.getHeight()/2);
        canvas.drawBitmap(arrow, rotateMatrix, null);
    }
    

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        // TODO Auto-generated method stub
        
    }
    
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // TODO Auto-generated method stub
        canvasthread.setRunning(true);
        canvasthread.start();    
    }
    
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // TODO Auto-generated method stub
        boolean retry = true;
        canvasthread.setRunning(false);
        while (retry) {
            try {
                canvasthread.join();
                retry = false;
            } catch (InterruptedException e) {
                // we will try it again and again...
            }
        }

    }
}

Finally, we have a CanvasThread class

package com.praystation;

import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class CanvasThread extends Thread {
    private SurfaceHolder _surfaceHolder;
    private Panel _panel;
    private boolean _run = false;

    public CanvasThread(SurfaceHolder surfaceHolder, Panel panel) {
        _surfaceHolder = surfaceHolder;
        _panel = panel;
    }

    public void setRunning(boolean run) {
        _run = run;
    }

    @Override
    public void run() {
        Canvas c;
        while (_run) {
            c = null;
            try {
                c = _surfaceHolder.lockCanvas(null);
                synchronized (_surfaceHolder) {
                    _panel.onDraw(c);
                }
            } finally {
                // do this in a finally so that if an exception is thrown
                // during the above, we don't leave the Surface in an
                // inconsistent state
                if (c != null) {
                    _surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
    }
}

Saturday, May 21, 2011

Using Grinder: some notes

Grinder is a free multi-purpose open source load testing tool. It can be used to drive Java/Jython tests as well as Web Browser tests, functional as well as load. Install Jython as well as Grinder.



There is also en Eclipse plugin to help run/debug Grinder scripts - called GrinderStone, you can find info about it here



To record and playback a Browser Test:



1) Configure your Browser to use a Proxy (e.g. localhost 8001 is the default - set this for http and https)

2) Set your classpath to include grinder/lib/grinder.jar and jython's jython.jar (not grinder's jython.jar !!!)

3) Start Grinder in Proxy Mode: java net.grinder.TCPProxy -console -http > grinder.py

launch with -localPort XXXX to change the Proxy port

4) Start your Browser and Do Your Actions

5) Press "Stop" on the TCPProxy console and the generated script will be written to grinder.py.



grinder.py is in python and can be edited to your heart's content (replace hard-coded users with variables read in from a file, for example)



To replay the file, create a grinder.properties text file with at least this setting:

grinder.script grinder.py

and run it

java net.grinder.Grinder grinder.properties



log files will be output in the current working directory, text and data (csv suitable for loading, parsing and charting in a spreadsheet)



other info here for proxy usage: http://grinder.sourceforge.net/g3/tcpproxy.html



for multi-threaaded, define more threads per process in your grinder.properties file:

grinder.processes 1

grinder.threads 100



if you customize your script to use python modules, you will need to set the python.home and python.path environment variables and pass them into your command-line arguments

java -Dpython.home=/Users/YOU/jython2.5.2 -Dpython.path=/Users/YOU/jython2.5.2/lib net.grinder.Grinder grinder.properties

and in grinder.properties:

grinder.jvm.arguments = -Dpython.home=/Users/YOU/jython2.5.2 -Dpython.path=/Users/YOU/jython2.5.2/lib





*Some lessons learned the hard way*:



disable cookies (that is to say, don't let ourselves get in the way of the record/replay process as far as cookies are concerned. leave your cookies alone)

connectionDefaults.setUseCookies(0)



avoid complex Wicket GETS

everything gets recorded but not everything needs to be replayed. for example, something like this:

self.token_wicketinterface = \

':2:ARightColumn:AudioAppsPanel:rows:1:cols:2:

cannot be relied on.

if you have a page method like that, simply "return ''" at the beginning of the method to avoid wicket/irresolution hell



definitely use the TCPProxy (formerly TCPSniffer) to watch what the hell is going on

java net.grinder.TCPProxy

and if you do so while replaying, make sure your replay test is using the proxy by uncommenting this line

connectionDefaults.setProxyServer("localhost", 8001)



*really gnarly grinder stuff*

say you have recorded a script and you want to add stuff to it from another recorded script.

yikes!!



several steps are involved because numbers will collide and it's all numbers in grinder (requestXXXX, pageXXX, TestXXXX)

1. in the new script, find all the 'request' methods of the areas you want to add, and give them real names



e.g. change

request1700 = HTTPRequest(url=url1, headers=headers4)

request1700 = Test(1700, 'GET redButton_White_150.png').wrap(1700)

request1701 = HTTPRequest(url=url1, headers=headers4)

request1701 = Test(1701, 'GET collapseButton.png').wrap(request1701)



to:

requestAolExtra1700 = HTTPRequest(url=url1, headers=headers4)

requestAolExtra1700 = Test(1700, 'GET redButton_White_150.png').wrap(requestAolExtra1700)

requestAolExtra1701 = HTTPRequest(url=url1, headers=headers4)

requestAolExtra1701 = Test(1701, 'GET collapseButton.png').wrap(requestAolExtra1701)



change the pageXXXX method to match (request170x will be page17, for example)

change:

def page17(self):

to:

def pageAolExtra17(self):



how change the instrumented 'Test' as well

change:

instrumentMethod(Test(1700, 'Page 17'), 'page17')

to:

instrumentMethod(Test(1700, 'Do Aol Extra'), 'pageAolExtra17')



now, copy those changed lines and methods (request lines, former 'page' methods, and InstrumentMethod lines) into the original file: BUT WAIT!! The Test number may well collide with some existing one, so change it too, for example by adding '1' or '2' (etc) in front of the number, e.g:

instrumentMethod(Test(11700, 'Go To Extras Tab'), 'pageAolExtra17')



In the __call__ method, pageXXX methods are called. Here is where you can call your newly renamed page methods as well. The pageXXXX methods call the requestXXXX methods, which use the Instrumented Test methods to make the actual call.



Also watch out for headers referenced in the new request methods - make sure you merge the right ones properly into the original file.

Ad-Hoc Selenium Development using Ruby and IRB: some notes

There are a number of ways to go about developing Selenium tests. You can use the Selenium IDE Firefox plugin and record browser actions, but often the xpaths the IDE selects are not necessarily the ones you want to use - for example, they might capture wicket id's that are dynamically created and may not even work on immediate playback. With any luck, your web page developers will use custom, unique id's for their html widgets, and you can find these using Google Chrome inspection (right-click on a widget, and select 'Inspect Element') or the Firebug plugin to Firefox which does the same thing. Even so, xpaths can be tricky to get exactly right, which is why I often do ad-hoc Selenium development using Ruby and IRB (the interactive ruby environment). With this approach, you can do trial and error and get immediate feedback while developing your Selenium test scripts.



There are of course a few things you need to get going: Ruby, for one, Java, and Selenium. You can get the selenium-server.jar here, and for Ruby, you will need the selenium client gem:

sudo gem install selenium-client



You will also want to bookmark the Ruby SeleniumRC docs



In a terminal window, you can start the default selenium server like this:

java -jar selenium-server.jar

(you can now use selenium-server-standalone-2.1.0.jar for Firefox 5 support)

but when using Firefox it can be helpful to use a Firefox Profile that includes Firebug - this allows you to explore xpaths while doing your ad-hoc selenium explorations.



Create a separate Firefox profile

1 - (on Mac) /Applications/Firefox.app/Contents/MacOS/firefox-bin -ProfileManager

2 - Create a new profile, name it an save it to a new folder (e.g. /Users/YOU/firefox)

3 - Start Firefox using the new profile, and install the firebug Plugin

4 - Edit the file prefs.js within the new profile's folder

comment out this line: user_pref("browser.startup.page", 0);

make sure this value is 'true': user_pref("extensions.firebug.console.enableSites", true);

5 - Launch the selenium server with option to use the new profile

java -jar selenium-server.jar -firefoxProfileTemplate "/Users/YOU/firefox"



Now, in a different terminal window, start IRB and launch a Selenium client (this example uses the a bogus page and assumes a signed-up account, which can easily be done manually from the same page)



> irb



---------



# create and launch the selenium client (naming it '@browser')

require 'selenium/client'

@browser = Selenium::Client::Driver.new \

:host => "localhost",

:port => 4444,

:browser => "*chrome",

:url => "http:/www.bogus.com",

:timeout_in_second => 60



@browser.start_new_browser_session

# goto nowhere

url = "http://www.bogus.com/nowhere"

@browser.open url

# login

@browser.type("//form[@id='signInForm']//input[@name='email']", "you@email.com")

@browser.type("//form[@id='signInForm']//input[@name='password']", "password")

@browser.click("//form[@id='signInForm']//a")



# once signed in, see how many tutorial links there are

@browser.get_xpath_count("//div[@id='videoCategories']//td")



--------



change the url string to any website you like, and once there, select any widget, right-click on it and select 'Inspect Element' to bring up the Firebug console and start experimenting with the xpaths.



Some of the more common Selenium Client commands:

@browser.click(xpath_to_clickable)

@browser.type(xpath_to_input_form)

@browser.is_text_present(some_string)

@browser.go_back



The Selenium client commands come in different languages, and are essentially the same in all of them. The main difference between methods in Ruby and Java is that the Java methods use camelCase (isTextPresent instead of is_text_present) - this makes it pretty easy to translate your ad-hoc Ruby experiments into Java test cases

Wednesday, May 11, 2011

Using PushToTest: some notes

This tool can be used to record browser activity using Selenium IDE, drive the recorded script with a csv file (users,passwords, etc ...) and run the same test as functional and load.

download from here

Steps:

Run PushToTest
Select Tools > Designer menu item
Select Script Type: Selenium from TestObject Window
Click on Record - enter a URL, select Firefox browser, and click OK
Perform your Browser actions
Click on 'End Record' when you're done

To drive from a csv file, select the Data Icon in the middle of the TestObject Window (looks like a book, to the right of the light bulb icon)
Select the 'Get Next Row' radio button
Select a .csv file, where the first line contains variable names:

EMAIL,PASSWORD
you@email.com,passwd9

The CSV file, when loaded, is presented in the Data tab in the bottom of the TestObjectWindow
Click and Drag a field from the CSV (in the Data tab) to a Value field in the Action window above - the value already there will be replaced by "$EMAIL" (for example)

Click the Play Icon to replay the recording and make sure it works

Select File > Save to save your recording (will be saved as a .ds file)
To Convert this functional test into a Load Test:

From PushToTest Main Window, select File > New Load Test
this brings up another Window - the PushToTest TestMaker Editor

Select the Use Cases tab
for 'Resource', Browse to select the .ds file you just saved
for 'Test Type', select 'Designer Script'
for 'Test Name', name your test
for 'Command Language', 'sahi'
for 'Browser', 'SahiHtmlUnit'
for 'Instance', you can leave this blank

Click on the 'Add DPL' link
for 'Data Source', browse for your .csv file
for 'DPL Type', select 'Hash DPL' (the default)
for 'DPL Name', give it a name or leave as untitled
for 'Action' select 'Get Next Row of Data'

Click on the 'General' Tab
Add Virtual Users Levels, e.g.
begin with one level for 1 user for 1 minute
add another level with 2 users for 1 minute
add another level with 4 users for 1 minute
etc ...

Click on the Save icon in the Left pane - (will be saved as a .scenario file)

Click on the Play icon in the left pane (to the right of the Save icon) to run the test

When the test is done, another Window pops up with all sorts of graphs and charts
Re-Open and explore other scenario options

Open the Scenario from the Main PushToTest Window through the Tools > Editor menu item