提交 296849ed 编写于 作者: K Kohsuke Kawaguchi

Merge pull request 1379

......@@ -105,7 +105,10 @@ import javax.annotation.Nonnull;
import static javax.servlet.http.HttpServletResponse.*;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.RunIdMigrator;
import jenkins.model.lazy.LazyBuildMixIn;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* A job is an runnable entity under the monitoring of Hudson.
......@@ -162,6 +165,9 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
// this should have been DescribableList but now it's too late
protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<JobProperty<? super JobT>>();
@Restricted(NoExternalUse.class)
public transient RunIdMigrator runIdMigrator;
protected Job(ItemGroup parent, String name) {
super(parent, name);
}
......@@ -172,11 +178,21 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
holdOffBuildUntilSave = holdOffBuildUntilUserSave;
}
@Override public void onCreatedFromScratch() {
super.onCreatedFromScratch();
runIdMigrator = new RunIdMigrator();
runIdMigrator.created(getBuildDir());
}
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name)
throws IOException {
super.onLoad(parent, name);
File buildDir = getBuildDir();
runIdMigrator = new RunIdMigrator();
runIdMigrator.migrate(buildDir, Jenkins.getInstance().getRootDir());
TextFile f = getNextBuildNumberFile();
if (f.exists()) {
// starting 1.28, we store nextBuildNumber in a separate file.
......@@ -188,7 +204,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
}
} catch (NumberFormatException e) {
// try to infer the value of the next build number from the existing build records. See JENKINS-11563
File[] folders = this.getBuildDir().listFiles(new FileFilter() {
File[] folders = buildDir.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("[0-9]+");
}
......@@ -638,8 +654,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
File oldBuildDir = getBuildDir();
super.movedTo(destination, newItem, destDir);
File newBuildDir = getBuildDir();
if (oldBuildDir.isDirectory() && !newBuildDir.isDirectory()) {
FileUtils.forceMkdir(destDir.getParentFile());
if (oldBuildDir.isDirectory()) {
FileUtils.moveDirectory(oldBuildDir, newBuildDir);
}
}
......
......@@ -47,6 +47,7 @@ import hudson.model.Descriptor.FormException;
import hudson.model.Run.RunExecution;
import hudson.model.listeners.RunListener;
import hudson.model.listeners.SaveableListener;
import hudson.model.queue.SubTask;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.AccessControlled;
......@@ -74,7 +75,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -108,6 +108,7 @@ import jenkins.model.PeepholePermalink;
import jenkins.model.RunAction2;
import jenkins.model.StandardArtifactManager;
import jenkins.model.lazy.BuildReference;
import jenkins.model.lazy.LazyBuildMixIn;
import jenkins.util.VirtualFile;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
......@@ -146,7 +147,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
* In earlier versions &lt; 1.24, this number is not unique nor continuous,
* but going forward, it will, and this really replaces the build id.
*/
public /*final*/ int number;
public transient /*final*/ int number;
/**
* Previous build. Can be null.
......@@ -172,10 +173,13 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
*/
/* does not compile on JDK 7: private*/ volatile transient RunT previousBuildInProgress;
/** ID as used for historical build records; otherwise null. */
private @CheckForNull String id;
/**
* When the build is scheduled.
*/
protected transient final long timestamp;
protected /*final*/ long timestamp;
/**
* When the build has started running.
......@@ -266,23 +270,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
*/
private @CheckForNull ArtifactManager artifactManager;
private static final SimpleDateFormat CANONICAL_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
public static final ThreadLocal<SimpleDateFormat> ID_FORMATTER = new IDFormatterProvider();
private static final class IDFormatterProvider extends ThreadLocal<SimpleDateFormat> {
@Override
protected SimpleDateFormat initialValue() {
synchronized (CANONICAL_ID_FORMATTER) {
return (SimpleDateFormat) CANONICAL_ID_FORMATTER.clone();
}
}
};
/**
* Creates a new {@link Run}.
* @param job Owner job
*/
protected Run(@Nonnull JobT job) throws IOException {
this(job, new GregorianCalendar());
this(job, System.currentTimeMillis());
this.number = project.assignBuildNumber();
LOGGER.log(FINER, "new {0} @{1}", new Object[] {this, hashCode()});
}
......@@ -290,24 +283,29 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
/**
* Constructor for creating a {@link Run} object in
* an arbitrary state.
* {@link #number} must be set manually.
* <p>May be used in a {@link SubTask#createExecutable} (instead of calling {@link LazyBuildMixIn#newBuild}).
* For example, {@code MatrixConfiguration.newBuild} does this
* so that the {@link #timestamp} as well as {@link #number} are shared with the parent build.
*/
protected Run(@Nonnull JobT job, @Nonnull Calendar timestamp) {
this(job,timestamp.getTimeInMillis());
}
/** @see #Run(Job, Calendar) */
protected Run(@Nonnull JobT job, long timestamp) {
this.project = job;
this.timestamp = timestamp;
this.state = State.NOT_STARTED;
getRootDir().mkdirs();
}
/**
* Loads a run from a log file.
*/
protected Run(@Nonnull JobT project, @Nonnull File buildDir) throws IOException {
this(project, parseTimestampFromBuildDir(buildDir));
this.project = project;
this.previousBuildInProgress = _this(); // loaded builds are always completed
number = Integer.parseInt(buildDir.getName());
reload();
}
......@@ -390,33 +388,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
}
}
static class InvalidDirectoryNameException extends IOException {
InvalidDirectoryNameException(File buildDir) {
super("Invalid directory name " + buildDir);
}
}
/*package*/ static long parseTimestampFromBuildDir(@Nonnull File buildDir)
throws IOException, InvalidDirectoryNameException {
try {
if(Util.isSymlink(buildDir)) {
// "Util.resolveSymlink(file)" resolves NTFS symlinks.
File target = Util.resolveSymlinkToFile(buildDir);
if(target != null)
buildDir = target;
}
// canonicalization to ensure we are looking at the ID in the directory name
// as opposed to build numbers which are used in symlinks
// (just in case the symlink check above did not work)
buildDir = buildDir.getCanonicalFile();
return ID_FORMATTER.get().parse(buildDir.getName()).getTime();
} catch (ParseException e) {
throw new InvalidDirectoryNameException(buildDir);
} catch (InterruptedException e) {
throw new IOException("Interrupted while resolving symlink directory "+buildDir,e);
}
}
/**
* Obtains 'this' in a more type safe signature.
*/
......@@ -1000,21 +971,13 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
/**
* Unique ID of this build.
* Usually the decimal form of {@link #number}, but may be a formatted timestamp for historical builds.
*/
@Exported
public @Nonnull String getId() {
return ID_FORMATTER.get().format(new Date(timestamp));
return id != null ? id : Integer.toString(number);
}
/**
* Get the date formatter used to convert the directory name in to a timestamp.
* This is nasty exposure of private data, but needed all the time the directory
* containing the build is used as it's timestamp.
*/
public static @Nonnull DateFormat getIDFormatter() {
return ID_FORMATTER.get();
}
@Override
public @CheckForNull Descriptor getDescriptorByName(String className) {
return Jenkins.getInstance().getDescriptorByName(className);
......@@ -1027,7 +990,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
*/
@Override
public @Nonnull File getRootDir() {
return new File(project.getBuildDir(),getId());
return new File(project.getBuildDir(), Integer.toString(number));
}
/**
......@@ -1502,10 +1465,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
RunListener.fireDeleted(this);
synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
// if we have a symlink, delete it, too
File link = new File(project.getBuildDir(), String.valueOf(getNumber()));
link.delete();
File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName());
if (tmp.exists()) {
......@@ -1832,8 +1791,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
}
/**
* Creates a symlink from build number to ID.
* Also makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist.
* Makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist.
* Normally you do not need to call this explicitly, since {@link #execute} does so,
* but this may be needed if you are creating synthetic {@link Run}s as part of a container project (such as Maven builds in a module set).
* You should also ensure that {@link RunListener#fireStarted} and {@link RunListener#fireCompleted} are called.
......@@ -1842,7 +1800,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
* @since 1.530
*/
public final void updateSymlinks(@Nonnull TaskListener listener) throws InterruptedException {
Util.createSymlink(getParent().getBuildDir(), getId(), String.valueOf(getNumber()), listener);
createSymlink(listener, "lastSuccessful", PermalinkProjectAction.Permalink.LAST_SUCCESSFUL_BUILD);
createSymlink(listener, "lastStable", PermalinkProjectAction.Permalink.LAST_STABLE_BUILD);
}
......
......@@ -24,10 +24,7 @@
package hudson.model;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
......@@ -38,10 +35,14 @@ import java.util.logging.Level;
import static java.util.logging.Level.*;
import java.util.logging.Logger;
import jenkins.model.RunIdMigrator;
import jenkins.model.lazy.AbstractLazyLoadRunMap;
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.*;
import jenkins.model.lazy.BuildReference;
import jenkins.model.lazy.LazyBuildMixIn;
import org.apache.commons.collections.comparators.ReverseComparator;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* {@link Map} from build number to {@link Run}.
......@@ -61,6 +62,10 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
private Constructor<R> cons;
/** Normally overwritten by {@link LazyBuildMixIn#onLoad} or {@link LazyBuildMixIn#onCreatedFromScratch}, in turn created during {@link Job#onLoad}. */
@Restricted(NoExternalUse.class)
public RunIdMigrator runIdMigrator = new RunIdMigrator();
// TODO: before first complete build
// patch up next/previous build link
......@@ -118,6 +123,7 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
@Override
public boolean removeValue(R run) {
run.dropLinks();
runIdMigrator.delete(dir, run.getId());
return super.removeValue(run);
}
......@@ -169,11 +175,32 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
return r.getId();
}
/**
* Add a <em>new</em> build to the map.
* Do not use when loading existing builds (use {@link #put(Integer, Object)}).
*/
@Override
public R put(R r) {
// Defense against JENKINS-23152 and its ilk.
File rootDir = r.getRootDir();
if (rootDir.isDirectory()) {
throw new IllegalStateException(rootDir + " already existed; will not overwite with " + r);
}
proposeNewNumber(r.getNumber());
rootDir.mkdirs();
return super._put(r);
}
@Override public R getById(String id) {
int n;
try {
n = Integer.parseInt(id);
} catch (NumberFormatException x) {
n = runIdMigrator.findNumber(id);
}
return getByNumber(n);
}
/**
* Reuses the same reference as much as we can.
* <p>
......@@ -185,31 +212,6 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
return r.createReference();
}
@Override
protected FilenameFilter createDirectoryFilter() {
final SimpleDateFormat formatter = Run.ID_FORMATTER.get();
return new FilenameFilter() {
@Override public boolean accept(File dir, String name) {
if (name.startsWith("0000")) {
// JENKINS-1461 sometimes create bogus data directories with impossible dates, such as year 0, April 31st,
// or August 0th. Date object doesn't roundtrip those, so we eventually fail to load this data.
// Don't even bother trying.
return false;
}
try {
if (formatter.format(formatter.parse(name)).equals(name)) {
return true;
}
} catch (ParseException e) {
// fall through
}
LOGGER.log(FINE, "Skipping {0} in {1}", new Object[] {name, dir});
return false;
}
};
}
@Override
protected R retrieve(File d) throws IOException {
if(new File(d,"build.xml").exists()) {
......@@ -221,17 +223,6 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
LOGGER.log(FINEST, "Loaded " + b.getFullDisplayName() + " in " + Thread.currentThread().getName(), new ThisIsHowItsLoaded());
}
return b;
} catch (Run.InvalidDirectoryNameException x) {
Level lvl;
try {
Integer.parseInt(d.getName());
// JENKINS-15587: just an mangled symlink
lvl = Level.FINE;
} catch (NumberFormatException x2) {
// potentially a real build dir, maybe a bug
lvl = Level.WARNING;
}
LOGGER.log(lvl, "skipping non-build directory {0}", d);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "could not load " + d, e);
} catch (InstantiationError e) {
......
package jenkins.diagnostics.ooom;
import hudson.FilePath;
import hudson.model.Job;
import hudson.model.TaskListener;
import java.io.File;
import java.io.IOException;
/**
* ID and build number of one build.
*/
public final class BuildPtr implements Comparable<BuildPtr> {
final Job job;
final File buildDir;
/**
* Timestamp build ID.
*/
public final String id;
/**
* Build number found from the disk.
*/
public final int n;
/**
* Position of this build according to the ordering induced by {@link #n}
*/
int posByN;
/**
* Position of this build according to the ordering induced by {@link #id}
*/
int posByID;
BuildPtr(Job job, File buildDir, int n) {
this.job = job;
this.n = n;
this.id = buildDir.getName();
this.buildDir = buildDir;
}
@Override
public String toString() {
return buildDir.toString()+":#"+n;
}
/**
* If this build and that build are inconsistent, in that
* their numbers and timestamps are ordering in the wrong direction.
*/
public boolean isInconsistentWith(BuildPtr that) {
return signOfCompare(this.posByN,that.posByN) * signOfCompare(this.posByID,that.posByID) < 0;
}
/**
* sign of (a-b).
*/
private static int signOfCompare(int a, int b) {
if (a>b) return 1;
if (a<b) return -1;
return 0;
}
/**
* Fix the problem by moving the out of order builds into a place that Jenkins won't look at.
*
* TODO: another way to fix this is by adjusting the ID and pretend that the build happened
* at a different timestamp.
*/
public void fix(TaskListener listener) throws IOException, InterruptedException {
File dir = new File(job.getRootDir(), "outOfOrderBuilds");
dir.mkdirs();
File dst = new File(dir, buildDir.getName());
listener.getLogger().println("Renaming "+buildDir);
listener.getLogger().println(" -> "+dst);
if (!buildDir.renameTo(dst)) {
FilePath bd = new FilePath(buildDir);
bd.copyRecursiveTo(new FilePath(dst));
bd.deleteRecursive();
}
// if there's a symlink delete it
new File(buildDir.getParentFile(),String.valueOf(n)).delete();
}
@Override
public int compareTo(BuildPtr that) {
return this.id.compareTo(that.id);
}
}
package jenkins.diagnostics.ooom;
import hudson.Extension;
import hudson.model.AsyncPeriodicWork;
import hudson.model.Job;
import hudson.model.TaskListener;
import hudson.util.TimeUnit2;
import jenkins.model.Jenkins;
import javax.inject.Inject;
import java.io.IOException;
/**
* Discovers {@link Problem}s periodically in the background and
* pass them on to {@link OutOfOrderBuildMonitor}.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class OutOfOrderBuildDetector extends AsyncPeriodicWork {
@Inject
private OutOfOrderBuildMonitor monitor;
public OutOfOrderBuildDetector() {
super("Out of order build detection");
}
@Override
protected void execute(TaskListener listener) throws IOException, InterruptedException {
execute(listener, 10*1000);
}
/**
* Performs the check synchronously.
*
* @param delay
* delay in the number of milli-seconds to reduce the load on I/O.
*/
public void execute(TaskListener listener, int delay) throws InterruptedException {
for (Job j : Jenkins.getInstance().getAllItems(Job.class)) {
listener.getLogger().println("Scanning " + j.getFullName());
Problem p = Problem.find(j);
if (p!=null) {
monitor.addProblem(p);
listener.getLogger().println(" found problems: "+p);
}
Thread.sleep(delay);
}
}
@Override
public long getRecurrencePeriod() {
return TimeUnit2.DAYS.toMillis(1);
}
}
package jenkins.diagnostics.ooom;
import hudson.Extension;
import hudson.model.TaskListener;
import hudson.util.HttpResponses;
import jenkins.management.AsynchronousAdministrativeMonitor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.File;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Reports any {@link Problem}s found and report them in the "Manage Jenkins" page.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class OutOfOrderBuildMonitor extends AsynchronousAdministrativeMonitor {
private final Set<Problem> problems = Collections.synchronizedSet(new LinkedHashSet<Problem>());
@Override
public boolean isActivated() {
return !problems.isEmpty() || getLogFile().exists();
}
void addProblem(Problem p) {
problems.add(p);
}
public Set<Problem> getProblems() {
return Collections.unmodifiableSet(new LinkedHashSet<Problem>(problems));
}
@RequirePOST
public HttpResponse doFix() {
start(false);
return HttpResponses.forwardToPreviousPage();
}
/**
* Discards the current log file so that the "stuff is completed" message will be gone.
*/
@RequirePOST
public HttpResponse doDismiss() {
getLogFile().delete();
return HttpResponses.forwardToPreviousPage();
}
@Override
public String getDisplayName() {
return "Fix Out-of-order Builds";
}
@Override
public File getLogFile() {
return super.getLogFile();
}
@Override
protected void fix(TaskListener listener) throws Exception {
Set<Problem> problems = getProblems();
for (Problem problem : problems) {
problem.fix(listener);
}
this.problems.removeAll(problems);
}
}
package jenkins.diagnostics.ooom;
import hudson.AbortException;
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.TaskListener;
import jenkins.model.lazy.AbstractLazyLoadRunMap;
import org.xml.sax.InputSource;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
/**
* Look at build numbers in build.xml and compare them with build IDs (timestamps).
*
* When they are inconsistent (newer build timestamp-wise has an older build number),
* it'll confuse the binary search in {@link AbstractLazyLoadRunMap}, so detect them and report them.
*
* @author Kohsuke Kawaguchi
*/
public final class Problem {
public final Job job;
/**
* A smallest set of builds whose removals would correct the order
* inconsistency problem.
*/
private final Set<BuildPtr> offenders = new TreeSet<BuildPtr>();
/**
* Scans the inconsistencies and return the information about it.
*
* @return null if no problems were found.
*/
public static Problem find(Job j) {
Problem p = new Problem(j);
if (p.countInconsistencies()==0) return null;
return p;
}
private Problem(Job j) {
this.job = j;
new Inspector().inspect();
}
public Set<BuildPtr> getOffenders() {
return Collections.unmodifiableSet(offenders);
}
/**
* Number of inconsistencies, which is the number of builds whose IDs
* have to be messed around on disk to collect problems.
*/
public int countInconsistencies() {
return offenders.size();
}
/**
* Equality is based on the job.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Problem problem = (Problem) o;
return job.equals(problem.job);
}
@Override
public int hashCode() {
return job.hashCode();
}
@Override
public String toString() {
return job.getFullName() + " " + Util.join(offenders);
}
public void fix(TaskListener listener) throws IOException, InterruptedException {
listener.getLogger().println("Fixing problems in " + job.getFullName());
for (BuildPtr o : offenders) {
o.fix(listener);
}
if (job instanceof AbstractProject) {
// let all the current references go and build a new one
AbstractProject a = (AbstractProject) job;
a._getRuns().purgeCache();
}
}
/**
* Finds the problems and builds up the data model of {@link Problem}.
*/
class Inspector {
/**
* All the builds sorted by their {@link BuildPtr#n}
*/
private List<BuildPtr> byN;
/**
* All the builds sorted by their {@link BuildPtr#id}
*/
private List<BuildPtr> byId;
private final XPathExpression xpath;
Inspector() {
try {
xpath = XPathFactory.newInstance().newXPath().compile("/*/number/text()");
} catch (XPathExpressionException e) {
throw new AssertionError(e);
}
}
/**
* Simply report inconsistencies in the ordering.
*/
protected void inspect() {
Map<Integer, BuildPtr> builds = scan();
byN = new ArrayList<BuildPtr>(builds.values());
// this is already sorted by BuildPtr.n
int i=0;
for (BuildPtr b : byN) {
b.posByN = i++;
}
byId = new ArrayList<BuildPtr>(byN);
Collections.sort(byId);
i=0;
for (BuildPtr b : byId) {
b.posByID = i++;
}
while (true) {
BuildPtr b = pick();
if (b==null)
break;
offenders.add(b);
}
}
/**
* Find the most inconsistent build, a build whose removal
* would reduce the # of inconsistencies by the most.
*
* This process takes {@link #offenders} into account.
*
* @return null if there's no more build to remove.
*/
private BuildPtr pick() {
BuildPtr worst=null;
int worstScore=0;
for (BuildPtr b : byN) {
if (offenders.contains(b))
continue;
int score = score(b);
if (score>worstScore) {
worst = b;
worstScore = score;
}
}
return worst;
}
/**
* Count the number of other builds the given build is inconsistent with,
* excluding inconsistencies with {@link #offenders} (since those inconsistencies
* are already effectively resolved by fixing offenders.)
*/
private int score(BuildPtr b) {
int i=0;
for (BuildPtr a : byN) {
if (offenders.contains(a))
continue;
if (a.isInconsistentWith(b))
i++;
}
return i;
}
/**
* Looks at the builds directory of the given job and builds up the full map of build number to its ID.
*/
protected SortedMap<Integer,BuildPtr> scan() {
LOGGER.fine("Inspecting "+job);
SortedMap<Integer,BuildPtr> builds = new TreeMap<Integer,BuildPtr>();
File[] files = job.getBuildDir().listFiles();
if (files==null) return builds;
for (File build : files) {
try {
LOGGER.finer("Inspecting " + build);
if (isInt(build.getName())) {
// if this is a number, then it must be a build number
String s = loadBuildNumberFromBuildXml(build);
if (!s.equals(build.getName())) {
LOGGER.warning(build+" contains build number "+s);
// this index is invalid.
if (build.delete()) {
// index should be a symlink, and if so we can just delete it without losing data.
LOGGER.info("Removed problematic index "+build);
} else {
// the deltion will fail if 'build' isn't just a symlink but an actual directory.
// That is good, as we don't want to delete any real data.
LOGGER.warning("Couldn't delete " + build);
}
}
continue;
}
if (isID(build.getName())) {
String bn = loadBuildNumberFromBuildXml(build);
if (bn==null) {
LOGGER.log(WARNING, "Failed to parse "+build);
continue;
}
int n;
try {
n = Integer.parseInt(bn);
} catch (NumberFormatException e) {
LOGGER.log(WARNING, "Expected number in " + build + " but found " + bn, e);
continue;
}
BuildPtr b = new BuildPtr(job,build,n);
BuildPtr o = builds.put(n, b);
if (o != null) {
LOGGER.log(WARNING, "Multiple builds have the same number: {0} vs. {1}", new Object[] {o, b});
offenders.add(b.compareTo(o) > 0 ? o : b);
}
}
} catch (XPathExpressionException e) {
LOGGER.log(WARNING, "Failed to inspect "+build, e);
} catch (AbortException e) {
LOGGER.log(WARNING, "Failed to inspect "+build+": "+e.getMessage());
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to inspect "+build, e);
}
}
return builds;
}
private boolean isInt(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
private boolean isID(String s) {
try {
Run.ID_FORMATTER.get().parse(s);
return true;
} catch (ParseException e) {
return false;
}
}
private String loadBuildNumberFromBuildXml(File dir) throws XPathExpressionException, IOException {
File buildXml = new File(dir, "build.xml");
if (!buildXml.exists())
throw new AbortException(buildXml+" doesn't exist");
String systemId = buildXml.toURI().toURL().toExternalForm();
return (String)xpath.evaluate(new InputSource(systemId), XPathConstants.STRING);
}
}
private static final Logger LOGGER = Logger.getLogger(Problem.class.getName());
}
/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* 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 jenkins.model;
import hudson.Util;
import hudson.model.Job;
import hudson.model.Run;
import hudson.util.AtomicFileWriter;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.tools.ant.BuildException;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.framework.io.WriterOutputStream;
/**
* Converts legacy {@code builds} directories to the current format.
* There would be one instance associated with each {@link Job}.
* The {@link Job#getBuildDir} is passed to every method call (rather than being cached) in case it is moved.
*/
@Restricted(NoExternalUse.class)
public final class RunIdMigrator {
static final Logger LOGGER = Logger.getLogger(RunIdMigrator.class.getName());
private static final DateFormat LEGACY_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
private static final String MAP_FILE = "legacyIds";
/** avoids wasting a map for new jobs */
private static final Map<String,Integer> EMPTY = new TreeMap<String,Integer>();
private static final Set<File> offeredToUnmigrate = Collections.synchronizedSet(new HashSet<File>());
private @Nonnull Map<String,Integer> idToNumber = EMPTY;
public RunIdMigrator() {}
/**
* @return whether there was a file to load
*/
private boolean load(File dir) {
File f = new File(dir, MAP_FILE);
if (!f.isFile()) {
return false;
}
if (f.length() == 0) {
return true;
}
idToNumber = new TreeMap<String,Integer>();
try {
for (String line : FileUtils.readLines(f)) {
int i = line.indexOf(' ');
idToNumber.put(line.substring(0, i), Integer.parseInt(line.substring(i + 1)));
}
} catch (Exception x) { // IOException, IndexOutOfBoundsException, NumberFormatException
LOGGER.log(Level.WARNING, "could not read from " + f, x);
}
return true;
}
private void save(File dir) {
File f = new File(dir, MAP_FILE);
try {
AtomicFileWriter w = new AtomicFileWriter(f);
try {
for (Map.Entry<String,Integer> entry : idToNumber.entrySet()) {
w.write(entry.getKey() + ' ' + entry.getValue() + '\n');
}
w.commit();
} finally {
w.abort();
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, "could not save changes to " + f, x);
}
}
/**
* Called when a job is first created.
* Just saves an empty marker indicating that this job needs no migration.
* @param dir as in {@link Job#getBuildDir}
*/
public void created(File dir) {
save(dir);
}
/**
* Perform one-time migration if this has not been done already.
* Where previously there would be a {@code 2014-01-02_03-04-05/build.xml} specifying {@code <number>99</number>} plus a symlink {@code 99 → 2014-01-02_03-04-05},
* after migration there will be just {@code 99/build.xml} specifying {@code <id>2014-01-02_03-04-05</id>} and {@code <timestamp>…</timestamp>} according to local time zone at time of migration.
* Newly created builds are untouched.
* Does not throw {@link IOException} since we make a best effort to migrate but do not consider it fatal to job loading if we cannot.
* @param dir as in {@link Job#getBuildDir}
* @param jenkinsHome root directory of Jenkins (for logging only)
* @return true if migration was performed
*/
public synchronized boolean migrate(File dir, @CheckForNull File jenkinsHome) {
if (load(dir)) {
LOGGER.log(Level.FINER, "migration already performed for {0}", dir);
return false;
}
if (!dir.isDirectory()) {
LOGGER.log(/* normal during Job.movedTo */Level.FINE, "{0} was unexpectedly missing", dir);
return false;
}
LOGGER.log(Level.INFO, "Migrating build records in {0}", dir);
doMigrate(dir);
save(dir);
if (jenkinsHome != null && offeredToUnmigrate.add(jenkinsHome)) {
StringBuilder cp = new StringBuilder();
for (Class<?> c : new Class<?>[] {RunIdMigrator.class, /* TODO how to calculate transitive dependencies automatically? */Charsets.class, WriterOutputStream.class, BuildException.class, FastDateFormat.class}) {
URL location = c.getProtectionDomain().getCodeSource().getLocation();
String locationS = location.toString();
if (location.getProtocol().equals("file")) {
try {
locationS = new File(location.toURI()).getAbsolutePath();
} catch (URISyntaxException x) {
// never mind
}
}
if (cp.length() > 0) {
cp.append(File.pathSeparator);
}
cp.append(locationS);
}
LOGGER.log(Level.WARNING, "Build record migration is one-way. If you need to downgrade Jenkins, run: java -classpath {0} {1} {2}", new Object[] {cp, RunIdMigrator.class.getName(), jenkinsHome});
}
return true;
}
private static final Pattern NUMBER_ELT = Pattern.compile("(?m)^ <number>(\\d+)</number>(\r?\n)");
private void doMigrate(File dir) {
idToNumber = new TreeMap<String,Integer>();
File[] kids = dir.listFiles();
// Need to process symlinks first so we can rename to them.
List<File> kidsList = new ArrayList<File>(Arrays.asList(kids));
Iterator<File> it = kidsList.iterator();
while (it.hasNext()) {
File kid = it.next();
String name = kid.getName();
try {
String link = Util.resolveSymlink(kid);
if (link == null) {
continue;
}
try {
Integer.parseInt(name);
if (kid.delete()) {
LOGGER.log(Level.FINE, "deleted build number symlink {0} → {1}", new Object[] {name, link});
} else {
LOGGER.log(Level.WARNING, "could not delete build number symlink {0} → {1}", new Object[] {name, link});
}
} catch (NumberFormatException x) {
LOGGER.log(Level.FINE, "skipping other symlink {0} → {1}", new Object[] {name, link});
}
it.remove();
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to process " + kid, x);
}
}
it = kidsList.iterator();
while (it.hasNext()) {
File kid = it.next();
try {
String name = kid.getName();
try {
Integer.parseInt(name);
LOGGER.log(Level.FINE, "skipping new build dir {0}", name);
continue;
} catch (NumberFormatException x) {
// OK, next…
}
if (!kid.isDirectory()) {
LOGGER.log(Level.FINE, "skipping non-directory {0}", name);
continue;
}
long timestamp;
try {
synchronized (LEGACY_ID_FORMATTER) {
timestamp = LEGACY_ID_FORMATTER.parse(name).getTime();
}
} catch (ParseException x) {
LOGGER.log(Level.WARNING, "found unexpected dir {0}", name);
continue;
}
File buildXml = new File(kid, "build.xml");
if (!buildXml.isFile()) {
LOGGER.log(Level.WARNING, "found no build.xml in {0}", name);
continue;
}
String xml = FileUtils.readFileToString(buildXml, Charsets.UTF_8);
Matcher m = NUMBER_ELT.matcher(xml);
if (!m.find()) {
LOGGER.log(Level.WARNING, "could not find <number> in {0}/build.xml", name);
continue;
}
int number = Integer.parseInt(m.group(1));
String nl = m.group(2);
xml = m.replaceFirst(" <id>" + name + "</id>" + nl + " <timestamp>" + timestamp + "</timestamp>" + nl);
File newKid = new File(dir, Integer.toString(number));
if (!kid.renameTo(newKid)) {
LOGGER.log(Level.WARNING, "failed to rename {0} to {1}", new Object[] {name, number});
continue;
}
FileUtils.writeStringToFile(new File(newKid, "build.xml"), xml, Charsets.UTF_8);
LOGGER.log(Level.FINE, "fully processed {0} → {1}", new Object[] {name, number});
idToNumber.put(name, number);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to process " + kid, x);
}
}
}
/**
* Look up a historical run by ID.
* @param id a nonnumeric ID which may be a valid {@link Run#getId}
* @return the corresponding {@link Run#number}, or 0 if unknown
*/
public synchronized int findNumber(@Nonnull String id) {
Integer number = idToNumber.get(id);
return number != null ? number : 0;
}
/**
* Delete the record of a build.
* @param dir as in {@link Job#getBuildDir}
* @param id a {@link Run#getId}
*/
public synchronized void delete(File dir, String id) {
if (idToNumber.remove(id) != null) {
save(dir);
}
}
/**
* Reverses the migration, in case you want to revert to the older format.
* @param args one parameter, {@code $JENKINS_HOME}
*/
public static void main(String... args) throws Exception {
if (args.length != 1) {
throw new Exception("pass one parameter, $JENKINS_HOME");
}
File root = new File(args[0]);
File jobs = new File(root, "jobs");
if (!jobs.isDirectory()) {
throw new FileNotFoundException("no such $JENKINS_HOME " + root);
}
unmigrateJobsDir(jobs);
}
private static void unmigrateJobsDir(File jobs) throws Exception {
for (File job : jobs.listFiles()) {
File[] kids = job.listFiles();
if (kids == null) {
continue;
}
for (File kid : kids) {
if (!kid.isDirectory()) {
continue;
}
if (kid.getName().equals("builds")) {
unmigrateBuildsDir(kid);
} else {
// Might be jobs, modules, promotions, etc.; we assume an ItemGroup.getRootDirFor implementation returns grandchildren.
unmigrateJobsDir(kid);
}
}
}
}
private static final Pattern ID_ELT = Pattern.compile("(?m)^ <id>([0-9_-]+)</id>(\r?\n)");
private static final Pattern TIMESTAMP_ELT = Pattern.compile("(?m)^ <timestamp>(\\d+)</timestamp>(\r?\n)");
/** Inverse of {@link #doMigrate}. */
private static void unmigrateBuildsDir(File builds) throws Exception {
File mapFile = new File(builds, MAP_FILE);
if (!mapFile.isFile()) {
System.err.println(builds + " does not look to have been migrated yet; skipping");
return;
}
for (File build : builds.listFiles()) {
int number;
try {
number = Integer.parseInt(build.getName());
} catch (NumberFormatException x) {
continue;
}
File buildXml = new File(build, "build.xml");
if (!buildXml.isFile()) {
System.err.println(buildXml + " did not exist");
continue;
}
String xml = FileUtils.readFileToString(buildXml, Charsets.UTF_8);
Matcher m = TIMESTAMP_ELT.matcher(xml);
if (!m.find()) {
System.err.println(buildXml + " did not contain <timestamp> as expected");
continue;
}
long timestamp = Long.parseLong(m.group(1));
String nl = m.group(2);
xml = m.replaceFirst(" <number>" + number + "</number>" + nl);
m = ID_ELT.matcher(xml);
String id;
if (m.find()) {
id = m.group(1);
xml = m.replaceFirst("");
} else {
// Post-migration build. We give it a new ID based on its timestamp.
id = LEGACY_ID_FORMATTER.format(new Date(timestamp));
}
FileUtils.write(buildXml, xml, Charsets.UTF_8);
if (!build.renameTo(new File(builds, id))) {
System.err.println(build + " could not be renamed");
}
Util.createSymlink(builds, id, Integer.toString(number), StreamTaskListener.fromStderr());
}
Util.deleteFile(mapFile);
System.err.println(builds + " has been restored to its original format");
}
}
......@@ -47,6 +47,7 @@ import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import static java.util.logging.Level.FINER;
import jenkins.model.RunIdMigrator;
/**
* Makes it easier to use a lazy {@link RunMap} from a {@link Job} implementation.
......@@ -63,9 +64,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
@SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called
private @Nonnull RunMap<RunT> builds = new RunMap<RunT>();
// keep track of the previous time we started a build
private long lastBuildStartTime;
/**
* Initializes this mixin.
* Call this from a constructor and {@link AbstractItem#onLoad} to make sure it is always initialized.
......@@ -92,14 +90,14 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
}
/**
* Something to be called from {@link AbstractItem#onCreatedFromScratch}.
* Something to be called from {@link Job#onCreatedFromScratch}.
*/
public final void onCreatedFromScratch() {
builds = createBuildRunMap();
}
/**
* Something to be called from {@link AbstractItem#onLoad}.
* Something to be called from {@link Job#onLoad}.
*/
@SuppressWarnings("unchecked")
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
......@@ -123,7 +121,8 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
// if we are reloading, keep all those that are still building intact
for (RunT r : currentBuilds.getLoadedBuilds().values()) {
if (r.isBuilding()) {
_builds.put(r);
// Do not use RunMap.put(Run):
_builds.put(r.getNumber(), r);
}
}
}
......@@ -131,11 +130,15 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
}
private RunMap<RunT> createBuildRunMap() {
return new RunMap<RunT>(asJob().getBuildDir(), new RunMap.Constructor<RunT>() {
RunMap<RunT> r = new RunMap<RunT>(asJob().getBuildDir(), new RunMap.Constructor<RunT>() {
@Override public RunT create(File dir) throws IOException {
return loadBuild(dir);
}
});
RunIdMigrator runIdMigrator = asJob().runIdMigrator;
assert runIdMigrator != null;
r.runIdMigrator = runIdMigrator;
return r;
}
/**
......@@ -169,19 +172,7 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
* Calls the ({@link Job}) constructor of {@link #getBuildClass}.
* Suitable for {@link SubTask#createExecutable}.
*/
@SuppressWarnings("SleepWhileHoldingLock")
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SWL_SLEEP_WITH_LOCK_HELD")
public final synchronized RunT newBuild() throws IOException {
// make sure we don't start two builds in the same second
// so the build directories will be different too
long timeSinceLast = System.currentTimeMillis() - lastBuildStartTime;
if (timeSinceLast < 1000) {
try {
Thread.sleep(1000 - timeSinceLast);
} catch (InterruptedException e) {
}
}
lastBuildStartTime = System.currentTimeMillis();
try {
RunT lastBuild = getBuildClass().getConstructor(asJob().getClass()).newInstance(asJob());
builds.put(lastBuild);
......
Problem.DisplayName={0} builds have inconsistent timestamps in <a href="{2}">{1}</a>
\ No newline at end of file
/*
The MIT License
Copyright (c) 2013, CloudBees, Inc.
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 jenkins.diagnostics.ooom.OutOfOrderBuildMonitor
import jenkins.diagnostics.ooom.BuildPtr
import jenkins.diagnostics.ooom.Problem;
def f = namespace(lib.FormTagLib)
if (my.isFixingActive()) {
div(class:"info") {
raw _("inProgress",my.url)
}
} else if (my.logFile.exists()) {
form(method:"POST",action:"${my.url}/dismiss",name:"dismissOutOfOrderBuilds") {
raw _("completed",my.url)
f.submit(name:"dismiss",value:_("Dismiss this message"))
}
}
if (!my.problems.isEmpty()) {
form(method:"POST",action:"${my.url}/fix",name:"fixOutOfOrderBuilds") {
div(class:"warning") {
raw _("buildsAreOutOfOrder")
}
ul {
my.problems.each { Problem p ->
li {
raw(_("problem",
p.countInconsistencies(),
p.job.fullDisplayName,
rootURL+'/'+p.job.url))
text(" : ")
p.offenders.each { BuildPtr o ->
a(href:rootURL+'/'+p.job.url+'/'+o.n, "#${o.n}")
raw(" ")
}
}
}
}
div(align:"right") {
f.submit(name:"fix",value:_("Correct those problems by moving offending records to a backup folder"))
}
}
}
problem={0} builds in <a href="{2}">{1}</a>
buildsAreOutOfOrder=Some projects have builds whose timestamps are inconsistent. \
<a href="http://jenkins-ci.org/issue/18289">These will confuse Jenkins when it tries to look up build records</a>.
inProgress=Out-of-order builds are being swept under the carpet. <a href="{0}/log">You can check the log</a>.
completed=Out-of-order builds were swept under the carpet. <a href="{0}/log">You can check the log</a>.
\ No newline at end of file
......@@ -24,12 +24,7 @@
package hudson.model;
import hudson.Util;
import hudson.model.Run.Artifact;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.Callable;
......@@ -87,32 +82,6 @@ public class RunTest {
}
@Bug(15587)
@Test
public void testParseTimestampFromBuildDir() throws Exception {
//Assume.assumeTrue(!Functions.isWindows() || (NTFS && JAVA7) || ...);
String buildDateTime = "2012-12-21_04-02-28";
int buildNumber = 155;
StreamTaskListener l = StreamTaskListener.fromStdout();
File tempDir = Util.createTempDir();
File buildDir = new File(tempDir, buildDateTime);
assertEquals(true, buildDir.mkdir());
File buildDirSymLink = new File(tempDir, Integer.toString(buildNumber));
try {
buildDir.mkdir();
Util.createSymlink(tempDir, buildDir.getAbsolutePath(), buildDirSymLink.getName(), l);
long time = Run.parseTimestampFromBuildDir(buildDirSymLink);
assertEquals(buildDateTime, Run.ID_FORMATTER.get().format(new Date(time)));
} finally {
Util.deleteRecursive(tempDir);
}
}
private List<? extends Run<?, ?>.Artifact> createArtifactList(String... paths) throws Exception {
Run r = new Run(new StubJob(), 0) {};
Run.ArtifactList list = r.new ArtifactList();
......
/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* 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 jenkins.model;
import hudson.Util;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
public class RunIdMigratorTest {
@Rule public TemporaryFolder tmp = new TemporaryFolder();
/** Ensures that legacy timestamps are interpreted in a predictable time zone. */
@BeforeClass public static void timezone() {
TimeZone.setDefault(TimeZone.getTimeZone("EST"));
}
@BeforeClass public static void logging() {
RunIdMigrator.LOGGER.setLevel(Level.ALL);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
RunIdMigrator.LOGGER.addHandler(handler);
}
private RunIdMigrator migrator;
private File dir;
@Before public void init() {
migrator = new RunIdMigrator();
dir = tmp.getRoot();
}
@Test public void newJob() throws Exception {
migrator.created(dir);
assertEquals("{legacyIds=''}", summarize());
assertEquals(0, migrator.findNumber("whatever"));
migrator.delete(dir, "1");
migrator = new RunIdMigrator();
assertFalse(migrator.migrate(dir, null));
assertEquals("{legacyIds=''}", summarize());
}
@Test public void legacy() throws Exception {
write("2014-01-02_03-04-05/build.xml", "<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <number>99</number>\n <otherstuff>ok</otherstuff>\n</run>");
link("99", "2014-01-02_03-04-05");
link("lastFailedBuild", "-1");
link("lastSuccessfulBuild", "99");
assertEquals("{2014-01-02_03-04-05={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <number>99</number>\n <otherstuff>ok</otherstuff>\n</run>'}, 99=→2014-01-02_03-04-05, lastFailedBuild=→-1, lastSuccessfulBuild=→99}", summarize());
assertTrue(migrator.migrate(dir, null));
assertEquals("{99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <id>2014-01-02_03-04-05</id>\n <timestamp>1388649845000</timestamp>\n <otherstuff>ok</otherstuff>\n</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99, legacyIds='2014-01-02_03-04-05 99\n'}", summarize());
assertEquals(99, migrator.findNumber("2014-01-02_03-04-05"));
migrator = new RunIdMigrator();
assertFalse(migrator.migrate(dir, null));
assertEquals(99, migrator.findNumber("2014-01-02_03-04-05"));
migrator.delete(dir, "2014-01-02_03-04-05");
FileUtils.deleteDirectory(new File(dir, "99"));
new File(dir, "lastSuccessfulBuild").delete();
assertEquals("{lastFailedBuild=→-1, legacyIds=''}", summarize());
}
@Test public void reverseImmediately() throws Exception {
File root = dir;
dir = new File(dir, "jobs/somefolder/jobs/someproject/promotions/OK/builds");
write("99/build.xml", "<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <id>2014-01-02_03-04-05</id>\n <timestamp>1388649845000</timestamp>\n <otherstuff>ok</otherstuff>\n</run>");
link("lastFailedBuild", "-1");
link("lastSuccessfulBuild", "99");
write("legacyIds", "2014-01-02_03-04-05 99\n");
assertEquals("{99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <id>2014-01-02_03-04-05</id>\n <timestamp>1388649845000</timestamp>\n <otherstuff>ok</otherstuff>\n</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99, legacyIds='2014-01-02_03-04-05 99\n'}", summarize());
RunIdMigrator.main(root.getAbsolutePath());
assertEquals("{2014-01-02_03-04-05={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <number>99</number>\n <otherstuff>ok</otherstuff>\n</run>'}, 99=→2014-01-02_03-04-05, lastFailedBuild=→-1, lastSuccessfulBuild=→99}", summarize());
}
@Test public void reverseAfterNewBuilds() throws Exception {
File root = dir;
dir = new File(dir, "jobs/someproject/modules/test$test/builds");
write("1/build.xml", "<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <timestamp>1388649845000</timestamp>\n <otherstuff>ok</otherstuff>\n</run>");
write("legacyIds", "");
assertEquals("{1={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <timestamp>1388649845000</timestamp>\n <otherstuff>ok</otherstuff>\n</run>'}, legacyIds=''}", summarize());
RunIdMigrator.main(root.getAbsolutePath());
assertEquals("{1=→2014-01-02_03-04-05, 2014-01-02_03-04-05={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n<run>\n <stuff>ok</stuff>\n <number>1</number>\n <otherstuff>ok</otherstuff>\n</run>'}}", summarize());
}
// TODO test sane recovery from various error conditions
private void write(String file, String text) throws Exception {
FileUtils.write(new File(dir, file), text);
}
private void link(String symlink, String dest) throws Exception {
Util.createSymlink(dir, dest, symlink, new StreamTaskListener(System.out, Charset.defaultCharset()));
}
private String summarize() throws Exception {
return summarize(dir);
}
private static String summarize(File dir) throws Exception {
File[] kids = dir.listFiles();
Map<String,String> m = new TreeMap<String,String>();
for (File kid : kids) {
String notation;
String symlink = Util.resolveSymlink(kid);
if (symlink != null) {
notation = "→" + symlink;
} else if (kid.isFile()) {
notation = "'" + FileUtils.readFileToString(kid) + "'";
} else if (kid.isDirectory()) {
notation = summarize(kid);
} else {
notation = "?";
}
m.put(kid.getName(), notation);
}
return m.toString();
}
}
\ No newline at end of file
......@@ -66,7 +66,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
return new FakeMap(getDir()) {
@Override
protected BuildReference<Build> createReference(Build r) {
return new BuildReference<Build>(getIdOf(r), /* pretend referent expired */ null);
return new BuildReference<Build>(Integer.toString(r.n), /* pretend referent expired */ null);
}
};
}
......@@ -80,7 +80,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Before
public void setUp() throws Exception {
a = aBuilder.add(1, "A").add(3, "B").add(5, "C").make();
a = aBuilder.add(1).add(3).add(5).make();
b = bBuilder.make();
}
......@@ -88,11 +88,11 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Test
public void lookup() {
assertNull(a.get(0));
a.get(1).asserts(1, "A");
a.get(1).asserts(1);
assertNull(a.get(2));
a.get(3).asserts(3, "B");
a.get(3).asserts(3);
assertNull(a.get(4));
a.get(5).asserts(5, "C");
a.get(5).asserts(5);
assertNull(a.get(6));
assertNull(b.get(1));
......@@ -108,8 +108,8 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Test
public void idempotentLookup() {
for (int i=0; i<5; i++) {
a.get(1).asserts(1,"A");
a.get((Object)1).asserts(1, "A");
a.get(1).asserts(1);
a.get((Object)1).asserts(1);
}
}
......@@ -153,24 +153,24 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Bug(19418)
@Test
public void searchExactWhenIndexedButSoftReferenceExpired() throws IOException {
final FakeMap m = localExpiredBuilder.add(1, "A").add(2, "B").make();
final FakeMap m = localExpiredBuilder.add(1).add(2).make();
// force index creation
m.entrySet();
m.search(1, Direction.EXACT).asserts(1, "A");
m.search(1, Direction.EXACT).asserts(1);
assertNull(m.search(3, Direction.EXACT));
assertNull(m.search(0, Direction.EXACT));
}
@Bug(22681)
@Test public void exactSearchShouldNotReload() throws Exception {
FakeMap m = localBuilder.add(1, "A").add(2, "B").make();
FakeMap m = localBuilder.add(1).add(2).make();
assertNull(m.search(0, Direction.EXACT));
Build a = m.search(1, Direction.EXACT);
a.asserts(1, "A");
a.asserts(1);
Build b = m.search(2, Direction.EXACT);
b.asserts(2, "B");
b.asserts(2);
assertNull(m.search(0, Direction.EXACT));
assertSame(a, m.search(1, Direction.EXACT));
assertSame(b, m.search(2, Direction.EXACT));
......@@ -186,82 +186,19 @@ public class AbstractLazyLoadRunMapTest extends Assert {
*/
@Test
public void unloadableData() throws IOException {
FakeMap m = localBuilder.add(1, "A").addUnloadable("B").add(5, "C").make();
FakeMap m = localBuilder.add(1).addUnloadable(3).add(5).make();
assertNull(m.search(3, Direction.EXACT));
m.search(3,Direction.DESC).asserts(1, "A");
m.search(3, Direction.ASC ).asserts(5, "C");
m.search(3,Direction.DESC).asserts(1);
m.search(3, Direction.ASC ).asserts(5);
}
@Test
public void eagerLoading() throws IOException {
Map.Entry[] b = a.entrySet().toArray(new Map.Entry[3]);
((Build)b[0].getValue()).asserts(5, "C");
((Build)b[1].getValue()).asserts(3, "B");
((Build)b[2].getValue()).asserts(1, "A");
}
@Test
public void fastLookup() throws IOException {
FakeMap a = localBuilder.addBoth(1, "A").addBoth(3, "B").addBoth(5, "C"). make();
a.get(1).asserts(1,"A");
assertNull(a.get(2));
a.get(3).asserts(3, "B");
assertNull(a.get(4));
a.get(5).asserts(5, "C");
}
@Test
public void fastSearch() throws IOException {
FakeMap a = localBuilder.addBoth(1, "A").addBoth(3, "B").addBoth(5, "C").addBoth(7, "D").make();
// we should be using the cache to find the entry efficiently
a.search(6, Direction.ASC).asserts(7, "D");
a.search(2, Direction.DESC).asserts(1, "A");
}
@Test
public void bogusCache() throws IOException {
FakeMap a = localBuilder.addUnloadableCache(1).make();
assertNull(a.get(1));
}
@Test
public void bogusCacheAndHiddenRealData() throws IOException {
FakeMap a = localBuilder.addUnloadableCache(1).add(1, "A").make();
a.get(1).asserts(1, "A");
}
@Test
public void bogusCache2() throws IOException {
FakeMap a = localBuilder.addBogusCache(1, 3, "A").make();
assertNull(a.get(1));
a.get(3).asserts(3,"A");
}
@Test
public void incompleteCache() throws IOException {
FakeMapBuilder setup = localBuilder.addBoth(1, "A").add(3, "B").addBoth(5, "C");
// each test uses a fresh map since cache lookup causes additional loads
// to verify the results
// if we just rely on cache,
// it'll pick up 5:C as the first ascending value,
// but we should be then verifying this by loading B, so in the end we should
// find the correct value
setup.make().search(2, Direction.ASC).asserts(3,"B");
setup.make().search(4, Direction.DESC).asserts(3,"B");
// variation of the cache based search where we find the outer-most value via cache
setup.make().search(0, Direction.ASC).asserts(1,"A");
setup.make().search(6, Direction.DESC).asserts(5,"C");
// variation of the cache search where the cache tells us that we are searching
// in the direction that doesn't have any records
assertNull(setup.make().search(0, Direction.DESC));
assertNull(setup.make().search(6, Direction.ASC));
((Build)b[0].getValue()).asserts(5);
((Build)b[1].getValue()).asserts(3);
((Build)b[2].getValue()).asserts(1);
}
@Test
......@@ -271,8 +208,8 @@ public class AbstractLazyLoadRunMapTest extends Assert {
Build[] b = m.values().toArray(new Build[2]);
assertEquals(2, b.length);
b[0].asserts(5, "C");
b[1].asserts(3, "B");
b[0].asserts(5);
b[1].asserts(3);
}
@Test
......@@ -287,77 +224,55 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Test
public void indexOutOfBounds() throws Exception {
FakeMapBuilder f = localBuilder;
f.add(100,"A")
.addUnloadable("B")
.addUnloadable("C")
.addUnloadable("D")
.addUnloadable("E")
.addUnloadable("F")
.addUnloadable("G")
.add(200,"H")
.add(201, "I");
f.add(100)
.addUnloadable(150)
.addUnloadable(151)
.addUnloadable(152)
.addUnloadable(153)
.addUnloadable(154)
.addUnloadable(155)
.add(200)
.add(201);
FakeMap map = f.make();
Build x = map.search(Integer.MAX_VALUE, Direction.DESC);
assert x.n==201;
}
@Bug(15652)
@Test public void outOfOrder() throws Exception {
FakeMap map = localBuilder
.add( 4, "2012-A")
.add( 5, "2012-B")
.add( 6, "2012-C")
.add( 7, "2012-D")
.add( 8, "2012-E")
.add( 9, "2012-F")
.add(10, "2012-G")
.add(11, "2012-H")
.add(12, "2012-I")
.add( 1, "2013-A")
.add( 7, "2013-B")
.add( 9, "2013-C")
.add(10, "2013-D")
.add(11, "2013-E")
.make();
map.entrySet(); // forces Index to be populated
assertNull(map.search(3, Direction.DESC));
}
@Bug(18065)
@Test public void all() throws Exception {
assertEquals("[]", a.getLoadedBuilds().keySet().toString());
Set<Map.Entry<Integer,Build>> entries = a.entrySet();
assertEquals("[]", a.getLoadedBuilds().keySet().toString());
assertFalse(entries.isEmpty());
assertEquals("5 since it is the latest, and 3 because, well, .search and pivot is weird", "[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals(5, a.getById("C").n);
assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals("A", a.getByNumber(1).id);
assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString());
assertEquals("5 since it is the latest", "[5]", a.getLoadedBuilds().keySet().toString());
assertEquals(5, a.getById("5").n);
assertEquals("[5]", a.getLoadedBuilds().keySet().toString());
assertEquals(1, a.getByNumber(1).n);
assertEquals("[5, 1]", a.getLoadedBuilds().keySet().toString());
a.purgeCache();
assertEquals("[]", a.getLoadedBuilds().keySet().toString());
Iterator<Map.Entry<Integer,Build>> iterator = entries.iterator();
assertEquals("iterator starts off checking for newest build, so for this crazy logic see above", "[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals("[5]", a.getLoadedBuilds().keySet().toString());
assertTrue(iterator.hasNext());
assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals("[5]", a.getLoadedBuilds().keySet().toString());
Map.Entry<Integer,Build> entry = iterator.next();
assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals(5, entry.getKey().intValue());
assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString());
assertEquals("C", entry.getValue().id);
assertEquals(5, entry.getValue().n);
assertEquals("[5, 3]", a.getLoadedBuilds().keySet().toString());
assertTrue(iterator.hasNext());
entry = iterator.next();
assertEquals(3, entry.getKey().intValue());
assertEquals(".next() precomputes the one after that too", "[5, 3, 1]", a.getLoadedBuilds().keySet().toString());
assertEquals("B", entry.getValue().id);
assertEquals(3, entry.getValue().n);
assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString());
assertTrue(iterator.hasNext());
entry = iterator.next();
assertEquals(1, entry.getKey().intValue());
assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString());
assertEquals("A", entry.getValue().id);
assertEquals(1, entry.getValue().n);
assertEquals("[5, 3, 1]", a.getLoadedBuilds().keySet().toString());
assertFalse(iterator.hasNext());
}
......@@ -375,7 +290,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
assertTrue(itr.hasNext());
Entry<Integer, Build> e = itr.next();
assertEquals((Integer)5,e.getKey());
e.getValue().asserts(5, "C");
e.getValue().asserts(5);
// now that the first entry is returned, we expect there to be two loaded
assertTrue(a.getLoadedBuilds().size() < 3);
......@@ -384,7 +299,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
assertTrue(itr.hasNext());
e = itr.next();
assertEquals((Integer)3, e.getKey());
e.getValue().asserts(3,"B");
e.getValue().asserts(3);
// repeat the process for the third one
assertTrue(a.getLoadedBuilds().size() <= 3);
......@@ -393,7 +308,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
assertTrue(itr.hasNext());
e = itr.next();
assertEquals((Integer) 1, e.getKey());
e.getValue().asserts(1,"A");
e.getValue().asserts(1);
assertFalse(itr.hasNext());
assertEquals(3, a.getLoadedBuilds().size());
......@@ -417,7 +332,7 @@ public class AbstractLazyLoadRunMapTest extends Assert {
@Issue("JENKINS-25655")
@Test public void entrySetChanges() {
assertEquals(3, a.entrySet().size());
a.put(new Build(7, "D"));
a.put(new Build(7));
assertEquals(4, a.entrySet().size());
}
......
......@@ -26,7 +26,6 @@ package jenkins.model.lazy;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
/**
......@@ -42,50 +41,27 @@ public class FakeMap extends AbstractLazyLoadRunMap<Build> {
return build.n;
}
@Override
protected String getIdOf(Build build) {
return build.id;
}
@Override
protected FilenameFilter createDirectoryFilter() {
return new FilenameFilter() {
public boolean accept(File dir, String name) {
try {
Integer.parseInt(name);
return false;
} catch (NumberFormatException e) {
return true;
}
}
};
}
@Override
protected Build retrieve(File dir) throws IOException {
String n = FileUtils.readFileToString(new File(dir, "n")).trim();
String id = FileUtils.readFileToString(new File(dir, "id")).trim();
//new Exception("loading " + id + " #" + n).printStackTrace();
return new Build(Integer.parseInt(n),id);
//new Exception("loading #" + n).printStackTrace();
return new Build(Integer.parseInt(n));
}
}
class Build {
final int n;
final String id;
Build(int n, String id) {
Build(int n) {
this.n = n;
this.id = id;
}
public void asserts(int n, String id) {
public void asserts(int n) {
assert this.n==n;
assert this.id.equals(id);
}
@Override public String toString() {
return "Build #" + n + " [" + id + "] @" + hashCode();
return "Build #" + n + " @" + hashCode();
}
}
\ No newline at end of file
......@@ -46,59 +46,19 @@ public class FakeMapBuilder implements TestRule {
public FakeMapBuilder() {
}
public FakeMapBuilder add(int n, String id) throws IOException {
verifyId(id);
File build = new File(dir,id);
build.mkdir();
public FakeMapBuilder add(int n) throws IOException {
File build = new File(dir, Integer.toString(n));
FileUtils.writeStringToFile(new File(build, "n"), Integer.toString(n));
FileUtils.writeStringToFile(new File(build,"id"),id);
return this;
}
/**
* Adds a symlink from n to build id.
*
* (in test we should ideally create a symlink, but we fake the test
* by actually making it a directory and staging the same data.)
*/
public FakeMapBuilder addCache(int n, String id) throws IOException {
return addBogusCache(n,n,id);
}
public FakeMapBuilder addBogusCache(int label, int actual, String id) throws IOException {
verifyId(id);
File build = new File(dir,Integer.toString(label));
build.mkdir();
FileUtils.writeStringToFile(new File(build, "n"), Integer.toString(actual));
FileUtils.writeStringToFile(new File(build,"id"),id);
return this;
}
public FakeMapBuilder addBoth(int n, String id) throws IOException {
return add(n,id).addCache(n,id);
}
private void verifyId(String id) {
try {
Integer.parseInt(id);
throw new IllegalMonitorStateException("ID cannot be a number");
} catch (NumberFormatException e) {
// OK
}
}
/**
* Adds a build record under the givn ID but make it unloadable,
* which will cause a failure when a load is attempted on this build ID.
*/
public FakeMapBuilder addUnloadable(String id) throws IOException {
File build = new File(dir,id);
build.mkdir();
return this;
}
public FakeMapBuilder addUnloadableCache(int n) throws IOException {
File build = new File(dir,String.valueOf(n));
public FakeMapBuilder addUnloadable(int n) throws IOException {
File build = new File(dir, Integer.toString(n));
build.mkdir();
return this;
}
......
package jenkins.model.lazy;
import static org.junit.Assert.*;
import org.junit.Test;
/**
......@@ -12,6 +13,25 @@ public class SortedIntListTest {
l.add(0);
l.add(5);
l.add(10);
System.out.println(l.lower(Integer.MAX_VALUE));
assertEquals(2, l.lower(Integer.MAX_VALUE));
}
@Test public void ceil() {
SortedIntList l = new SortedIntList(5);
l.add(1);
l.add(3);
l.add(5);
assertEquals(0, l.ceil(0));
assertEquals(0, l.ceil(1));
assertEquals(1, l.ceil(2));
assertEquals(1, l.ceil(3));
assertEquals(2, l.ceil(4));
assertEquals(2, l.ceil(5));
assertEquals(3, l.ceil(6));
assertTrue(l.isInRange(0));
assertTrue(l.isInRange(1));
assertTrue(l.isInRange(2));
assertFalse(l.isInRange(3));
}
}
package jenkins.diagnostics.ooom
import com.gargoylesoftware.htmlunit.html.HtmlPage
import hudson.model.FreeStyleProject
import hudson.model.Job
import hudson.model.TaskListener
import hudson.util.StreamTaskListener
import static org.junit.Assert.*;
import org.junit.Rule
import org.junit.Test
import org.jvnet.hudson.test.Bug
import org.jvnet.hudson.test.JenkinsRule
import org.jvnet.hudson.test.recipes.LocalData
import javax.inject.Inject
/**
*
*
* @author Kohsuke Kawaguchi
*/
class OutOfOrderBuildDetectorTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Inject
OutOfOrderBuildDetector oobd;
@Inject
OutOfOrderBuildMonitor oobm;
/**
* The test case looks like [#3,#1,#2,#4] and we should find #3 to be the problem.
*/
@Test @LocalData
public void oneProblem() {
j.jenkins.injector.injectMembers(this);
Job dt = j.jenkins.getItem("deletion-test");
def p = Problem.find(dt);
assert p.offenders.size()==1;
def b = (p.offenders as List)[0];
assert b.id=="2013-07-29_17-09-09"
assert b.n==3
def l = StreamTaskListener.fromStdout()
// find all the problems now
oobd.execute(l,0);
// verify that the monitor is activated
assert oobm.isActivated();
assert oobm.problems.size()==1;
def wc = j.createWebClient()
// at this point there should be a link to start a fix but not the button to dismiss
def manage = wc.goTo("manage")
assertNoForm(manage,"dismissOutOfOrderBuilds")
j.submit(manage.getFormByName("fixOutOfOrderBuilds"));
// give it a break until the fix is complete
while (oobm.isFixingActive())
Thread.sleep(100);
// there should be a log file now
oobm.getLogFile().exists()
wc.goTo("administrativeMonitor/${oobm.class.name}/log")
// at this point the UI should change to show a dismiss action
manage = wc.goTo("manage")
assertNoForm(manage,"fixOutOfOrderBuilds")
j.submit(manage.getFormByName("dismissOutOfOrderBuilds"));
// that should stop the alarm and there should be no more forms
assert !oobm.isActivated();
manage = wc.goTo("manage")
assertNoForm(manage,"fixOutOfOrderBuilds")
assertNoForm(manage,"dismissOutOfOrderBuilds")
// verify that the problem is actually fixed
assert dt.getBuildByNumber(1)!=null;
assert dt.getBuildByNumber(2)!=null;
assert dt.getBuildByNumber(3)==null;
assert dt.getBuildByNumber(4)!=null;
assert Problem.find(dt)==null;
// and there should be a backup
assert new File(dt.rootDir,"outOfOrderBuilds/2013-07-29_17-09-09/build.xml").exists()
}
def assertNoForm(HtmlPage p, String name) {
def forms = p.documentElement.getElementsByAttribute("form", "name", name);
assert forms.size()==0;
}
/**
* If there's no problem, it shouldn't find it.
*/
@Test
public void thereShouldBeNoFailure() {
def f = j.createFreeStyleProject()
j.assertBuildStatusSuccess(f.scheduleBuild2(0));
j.assertBuildStatusSuccess(f.scheduleBuild2(0));
j.assertBuildStatusSuccess(f.scheduleBuild2(0));
assert Problem.find(f)==null;
}
@Bug(22631)
@LocalData
@Test public void buildNumberClash() throws Exception {
j.jenkins.injector.injectMembers(this);
FreeStyleProject p = j.jenkins.getItemByFullName("problematic", FreeStyleProject.class);
assertNotNull(p);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
TaskListener l = new StreamTaskListener(baos);
oobd.execute(l, 0);
String log = baos.toString();
String idA = "2014-04-15_11-22-11";
String idB = "2014-04-15_11-22-14";
assertEquals(idB, p.getBuildByNumber(2).getId());
assertEquals(3, p.getLastBuild().getNumber());
File dir = p.getBuildDir();
File buildDirA = new File(dir, idA);
File buildDirB = new File(dir, idB);
BuildPtr b2A = new BuildPtr(p, buildDirA, 2);
BuildPtr b2B = new BuildPtr(p, buildDirB, 2);
String expected = "[" + b2A + "]";
assertTrue("Should see " + expected + " in:\n" + log, log.contains(expected));
baos = new ByteArrayOutputStream();
l = new StreamTaskListener(baos);
oobm.fix(l);
log = baos.toString();
assertTrue(buildDirB.isDirectory());
assertTrue("Should see " + buildDirA + " in:\n" + log, log.contains(buildDirA.toString()));
File dest = new File(new File(p.getRootDir(), "outOfOrderBuilds"), idA);
assertTrue("Should see " + dest + " in:\n" + log, log.contains(dest.toString()));
assertFalse(buildDirA.isDirectory());
assertTrue(dest.isDirectory());
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册