/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Michael B. Donohue, Yahoo!, Inc.
*
*
*******************************************************************************/
package hudson.tasks.test;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import static hudson.Util.fixNull;
import hudson.model.BuildListener;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Aggregates downstream test reports into a single consolidated report, so that
* people can see the overall test results in one page when tests are scattered
* across many different jobs.
*
* @author Kohsuke Kawaguchi
*/
public class AggregatedTestResultPublisher extends Recorder {
/**
* Jobs to aggregate. Comma separated. Null if triggering downstreams.
*/
public final String jobs;
public AggregatedTestResultPublisher(String jobs) {
this.jobs = Util.fixEmptyAndTrim(jobs);
}
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// add a TestResult just so that it can show up later.
build.addAction(new TestResultAction(jobs, build));
return true;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
/**
* Action that serves the aggregated record.
*
* TODO: persist some information so that even when some of the individuals
* are gone, we can still retain some useful information.
*/
public static final class TestResultAction extends AbstractTestResultAction {
/**
* Jobs to aggregate. Comma separated. Never null.
*/
private final String jobs;
/**
* The last time the fields of this object is computed from the rest.
*/
private transient long lastUpdated = 0;
/**
* When was the last time any build completed?
*/
private static long lastChanged = 0;
private transient int failCount;
private transient int totalCount;
private transient List<AbstractTestResultAction> individuals;
/**
* Projects that haven't run yet.
*/
private transient List<AbstractProject> didntRun;
private transient List<AbstractProject> noFingerprints;
public TestResultAction(String jobs, AbstractBuild<?, ?> owner) {
super(owner);
if (jobs == null) {
// resolve null as the transitive downstream jobs
StringBuilder buf = new StringBuilder();
for (AbstractProject p : getProject().getTransitiveDownstreamProjects()) {
if (buf.length() > 0) {
buf.append(',');
}
buf.append(p.getFullName());
}
jobs = buf.toString();
}
this.jobs = jobs;
}
/**
* Gets the jobs to be monitored.
*/
public Collection<AbstractProject> getJobs() {
List<AbstractProject> r = new ArrayList<AbstractProject>();
for (String job : Util.tokenize(jobs, ",")) {
AbstractProject j = Hudson.getInstance().getItemByFullName(job.trim(), AbstractProject.class);
if (j != null) {
r.add(j);
}
}
return r;
}
private AbstractProject<?, ?> getProject() {
return owner.getProject();
}
public int getFailCount() {
upToDateCheck();
return failCount;
}
public int getTotalCount() {
upToDateCheck();
return totalCount;
}
public Object getResult() {
upToDateCheck();
return this;
}
/**
* Since there's no TestObject that points this action as the owner
* (aggregated {@link TestObject}s point to their respective real
* owners, not 'this'), so this method should be never invoked.
*
* @deprecated so that IDE warns you if you accidentally try to call it.
*/
@Override
protected String getDescription(TestObject object) {
throw new AssertionError();
}
/**
* See {@link #getDescription(TestObject)}
*
* @deprecated so that IDE warns you if you accidentally try to call it.
*/
@Override
protected void setDescription(TestObject object, String description) {
throw new AssertionError();
}
/**
* Returns the individual test results that are aggregated.
*/
public List<AbstractTestResultAction> getIndividuals() {
upToDateCheck();
return Collections.unmodifiableList(individuals);
}
/**
* Gets the downstream projects that haven't run yet, but expected to
* produce test results.
*/
public List<AbstractProject> getDidntRun() {
return Collections.unmodifiableList(didntRun);
}
/**
* Gets the downstream projects that have available test results, but do
* not appear to have fingerprinting enabled.
*/
public List<AbstractProject> getNoFingerprints() {
return Collections.unmodifiableList(noFingerprints);
}
/**
* Makes sure that the data fields are up to date.
*/
private synchronized void upToDateCheck() {
// up to date check
if (lastUpdated > lastChanged) {
return;
}
lastUpdated = lastChanged + 1;
int failCount = 0;
int totalCount = 0;
List<AbstractTestResultAction> individuals = new ArrayList<AbstractTestResultAction>();
List<AbstractProject> didntRun = new ArrayList<AbstractProject>();
List<AbstractProject> noFingerprints = new ArrayList<AbstractProject>();
for (AbstractProject job : getJobs()) {
RangeSet rs = owner.getDownstreamRelationship(job);
if (rs.isEmpty()) {
// is this job expected to produce a test result?
Run b = job.getLastSuccessfulBuild();
if (b != null && b.getAction(AbstractTestResultAction.class) != null) {
if (b.getAction(FingerprintAction.class) != null) {
didntRun.add(job);
} else {
noFingerprints.add(job);
}
}
} else {
for (int n : rs.listNumbersReverse()) {
Run b = job.getBuildByNumber(n);
if (b == null) {
continue;
}
if (b.isBuilding() || b.getResult().isWorseThan(Result.UNSTABLE)) {
continue; // don't count them
}
for (AbstractTestResultAction ta : b.getActions(AbstractTestResultAction.class)) {
failCount += ta.getFailCount();
totalCount += ta.getTotalCount();
individuals.add(ta);
}
break;
}
}
}
this.failCount = failCount;
this.totalCount = totalCount;
this.individuals = individuals;
this.didntRun = didntRun;
this.noFingerprints = noFingerprints;
}
public boolean getHasFingerprintAction() {
return this.owner.getAction(FingerprintAction.class) != null;
}
@Override
public String getDisplayName() {
return Messages.AggregatedTestResultPublisher_Title();
}
@Override
public String getUrlName() {
return "aggregatedTestReport";
}
@Extension
public static class RunListenerImpl extends RunListener<Run> {
@Override
public void onCompleted(Run run, TaskListener listener) {
lastChanged = System.currentTimeMillis();
}
}
}
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true; // for all types
}
public String getDisplayName() {
return Messages.AggregatedTestResultPublisher_DisplayName();
}
@Override
public String getHelpFile() {
return "/help/tasks/aggregate-test/help.html";
}
public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) {
// Require CONFIGURE permission on this project
if (!project.hasPermission(Item.CONFIGURE)) {
return FormValidation.ok();
}
for (String name : Util.tokenize(fixNull(value), ",")) {
name = name.trim();
if (Hudson.getInstance().getItemByFullName(name) == null) {
return FormValidation.error(hudson.tasks.Messages.BuildTrigger_NoSuchProject(name, AbstractProject.findNearest(name).getName()));
}
}
return FormValidation.ok();
}
@Override
public AggregatedTestResultPublisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
JSONObject s = formData.getJSONObject("specify");
if (s.isNullObject()) {
return new AggregatedTestResultPublisher(null);
} else {
return new AggregatedTestResultPublisher(s.getString("jobs"));
}
}
}
}