Posts tagged as:

TestNG

Last week, I evaluated InfoStretch framework (ISFW) and Sauce on Demand (SOD) integration. My main concern was to test not only the integration, but also to verify parallel execution of test that is supported by ISFW and SOD. I found that it worked without any additional efforts along with all benefits of the framework including parallel execution and auto screenshots.  In this blog, I will provide details on integration and how to configure your test to run on SOD.

Preface:

Testing web applications often requires running tests against multiple browsers and in multiple environments. Using ISFW, one can run tests on a local or a remote physical machine. For that, you need to set server, port and browser in the properties file. You can configure to execute test in parallel with different options. Furthermore, you can configure your test to run in parallel on distributed servers in different environment as well. If you are using distributed servers, then the only requirement is that selenium server must be running on a remote machine. We have implemented this for some of our clients. ISFW supports overriding server, port and browser properties from configuration file by providing appropriate parameters at suite/test/package/class level. Thus, you can configure multiple server-browser combinations.

Sauce on demand Integration

To run test on cloud with ISFW, all you need to change is the server, port and browser info provided by sauce labs. You can run your test in one or more environments without having your own infrastructure. The most powerful feature of ISFW is that it provides parallel-ready test harness to connect tests to Sauce Labs’ service. As ISFW can run tests in parallel in multiple threads you can achieve parallelism with SOD as well. To use multiple environments/browsers you just need to override browser parameter in the configuration file.

One common issue is related to json string for browser, faced when you want to set browser from xml configuration file. Normally, value of the browser parameter in the configuration file is a string like *firefox, *iehta, etc. But, for SOD you need to provide json string which contains double quotes (“). So, it will not work as parameter value.To overcome this limitation you can provide any property, defined in properties file, as browser parameter value. It’s really helpful that ISFW supports browser parameter value as property!..

Benefits:

  • By using Sauce Labs’ service you don’t required to setup different environments.
  • With ISFW, no additional efforts are required to run your test using Sauce Labs’ service
  • Get all benefits of the IS framework, including parallel execution and auto screenshots.

Those who are interested getting details of the code and configuration used for evaluation, see Code: test-cases used for evaluation, Configuration 1 and Configuration 2. You might also be interested in reading an  informative blog on Selenium UI test automation with zero infrastructure cost authored by Akhil.

 

Code: test-cases used for evaluation.

package com.sample.automation.tests;
import
/**
* Demo on how to write quick tests.

* These test not uses {@link TestPage} implementation provided by FW.
* @author chirag
*/

public class Demo extends BaseTestCase {
@Test(description = “Sample test”)
public
void TCtest() throws Exception {
final
IsSelenium selenium = getSTB().getSelenium();
WaitService waitService =
new WaitService();
selenium.open(
“/”);

selenium.type(
“q”, “infostretch automation framework”);

selenium.click(
“btnG”);

selenium.click(
“link=glob:*Automation Framework”);
// selenium.waitForPageToLoad(“5000″);
waitService.waitForPageToLoad();

getSTB
().assertEquals(
“InfoStretch Test Automation Framework”,

selenium.getText(
“css=h1.entry-title”), “Heading”);

}

/**
* Data driven test that aspect csv data file. The file path must be set
* using property
<code>test.TCDataDriven.datafile</code>

*
@param query
*
@param linkloctoverify
*
@throws Exception
*/

@Test(description = “Sample data driven test from above one”, dataProvider = “csvDataProvider”, dataProviderClass = DataProviderUtil.class)

public void TCDataDriven(String query, String linkloctoverify) throws Exception {
final
IsSelenium selenium = getSTB().getSelenium();
selenium.open(
“/”);

selenium.type(
“q”, query);

selenium.click(
“btnG”);

getSTB
().verifyElementPresent(linkloctoverify,
“Search result”);

}

@Test(description = “using property from property file”)
public
void propTest() {

final IsSelenium selenium = getSTB().getSelenium();selenium.open(“/”);
selenium.type(props.getPropertyValue(“search.txt.loc”, “q”),”infostretch automation framework”);
selenium.click(
props.getPropertyValue(“search.submit.loc”, “btnG”));

selenium.click(
“link=glob:*Automation Framework”);

}

}

Data File

TCDataDriven is data driven test and the searchText.csv data file provided with following entries. So TCDataDriven will execute in separate threads for each data set, results in 3 tests running parallel.

