com.saucelabs.bamboo.sod.action.PostBuildAction Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bamboo-sauceondemand-plugin Show documentation
Show all versions of bamboo-sauceondemand-plugin Show documentation
This is the Sauce OnDemand plugin for Bamboo
The newest version!
package com.saucelabs.bamboo.sod.action;
import com.atlassian.bamboo.build.BuildLoggerManager;
import com.atlassian.bamboo.build.CustomBuildProcessor;
import com.atlassian.bamboo.build.LogEntry;
import com.atlassian.bamboo.build.logger.BuildLogUtils;
import com.atlassian.bamboo.build.logger.BuildLogger;
import com.atlassian.bamboo.builder.BuildState;
import com.atlassian.bamboo.plan.PlanManager;
import com.atlassian.bamboo.results.tests.TestResults;
import com.atlassian.bamboo.resultsummary.tests.TestState;
import com.atlassian.bamboo.v2.build.BuildContext;
import com.atlassian.bamboo.v2.build.CurrentBuildResult;
import com.atlassian.bamboo.variable.CustomVariableContext;
import com.atlassian.spring.container.ContainerManager;
import com.saucelabs.bamboo.sod.AbstractSauceBuildPlugin;
import com.saucelabs.bamboo.sod.config.SODMappedBuildConfiguration;
import com.saucelabs.ci.sauceconnect.SauceConnectFourManager;
import com.saucelabs.ci.sauceconnect.SauceTunnelManager;
import com.saucelabs.saucerest.SauceREST;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Invoked after a build has finished to reset the environment variables for the builder back to what they were prior
* to the invocation of Sauce. The class will also invoke the Sauce REST API to store the Bamboo build number against
* the Sauce Job. This will be performed if the output from the Bamboo Build includes a line beginning with 'SauceOnDemandSessionID'
* (the selenium-client-factory library will output this line).
*
* @author Jonathan Doklovic
* @author Ross Rowe
*/
public class PostBuildAction extends AbstractSauceBuildPlugin implements CustomBuildProcessor {
private static final Logger logger = Logger.getLogger(PostBuildAction.class);
public static final String SAUCE_ON_DEMAND_SESSION_ID = "SauceOnDemandSessionID";
private static final Pattern SESSION_ID_PATTERN = Pattern.compile("SauceOnDemandSessionID=([0-9a-fA-F]+)(?:.job-name=(.*))?");
private static final String JOB_NAME_PATTERN = "\\b({0})\\b";
/**
* Populated via dependency injection.
*/
private PlanManager planManager;
/**
* Populated via dependency injection.
*/
private BuildLoggerManager buildLoggerManager;
private SauceConnectFourManager sauceConnectFourTunnelManager;
private CustomVariableContext customVariableContext;
@NotNull
public BuildContext call() {
final SODMappedBuildConfiguration config = new SODMappedBuildConfiguration(buildContext.getBuildDefinition().getCustomConfiguration());
if (config.isEnabled()) {
BuildLoggerManager buildLoggerManager = (BuildLoggerManager) ContainerManager.getComponent("buildLoggerManager");
final BuildLogger buildLogger = buildLoggerManager.getLogger(buildContext.getResultKey());
PrintStream printLogger = new PrintStream(new NullOutputStream()) {
@Override
public void println(String x) {
buildLogger.addBuildLogEntry(x);
}
};
try {
SauceTunnelManager sauceTunnelManager = getSauceConnectFourTunnelManager();
String options = customVariableContext.substituteString(config.getSauceConnectOptions(), buildContext, null);
sauceTunnelManager.closeTunnelsForPlan(
config.getTempUsername(),
options,
printLogger
);
recordSauceJobResult(config);
} catch (IOException e) {
logger.error(e);
}
}
return buildContext;
}
public void init(@NotNull BuildContext context) {
this.buildContext = context;
}
public void setPlanManager(PlanManager planManager) {
this.planManager = planManager;
}
/**
* Iterates over the output lines from the build. For each line that begins with 'SauceOnDemandSessionID',
* store the session id from the line in the custom build data of the build, and invoke the Sauce REST API
* to store the Bamboo build number
*
* @param config
*/
private void recordSauceJobResult(SODMappedBuildConfiguration config) throws IOException {
//iterate over the entries of the build logger to see if one starts with 'SauceOnDemandSessionID'
boolean foundLogEntry = false;
logger.info("Checking log interceptor entries");
CurrentBuildResult buildResult = buildContext.getBuildResult();
for (Map.Entry entry : buildResult.getCustomBuildData().entrySet()) {
if (entry.getKey().contains("SAUCE_JOB_ID")) {
if (processLine(config, entry.getValue())) {
foundLogEntry = true;
}
}
}
logger.info("Reading from log file");
//try read from the log file directly
File logDirectory = BuildLogUtils.getLogFileDirectory(buildContext.getPlanKey());
String logFileName = BuildLogUtils.getLogFileName(buildContext.getPlanResultKey());
List lines = FileUtils.readLines(new File(logDirectory, logFileName));
for (Object object : lines) {
String line = (String) object;
if (logger.isDebugEnabled()) {
logger.debug("Processing line: " + line);
}
if (processLine(config, line)) {
foundLogEntry = true;
}
}
logger.info("Reading from build logger output");
BuildLogger buildLogger = buildLoggerManager.getLogger(buildContext.getResultKey());
for (LogEntry logEntry : buildLogger.getBuildLog()) {
if (processLine(config, logEntry.getLog())) {
foundLogEntry = true;
}
}
if (!foundLogEntry) {
logger.warn("No Sauce Session ids found in build output");
}
}
private boolean processLine(SODMappedBuildConfiguration config, String line) {
//extract session id
String sessionId = null;
String jobName = null;
Matcher m = SESSION_ID_PATTERN.matcher(line);
while (m.find()) {
sessionId = m.group(1);
if (m.groupCount() == 2) {
jobName = m.group(2);
}
}
if (sessionId == null) {
sessionId = StringUtils.substringBetween(line, SAUCE_ON_DEMAND_SESSION_ID + "=", " ");
}
if (sessionId == null) {
//we might not have a space separating the session id and job-name, so retrieve the text up to the end of the string
sessionId = StringUtils.substringAfter(line, SAUCE_ON_DEMAND_SESSION_ID + "=");
}
if (sessionId != null && !sessionId.equalsIgnoreCase("null")) {
if (sessionId.trim().equals("")) {
logger.error("Session id for line" + line + " was blank");
return false;
} else {
//TODO extract Sauce Job name (included on log line as 'job-name=')?
storeBambooBuildNumberInSauce(config, sessionId, jobName);
return true;
}
}
return false;
}
/**
* Invokes the Sauce REST API to store the build number and pass/fail status against the Sauce Job.
*
* @param config
* @param sessionId the Sauce Job Id
* @param jobName
*/
private void storeBambooBuildNumberInSauce(SODMappedBuildConfiguration config, String sessionId, String jobName) {
SauceREST sauceREST = new SauceREST(config.getTempUsername(), config.getTempApikey());
Map updates = new HashMap();
try {
logger.debug("Invoking Sauce REST API for " + sessionId);
String json = sauceREST.getJobInfo(sessionId);
logger.debug("Results: " + json);
JSONObject jsonObject = (JSONObject) new JSONParser().parse(json);
updates.put("build", getBuildNumber());
if (jsonObject.get("passed") == null || jsonObject.get("passed").equals("")) {
if (jsonObject.containsKey("name")) {
//use the job name stored on the job if available
jobName = (String) jsonObject.get("name");
}
Boolean testPassed = hasTestPassed(jobName);
updates.put("passed", testPassed);
}
logger.debug("About to update job " + sessionId + " with build number " + getBuildNumber());
sauceREST.updateJobInfo(sessionId, updates);
} catch (ParseException e) {
logger.error("Unable to set build number for " + sessionId, e);
} catch (Exception e) {
logger.error("Unexpected error processing " + sessionId, e);
}
}
private Boolean hasTestPassed(String name) {
//do we have a test which matches the job name?
TestResults testResults = findTestResult(name);
if (testResults != null) {
return testResults.getState().equals(TestState.SUCCESS);
}
return (buildContext.getBuildResult().getBuildState().equals(BuildState.SUCCESS));
}
private TestResults findTestResult(String name) {
if (name == null) {
return null;
}
TestResults testResult = findTestResult(name, buildContext.getBuildResult().getFailedTestResults());
if (testResult == null) {
testResult = findTestResult(name, buildContext.getBuildResult().getSuccessfulTestResults());
}
return testResult;
}
private TestResults findTestResult(String name, Collection testResults) {
for (TestResults testResult : testResults) {
Pattern jobNamePattern = Pattern.compile(MessageFormat.format(JOB_NAME_PATTERN, name));
Matcher matcher = jobNamePattern.matcher(testResult.getActualMethodName());
if (name.equals(testResult.getActualMethodName()) //if job name equals full name of test
|| name.contains(testResult.getActualMethodName()) //or if job name contains the test name
|| matcher.find()) { //or if the full name of the test contains the job name (matching whole words only)
//then we have a match
return testResult;
}
}
return null;
}
private String getBuildNumber() {
return getBuildContextToUse().getBuildResultKey();
}
/**
* Use the parent build context if available, otherwise use the build context.
*
* @return
*/
private BuildContext getBuildContextToUse() {
return buildContext.getParentBuildContext() == null ? buildContext : buildContext.getParentBuildContext();
}
public void setBuildLoggerManager(BuildLoggerManager buildLoggerManager) {
this.buildLoggerManager = buildLoggerManager;
}
public SauceConnectFourManager getSauceConnectFourTunnelManager() {
if (sauceConnectFourTunnelManager == null) {
setSauceConnectFourTunnelManager(new SauceConnectFourManager());
}
return sauceConnectFourTunnelManager;
}
public void setSauceConnectFourTunnelManager(SauceConnectFourManager sauceConnectFourTunnelManager) {
this.sauceConnectFourTunnelManager = sauceConnectFourTunnelManager;
}
public void setCustomVariableContext(CustomVariableContext customVariableContext) {
this.customVariableContext = customVariableContext;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy