package com.ikokoon.serenity.hudson;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.json.JSONObject;
import org.apache.log4j.Logger;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import com.ikokoon.serenity.IConstants;
import com.ikokoon.serenity.persistence.DataBaseOdb;
import com.ikokoon.serenity.persistence.DataBaseRam;
import com.ikokoon.serenity.persistence.DataBaseToolkit;
import com.ikokoon.serenity.persistence.IDataBase;
import com.ikokoon.serenity.process.Aggregator;
import com.ikokoon.serenity.process.Pruner;
import com.ikokoon.toolkit.LoggingConfigurator;
/**
* This class runs at the end of the build, called by Hudson. The purpose is to copy the database files from the output directories for each module in
* the case of Maven and Ant builds to the output directory for the build for display in the Hudson front end plugin. As well as this the source that
* was found for the project is copied to the source directory where the front end can access it.
*
* Once all the database files are copied to a location on the local machine then they are merged together and pruned.
*
* @author Michael Couck
* @since 12.07.09
* @version 01.00
*/
@SuppressWarnings("unchecked")
public class SerenityPublisher extends Recorder implements Serializable {
/**
* The file filter that matches all the files available. We'll filter then later.
*
* @author Michael Couck
* @since 12.05.10
* @version 01.00
*/
class FileFilterImpl implements FileFilter, Serializable {
public boolean accept(File pathname) {
return true;
}
}
/** Initialise the logging. */
static {
LoggingConfigurator.configure();
}
/** The pattern to exclude from the file filter. */
private static final String SERENITY_ODB_REGEX = ".*serenity.odb";
private static final String SERENITY_SOURCE_REGEX = ".*serenity.*.source.*";
/** The logger. */
protected static Logger logger = Logger.getLogger(SerenityPublisher.class);
/** The description for Hudson. */
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
@DataBoundConstructor
public SerenityPublisher() {
}
/**
* {@inheritDoc}
*/
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener buildListener) throws InterruptedException, IOException {
PrintStream printStream = buildListener.getLogger();
printStream.println("Publishing Serenity reports...");
if (!Result.SUCCESS.equals(build.getResult())) {
printStream.println("Build was not successful... but will still try to publish the report");
}
// Copy the source for the Java files to the build directory
copySourceToBuildDirectory(build, buildListener);
// Copy the database files from the output directories to the build directory. and
// merge them and then aggregate all the data, then prune the data
IDataBase targetDataBase = copyDataBasesToBuildDirectory(build, buildListener);
aggregate(build, buildListener, targetDataBase);
prune(build, buildListener, targetDataBase);
targetDataBase.close();
printStream.println("Publishing the Serenity results...");
ISerenityResult result = new SerenityResult(build);
SerenityBuildAction buildAction = new SerenityBuildAction(build, result);
build.getActions().add(buildAction);
return true;
}
/**
* Scans the output directory for the project and locates all the database files. These files are then copied to a local file in the user temp
* directory. The databases are merged into the main database.
*
* @param build
* the build for this project
* @param buildListener
* the listener of the build
* @return the primary database file for the final result
* @throws InterruptedException
* @throws IOException
*/
private IDataBase copyDataBasesToBuildDirectory(AbstractBuild<?, ?> build, BuildListener buildListener) throws InterruptedException, IOException {
final PrintStream printStream = buildListener.getLogger();
File buildDirectory = build.getRootDir();
printStream.println("Build directory... " + buildDirectory);
File targetDataBaseFile = new File(buildDirectory, IConstants.DATABASE_FILE_ODB);
printStream.println("Target database... " + targetDataBaseFile);
// Create the final output database file for the build
String targetPath = targetDataBaseFile.getAbsolutePath();
IDataBase odbDataBase = IDataBase.DataBaseManager.getDataBase(DataBaseOdb.class, targetPath, null);
IDataBase targetDataBase = IDataBase.DataBaseManager.getDataBase(DataBaseRam.class, IConstants.DATABASE_FILE_RAM + "." + Math.random(),
odbDataBase);
// Scan the build output roots for database files to merge
FilePath[] moduleRoots = build.getModuleRoots();
FileFilter fileFilter = new FileFilterImpl();
// The list of Serenity database files found in the module roots
List<FilePath> serenityOdbs = new ArrayList<FilePath>();
Pattern pattern = Pattern.compile(SERENITY_ODB_REGEX);
for (FilePath moduleRoot : moduleRoots) {
// printStream.println("Module root : " + moduleRoot.toURI());
try {
findFilesAndDirectories(moduleRoot, serenityOdbs, fileFilter, pattern, printStream);
} catch (Exception e) {
printStream.println("Exception searching for database files : " + moduleRoot);
e.printStackTrace(buildListener.fatalError("Exception searching for Serenity database files : " + moduleRoot));
}
}
// Iterate over the database files that were found and merge them to the final database
for (FilePath serenityOdb : serenityOdbs) {
File sourceFile = File.createTempFile("serenity", ".odb");
FilePath sourceFilePath = new FilePath(sourceFile);
serenityOdb.copyTo(sourceFilePath);
String sourcePath = sourceFile.getAbsolutePath();
IDataBase sourceDataBase = IDataBase.DataBaseManager.getDataBase(DataBaseOdb.class, sourcePath, null);
try {
// Copy the data from the source into the target, then close the source
printStream.println("Copying database from... " + sourcePath + " to... " + targetPath);
DataBaseToolkit.copyDataBase(sourceDataBase, targetDataBase);
boolean deleted = sourceFile.delete();
if (!deleted) {
printStream.println("Couldn't delete temp database file... " + sourcePath);
sourceFile.deleteOnExit();
}
} catch (Exception e) {
e.printStackTrace(buildListener.fatalError("Unable to copy Serenity database file from : " + sourcePath + ", to : " + targetPath));
build.setResult(Result.UNSTABLE);
} finally {
sourceDataBase.close();
}
}
return targetDataBase;
}
/**
* Scans recursively the {@link FilePath}(s) for the database files.
*
* @param filePath
* the starting file path to start scanning from
* @param filePaths
* the list of file paths that were found
* @param fileFilter
* the file filter that will return all files in the path
* @param printStream
* the logger to the front end
* @throws Exception
*/
private void findFilesAndDirectories(FilePath filePath, List<FilePath> filePaths, FileFilter fileFilter, Pattern pattern, PrintStream printStream)
throws Exception {
// printStream.println("File path : " + filePath.toURI());
List<FilePath> list = filePath.list(fileFilter);
if (list != null) {
for (FilePath childFilePath : list) {
findFilesAndDirectories(childFilePath, filePaths, fileFilter, pattern, printStream);
}
}
Matcher matcher = pattern.matcher(filePath.toURI().toString());
if (matcher.find()) {
filePaths.add(filePath);
}
}
/**
* Runs the aggregator on the final database to generate the statistics etc.
*
* @param build
* the build for the project
* @param buildListener
* the build listener that has the logger in it
* @param targetDataBase
* the target database to aggregate
*/
private void aggregate(AbstractBuild<?, ?> build, BuildListener buildListener, IDataBase targetDataBase) {
buildListener.getLogger().println("Aggregating data... ");
new Aggregator(null, targetDataBase).execute();
}
/**
* Prunes the database removing all the objects that are no longer needed, like the lines and afferent/efferent objects.
*
* @param build
* the build for the project
* @param buildListener
* the build listener that has the logger in it
* @param targetDataBase
* the target database to aggregate
*/
private void prune(AbstractBuild<?, ?> build, BuildListener buildListener, IDataBase targetDataBase) {
buildListener.getLogger().println("Pruning data...");
new Pruner(null, targetDataBase).execute();
}
private boolean copySourceToBuildDirectory(AbstractBuild<?, ?> build, final BuildListener buildListener) throws InterruptedException, IOException {
FilePath workSpace = build.getWorkspace();
PrintStream printStream = buildListener.getLogger();
printStream.println("Workspace root... " + workSpace.toURI().getRawPath());
FilePath[] moduleRoots = build.getModuleRoots();
FileFilter fileFilter = new FileFilterImpl();
// The list of Serenity source directories found in the module roots
List<FilePath> sourceDirectories = new ArrayList<FilePath>();
Pattern pattern = Pattern.compile(SERENITY_SOURCE_REGEX);
for (FilePath moduleRoot : moduleRoots) {
// printStream.println("Module root : " + moduleRoot.toURI());
try {
findFilesAndDirectories(moduleRoot, sourceDirectories, fileFilter, pattern, printStream);
} catch (Exception e) {
printStream.println("Exception searching for source directories : " + moduleRoot);
e.printStackTrace(buildListener.fatalError("Exception searching for Serenity source files : " + moduleRoot));
}
}
FilePath buildDirectory = new FilePath(build.getRootDir());
FilePath buildSourceDirectory = new FilePath(buildDirectory, IConstants.SERENITY_SOURCE);
try {
buildSourceDirectory.deleteContents();
for (FilePath sourceDirectory : sourceDirectories) {
String sourcePath = sourceDirectory.toURI().toString();
// This is a hack. The pattern for the source directories (.*serenity.*.source) doesn't work in a slave for some reason. So we
// have to use .*serenity.*.source.*, which returns all the directories and files with the pattern in it, and after that we have to
// check that this is not a sub directory of the source folder and that it is not a file either. Pity that, try to get the bloody
// pattern working in the future
boolean isDirectoryAndSerenitySource = !sourceDirectory.isDirectory() && !sourcePath.endsWith("serenity/source/")
&& !sourcePath.endsWith("serenity\\source\\");
if (isDirectoryAndSerenitySource) {
continue;
}
printStream.println("Copying source from... " + sourceDirectory.toURI().toString() + " to... "
+ buildSourceDirectory.toURI().getRawPath());
sourceDirectory.copyRecursiveTo(buildSourceDirectory);
}
} catch (IOException e) {
Util.displayIOException(e, buildListener);
e.printStackTrace(buildListener.fatalError("Unable to copy Serenity source directories from : " + sourceDirectories + ", to : "
+ buildDirectory));
}
return true;
}
@Override
public Action getProjectAction(AbstractProject abstractProject) {
logger.debug("getProjectAction(AbstractProject)");
return new SerenityProjectAction(abstractProject);
}
/**
* Descriptor for {@link SerenityPublisher}. Used as a singleton. The class is marked as public so that it can be accessed from views.
* <p/>
* <p/>
* See <tt>views/hudson/plugins/coverage/CoveragePublisher/*.jelly</tt> for the actual HTML fragment for the configuration screen.
*/
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
/**
* Constructs a new DescriptorImpl.
*/
DescriptorImpl() {
super(SerenityPublisher.class);
logger.debug("DescriptorImpl");
}
/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
logger.debug("getDisplayName");
return "Publish Serenity Report";
}
@Override
public boolean isApplicable(java.lang.Class<? extends AbstractProject> jobType) {
logger.debug("isApplicable");
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
logger.debug("configure");
req.bindParameters(this, "serenity.");
save();
return super.configure(req, json);
}
/**
* Creates a new instance of {@link SerenityPublisher} from a submitted form.
*/
@Override
public SerenityPublisher newInstance(StaplerRequest req, JSONObject json) throws FormException {
logger.debug("newInstance");
SerenityPublisher instance = req.bindParameters(SerenityPublisher.class, "serenity.");
return instance;
}
}
public BuildStepMonitor getRequiredMonitorService() {
logger.debug("getRequiredMonitorService");
return BuildStepMonitor.STEP;
}
}