Qmetry,css=a[href*=www.qmetry.com]Infostretch,css=a[href*=www.infostretch.com]

infostretch selenium,css=a[href*=blog.infostretch.com]

Configuration

Configuration 1:

Here are the settings to run test in windows environment with Firefox. With this configuration data driven test will get executed in parallel.

Properties:

selenium.server=ondemand.saucelabs.com
selenium.port=80
selenium.defaultBrowser=
{“username”: “cjayswal”,\
“access-key”
: “????????-????-????-????-????????????”,\
“os”
: “Windows 2003″,\
“browser”
: “firefox”,\
“browser-version”
: “3.6″,\
“name”
: “This is an example test”}

Configuration file:

<!DOCTYPE suite SYSTEM “http://testng.org/testng-1.0.dtd”>
<suite name=“Sample Test Automation” verbose=“0″ data-provider-thread-count=“10″>
<
test name=“Sample Test”>
<
packages>
<
package name=“com.sample.automation.tests”/>
</
packages>
</
test>

</
suite>

Configuration 2:

 

Configuration to run test in IE8, FF on Windows and FF on Linux. Attribute parallel=“tests” will execute each xml test in separate thread which in turn executes data driven test in parallel (check the time stamp in attached report).

Configuration file:

<!DOCTYPE suite SYSTEM “http://testng.org/testng-1.0.dtd”>
<suite name=“Sample Test Automation” verbose=“0″ parallel=“tests” data-provider-thread-count=“10″ >
<
test name=“Test on Win2003 FireFox3.6″ >
<
parameter name=“browser” value=“sauce.json.firefox” />
<packages>
<
package name=“com.sample.automation.tests”/>
</
packages>
</
test>
<test name=“Test on Win2003 IE8″>
<
parameter name=“browser” value=“sauce.json.iehta”/>

<
packages>
<
package name=“com.sample.automation.tests”/>
</
packages>
</
test>

<test name=“Test on Linux FireFox3.6″>
<
parameter name=“browser” value=“sauce.json.firefoxOnLinux”/>
<
packages>
<
package name=“com.sample.automation.tests”/>
</
packages>
</
test>

</suite>

Properties:

selenium.server=ondemand.saucelabs.comselenium.port=80
sauce.json.firefoxOnLinux=
{“username”: “cjayswal”,\
“access-key”
: “????????-????-????-????-????????????”,\
os
: Linux“,\
“browser”
: firefox“,\
“browser-version”
: “3.6″,\
“name”
: “Parallel run evaluation with InfoStretch fw“}

sauce.json.firefox={“username”: “cjayswal”,\
“access-key”
: “????????-????-????-????-????????????”,\
os
: “Windows 2003″,\
“browser”
: firefox“,\
“browser-version”
: “3.6″,\
“name”
: “Parallel runevaluationwithInfoStretchfw“}

sauce.json.iehta={“username”: “cjayswal”,\
“access-key”
: “????????-????-????-????-????????????”,\
os
: “Windows 2003″,\
“browser”
: iehta“,\
“browser-version”
: “8″,\
“name”
: “Parallelrun evaluation withInfoStretchfw“}

Screenshots:

Report: overview
Report: Test on Win2003 FireFox3.6

package com.sample.automation.tests;

import

/**

* Demo on how to write quick tests.

* These test not uses {@link TestPage} implementation provided by FW.

* You can also utilize IDE plug-in for InfoStrech framework.

*

* @author chirag

*/

publicclass Demo extends BaseTestCase {

@Test(description = “Sample test”)

publicvoid TCtest() throws Exception {

final IsSelenium selenium = getSTB().getSelenium();

WaitService waitService = new WaitService();

selenium.open(“/”);

selenium.type(“q”, “infostretch automation framework”);

selenium.click(“btnG”);

selenium.click(“link=glob:*Automation Framework”);

// selenium.waitForPageToLoad(“5000″);

waitService.waitForPageToLoad();

getSTB().assertEquals(“InfoStretch Test Automation Framework”,

selenium.getText(“css=h1.entry-title”), “Heading”);

}

/***

* Data driven test that aspect csv data file. The file path must be set

* using property <code>test.TCDataDriven.datafile</code>

*

* @param query

* @param linkloctoverify

* @throws Exception

*/

@Test(description = “Sample data driven test from above one”, dataProvider = “csvDataProvider”, dataProviderClass = DataProviderUtil.class)

publicvoid TCDataDriven(String query, String linkloctoverify) throws Exception {

final IsSelenium selenium = getSTB().getSelenium();

selenium.open(“/”);

selenium.type(“q”, query);

selenium.click(“btnG”);

getSTB().verifyElementPresent(linkloctoverify, “Search result”);

}

@Test(description = “using property from property file”)

publicvoid propTest() {

final IsSelenium selenium = getSTB().getSelenium();

selenium.open(“/”);

selenium.type(props.getPropertyValue(“search.txt.loc”, “q”),

“infostretch automation framework”);

selenium.click(props.getPropertyValue(“search.submit.loc”, “btnG”));

selenium.click(“link=glob:*Automation Framework”);

}

}

