/*
* .NET tools :: Gallio Runner
* Copyright (C) 2010 Jose Chillan, Alexandre Victoor and SonarSource
* dev@sonar.codehaus.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.dotnet.tools.gallio;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.utils.command.Command;
import org.sonar.dotnet.tools.commons.visualstudio.VisualStudioProject;
import org.sonar.dotnet.tools.commons.visualstudio.VisualStudioSolution;
import com.google.common.collect.Lists;
/**
* Class used to build the command line to run Gallio.
*/
public final class GallioCommandBuilder {
private static final Logger LOG = LoggerFactory.getLogger(GallioCommandBuilder.class);
private static final String DEFAULT_GALLIO_RUNNER = "IsolatedProcess";
private static final String PART_COVER_EXE = "PartCover.exe";
private VisualStudioSolution solution;
private String buildConfigurations = "Debug";
// Information needed for simple Gallio execution
private File gallioExecutable;
private File gallioReportFile;
private String filter;
private File workDir;
// Information needed for coverage execution
private CoverageTool coverageTool;
private File partCoverInstallDirectory;
private String coverageExcludes;
private File coverageReportFile;
private GallioCommandBuilder() {
}
/**
* Constructs a {@link GallioCommandBuilder} object for the given Visual Studio solution.
*
* @param solution
* the solution to analyse
* @return a Gallio builder for this solution
*/
public static GallioCommandBuilder createBuilder(VisualStudioSolution solution) {
GallioCommandBuilder builder = new GallioCommandBuilder();
builder.solution = solution;
return builder;
}
/**
* Sets the install dir for Gallio
*
* @param gallioExecutable
* the executable
* @return the current builder
*/
public GallioCommandBuilder setExecutable(File gallioExecutable) {
this.gallioExecutable = gallioExecutable;
return this;
}
/**
* Sets the report file to generate
*
* @param reportFile
* the report file
* @return the current builder
*/
public GallioCommandBuilder setReportFile(File reportFile) {
this.gallioReportFile = reportFile;
return this;
}
/**
* Sets Gallio test filter. <br/>
* This can be used to execute only a specific test category (i.e. CategotyName:unit to consider only tests from the 'unit' category)
*
* @param gallioFilter
* the filter for Gallio
* @return the current builder
*/
public GallioCommandBuilder setFilter(String gallioFilter) {
this.filter = gallioFilter;
return this;
}
/**
* Sets the build configurations. By default, it is "Debug".
*
* @param buildConfigurations
* the build configurations
* @return the current builder
*/
public GallioCommandBuilder setBuildConfigurations(String buildConfigurations) {
this.buildConfigurations = buildConfigurations;
return this;
}
/**
* Set the working directory
*
* @param workDir
* the working directory
*/
public GallioCommandBuilder setWorkDir(File workDir) {
this.workDir = workDir;
return this;
}
/**
* Sets the coverage tool to use, given its name (insensitive, for instance "ncover" or "NCover"). If none corresponding to the given name
* is found, or if an empty string is passed, then no coverage tool will be used and no coverage report will be generated. <br>
* <br/>
* To know which tools are currently supported, check {@link CoverageTool}
*
* @see CoverageTool
* @param coverageToolName
* the name of the tool
*/
public GallioCommandBuilder setCoverageTool(String coverageToolName) {
this.coverageTool = CoverageTool.findFromName(coverageToolName);
return this;
}
/**
* Sets PartCover installation directory.
*
* @param partCoverInstallDirectory
* the install dir
*/
public GallioCommandBuilder setPartCoverInstallDirectory(File partCoverInstallDirectory) {
this.partCoverInstallDirectory = partCoverInstallDirectory;
return this;
}
/**
* Sets the namespaces and assemblies excluded from the code coverage, seperated by a comma. The format for an exclusion is the PartCover
* format: "[assembly]namespace".
*
* @param coverageExcludes
* the excludes
*/
public GallioCommandBuilder setCoverageExcludes(String coverageExcludes) {
this.coverageExcludes = coverageExcludes;
return this;
}
/**
* Sets the coverage report file to generate
*
* @param coverageReportFile
* the report file
* @return the current builder
*/
public GallioCommandBuilder setCoverageReportFile(File coverageReportFile) {
this.coverageReportFile = coverageReportFile;
return this;
}
/**
* Transforms this command object into a Command object that can be passed to the CommandExecutor.
*
* @return the Command object that represents the command to launch.
*/
public Command toCommand() throws GallioException {
List<File> testAssemblies = findTestAssemblies();
validateGallioInfo(testAssemblies);
Command command = createCommand();
List<String> gallioArguments = generateGallioArguments(testAssemblies);
if (CoverageTool.PARTCOVER.equals(coverageTool)) {
addPartCoverArguments(command, gallioArguments);
} else if (CoverageTool.NCOVER.equals(coverageTool)) {
addNCoverArguments(command, gallioArguments);
} else {
command.addArguments(gallioArguments);
}
return command;
}
protected Command createCommand() throws GallioException {
Command command = null;
LOG.debug("- Gallio executable : " + gallioExecutable);
if (CoverageTool.PARTCOVER.equals(coverageTool)) {
// In case of PartCover, the executable is not Gallio but PartCover itself
File partCoverExecutable = new File(partCoverInstallDirectory, PART_COVER_EXE);
validatePartCoverInfo(partCoverExecutable);
LOG.debug("- PartCover executable: " + partCoverExecutable);
command = Command.create(partCoverExecutable.getAbsolutePath());
} else {
command = Command.create(gallioExecutable.getAbsolutePath());
}
command.setDirectory(workDir);
return command;
}
protected List<String> generateGallioArguments(List<File> testAssemblies) {
List<String> gallioArguments = Lists.newArrayList();
String runner = DEFAULT_GALLIO_RUNNER;
if (coverageTool != null) {
LOG.debug("- Coverage tool : {}", coverageTool.getName());
runner = coverageTool.getGallioRunner();
}
LOG.debug("- Runner : {}", runner);
gallioArguments.add("/r:" + runner);
File reportDirectory = gallioReportFile.getParentFile();
LOG.debug("- Report directory : {}", reportDirectory.getAbsolutePath());
gallioArguments.add("/report-directory:" + reportDirectory.getAbsolutePath());
String reportName = trimFileReportName();
LOG.debug("- Report file : {}", reportName);
gallioArguments.add("/report-name-format:" + reportName);
gallioArguments.add("/report-type:Xml");
if (StringUtils.isNotEmpty(filter)) {
LOG.debug("- Filter : {}", filter);
gallioArguments.add("/f:" + filter);
}
LOG.debug("- Test assemblies :");
for (File testAssembly : testAssemblies) {
LOG.debug(" o {}", testAssembly);
gallioArguments.add(testAssembly.getAbsolutePath());
}
return gallioArguments;
}
protected void addPartCoverArguments(Command command, List<String> gallioArguments) {
// DEBUG info has already been printed out for "--target"
command.addArgument("--target");
command.addArgument(gallioExecutable.getAbsolutePath());
LOG.debug("- Working directory : {}", workDir.getAbsolutePath());
command.addArgument("--target-work-dir");
command.addArgument(workDir.getAbsolutePath());
// DEBUG info has already been printed out for "--target-args"
command.addArgument("--target-args");
command.addArgument(escapeGallioArguments(gallioArguments));
// We add all the covered assemblies
for (String assemblyName : listCoveredAssemblies()) {
LOG.debug("- Partcover include : [{}]*", assemblyName);
command.addArgument("--include");
command.addArgument("[" + assemblyName + "]*");
}
// We add all the configured exclusions
if ( !StringUtils.isEmpty(coverageExcludes)) {
for (String exclusion : StringUtils.split(coverageExcludes, ",")) {
LOG.debug("- Partcover exclude : {}", exclusion.trim());
command.addArgument("--exclude");
command.addArgument(exclusion.trim());
}
}
LOG.debug("- Coverage report : {}", coverageReportFile.getAbsolutePath());
command.addArgument("--output");
command.addArgument(coverageReportFile.getAbsolutePath());
}
private void addNCoverArguments(Command command, List<String> gallioArguments) {
command.addArguments(gallioArguments);
LOG.debug("- Coverage report : {}", coverageReportFile.getAbsolutePath());
command.addArgument("/runner-property:NCoverCoverageFile=" + coverageReportFile.getAbsolutePath());
String coveredAssemblies = StringUtils.join(listCoveredAssemblies().toArray(), ";");
LOG.debug("- NCover arguments : {}", coveredAssemblies);
command.addArgument("/runner-property:NCoverArguments=//ias " + coveredAssemblies);
}
protected List<String> listCoveredAssemblies() {
List<String> coveredAssemblyNames = new ArrayList<String>();
for (VisualStudioProject visualProject : solution.getProjects()) {
if ( !visualProject.isTest()) {
coveredAssemblyNames.add(visualProject.getAssemblyName());
}
}
return coveredAssemblyNames;
}
// TODO : try to refactor this
protected String escapeGallioArguments(List<String> gallioArguments) {
StringBuilder targetArgsBuilder = new StringBuilder();
boolean isFirst = true;
for (String currentArg : gallioArguments) {
if (isFirst) {
isFirst = false;
} else {
targetArgsBuilder.append(' ');
}
String escapedArg = escapeQuotes(currentArg);
targetArgsBuilder.append(escapedArg);
}
return targetArgsBuilder.toString();
}
/*
* Escapes the quotes of a string. TODO : try to refactor this
*/
protected String escapeQuotes(String input) {
StringBuilder result = new StringBuilder(input.length());
for (int idxChar = 0, len = input.length(); idxChar < len; idxChar++) {
char currentChar = input.charAt(idxChar);
if (currentChar == '"') {
result.append('\\');
} else if (idxChar == 0) {
result.append("\\\"");
}
result.append(currentChar);
if (currentChar != '"' && idxChar == len - 1) {
result.append("\\\"");
}
}
return result.toString();
}
protected String trimFileReportName() {
String reportName = gallioReportFile.getName();
if (StringUtils.endsWithIgnoreCase(reportName, ".xml")) {
// We remove the terminal .xml that will be added by the Gallio runner
reportName = reportName.substring(0, reportName.length() - 4);
}
return reportName;
}
protected List<File> findTestAssemblies() throws GallioException {
List<File> assemblyFileList = Lists.newArrayList();
if (solution != null) {
for (VisualStudioProject visualStudioProject : solution.getTestProjects()) {
addAssembly(assemblyFileList, visualStudioProject);
}
} else {
throw new GallioException("No .NET solution or project has been given to the Gallio command builder.");
}
return assemblyFileList;
}
protected void addAssembly(List<File> assemblyFileList, VisualStudioProject visualStudioProject) {
File assembly = visualStudioProject.getArtifact(buildConfigurations);
if (assembly != null && assembly.isFile()) {
assemblyFileList.add(assembly);
}
}
protected void validateGallioInfo(List<File> testAssemblies) throws GallioException {
if (gallioExecutable == null || !gallioExecutable.isFile()) {
throw new GallioException("Gallio executable cannot be found at the following location:" + gallioExecutable);
}
if (gallioReportFile == null) {
throw new GallioException("Gallio report file has not been specified.");
}
if (workDir == null || !workDir.isDirectory()) {
throw new GallioException("The working directory cannot be found at the following location:" + workDir);
}
if (testAssemblies.isEmpty()) {
throw new GallioException("No test assembly was found. Please check your project's Gallio plugin configuration.");
}
}
protected void validatePartCoverInfo(File partCoverExecutable) throws GallioException {
if (partCoverExecutable == null || !partCoverExecutable.isFile()) {
throw new GallioException("PartCover executable cannot be found at the following location:" + partCoverExecutable);
}
if (coverageReportFile == null) {
throw new GallioException("Gallio coverage report file has not been specified.");
}
}
}