/*
* The MIT License
*
* Copyright (c) 2010-2012, Manufacture Française des Pneumatiques Michelin,
* Thomas Maurel, Romain Seguy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.michelin.cio.hudson.plugins.qc;
import com.michelin.cio.hudson.plugins.qc.client.QualityCenterClientInstallation;
import com.michelin.cio.hudson.plugins.qc.qtpaddins.QualityCenterQTPAddinsInstallation;
import hudson.AbortException;
import hudson.CopyOnWrite;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.tools.ToolInstallation;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;
import hudson.util.VariableResolver;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
/**
* {@link BuildStep} to run a TestSet from a Quality Center server.
*
* @author Thomas Maurel
*/
public class QualityCenter extends Builder {
private final static String VB_SCRIPT_NAME = "runTestSet.vbs";
public final static String RUN_MODE_LOCAL = "RUN_LOCAL";
public final static String RUN_MODE_PLANNED_HOST = "RUN_PLANNED_HOST";
public final static String RUN_MODE_REMOTE = "RUN_REMOTE";
public final static String[] RUN_MODES = { RUN_MODE_PLANNED_HOST, RUN_MODE_REMOTE, RUN_MODE_LOCAL };
public final static int DEFAULT_TIMEOUT = 600;
/** Quality Center installation name. */
private final String qcClientInstallationName;
/** QTP Addin for Quality Center installation name. */
private final String qcQTPAddinInstallationName;
/** Quality Center server URL. */
private final String qcServerURL;
/** Username to log into the Quality Center server. */
private final String qcLogin;
/** The password associated to the username to log into the Quality Center server. */
private final String qcPass;
/** The domain where the Quality Center project is located. */
private final String qcDomain;
/** The Quality Center project to connect to. */
private final String qcProject;
/** The folder where the TestSet to run is located. */
private final String qcTSFolder;
/** The name of the TestSet to run. */
private final String qcTSName;
/** The name of the report file. */
private final String qcTSLogFile;
/** Timeout */
private final int qcTimeOut;
/** The parsed name (env vars) of the report file. */
private String parsedQcTSLogFile;
private String runMode;
private String runHost;
// parsedQcTSLogFiles is used by QualityCenterResultArchiver in order to gather
// the name of the report files which have been generated by the build step. This
// is mandatory to have this because the name of the generated report files may
// not be the one specified by the user (which happens if several test sets have
// to be run in one build step and if the name specified by the user can't be
// unique).
private transient List<String> testSetLogFiles;
@DataBoundConstructor
public QualityCenter(
String qcClientInstallationName,
String qcQTPAddinInstallationName,
String qcServerURL,
String qcLogin,
String qcPass,
String qcDomain,
String qcProject,
String qcTSFolder,
String qcTSName,
String qcTSLogFile,
int qcTimeOut,
String runMode,
String runHost) {
this.qcClientInstallationName = qcClientInstallationName;
this.qcQTPAddinInstallationName = qcQTPAddinInstallationName;
this.qcServerURL = qcServerURL;
this.qcLogin = qcLogin;
this.qcPass = qcPass;
this.qcDomain = qcDomain;
this.qcProject = qcProject;
this.qcTSFolder = qcTSFolder;
this.qcTSName = qcTSName;
this.qcTSLogFile = qcTSLogFile;
if(qcTimeOut <= 0) {
this.qcTimeOut = DEFAULT_TIMEOUT;
}
else {
this.qcTimeOut = qcTimeOut;
}
if(Arrays.asList(RUN_MODES).contains(runMode)) {
this.runMode = runMode;
}
else {
this.runMode = RUN_MODE_PLANNED_HOST;
}
if(this.runMode.equals(RUN_MODE_REMOTE)) {
this.runHost = runHost;
}
else {
this.runHost = "";
}
}
public String getQcDomain() {
return qcDomain;
}
public String getQcClientInstallationName() {
return qcClientInstallationName;
}
public String getQcQTPAddinInstallationName() {
return qcQTPAddinInstallationName;
}
public String getQcLogin() {
return qcLogin;
}
public String getQcPass() {
return qcPass;
}
public String getQcProject() {
return qcProject;
}
public String getQcServerURL() {
return qcServerURL;
}
public String getQcTSFolder() {
return qcTSFolder;
}
public String getQcTSLogFile() {
return qcTSLogFile;
}
public String getParsedQcTSLogFile() {
return parsedQcTSLogFile;
}
public String getQcTSName() {
return qcTSName;
}
public int getQcTimeOut() {
return qcTimeOut;
}
public String getRunMode() {
return runMode;
}
public String getRunHost() {
return runHost;
}
/**
* @see QualityCenterResultArchiver#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener)
*/
List<String> getTestSetLogFiles() {
return testSetLogFiles;
}
public static String getVbScriptName() {
return VB_SCRIPT_NAME;
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
public QualityCenterClientInstallation getQualityCenterClientInstallation() {
for(QualityCenterClientInstallation installation: getDescriptor().getClientInstallations()) {
if(this.qcClientInstallationName != null && installation.getName().equals(this.qcClientInstallationName)) {
return installation;
}
}
return null;
}
public QualityCenterQTPAddinsInstallation getQualityCenterQTPAddinInstallation() {
for(QualityCenterQTPAddinsInstallation installation: getDescriptor().getQTPAddinsInstallations()) {
if(this.qcQTPAddinInstallationName != null && installation.getName().equals(this.qcQTPAddinInstallationName)) {
return installation;
}
}
return null;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
EnvVars env = build.getEnvironment(listener);
// Has a QC installation been set? If yes, is it really a QC installation?
QualityCenterClientInstallation qcInstallation = getQualityCenterClientInstallation();
if(qcInstallation == null) {
// No installation has been set
listener.fatalError(Messages.QualityCenter_NoInstallationSet());
return false;
}
else {
// Get an installation instance for this specific node.
// Will run the QC Client auto-installer if not installed on this node
qcInstallation = qcInstallation.forNode(Computer.currentComputer().getNode(), listener);
// Will be null if not on a Windows Node
if(qcInstallation == null) {
listener.fatalError(Messages.QualityCenter_NotAvailableOnThisOS());
return false;
}
qcInstallation = qcInstallation.forEnvironment(env);
String qcDll = qcInstallation.getQCDll(launcher);
// If we cant find the OTAClient DLL, then we can't run the testSet
if(qcDll == null) {
listener.fatalError(Messages.QualityCenter_DllNotFound());
return false;
}
// Get an installation instance of the QTP Addin
// QTP Addin must be installed but we dont need to call it directly.
// It means defining QTPAddinsInstallations is only useful to auto-install it on nodes
QualityCenterQTPAddinsInstallation qcQTPInstallation = getQualityCenterQTPAddinInstallation();
if(qcQTPInstallation != null) {
// Get the QTPAddin for this node, and install it if necessary
qcQTPInstallation = qcQTPInstallation.forNode(Computer.currentComputer().getNode(), listener);
qcQTPInstallation = qcQTPInstallation.forEnvironment(env);
}
FilePath projectWS = build.getWorkspace();
// Get the URL to the VBScript used to run the test, which is bundled in the plugin
URL vbsUrl = Hudson.getInstance().pluginManager.uberClassLoader.getResource(VB_SCRIPT_NAME);
if(vbsUrl == null) {
listener.fatalError(Messages.QualityCenter_VBSNotFound());
return false;
}
// Copy the script to the project workspace
FilePath vbScript = projectWS.child(VB_SCRIPT_NAME);
vbScript.copyFrom(vbsUrl);
try {
testSetLogFiles = new ArrayList<String>();
// For each TestSet, run the VBScript
String[] testSetNames = Util.replaceMacro(env.expand(this.qcTSName), build.getBuildVariableResolver()).split("[\t\r\n,]+");
for(String testSetName : testSetNames) {
String logFile = runVBScript(testSetName, build, launcher, listener, vbScript, (testSetNames.length == 1));
// Has the report been successfuly generated?
if(!projectWS.child(logFile).exists()) {
listener.fatalError(Messages.QualityCenter_ReportNotGenerated());
return false;
}
}
}
catch(IOException ioe) {
Util.displayIOException(ioe, listener);
return false;
}
finally {
// Remove the VBScript from workspace
vbScript.delete();
}
}
return true;
}
/**
* Adds some {@link EnvVars} to the project's {@link EnvVars} table.
*
* <p>This {@link EnvVars} can be used in the report file name.</p>
*/
private void pushEnvVars(EnvVars env) {
env.put("QC_DOMAIN", this.qcDomain);
env.put("QC_PROJECT", this.qcProject);
env.put("TS_FOLDER", this.qcTSFolder);
env.put("TS_NAME", this.qcTSName);
}
/**
* Removes the {@link EnvVars} from the project's {@link EnvVars} table.
*/
private void removeEnvVars(EnvVars env) {
env.remove("QC_DOMAIN");
env.remove("QC_PROJECT");
env.remove("TS_FOLDER");
env.remove("TS_NAME");
}
/**
* Runs the given TestSet ({@code testSetName}) through VBScript.
*/
private String runVBScript(String testSetName, AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener, FilePath file, boolean isRunOnce) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder();
EnvVars env = build.getEnvironment(listener);
VariableResolver<String> varResolver = build.getBuildVariableResolver();
PrintStream out = listener.getLogger();
// Add the qc specific env vars
pushEnvVars(env);
// Parse the report file name using env vars
this.parsedQcTSLogFile = Util.replaceMacro(env.expand(this.qcTSLogFile), varResolver);
if(!parsedQcTSLogFile.endsWith(".xml")) {
parsedQcTSLogFile = parsedQcTSLogFile + ".xml";
}
if(!isRunOnce && !parsedQcTSLogFile.contains(testSetName)) {
// JENKINS-12384: One file must be generated per test set. As such we must
// ensure that the name of each file is unigue. We consider it is the case
// if the name of the file contains the name of the test set. Otherwise,
// we add it.
parsedQcTSLogFile = parsedQcTSLogFile.substring(0, parsedQcTSLogFile.length()-4) + '_' + testSetName + ".xml";
}
testSetLogFiles.add(parsedQcTSLogFile);
// Use cscript to run the vbscript and get the console output
args.add("cscript");
args.add("/nologo");
args.add(file);
args.add(Util.replaceMacro(env.expand(this.qcServerURL), varResolver));
args.add(Util.replaceMacro(env.expand(this.qcLogin), varResolver));
// If no password, then replace by ""
if(StringUtils.isNotBlank(this.qcPass)) {
args.addMasked(Util.replaceMacro(env.expand(this.qcPass), varResolver));
}
else {
args.addMasked("\"\"");
}
args.add(Util.replaceMacro(env.expand(this.qcDomain), varResolver));
args.add(Util.replaceMacro(env.expand(this.qcProject), varResolver));
args.add(Util.replaceMacro(env.expand(this.qcTSFolder), varResolver));
args.add(Util.replaceMacro(env.expand(testSetName), varResolver));
args.add(this.parsedQcTSLogFile);
args.add(this.qcTimeOut);
args.add(runMode);
if(runMode.equals(RUN_MODE_REMOTE)) {
args.add(runHost);
}
// Remove qc specific environment variables
removeEnvVars(env);
// Run the script on node
// Execution result should be 0
if(launcher.launch().cmds(args).stdout(out).pwd(file.getParent()).join() != 0) {
listener.fatalError(Messages.QualityCenter_TSSchedulerFailed());
// log file is in UTF-16
InputStream is= new FileInputStream(this.parsedQcTSLogFile);
InputStreamReader in = new InputStreamReader(is, "UTF-16");
try {
// Copy the console output to our logger
IOUtils.copy(in,new OutputStreamWriter(out));
} finally {
in.close();
is.close();
}
throw new AbortException();
}
return this.parsedQcTSLogFile;
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
/**
* Installations of the QCClient
*/
@CopyOnWrite
private volatile QualityCenterClientInstallation[] clientInstallations = new QualityCenterClientInstallation[0];
/**
* Installations of the QTP Addin
*/
@CopyOnWrite
private volatile QualityCenterQTPAddinsInstallation[] qtpAddinsInstallations = new QualityCenterQTPAddinsInstallation[0];
public DescriptorImpl() {
load();
}
protected DescriptorImpl(Class<? extends QualityCenter> clazz) {
super(clazz);
}
@Override
public String getDisplayName() {
return Messages.QualityCenter_DisplayName();
}
public QualityCenterClientInstallation.DescriptorImpl getToolDescriptor() {
return ToolInstallation.all().get(QualityCenterClientInstallation.DescriptorImpl.class);
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
if(getClientInstallations() != null && getClientInstallations().length > 0 &&
getQTPAddinsInstallations() != null && getQTPAddinsInstallations().length > 0) {
return true;
}
return false;
}
@Override
public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(QualityCenter.class, formData);
}
public QualityCenterClientInstallation[] getClientInstallations() {
return clientInstallations;
}
public void setClientInstallations(QualityCenterClientInstallation... installations) {
this.clientInstallations = installations;
save();
}
public QualityCenterQTPAddinsInstallation[] getQTPAddinsInstallations() {
return qtpAddinsInstallations;
}
public void setQTPAddinsInstallations(QualityCenterQTPAddinsInstallation... installations) {
this.qtpAddinsInstallations = installations;
save();
}
public FormValidation doCheckQcServerURL(@QueryParameter String value) {
return QualityCenterUtils.checkQcServerURL(value);
}
public FormValidation doCheckQcLogin(@QueryParameter String value) {
if(StringUtils.isBlank(value)) {
return FormValidation.error(Messages.QualityCenter_UsernameShouldBeDefined());
}
return FormValidation.ok();
}
public FormValidation doCheckQcDomain(@QueryParameter String value) {
if(StringUtils.isBlank(value)) {
return FormValidation.error(Messages.QualityCenter_DomainShouldBeDefined());
}
return FormValidation.ok();
}
public FormValidation doCheckQcProject(@QueryParameter String value) {
if(StringUtils.isBlank(value)) {
return FormValidation.error(Messages.QualityCenter_ProjectShouldBeDefined());
}
return FormValidation.ok();
}
public FormValidation doCheckQcTSFolder(@QueryParameter String value) {
if(StringUtils.isBlank(value)) {
return FormValidation.error(Messages.QualityCenter_TSFolderShouldBeDefined());
}
return FormValidation.ok();
}
public FormValidation doCheckQcTSName(@QueryParameter String value) {
if(StringUtils.isBlank(value)) {
return FormValidation.error(Messages.QualityCenter_TSNameShouldBeDefined());
}
return FormValidation.ok();
}
/**
* Returns the possible run modes.
*
* <p>This method needs to be placed here so that the list can be
* accessible from QualityCenter's config.jelly file: config.jelly
* is not able to access such a method if it is placed, even statically,
* directly into QualityCenter.</p>
*/
public String[] getRunModes() {
return QualityCenter.RUN_MODES;
}
}
}