VN:F [1.9.10_1130]
Rating: 9.7/10 (7 votes cast)

  

{ 2 comments }

InfoStretch test automation framework provides test page concept in a best efficient way by which you can manipulate page navigation same as on actual web application under test. Once page get created page objects/functionalities can be used in any test case, makes code more reusable. The framework takes care of not only launching that page but the entire page hierarchy to reach that specific page. Furthermore it also checks that is page already active in browser? If so then it will continue from there, results in reduced execution time.

Following are two of the test cases that demonstrates

  • Reusability of code
  • Reduced execution time
  • Less maintenance

As the class the derived from framework’s base class, test case developer only need to concentrate on writing the tests and not spend time on adjusting the underlying framework.

      @Priority(value = 1)
      @Test(enabled = true, groups = {"BulkUpload", "Supplier", "Report"},
      description = "TC4277: Verify that 'Upload Status Report' link is visible to buyer on 'Supplier Upload Status' page.")
      public void TC4277() {
            String jobid = context.getAttribute("upload.jobid.TC4277").toString();
            UploadStatusPage statusPage = new UploadStatusPage(getSTB());
            statusPage.launchPage(jobid);
            getSTB().assertElementPresent(UploadStatusPage.UPLOAD_STATUS_REPORT_LINK_LOC,
                        "'View Report' link");
            getSTB().verifyIsVisible(UploadStatusPage.UPLOAD_STATUS_REPORT_LINK_DIV_LOC,
                        "View Report link");
      }

Commands executed in selenium. You can see commands executed for the entire page hierarchy to reach that specific page.

getNewBrowserSession *iehta https://www.domainname.com/ OK,6636575977794f15be1fd6bbdabc5642
setTimeout 100000 OK
setContext TC4277 OK
isTextPresent 10560452 OK,false
isElementPresent //td[@class='pageTitle'] OK,false
isElementPresent link=Suppliers OK,false
isElementPresent link=Home OK,false
isElementPresent link=Suppliers OK,false
isTextPresent Login OK,true
isElementPresent xpath=(//input[@name='j_username'])[1] OK,false
open /aems/login.do OK
waitForPageToLoad 100000 OK
isTextPresent Home OK,false
isTextPresent Login OK,true
isElementPresent xpath=(//input[@name='j_username'])[1] OK,true
isTextPresent Home OK,false
waitForCondition selenium.isElementPresent(“xpath=(//input[@name='j_username'])[1]“) 100000 OK
type xpath=(//input[@name='j_username'])[1] xxx OK
type xpath=(//input[@name='j_password'])[1] yyy OK
click xpath=(//input[@type='submit'])[1] OK
waitForPageToLoad 100000 OK
click link=Suppliers OK
waitForPageToLoad 100000 OK
isElementPresent link=Upload History OK,true
click link=Upload History OK
waitForPageToLoad 100000 OK
isElementPresent //table/tbody/tr[td/a[contains(text(),'10560452')]][1]/td/a[contains(text(),'View')] OK,true
click //table/tbody/tr[td/a[contains(text(),'10560452')]][1]/td/a[contains(text(),'View')] OK
waitForPageToLoad 100000 OK
isElementPresent link=View Report OK,true
isVisible //div[@id='statusReportLink'] OK,true

Framework concept is based on page services so your page and related actions will be reusable from any test case. Thus test case becomes highly maintainable and utilize reusable test asset with proper modularity and semantic structure.

In case of sequential execution it will take advantage of sharing browser sessions between multiple test cases. No special coding or design required to run test in parallel, you just need to set parallel attribute’s appropriate value in configuration file (eg. false, Test, methods, classes) and framework will take care for providing thread safe browser sessions with maximum level of sharing browser session between multiple test cases. This will result in reducing time by parallel processing as well as by some level of sharing browser session(depends on configuration). You also can configure to run parallel in different browser (eg. iexplorer, firefox) with or without selenium grid.

Here is another test case, which get executed after above one. It will found the page loaded and get continued for test steps. Thus results in less execution time.

       @Priority(value = 2)
       @Test(enabled = true, groups = {"BulkUpload", "Supplier", "Report"},
       description = "Verify that application generates in-progress status bar during report execution.")
       public void TC4278() {
              String jobid = context.getAttribute("upload.jobid.TC4278").toString();
              UploadStatusPage statusPage = new UploadStatusPage(getSTB());
              statusPage.launchPage(jobid);
              getSTB().assertElementPresent(UploadStatusPage.UPLOAD_STATUS_REPORT_LINK_LOC,
                           "'View Report' link");
              getSTB().verifyIsVisible(UploadStatusPage.UPLOAD_STATUS_REPORT_LINK_DIV_LOC,
                           "'View Report' link");
              statusPage.clickUploadSatatusReportLink();
              getSTB().verifyIsVisible (UploadStatusPage.INPROGRESS_STATUS_BAR_DIV_LOC,
                           "In-progress status bar");
       }
Selenium-Command Parameter-1 Parameter-2 Res.RC
setContext TC4278 OK
isTextPresent 10560452 OK,true
isElementPresent //td[@class='pageTitle'] OK,true
getText //td[@class='pageTitle'] OK,Supplier Upload Status
isTextPresent 10560452 OK,true
isElementPresent link=View Report OK,true
isVisible //div[@id='statusReportLink'] OK,true
click link=View Report OK
isVisible //div[@id='statusReportProgress'] OK,false

When functionality changes only the specific test page file needs to be updated: if there is any change in page/ui of web application under test you need to update just in particular page rather than each and every test case, thus result in less maintenance.

Following page class illustrate how the navigation took place by derived page object. Whenever page’s launchPage method called framework will check for existence of page in browser if it is not loaded then it will call openPage method to open page from parent/launcher page (UploadHistoryPage in our case) Framework will call openPage method only if page is not loaded and parent is loaded. If parent is not loaded framework will call parent’s launch method.

public class UploadStatusPage extends BaseTestPage<UploadHistoryPage> {

      @Override
      protected void openPage(PageLocator locator) {
            parent.viewPostUploadResults(locator.getLocator());
      }

      //method below check is supplier upload status page open and for given job?
      @Override
      public boolean isPageActive() { 

            return pageLocator != null
                  && pageLocator.getLocator() != null
                  && selenium.isElementPresent("//td[@class='pageTitle']")
                        && selenium.getText("//td[@class='pageTitle']").trim().equalsIgnoreCase(
                                    "Supplier Upload Status")
                        && selenium.isTextPresent(pageLocator.getLocator());
      }
      //overloaded method for simplicity
      public void launchPage(String fileNameOrJobID) {
            launchPage(new DefaultPageLocator(fileNameOrJobID));
      }

      @Override
      protected void initParent() {
            parent = new UploadHistoryPage(stb);
      }

//all page specific functionality goes here

}

Generated Report displays:

  • description of test case
  • browser name
  • duration
  • selenium command log
  • assertion/verification/information message Screens-shots for failure (also can configure for pass assertion/verification)

FAQ

Can I run each test without sharing browser session?

Yes, set property selenium.singletone=0

It will start new selenium session for each test. Still you can save execution time by configure to run methods in parallel

How to run test in parallel?

To run test in parallel you need to set parallel attribute appropriate value in configuration file. You can found configuration details in TestNG documentation.

Can I capture screenshot for passed assertion/verification?

Yes, set selenium.success.screenshots=1. It will automatically cupture screenshot and create link for pass messages.

VN:F [1.9.10_1130]
Rating: 10.0/10 (6 votes cast)

  

{ 12 comments }

InfoStretch Test Automation Framework

[click to continue…]

VN:F [1.9.10_1130]
Rating: 9.7/10 (28 votes cast)

  

{ 27 comments }

After my last post on integrating Selenium RC tests with Hudson,I had few folks asking me on how I organized my test execution code with selenium RC tests.

If you are like me you probably started with running Selenium RC tests on your local machine and didn’t give much thought about integrating with Hudson,but when it was time,there was lot of files moving around and unnecessary cruft of code.

To run this sample,you will need ANT and Java on your machine

So here is a sample TestNG runner code which is ready for Hudson to consume and at the same time having a clean separation between Selenium RC tests and execution.This structure should make things easier when multiple team members are writing test cases allowing easier test code checkin to your version control system.So look at the attached zip file.Please find instructions on how to use this in the README folder.I have also included a sample Selenium RC project which the current TestRunner configuration points to.

This code is not intended to replace the Eclipse+TestNG plugin and it’s use in dev environment is highly recommended.But this code does the next step of getting your Selenium RC tests ready to be used by Hudson.For now the test runner is overtly simplistic and can only run a single TestNG configuration file,but feel free to extend this and make your own modifications to suit your needs.

When you are ready for Hudson to run your Selenium RC + TestNG tests,have your Hudson job execute the STunner/bin/seleniumtestrunner.xml and sit back and watch your tests run.

If your Hudson is on a Unix machine,look at my earlier post on Integrating Selenium Tests with Hudson CI

If you want how to convert your hudson cluster to Selenium Grid ,this post should help you.

Let me know if it doesn’t work for you or other issues that you face.

Follow us on @infostretch on twitter to get more updates on Selenium as well how we can help your organization get Selenium ready.We offer multiple Selenium solutions that fits every project size and needs.

Download the Selenium TestNG Runner from here

Important:The bundle doesn’t contain the selenium-server.jar as the size of the bundle increases.So the very first thing you will need to do is to download selenium server and copy it under ‘server’ directory.

VN:F [1.9.10_1130]
Rating: 4.9/10 (9 votes cast)

  

{ 0 comments }

In my last post about Hudson integration with Selenium RC,I talked about how to use Hudson to drive your Selenium RC tests.This configuration would work fine in most scenarios.However this is not a optimized configuration if you want to take advantage of executing your tests in parallel.For e.g if you have two machines in your lab with FF 35,you might want to run RC tests on both machines at same time.

Selenium Grid offers this capabilities.Below I will discuss how to take advantage of this in a continuous integration process.

I strongly recommend that you try to setup Selenium Grid on your local machine first if you had no prior experience with Selenium Grid.There are some very good Selenium Grid demos available here

Step 1

Download and Install Selenium Grid plugin for Hudson.You should see a small icon on your Hudson home page.Clicking on which you would see the RC’s registered to this Selenium Grid

Step 2

Important before this step,make sure that you are able to ping the RC server from Hudson and viceversa.If they cannot see each other it will not work

Registering your RC to Selenium Grid.This took me quite a while to figure out,but the trick is in the way you register this RC.

ant -Denvironment=”/qalab_01/windowsxp_1/:*firefox”  -Dport=5555 -Dhost=10.63.87.192 -DhubURL=http://myhudson.com:4444 launch-remote-control

Let’s go through the parameters one by one,you should be familiar with the parameters if you did a local setup of Selenium Grid.

environment=Machine Name/Label Name:browerCommand (Look at my earlier post about Selenium Hudson Integration to understand how to setup machine names and label names)

port=Available port on the machine where you are launching RC

host=IP address of your RC machine

hubURL=location of your Hudson server (which also doubles up as Selenium Grid now)

Step 3

Repeat similar process for another RC and when you click on the Selenium icon on your Hudson page,you should see that there are two RC’s registered to this Selenium Grid.

Step 4

Configure your Hudson job to run on master(i.e Selenium Grid) and it will take care of assigning the jobs to correct RC

Some important resources that helped me during the setup are

Amit Easow’s post on Hudson integration

Unregistering the Selenium RC command by this Selenium Blog.The command is lifesaver and I am repeating a example here

http://localhost:4444/registration-manager/unregister?host=locahost&port=5556&environment=*iexplore

To give a recap,I used ANT,TestNG,Selenium Grid,Hudson to accomplish this.

One last thing,The Selenium server jar is part of the Selenium Grid plugin bundle.You might want to replace it with a version of Selenium server you like on the Hudson server.

Follow us on @infostretch

VN:F [1.9.10_1130]
Rating: 7.8/10 (13 votes cast)

  

{ 43 comments }

For most folks the challenge is that their CI system lives in some Unix environment and Selenium tests need to run on a Windows machine.To get this working I did the following

My goal was to

a)Have the test drive and report selenium tests from a single interface

b)Ability to run it on different machines with various browsers

I picked Hudson for a) for the simple reason that it’s widely used in our organization as well as it had a Selenium Grid plugin which will probably be used for accomplishing b).It’s easily available and can display JUnit reports in graphical format with drilldown capabilities.In short the works and then some:)

Tools used are :Hudson,Selenium RC 1.0.1,FF 3.5,Ant 1.7,Selenium Grid plugin for hudson,Java,TestNG,SVN,Windows XP,Hudson JUnit plugin

Note:Selenium Grid is only introduced to run the tests on multiple machines and different browsers.If this is what you don’t need,you can avoid one more layer of complexity

Step 1

This step is crucial and can differ from org to org,but if your hudson master and slave are in same subnet this should generally work.Make sure of this by pinging each host to other.If not you might need to get help from your IT such that the master hudson can ping the slave hudson and viceversa.

Step 2 (skip if not running on multiple machines)

Install Selenium Grid plugin from Manage Hudson->Plugins->Install

Hudson restart will be required

Step 3

Create Hudson slave using following method

a)Go to Manage Hudson->Manage Nodes->Add Node(Dumb Slave)

b) Enter in Name field anything you wish.However hudson recommends giving same name as the host name of slave.So give the computer name of your windows machine where the tests will run

c)Num of executors is directly corresponding to number of RC’s you will run.For now keep it 1.If you want to run two browsers(e.g FF 3.5 and IE 7) this number needs to be bumped upto 2

d)Remote FS Root:c:\hudson (this is where hudson Workspace artifacts will be stored if needed).If you are going to use Selenium Grid plugin to run,you won’t need this

e)Labels:Give a unique label e.g(windowsxp_1).You will see it’s use later with Selenium Grid

f)Usage:Leave this machine for tied jobs only

g)Launch slave agents via JNLP

h)Availability keep this machines as much online as possible

Rest of the options keep blank.

Repeat the process for each Slave machine,but to keep things simple let’s just configure 1 machine at a time

Step 4

Go to the Slave machine and

a)Open your browser and point to Hudson Master

b)Go to the Manage Nodes page and you should see a launch JNLP jar icon.Click on it and it should start the process of downloading “remoting.jar” from Hudson Master(I had trouble with this and found out that somehow hudson master didn’t have the jar file at a specific location.With the help of my Hudson admin,I was able to copy this jar correctly).So if you repeatedly fail to launch the slave agent,first thing to check would be the location of remoting.jar.Also look at the java exception and it will give you clues in what could have gone wrong.

c)If all goes well you should see a small window with Hudson icon showing the message “Connected”.

The above step is very important and needs to happen in order to Hudson run selenium tests.If this doesn’t work look into hudson logs,slave logs found on hudson server and there should be some clue on what’s going on.

Step 5

Create a new job on hudson and the configure the option to “run it on the slave node” that you just created.

Publish the Junit report by pointing to the output directory xml file.This will be something like c:\hudson\workspace\xx\xx.xml

Depending on what your hudson job is doing the slave machine should have appropriate software.E.g if you are going to run ANT files,the slave machine needs to have ANT properly configured.Same thing goes with SVN.Essentially everything that your job would need to run on master should be available on slave as well.

Step 6

Start a selenium server on the slave machine as you would if it was your local machine.For this testing make sure that versions of browser that you used for your environment to run selenium test locally is same.This will eliminate unnecessary issues that are not due to this setup but browser configuration issues.

Step 7

Run the hudson job from your master and you should see the browser window popping up on the slave and running the tests.

The above should accomplish the task of running your Selenium tests on a single Windows machine.However many times you would need to run your tests in parallel as well as to optimize hardware use,make sure that Hudson can trigger test on same machine with different browser and report results back.In order to accomplish this we will use the Selenium Grid plugin which I will cover in next part if folks are interested.

Some resources that were helpful to me in this exercise were

Amit Easow’s writeup on Hudson integration


VN:D [1.9.10_1130]
Rating: 8.6/10 (22 votes cast)

  

{ 20 comments }