diff --git a/Tasks/ANT/anttask.ts b/Tasks/ANT/anttask.ts index e9638ff74332..bb0befd973a1 100644 --- a/Tasks/ANT/anttask.ts +++ b/Tasks/ANT/anttask.ts @@ -4,6 +4,8 @@ import tl = require('vsts-task-lib/task'); import path = require('path'); import fs = require('fs'); import os = require('os'); +import * as Q from "q"; +import {CodeCoverageEnablerFactory} from 'codecoverage-tools/codecoveragefactory'; var isWindows = os.type().match(/^Win/); @@ -78,25 +80,45 @@ function processAntOutputLine(line) { async function doWork() { - function enableCodeCoverage() { + function execEnableCodeCoverage(): Q.Promise<string> { + return enableCodeCoverage() + .then(function (resp) { + tl.debug("Enabled code coverage successfully"); + return "CodeCoverage_9064e1d0"; + }).catch(function (err) { + tl.warning("Failed to enable code coverage: " + err); + return ""; + }); + }; + + function enableCodeCoverage(): Q.Promise<any> { + if (!isCodeCoverageOpted) { + return Q.resolve(true); + } + var classFilter: string = tl.getInput('classFilter'); var classFilesDirectories: string = tl.getInput('classFilesDirectories', true); var sourceDirectories: string = tl.getInput('srcDirectories'); // appending with small guid to keep it unique. Avoiding full guid to ensure no long path issues. var reportDirectoryName = "CCReport43F6D5EF"; reportDirectory = path.join(buildRootPath, reportDirectoryName); - ccReportTask = "CodeCoverage_9064e1d0"; var reportBuildFileName = "CCReportBuildA4D283EG.xml"; reportBuildFile = path.join(buildRootPath, reportBuildFileName); var summaryFileName = "coverage.xml"; summaryFile = path.join(buildRootPath, reportDirectoryName); summaryFile = path.join(summaryFile, summaryFileName); var coberturaCCFile = path.join(buildRootPath, "cobertura.ser"); + var instrumentedClassesDirectory = path.join(buildRootPath, "InstrumentedClasses"); // clean any previous reports. - tl.rmRF(coberturaCCFile, true); - tl.rmRF(reportDirectory, true); - tl.rmRF(reportBuildFile, true); + try { + tl.rmRF(coberturaCCFile, true); + tl.rmRF(reportDirectory, true); + tl.rmRF(reportBuildFile, true); + tl.rmRF(instrumentedClassesDirectory, true); + } catch (err) { + tl.debug("Error removing previous cc files: " + err); + } var buildProps: { [key: string]: string } = {}; buildProps['buildfile'] = antBuildFile; @@ -105,20 +127,15 @@ async function doWork() { buildProps['sourcedirectories'] = sourceDirectories; buildProps['summaryfile'] = summaryFileName; buildProps['reportdirectory'] = reportDirectory; - buildProps['ccreporttask'] = ccReportTask + buildProps['ccreporttask'] = "CodeCoverage_9064e1d0" buildProps['reportbuildfile'] = reportBuildFile; - try { - var codeCoverageEnabler = new tl.CodeCoverageEnabler('Ant', ccTool); - codeCoverageEnabler.enableCodeCoverage(buildProps); - tl.debug("Code coverage is successfully enabled."); - } - catch (Error) { - tl.warning("Enabling code coverage failed. Check the build logs for errors."); - } + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("ant", ccTool.toLowerCase()); + return ccEnabler.enableCodeCoverage(buildProps); } - function publishCodeCoverage(codeCoverageOpted: boolean) { - if (codeCoverageOpted) { + function publishCodeCoverage(codeCoverageOpted: boolean, ccReportTask: string) { + if (codeCoverageOpted && ccReportTask) { tl.debug("Collecting code coverage reports"); var antRunner = tl.tool(anttool); antRunner.arg('-buildfile'); @@ -208,39 +225,26 @@ async function doWork() { var ccTool = tl.getInput('codeCoverageTool'); var isCodeCoverageOpted = (typeof ccTool != "undefined" && ccTool && ccTool.toLowerCase() != 'none'); - var buildRootPath = path.dirname(antBuildFile); - var instrumentedClassesDirectory = path.join(buildRootPath, "InstrumentedClasses"); - //delete any previous cobertura instrumented classes as they might interfere with ant execution. - tl.rmRF(instrumentedClassesDirectory, true); - - if (isCodeCoverageOpted) { - var summaryFile: string = null; - var reportDirectory: string = null; - var ccReportTask: string = null; - var reportBuildFile: string = null; - enableCodeCoverage(); - } - else { - tl.debug("Option to enable code coverage was not selected and is being skipped."); - } - + + var summaryFile: string = null; + var reportDirectory: string = null; + var ccReportTask: string = null; + var reportBuildFile: string = null; var publishJUnitResults = tl.getInput('publishJUnitResults'); var testResultsFiles = tl.getInput('testResultsFiles', true); - await antv.exec(); + ccReportTask = await execEnableCodeCoverage(); + await antv.exec(); var buffer; antb.on('stdout', (data) => { if (data) { buffer += data.toString(); - let idx = buffer.indexOf(os.EOL); while (idx > -1) { let line = buffer.substring(0, idx); - processAntOutputLine(line); - buffer = buffer.substring(idx + os.EOL.length); idx = buffer.indexOf(os.EOL); } @@ -250,11 +254,10 @@ async function doWork() { antb.exec() .then(function (code) { publishTestResults(publishJUnitResults, testResultsFiles); - publishCodeCoverage(isCodeCoverageOpted); + publishCodeCoverage(isCodeCoverageOpted, ccReportTask); tl.setResult(tl.TaskResult.Succeeded, "Task succeeded"); }) .fail(function (err) { - publishTestResults(publishJUnitResults, testResultsFiles); console.error(err.message); tl.debug('taskRunner fail'); tl.setResult(tl.TaskResult.Failed, err); @@ -264,7 +267,6 @@ async function doWork() { tl._writeError(e); tl.setResult(tl.TaskResult.Failed, e.message); } - } doWork(); diff --git a/Tasks/ANT/package.json b/Tasks/ANT/package.json index 4d02da0f4c94..5239b0521415 100644 --- a/Tasks/ANT/package.json +++ b/Tasks/ANT/package.json @@ -7,6 +7,7 @@ "url": "https://github.com/Microsoft/vso-agent-tasks/issues" }, "dependencies": { + "xml2js": "^0.4.16", "vsts-task-lib": "0.9.7" } } diff --git a/Tasks/ANT/task.json b/Tasks/ANT/task.json index 483d3ce69cc2..5238c0035292 100644 --- a/Tasks/ANT/task.json +++ b/Tasks/ANT/task.json @@ -13,7 +13,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 51 + "Patch": 54 }, "demands": [ "ant" diff --git a/Tasks/ANT/task.loc.json b/Tasks/ANT/task.loc.json index 1d8622a08f42..e0f7d90fc2c8 100644 --- a/Tasks/ANT/task.loc.json +++ b/Tasks/ANT/task.loc.json @@ -13,7 +13,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 51 + "Patch": 54 }, "demands": [ "ant" diff --git a/Tasks/Common/codecoverage-tools/Strings/resources.resjson/en-US/resources.resjson b/Tasks/Common/codecoverage-tools/Strings/resources.resjson/en-US/resources.resjson new file mode 100644 index 000000000000..1ad61bc14d8a --- /dev/null +++ b/Tasks/Common/codecoverage-tools/Strings/resources.resjson/en-US/resources.resjson @@ -0,0 +1,5 @@ +{ + "loc.messages.InvalidBuildFile": "Invalid or unsupported build file", + "loc.messages.FileNotFound": "File or folder doesn't exist: %s", + "loc.messages.FailedToAppendCC": "Unable to append code coverage data: %s" +} \ No newline at end of file diff --git a/Tasks/Common/codecoverage-tools/cobertura/cobertura.ant.ccenabler.ts b/Tasks/Common/codecoverage-tools/cobertura/cobertura.ant.ccenabler.ts new file mode 100644 index 000000000000..68d01f3e0cd7 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/cobertura/cobertura.ant.ccenabler.ts @@ -0,0 +1,190 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as Q from "q"; +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as path from "path"; + +export class CoberturaAntCodeCoverageEnabler extends cc.CoberturaCodeCoverageEnabler { + + reportDir: string; + reportbuildfile: string; + classDirs: string; + includeFilter: string; + excludeFilter: string; + sourceDirs: string; + + // ----------------------------------------------------- + // Enable code coverage for Cobertura Ant Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.reportDir = "CCReport43F6D5EF"; + _this.reportbuildfile = "CCReportBuildA4D283EG.xml"; + _this.buildFile = ccProps["buildfile"]; + let classFilter = ccProps["classfilter"]; + let srcDirs = ccProps["sourcedirectories"]; + if (str(srcDirs).isEmpty()) { + srcDirs = "."; + } + _this.sourceDirs = srcDirs; + _this.classDirs = ccProps["classfilesdirectories"]; + + let filter = _this.extractFilters(classFilter); + _this.excludeFilter = _this.applyFilterPattern(filter.excludeFilter).join(","); + _this.includeFilter = _this.applyFilterPattern(filter.includeFilter).join(","); + + tl.debug("Reading the build file: " + _this.buildFile); + + return util.readXmlFileAsJson(_this.buildFile) + .then(function (resp) { + return _this.addCodeCoverageData(resp); + }) + .thenResolve(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + str(util.trimToEmptyString(filter)).replaceAll(".", "/").s.split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? ("**/" + exFilter + "/**") : ("**/" + exFilter + ".class")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } + + protected getClassData(): any { + let _this = this; + let fileset = []; + let classDirs = _this.classDirs; + + if (str(classDirs).isEmpty()) { + classDirs = "."; + } + classDirs.split(",").forEach(cdir => { + let filter = { + $: { + dir: cdir, + includes: _this.includeFilter, + excludes: _this.excludeFilter + } + }; + fileset.push(filter); + }); + return fileset; + } + + protected createReportFile(reportContent: string): Q.Promise<void> { + let _this = this; + tl.debug("Creating the report file: " + _this.reportbuildfile); + + let reportFile = path.join(path.dirname(_this.buildFile), _this.reportbuildfile); + return util.writeFile(reportFile, reportContent); + } + + protected addCodeCoverageData(pomJson: any): Q.Promise<any[]> { + let _this = this; + + if (!pomJson || !pomJson.project) { + return Q.reject<any>(tl.loc("InvalidBuildFile")); + } + + let reportPluginData = ccc.coberturaAntReport(_this.sourceDirs, path.join(path.dirname(_this.buildFile), _this.reportDir)); + return Q.all([_this.addCodeCoverageNodes(pomJson), _this.createReportFile(reportPluginData)]); + } + + protected addCodeCoverageNodes(buildJsonContent: any): Q.Promise<any> { + let _this = this; + + if (!buildJsonContent.project.target) { + tl.debug("Build tag is not present"); + return Q.reject(tl.loc("InvalidBuildFile")); + } + + ccc.coberturaAntCoverageEnable(buildJsonContent.project); + + if (!buildJsonContent.project.target || typeof buildJsonContent.project.target === "string") { + buildJsonContent.project.target = {}; + } + + if (buildJsonContent.project.target instanceof Array) { + buildJsonContent.project.target.forEach(element => { + _this.enableForking(element); + }); + } else { + _this.enableForking(buildJsonContent.project.target); + } + return util.writeJsonAsXmlFile(_this.buildFile, buildJsonContent); + } + + protected enableForking(targetNode: any) { + let _this = this; + let coberturaNode = ccc.coberturaAntInstrumentedClasses(path.dirname(_this.buildFile), _this.reportDir); + coberturaNode.fileset = _this.getClassData(); + let testNodes = ["junit", "java", "testng", "batchtest"]; + + if (targetNode.javac) { + if (targetNode.javac instanceof Array) { + targetNode.javac.forEach(jn => { + jn.$.debug = "true"; + }); + } + } + + testNodes.forEach(tn => { + if (!targetNode[tn]) { + return; + } + + let node = targetNode[tn]; + _this.enableForkOnTestNodes(node, true); + if (node instanceof Array) { + node.forEach(n => { + ccc.coberturaAntProperties(n, _this.reportDir, path.dirname(_this.buildFile)); + }); + } else { + ccc.coberturaAntProperties(node, _this.reportDir, path.dirname(_this.buildFile)); + } + + targetNode["cobertura-instrument"] = coberturaNode; + }); + } + + protected enableForkOnTestNodes(testNode: any, enableForkMode: boolean) { + if (testNode instanceof Array) { + testNode.forEach(element => { + if (!element.$) { + element.$ = {}; + } + if (enableForkMode) { + element.$.forkmode = "once"; + } + element.$.fork = "true"; + + }); + } else { + if (!testNode.$) { + testNode.$ = {}; + } + if (enableForkMode) { + testNode.$.forkmode = "once"; + } + testNode.$.fork = "true"; + } + } +} diff --git a/Tasks/Common/codecoverage-tools/cobertura/cobertura.gradle.ccenabler.ts b/Tasks/Common/codecoverage-tools/cobertura/cobertura.gradle.ccenabler.ts new file mode 100644 index 000000000000..259310c2e754 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/cobertura/cobertura.gradle.ccenabler.ts @@ -0,0 +1,64 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as Q from "q"; + +export class CoberturaGradleCodeCoverageEnabler extends cc.CoberturaCodeCoverageEnabler { + // ----------------------------------------------------- + // Enable code coverage for Cobertura Gradle Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.buildFile = ccProps["buildfile"]; + let classFilter = ccProps["classfilter"]; + let isMultiModule = ccProps["ismultimodule"] && ccProps["ismultimodule"] === "true"; + let classFileDirs = ccProps["classfilesdirectories"]; + let reportDir = ccProps["reportdirectory"]; + let codeCoveragePluginData = null; + + let filter = _this.extractFilters(classFilter); + let cobExclude = _this.applyFilterPattern(filter.excludeFilter); + let cobInclude = _this.applyFilterPattern(filter.includeFilter); + + if (isMultiModule) { + codeCoveragePluginData = ccc.coberturaGradleMultiModuleEnable(cobExclude.join(","), cobInclude.join(","), classFileDirs, null, reportDir); + } else { + codeCoveragePluginData = ccc.coberturaGradleSingleModuleEnable(cobExclude.join(","), cobInclude.join(","), classFileDirs, null, reportDir); + } + + try { + tl.debug("Code Coverage data will be appeneded to build file: " + _this.buildFile); + util.insertTextToFileSync(_this.buildFile, ccc.coberturaGradleBuildScript, codeCoveragePluginData); + tl.debug("Appended code coverage data"); + } catch (error) { + return Q.reject<boolean>(error); + } + return Q.resolve(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + util.trimToEmptyString(filter).split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? ("'.*" + util.trimEnd(exFilter, "*") + ".*'") : ("'.*" + exFilter + "'")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } +} diff --git a/Tasks/Common/codecoverage-tools/cobertura/cobertura.maven.ccenabler.ts b/Tasks/Common/codecoverage-tools/cobertura/cobertura.maven.ccenabler.ts new file mode 100644 index 000000000000..70941d1f33c4 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/cobertura/cobertura.maven.ccenabler.ts @@ -0,0 +1,143 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as Q from "q"; + +export class CoberturaMavenCodeCoverageEnabler extends cc.CoberturaCodeCoverageEnabler { + + protected includeFilter: string; + protected excludeFilter: string; + // ----------------------------------------------------- + // Enable code coverage for Cobertura Maven Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.buildFile = ccProps["buildfile"]; + let classFilter = ccProps["classfilter"]; + + let filter = _this.extractFilters(classFilter); + _this.excludeFilter = _this.applyFilterPattern(filter.excludeFilter).join(","); + _this.includeFilter = _this.applyFilterPattern(filter.includeFilter).join(","); + + return util.readXmlFileAsJson(_this.buildFile) + .then(function (resp) { + tl.debug("Read XML: " + resp); + return _this.addCodeCoveragePluginData(resp); + }) + .thenResolve(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + str(util.trimToEmptyString(filter)).replaceAll(".", "/").s.split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? (exFilter + "/**") : (exFilter + ".class")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } + + protected addCodeCoverageNodes(buildJsonContent: any): Q.Promise<any> { + let _this = this; + let isMultiModule = false; + + if (!buildJsonContent.project) { + return Q.reject(tl.loc("InvalidBuildFile")); + } + + if (buildJsonContent.project.modules) { + tl.debug("Multimodule project detected"); + isMultiModule = true; + } + + if (!buildJsonContent.project.build) { + tl.debug("Build tag is not present"); + buildJsonContent.project.build = {}; + } + + let buildNode = _this.getBuildDataNode(buildJsonContent); + let pluginsNode = _this.getPluginDataNode(buildNode); + let ccPluginData = ccc.coberturaMavenEnable(_this.includeFilter, _this.excludeFilter, String(isMultiModule)); + let reportContent = ccc.coberturaMavenReport(); + + return Q.allSettled([ccPluginData, reportContent]) + .then(function (resp) { + util.addPropToJson(pluginsNode, "plugin", resp[0].value.plugin); + util.addPropToJson(buildJsonContent.project.reporting, "plugins", resp[1].value); + tl.debug("Final buildContent: " + buildJsonContent); + return Q.resolve(buildJsonContent); + }); + } + + private getBuildDataNode(buildJsonContent: any): any { + let buildNode = null; + if (!buildJsonContent.project.build || typeof buildJsonContent.project.build === "string") { + buildNode = {}; + buildJsonContent.project.build = buildNode; + } + + if (buildJsonContent.project.build instanceof Array) { + if (typeof buildJsonContent.project.build[0] === "string") { + buildNode = {}; + buildJsonContent.project.build[0] = buildNode; + } else { + buildNode = buildJsonContent.project.build[0]; + } + } + return buildNode; + } + + private getPluginDataNode(buildNode: any): any { + let pluginsNode = {}; + if (buildNode.pluginManagement) { + if (typeof buildNode.pluginManagement === "string") { + buildNode.pluginManagement = {}; + } + if (buildNode.pluginManagement instanceof Array) { + pluginsNode = buildNode.pluginManagement[0].plugins; + } else { + pluginsNode = buildNode.pluginManagement.plugins; + } + } else { + if (!buildNode.plugins || typeof buildNode.plugins === "string") { + buildNode.plugins = {}; + } + if (buildNode.plugins instanceof Array) { + if (typeof buildNode.plugins[0] === "string") { + pluginsNode = {}; + buildNode.plugins[0] = pluginsNode; + } else { + pluginsNode = buildNode.plugins[0]; + } + } else { + pluginsNode = buildNode.plugins; + } + } + return pluginsNode; + } + + protected addCodeCoveragePluginData(pomJson: any): Q.Promise<void> { + let _this = this; + tl.debug("Adding coverage plugin data"); + return _this.addCodeCoverageNodes(pomJson) + .then(function (buildContent) { + return util.writeJsonAsXmlFile(_this.buildFile, buildContent); + }); + } +} diff --git a/Tasks/Common/codecoverage-tools/codecoverageconstants.ts b/Tasks/Common/codecoverage-tools/codecoverageconstants.ts new file mode 100644 index 000000000000..72fc3804c165 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/codecoverageconstants.ts @@ -0,0 +1,513 @@ +/// <reference path="../../../definitions/node.d.ts" /> +/// <reference path="../../../definitions/string.d.ts" /> + +import * as path from "path"; +import * as util from "./utilities"; +import * as os from "os"; +import * as str from "string"; + + +// Enable Jacoco Code Coverage for Gradle builds using this props +export function jacocoGradleMultiModuleEnable(excludeFilter: string, includeFilter: string, classFileDirectory: string, reportDir: string) { + return ` +allprojects { + repositories { + mavenCentral() + } + + apply plugin: 'jacoco' +} + +def jacocoExcludes = [${excludeFilter}] +def jacocoIncludes = [${includeFilter}] + +subprojects { + jacocoTestReport { + doFirst { + classDirectories = fileTree(dir: "${classFileDirectory}").exclude(jacocoExcludes).include(jacocoIncludes) + } + + reports { + html.enabled = true + html.destination "\${buildDir}/jacocoHtml" + xml.enabled = true + xml.destination "\${buildDir}/summary.xml" + } + } + test { + jacoco { + append = true + destinationFile = file("${reportDir}/jacoco.exec") + } + } +} + +task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { + dependsOn = subprojects.test + executionData = files(subprojects.jacocoTestReport.executionData) + sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) + classDirectories = files() + + doFirst { + subprojects.each { + if (new File("\${it.sourceSets.main.output.classesDir}").exists()) { + logger.info("Class directory exists in sub project: \${it.name}") + logger.info("Adding class files \${it.sourceSets.main.output.classesDir}") + classDirectories += fileTree(dir: "\${it.sourceSets.main.output.classesDir}", includes: jacocoIncludes, excludes: jacocoExcludes) + } else { + logger.error("Class directory does not exist in sub project: \${it.name}") + } + } + } + + reports { + html.enabled = true + xml.enabled = true + xml.destination "${reportDir}/summary.xml" + html.destination "${reportDir}/" + } +}`; +} + +export function jacocoGradleSingleModuleEnable(excludeFilter: string, includeFilter: string, classFileDirectory: string, reportDir: string) { + return ` +allprojects { + repositories { + mavenCentral() + } + + apply plugin: 'jacoco' +} + +def jacocoExcludes = [${excludeFilter}] +def jacocoIncludes = [${includeFilter}] + +jacocoTestReport { + doFirst { + classDirectories = fileTree(dir: "${classFileDirectory}").exclude(jacocoExcludes).include(jacocoIncludes) + } + + reports { + html.enabled = true + xml.enabled = true + xml.destination "${reportDir}/summary.xml" + html.destination "${reportDir}" + } +} + +test { + finalizedBy jacocoTestReport + jacoco { + append = true + destinationFile = file("${reportDir}/jacoco.exec") + } +}`; +} + + +// Enable Cobertura Code Coverage for Gradle builds using this props +export function coberturaGradleSingleModuleEnable(excludeFilter: string, includeFilter: string, classDir: string, sourceDir: string, reportDir: string) { + if (!classDir) { + classDir = "${project.sourceSets.main.output.classesDir}"; + } + if (!sourceDir) { + sourceDir = "project.sourceSets.main.java.srcDirs"; + } + + return ` +allprojects { + repositories { + mavenCentral() + } + apply plugin: 'net.saliman.cobertura' + + dependencies { + testCompile 'org.slf4j:slf4j-api:1.7.12' + } + + cobertura.coverageIncludes = [${includeFilter}] + cobertura.coverageExcludes = [${excludeFilter}] +} + +cobertura { + coverageDirs = ["${classDir}"] + coverageSourceDirs = ${sourceDir} + coverageReportDir = new File('${reportDir}') + coverageFormats = ['xml', 'html'] +}`; +} +export function coberturaGradleMultiModuleEnable(excludeFilter: string, includeFilter: string, classDir: string, sourceDir: string, reportDir: string) { + let data = ` +allprojects { + repositories { + mavenCentral() + } + apply plugin: 'net.saliman.cobertura' + + dependencies { + testCompile 'org.slf4j:slf4j-api:1.7.12' + } + + cobertura.coverageIncludes = [${includeFilter}] + cobertura.coverageExcludes = [${excludeFilter}] +} + +test { + dependsOn = subprojects.test +} + +cobertura { + coverageSourceDirs = []`; + + if (classDir) { + data += ` + coverageDirs = ["${classDir}"]`; + } else { + data += ` + rootProject.subprojects.each { + coverageDirs << file("\${it.sourceSets.main.output.classesDir}") + }`; + } + + if (sourceDir) { + data += ` + coverageDirs = ["${sourceDir}"]`; + } else { + data += ` + rootProject.subprojects.each { + coverageSourceDirs += it.sourceSets.main.java.srcDirs + }`; + } + data += ` + coverageFormats = [ 'xml', 'html' ] + coverageMergeDatafiles = subprojects.collect { new File(it.projectDir, '/build/cobertura/cobertura.ser') } + coverageReportDir = new File('${reportDir}') +}`; + + return data; +}; + +// Enable Jacoco Code Coverage for Maven builds using this props +export function jacocoMavenPluginEnable(includeFilter: string[], excludeFilter: string[], outputDirectory: string): any { + let plugin = { + "groupId": "org.jacoco", + "artifactId": "jacoco-maven-plugin", + "version": "0.7.5.201505241946", + "configuration": { + "destFile": path.join(outputDirectory, "jacoco.exec"), + "outputDirectory": outputDirectory, + "dataFile": path.join(outputDirectory, "jacoco.exec"), + "append": "true", + "includes": [{ + "include": includeFilter, + }], + "excludes": [{ + "exclude": excludeFilter + }] + }, + "executions": { + "execution": [ + { + "configuration": + { + "includes": [{ + "include": "**/*", + }] + }, + "id": "default-prepare-agent-vsts", + "goals": { "goal": "prepare-agent" } + }, + { + "id": "default-report-vsts", + "goals": { "goal": "report" }, + "phase": "test" + } + ] + } + }; + + return plugin; +}; +export function jacocoMavenMultiModuleReport(reportDir: string, srcData: string, classData: string, includeFilter: string, excludeFilter: string): string { + let classNode = ""; + classData.split(",").forEach(c => { + classNode += `<fileset dir="${c}"`; + if (includeFilter) { + classNode += ` includes="${includeFilter}"`; + } + if (excludeFilter) { + classNode += ` excludes="${excludeFilter}"`; + } + classNode += ` />` + os.EOL; + }); + let srcNode = ""; + if (str(srcData).isEmpty()) { + srcNode = `<fileset dir="." />`; + } else { + srcData.split(",").forEach(c => { + srcNode += `<fileset dir="${c}" />` + os.EOL; + }); + } + + let report = `<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>VstsReport</groupId> + <artifactId>VstsReport</artifactId> + <version>0.0.1-SNAPSHOT</version> + <packaging>pom</packaging> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-antrun-plugin</artifactId> + <version>1.8</version> + <executions> + <execution> + <phase>post-integration-test</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <target> + <echo message="Generating JaCoCo Reports" /> + <taskdef name="report" classname="org.jacoco.ant.ReportTask"> + <classpath path="{basedir}/target/jacoco-jars/org.jacoco.ant.jar" /> + </taskdef> + <report> + <executiondata> + <file file="${path.join(reportDir, "jacoco.exec")}" /> + </executiondata> + <structure name="Jacoco report"> + <classfiles> + ${classNode} + </classfiles> + <sourcefiles encoding="UTF-8"> + ${srcNode} + </sourcefiles> + </structure> + <html destdir="${reportDir}" /> + <xml destfile="${reportDir + path.sep}jacoco.xml" /> + <csv destfile="${reportDir + path.sep}report.csv" /> + </report> + </target> + </configuration> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.jacoco</groupId> + <artifactId>org.jacoco.ant</artifactId> + <version>0.7.5.201505241946</version> + </dependency> + </dependencies> + </plugin> + </plugins> + </build> +</project> + `; + + return report; +}; + +// Enable Cobertura Code Coverage for Maven builds using this props +export function coberturaMavenEnable(includeFilter: string, excludeFilter: string, aggregate: string): Q.Promise<any> { + let includeTag = ""; + let excludeTag = ""; + if (!str(excludeFilter).isEmpty()) { + excludeFilter.split(",").forEach(ex => { + excludeTag += `<exclude>${ex}</exclude>` + os.EOL; + }); + } + if (!str(includeFilter).isEmpty()) { + includeFilter.split(",").forEach(ex => { + includeTag += `<include>${ex}</include>` + os.EOL; + }); + } + + let ccProperty = ` + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>cobertura-maven-plugin</artifactId> + <version>2.7</version> + <configuration> + <formats> + <format>xml</format> + <format>html</format> + </formats> + <instrumentation> + <includes>${includeTag}</includes> + <excludes>${excludeTag}</excludes> + </instrumentation> + <aggregate>${aggregate}</aggregate> + </configuration> + <executions> + <execution> + <id>package-9af52907-6506-4b87-b16a-9883edee41bc</id> + <goals> + <goal>cobertura</goal> + </goals> + <phase>package</phase> + </execution> + </executions> + </plugin> + `; + return util.convertXmlStringToJson(ccProperty); +}; + +export function coberturaMavenReport(): Q.Promise<any> { + let ccProperty = ` + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>cobertura-maven-plugin</artifactId> + <version>2.7</version> + <configuration> + <formats> + <format>xml</format> + <format>html</format> + </formats> + </configuration> + </plugin> + `; + return util.convertXmlStringToJson(ccProperty); +} + +export function jacocoAntReport(reportDir: string, classData: string, sourceData: string): string { + return ` + <?xml version='1.0'?> + <project name='JacocoReport'> + <target name='CodeCoverage_9064e1d0'> + <jacoco:report xmlns:jacoco='antlib:org.jacoco.ant'> + <executiondata> + <file file='${path.join(reportDir, "jacoco.exec")}'/> + </executiondata> + <structure name = 'Jacoco report'> + <classfiles>${classData}</classfiles> + <sourcefiles>${sourceData}</sourcefiles> + </structure> + <html destdir='${reportDir}' /> + <csv destfile='${reportDir + path.sep}summary.csv' /> + <xml destfile='${reportDir + path.sep}summary.xml' /> + </jacoco:report> + </target> + </project> + `; +} + +export function jacocoAntCoverageEnable(): any { + return { + $: + { + "destfile": "jacoco.exec", + "append": true, + "xlmns:jacoco": "antlib:org.jacoco.ant" + } + }; +} + +export function coberturaAntReport(srcDir: string, reportDir: string): string { + return `<?xml version="1.0"?> +<project name="CoberturaReport"> + <property environment="env" /> + <path id="cobertura-classpath" description="classpath for instrumenting classes"> + <fileset dir="\${env.COBERTURA_HOME}"> + <include name="cobertura*.jar" /> + <include name="**/lib/**/*.jar" /> + </fileset> + </path> + <taskdef classpathref="cobertura-classpath" resource="tasks.properties" /> + <target name="CodeCoverage_9064e1d0"> + <cobertura-report format="html" destdir="${reportDir}" datafile="${reportDir + path.sep}cobertura.ser" srcdir="${srcDir}" /> + <cobertura-report format="xml" destdir="${reportDir}" datafile="${reportDir + path.sep}cobertura.ser" srcdir="${srcDir}" /> + </target> +</project> + `; +} + +export function coberturaAntCoverageEnable(buildJsonContent: any): void { + let propertyNode = { + $: { + environment: "env" + } + }; + util.addPropToJson(buildJsonContent, "property", propertyNode); + + let pathNode = { + $: { + id: "cobertura-classpath", + description: "classpath for instrumenting classes" + }, + fileset: { + $: { + dir: "${env.COBERTURA_HOME}" + }, + include: [ + { + $: { + name: "cobertura*.jar" + } + }, + { + $: { + name: "**/lib/**/*.jar" + } + } + ] + } + }; + util.addPropToJson(buildJsonContent, "path", pathNode); + + let taskdefNode = { + $: { + classpathref: "cobertura-classpath", + resource: "tasks.properties" + } + }; + util.addPropToJson(buildJsonContent, "taskdef", taskdefNode); +} + +export function coberturaAntInstrumentedClasses(baseDir: string, reportDir: string): any { + let ccProperty = { + $: { + todir: path.join(baseDir, "InstrumentedClasses"), + datafile: path.join(baseDir, reportDir, "cobertura.ser") + }, + fileset: [] + }; + return ccProperty; +} + +export function coberturaAntProperties(node: any, reportDir: string, baseDir: string): any { + node.sysproperty = { + $: { + key: "net.sourceforge.cobertura.datafile", + file: path.join(baseDir, reportDir, "cobertura.ser") + } + }; + + let classpath = [ + { + $: { + location: path.join(baseDir, "InstrumentedClasses"), + } + } + ]; + + if (node.classpath && node.classpath instanceof Array) { + node.classpath = classpath.concat(node.classpath); + } else { + node.classpath = classpath; + } +} + +// Gradle Coberutra plugin +export const coberturaGradleBuildScript = ` +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'net.saliman:gradle-cobertura-plugin:2.2.7' + } +} +`; diff --git a/Tasks/Common/codecoverage-tools/codecoverageenabler.ts b/Tasks/Common/codecoverage-tools/codecoverageenabler.ts new file mode 100644 index 000000000000..55c78c3768f5 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/codecoverageenabler.ts @@ -0,0 +1,79 @@ +/// <reference path="../../../definitions/Q.d.ts" /> +/// <reference path="../../../definitions/string.d.ts" /> +/// <reference path="../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../definitions/node.d.ts" /> + +import * as Q from "q"; +import * as tl from "vsts-task-lib/task"; +import * as util from "./utilities"; + +// ----------------------------------------------------- +// Interface to be implemented by all code coverage enablers +// ----------------------------------------------------- +export interface ICodeCoverageEnabler { + // enable code coverage for the given build tool and code coverage tool + enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean>; +} + +/* Code Coverage enabler for different type of build tools and code coverage tools*/ +export abstract class CodeCoverageEnabler implements ICodeCoverageEnabler { + protected buildFile: string; + + abstract enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean>; + + // ----------------------------------------------------- + // Convert the VSTS specific filter to comma seperated specific filter pattern + // - +:com.abc,-:com.xy -> com.abc,com.xy + // ----------------------------------------------------- + protected extractFilters(classFilter: string) { + let includeFilter = ""; + let excludeFilter = ""; + + tl.debug("Extracting VSTS filter: " + classFilter); + if (util.isNullOrWhitespace(classFilter)) { + return { + includeFilter: includeFilter, + excludeFilter: excludeFilter + }; + } + + classFilter.split(",").forEach(inputFilter => { + if (util.isNullOrWhitespace(inputFilter) || inputFilter.length < 2) { + throw new Error("Invalid class filter " + inputFilter); + } + + switch (inputFilter.charAt(0)) { + case "+": + includeFilter += inputFilter.substr(1); + break; + case "-": + excludeFilter += inputFilter.substr(1); + break; + default: + throw new Error("Invalid class filter " + inputFilter); + } + }); + + tl.debug("Include Filter pattern: " + includeFilter); + tl.debug("Exclude Filter pattern: " + excludeFilter); + + return { + includeFilter: includeFilter, + excludeFilter: excludeFilter + }; + } +} + +export abstract class CoberturaCodeCoverageEnabler extends CodeCoverageEnabler { + // ----------------------------------------------------- + // Convert the VSTS specific filter to Code Coverage Tool specific filter pattern + // ----------------------------------------------------- + protected abstract applyFilterPattern(filter: string): string[]; +} + +export abstract class JacocoCodeCoverageEnabler extends CodeCoverageEnabler { + // ----------------------------------------------------- + // Convert the VSTS specific filter to Code Coverage Tool specific filter pattern + // ----------------------------------------------------- + protected abstract applyFilterPattern(filter: string): string[]; +} diff --git a/Tasks/Common/codecoverage-tools/codecoveragefactory.ts b/Tasks/Common/codecoverage-tools/codecoveragefactory.ts new file mode 100644 index 000000000000..375107a43eb7 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/codecoveragefactory.ts @@ -0,0 +1,41 @@ +/// <reference path="../../../definitions/Q.d.ts" /> +/// <reference path="../../../definitions/string.d.ts" /> +/// <reference path="../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../definitions/node.d.ts" /> + +import {ICodeCoverageEnabler} from "./codecoverageenabler"; +import {JacocoAntCodeCoverageEnabler} from "./jacoco/jacoco.ant.ccenabler"; +import {JacocoGradleCodeCoverageEnabler} from "./jacoco/jacoco.gradle.ccenabler"; +import {JacocoMavenCodeCoverageEnabler} from "./jacoco/jacoco.maven.ccenabler"; +import {CoberturaAntCodeCoverageEnabler} from "./cobertura/cobertura.ant.ccenabler"; +import {CoberturaMavenCodeCoverageEnabler} from "./cobertura/cobertura.maven.ccenabler"; +import {CoberturaGradleCodeCoverageEnabler} from "./cobertura/cobertura.gradle.ccenabler"; + +export interface ICodeCoverageEnablerFactory { + getTool(buildTool: string, ccTool: string): ICodeCoverageEnabler; +} + +export class CodeCoverageEnablerFactory implements ICodeCoverageEnablerFactory { + public getTool(buildTool: string, ccTool: string): ICodeCoverageEnabler { + if (!buildTool || !ccTool) { + throw new Error("Invalid build tool/code coverage tool"); + } + + switch (buildTool.toLowerCase() + "-" + ccTool.toLowerCase()) { + case "ant-jacoco": + return new JacocoAntCodeCoverageEnabler(); + case "ant-cobertura": + return new CoberturaAntCodeCoverageEnabler(); + case "maven-jacoco": + return new JacocoMavenCodeCoverageEnabler(); + case "maven-cobertura": + return new CoberturaMavenCodeCoverageEnabler(); + case "gradle-jacoco": + return new JacocoGradleCodeCoverageEnabler(); + case "gradle-cobertura": + return new CoberturaGradleCodeCoverageEnabler(); + default: + throw new Error("Invalid build tool/code coverage tool"); + } + } +} diff --git a/Tasks/Common/codecoverage-tools/jacoco/jacoco.ant.ccenabler.ts b/Tasks/Common/codecoverage-tools/jacoco/jacoco.ant.ccenabler.ts new file mode 100644 index 000000000000..b9256dd02b7f --- /dev/null +++ b/Tasks/Common/codecoverage-tools/jacoco/jacoco.ant.ccenabler.ts @@ -0,0 +1,195 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as os from "os"; +import * as Q from "q"; + +export class JacocoAntCodeCoverageEnabler extends cc.JacocoCodeCoverageEnabler { + + reportDir: string; + excludeFilter: string; + includeFilter: string; + sourceDirs: string; + classDirs: string; + reportBuildFile: string; + + // ----------------------------------------------------- + // Enable code coverage for Jacoco Ant Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.buildFile = ccProps["buildfile"]; + _this.sourceDirs = ccProps["sourcedirectories"]; + _this.classDirs = ccProps["classfilesdirectories"]; + _this.reportDir = ccProps["reportdirectory"]; + _this.reportBuildFile = ccProps["reportbuildfile"]; + + let classFilter = ccProps["classfilter"]; + let filter = _this.extractFilters(classFilter); + _this.excludeFilter = _this.applyFilterPattern(filter.excludeFilter).join(","); + _this.includeFilter = _this.applyFilterPattern(filter.includeFilter).join(","); + + return util.readXmlFileAsJson(_this.buildFile) + .then(function (resp) { + return _this.addCodeCoverageData(resp); + }) + .thenResolve(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + str(util.trimToEmptyString(filter)).replaceAll(".", "/").s.split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? ("**/" + exFilter + "/**") : ("**/" + exFilter + ".class")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } + + protected getSourceFilter(): string { + let srcData = ""; + this.sourceDirs.split(",").forEach(dir => { + if (!str(dir).isEmpty()) { + srcData += `<fileset dir='${dir}'/>`; + srcData += os.EOL; + } + }); + if (str(srcData).isEmpty()) { + srcData = `<fileset dir='.'/>`; + srcData += os.EOL; + } + return srcData; + } + + protected getClassData(): string { + let classData = ""; + this.classDirs.split(",").forEach(dir => { + classData += `<fileset dir='${dir}' includes="${this.includeFilter}" excludes="${this.excludeFilter}" />`; + classData += os.EOL; + }); + if (str(classData).isEmpty()) { + classData += `<fileset dir='.'${this.includeFilter} ${this.excludeFilter} />`; + classData += os.EOL; + } + return classData; + } + + protected createReportFile(reportContent: string): Q.Promise<void> { + let _this = this; + return util.writeFile(_this.reportBuildFile, reportContent); + } + + protected addCodeCoverageData(pomJson: any): Q.Promise<any[]> { + let _this = this; + if (!pomJson.project) { + Q.reject(tl.loc("InvalidBuildFile")); + } + + let sourceData = _this.getSourceFilter(); + let classData = _this.getClassData(); + let reportPluginData = ccc.jacocoAntReport(_this.reportDir, classData, sourceData); + + return Q.all([_this.addCodeCoveragePluginData(pomJson), _this.createReportFile(reportPluginData)]); + } + + protected addCodeCoverageNodes(buildJsonContent: any): Q.Promise<any> { + let _this = this; + + if (!buildJsonContent.project.target) { + tl.debug("Build tag is not present"); + return Q.reject(tl.loc("InvalidBuildFile")); + } + + if (!buildJsonContent.project.target || typeof buildJsonContent.project.target === "string") { + buildJsonContent.project.target = {}; + } + + if (buildJsonContent.project.target instanceof Array) { + buildJsonContent.project.target.forEach(element => { + _this.enableForking(element); + }); + } else { + _this.enableForking(buildJsonContent.project.target); + } + + return Q.resolve(buildJsonContent); + } + + protected enableForking(targetNode: any) { + let _this = this; + let testNodes = ["junit", "java", "testng", "batchtest"]; + let coverageNode = ccc.jacocoAntCoverageEnable(); + + if (!str(_this.includeFilter).isEmpty()) { + coverageNode.$.includes = _this.includeFilter; + } + if (!str(_this.excludeFilter).isEmpty()) { + coverageNode.$.excludes = _this.excludeFilter; + } + + if (targetNode.javac) { + if (targetNode.javac instanceof Array) { + targetNode.javac.forEach(jn => { + jn.$.debug = "true"; + }); + } + } + + testNodes.forEach(tn => { + if (!targetNode[tn]) { + return; + } + _this.enableForkOnTestNodes(targetNode[tn], true); + coverageNode[tn] = targetNode[tn]; + delete targetNode[tn]; + targetNode["jacoco:coverage"] = coverageNode; + }); + } + + protected enableForkOnTestNodes(testNode: any, enableForkMode: boolean) { + if (testNode instanceof Array) { + testNode.forEach(element => { + if (!element.$) { + element.$ = {}; + } + if (enableForkMode) { + element.$.forkmode = "once"; + } + element.$.fork = "true"; + + }); + } else { + if (!testNode.$) { + testNode.$ = {}; + } + if (enableForkMode) { + testNode.$.forkmode = "once"; + } + testNode.$.fork = "true"; + } + } + + protected addCodeCoveragePluginData(pomJson: any): Q.Promise<any> { + let _this = this; + return _this.addCodeCoverageNodes(pomJson) + .then(function (content) { + return util.writeJsonAsXmlFile(_this.buildFile, content); + }); + } +} diff --git a/Tasks/Common/codecoverage-tools/jacoco/jacoco.gradle.ccenabler.ts b/Tasks/Common/codecoverage-tools/jacoco/jacoco.gradle.ccenabler.ts new file mode 100644 index 000000000000..c4f0cff0f186 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/jacoco/jacoco.gradle.ccenabler.ts @@ -0,0 +1,65 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as Q from "q"; + +export class JacocoGradleCodeCoverageEnabler extends cc.JacocoCodeCoverageEnabler { + // ----------------------------------------------------- + // Enable code coverage for Jacoco Gradle Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.buildFile = ccProps["buildfile"]; + let classFilter = ccProps["classfilter"]; + let isMultiModule = ccProps["ismultimodule"] && ccProps["ismultimodule"] === "true"; + let classFileDirs = ccProps["classfilesdirectories"]; + let reportDir = ccProps["reportdirectory"]; + let codeCoveragePluginData = null; + + let filter = _this.extractFilters(classFilter); + let jacocoExclude = _this.applyFilterPattern(filter.excludeFilter); + let jacocoInclude = _this.applyFilterPattern(filter.includeFilter); + + if (isMultiModule) { + codeCoveragePluginData = ccc.jacocoGradleMultiModuleEnable(jacocoExclude.join(","), jacocoInclude.join(","), classFileDirs, reportDir); + } else { + codeCoveragePluginData = ccc.jacocoGradleSingleModuleEnable(jacocoExclude.join(","), jacocoInclude.join(","), classFileDirs, reportDir); + } + + try { + tl.debug("Code Coverage data will be appeneded to build file: " + this.buildFile); + util.appendTextToFileSync(this.buildFile, codeCoveragePluginData); + tl.debug("Appended code coverage data"); + } catch (error) { + tl.warning(tl.loc("FailedToAppendCC", error)); + return Q.reject<boolean>(tl.loc("FailedToAppendCC", error)); + } + return Q.resolve<boolean>(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + str(util.trimToEmptyString(filter)).replaceAll(".", "/").s.split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? ("'" + exFilter + "/**'") : ("'" + exFilter + ".class'")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } +} diff --git a/Tasks/Common/codecoverage-tools/jacoco/jacoco.maven.ccenabler.ts b/Tasks/Common/codecoverage-tools/jacoco/jacoco.maven.ccenabler.ts new file mode 100644 index 000000000000..7fc10c8ecee3 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/jacoco/jacoco.maven.ccenabler.ts @@ -0,0 +1,172 @@ +/// <reference path="../../../../definitions/Q.d.ts" /> +/// <reference path="../../../../definitions/string.d.ts" /> +/// <reference path="../../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../../definitions/node.d.ts" /> + +import * as util from "../utilities"; +import * as tl from "vsts-task-lib/task"; +import * as ccc from "../codecoverageconstants"; +import * as cc from "../codecoverageenabler"; +import * as str from "string"; +import * as Q from "q"; + +export class JacocoMavenCodeCoverageEnabler extends cc.JacocoCodeCoverageEnabler { + + excludeFilter: string[]; + includeFilter: string[]; + reportDir: string; + sourceDirs: string; + classDirs: string; + reportBuildFile: string; + + // ----------------------------------------------------- + // Enable code coverage for Jacoco Maven Builds + // - enableCodeCoverage: CodeCoverageProperties - ccProps + // ----------------------------------------------------- + public enableCodeCoverage(ccProps: { [name: string]: string }): Q.Promise<boolean> { + let _this = this; + + tl.debug("Input parameters: " + JSON.stringify(ccProps)); + + _this.buildFile = ccProps["buildfile"]; + _this.reportDir = ccProps["reportdirectory"]; + _this.sourceDirs = ccProps["sourcedirectories"]; + _this.classDirs = ccProps["classfilesdirectories"]; + _this.reportBuildFile = ccProps["reportbuildfile"]; + + let classFilter = ccProps["classfilter"]; + let filter = _this.extractFilters(classFilter); + _this.excludeFilter = _this.applyFilterPattern(filter.excludeFilter); + _this.includeFilter = _this.applyFilterPattern(filter.includeFilter); + + return util.readXmlFileAsJson(_this.buildFile) + .then(function (resp) { + return _this.addCodeCoverageData(resp); + }) + .thenResolve(true); + } + + protected applyFilterPattern(filter: string): string[] { + let ccfilter = []; + + if (!util.isNullOrWhitespace(filter)) { + str(util.trimToEmptyString(filter)).replaceAll(".", "/").s.split(":").forEach(exFilter => { + if (exFilter) { + ccfilter.push(str(exFilter).endsWith("*") ? ("**/" + exFilter + "/**") : ("**/" + exFilter + ".class")); + } + }); + } + + tl.debug("Applying the filter pattern: " + filter + " op: " + ccfilter); + return ccfilter; + } + + protected addCodeCoverageData(pomJson: any): Q.Promise<any[]> { + let _this = this; + + if (!pomJson.project) { + Q.reject(tl.loc("InvalidBuildFile")); + } + + let isMultiModule = false; + if (pomJson.project.modules) { + tl.debug("Multimodule project detected"); + isMultiModule = true; + } + + let promises = [_this.addCodeCoveragePluginData(pomJson)]; + if (isMultiModule) { + promises.push(_this.createMultiModuleReport(_this.reportDir)); + } + + return Q.all(promises); + } + + protected addCodeCoverageNodes(buildJsonContent: any): Q.Promise<any> { + let _this = this; + + if (!buildJsonContent.project.build) { + tl.debug("Build tag is not present"); + buildJsonContent.project.build = {}; + } + + let buildNode = _this.getBuildDataNode(buildJsonContent); + let pluginsNode = _this.getPluginDataNode(buildNode); + let ccContent = ccc.jacocoMavenPluginEnable(_this.includeFilter, _this.excludeFilter, _this.reportDir); + util.addPropToJson(pluginsNode, "plugin", ccContent); + return Q.resolve(buildJsonContent); + } + + private getBuildDataNode(buildJsonContent: any): any { + let buildNode = null; + if (!buildJsonContent.project.build || typeof buildJsonContent.project.build === "string") { + buildNode = {}; + buildJsonContent.project.build = buildNode; + } + + if (buildJsonContent.project.build instanceof Array) { + if (typeof buildJsonContent.project.build[0] === "string") { + buildNode = {}; + buildJsonContent.project.build[0] = buildNode; + } else { + buildNode = buildJsonContent.project.build[0]; + } + } + return buildNode; + } + + private getPluginDataNode(buildNode: any): any { + let pluginsNode = {}; + + if (buildNode.pluginManagement) { + if (typeof buildNode.pluginManagement === "string") { + buildNode.pluginManagement = {}; + } + if (buildNode.pluginManagement instanceof Array) { + pluginsNode = buildNode.pluginManagement[0].plugins; + } else { + pluginsNode = buildNode.pluginManagement.plugins; + } + } else { + if (!buildNode.plugins || typeof buildNode.plugins === "string") { + buildNode.plugins = {}; + } + if (buildNode.plugins instanceof Array) { + if (typeof buildNode.plugins[0] === "string") { + pluginsNode = {}; + buildNode.plugins[0] = pluginsNode; + } else { + pluginsNode = buildNode.plugins[0]; + } + } else { + pluginsNode = buildNode.plugins; + } + } + return pluginsNode; + } + + protected createMultiModuleReport(reportDir: string): Q.Promise<any> { + let _this = this; + let srcDirs = _this.sourceDirs; + let classDirs = _this.classDirs; + let includeFilter = _this.includeFilter.join(","); + let excludeFilter = _this.excludeFilter.join(","); + + if (str(srcDirs).isEmpty()) { + srcDirs = "."; + } + if (str(classDirs).isEmpty()) { + classDirs = "."; + } + + return util.writeFile(_this.reportBuildFile, ccc.jacocoMavenMultiModuleReport(reportDir, srcDirs, classDirs, includeFilter, excludeFilter)); + } + + protected addCodeCoveragePluginData(pomJson: any): Q.Promise<any> { + let _this = this; + return _this.addCodeCoverageNodes(pomJson) + .then(function (content) { + return util.writeJsonAsXmlFile(_this.buildFile, content); + }); + } +} diff --git a/Tasks/Common/codecoverage-tools/module.json b/Tasks/Common/codecoverage-tools/module.json new file mode 100644 index 000000000000..12d27d8ded36 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/module.json @@ -0,0 +1,7 @@ +{ + "messages": { + "InvalidBuildFile": "Invalid or unsupported build file", + "FileNotFound": "File or folder doesn't exist: %s", + "FailedToAppendCC": "Unable to append code coverage data: %s" + } +} \ No newline at end of file diff --git a/Tasks/Common/codecoverage-tools/package.json b/Tasks/Common/codecoverage-tools/package.json new file mode 100644 index 000000000000..51659b926041 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/package.json @@ -0,0 +1,17 @@ +{ + "name": "codecoverage-tools", + "version": "1.0.0", + "author": "Microsoft Corporation", + "description": "Build with Gradle", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/vso-agent-tasks/issues" + }, + "dependencies": { + "xml2js": "^0.4.17", + "fs-extra": "^0.30.0", + "os": "^0.1.1", + "string": "^3.3.1", + "vsts-task-lib": "0.9.7" + } +} \ No newline at end of file diff --git a/Tasks/Common/codecoverage-tools/utilities.d.ts b/Tasks/Common/codecoverage-tools/utilities.d.ts new file mode 100644 index 000000000000..fe9f34f74f31 --- /dev/null +++ b/Tasks/Common/codecoverage-tools/utilities.d.ts @@ -0,0 +1,27 @@ +/// <reference path="../../../definitions/Q.d.ts" /> +/// <reference path="../../../definitions/string.d.ts" /> +/// <reference path="../../../definitions/xml2js.d.ts" /> +/// <reference path="../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../definitions/node.d.ts" /> +import * as Q from "q"; +export interface GetOrCreateResult<T> { + created: boolean; + result: T; +} +export declare function sharedSubString(string1: string, string2: string): string; +export declare function sortStringArray(list: any): string[]; +export declare function isDirectoryExists(path: string): boolean; +export declare function isFileExists(path: string): boolean; +export declare function isNullOrWhitespace(input: any): boolean; +export declare function trimToEmptyString(input: any): any; +export declare function appendTextToFileSync(filePath: string, fileContent: string): void; +export declare function prependTextToFileSync(filePath: string, fileContent: string): void; +export declare function insertTextToFileSync(filePath: string, prependFileContent?: string, appendFileContent?: string): void; +export declare function trimEnd(data: string, trimChar: string): string; +export declare function readXmlFileAsJson(filePath: string): Q.Promise<any>; +export declare function readFile(filePath: string, encoding: string): Q.Promise<string>; +export declare function convertXmlStringToJson(xmlContent: string): Q.Promise<any>; +export declare function convertXmlStringToJsonSync(xmlContent: string): any; +export declare function writeJsonAsXmlFile(filePath: string, jsonContent: any): Q.Promise<void>; +export declare function writeFile(filePath: string, fileContent: string): Q.Promise<void>; +export declare function addPropToJson(obj: any, propName: string, value: any): void; diff --git a/Tasks/Common/codecoverage-tools/utilities.ts b/Tasks/Common/codecoverage-tools/utilities.ts new file mode 100644 index 000000000000..638af90bbb5a --- /dev/null +++ b/Tasks/Common/codecoverage-tools/utilities.ts @@ -0,0 +1,189 @@ +/// <reference path="../../../definitions/Q.d.ts" /> +/// <reference path="../../../definitions/string.d.ts" /> +/// <reference path="../../../definitions/xml2js.d.ts" /> +/// <reference path="../../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../../definitions/node.d.ts" /> + +import * as Q from "q"; +import * as fs from "fs"; +import * as tl from "vsts-task-lib/task"; +import * as path from "path"; +import * as str from "string"; +import * as xml2js from "xml2js"; +import * as fse from "fs-extra"; + +export interface GetOrCreateResult<T> { + created: boolean; + result: T; +} + +// returns a substring that is common from first. For example, for "abcd" and "abdf", "ab" is returned. +export function sharedSubString(string1: string, string2: string): string { + let ret = ""; + let index = 1; + while (string1.substring(0, index) === string2.substring(0, index)) { + ret = string1.substring(0, index); + index++; + } + return ret; +} + +// sorts string array in ascending order +export function sortStringArray(list): string[] { + let sortedFiles: string[] = list.sort((a, b) => { + if (a > b) { + return 1; + } else if (a < b) { + return -1; + } + return 0; + }); + return sortedFiles; +} + +// returns true if path exists and it is a directory else false. +export function isDirectoryExists(path: string): boolean { + try { + return tl.stats(path).isDirectory(); + } catch (error) { + return false; + } +} + +// returns true if path exists and it is a file else false. +export function isFileExists(path: string): boolean { + try { + return tl.stats(path).isFile(); + } catch (error) { + return false; + } +} + +// returns true if given string is null or whitespace. +export function isNullOrWhitespace(input) { + if (typeof input === "undefined" || input == null) { + return true; + } + return input.replace(/\s/g, "").length < 1; +} + +// returns empty string if the given value is undefined or null. +export function trimToEmptyString(input) { + if (typeof input === "undefined" || input == null) { + return ""; + } + return input.trim(); +} + +// appends given text to file. +export function appendTextToFileSync(filePath: string, fileContent: string) { + if (isFileExists(filePath)) { + fs.appendFileSync(filePath, fileContent); + } else { + throw new Error(tl.loc("FileNotFound", filePath)); + } +} + +// prepends given text to start of file. +export function prependTextToFileSync(filePath: string, fileContent: string) { + if (isFileExists(filePath)) { + let data = fs.readFileSync(filePath); // read existing contents into data + let fd = fs.openSync(filePath, "w+"); + let buffer = new Buffer(fileContent); + fs.writeSync(fd, buffer, 0, buffer.length, 0); // write new data + fs.writeSync(fd, data, 0, data.length, 0); // append old data + fs.close(fd); + } +} + +// single utility for appending text and prepending text to file. +export function insertTextToFileSync(filePath: string, prependFileContent?: string, appendFileContent?: string) { + if (isFileExists(filePath) && (prependFileContent || appendFileContent)) { + let existingData = fs.readFileSync(filePath); // read existing contents into data + let fd = fs.openSync(filePath, "w+"); + let preTextLength = prependFileContent ? prependFileContent.length : 0; + + if (prependFileContent) { + let prependBuffer = new Buffer(prependFileContent); + fs.writeSync(fd, prependBuffer, 0, prependBuffer.length, 0); // write new data + } + fs.writeSync(fd, existingData, 0, existingData.length, preTextLength); // append old data + if (appendFileContent) { + let appendBuffer = new Buffer(appendFileContent); + fs.writeSync(fd, appendBuffer, 0, appendBuffer.length, existingData.length + preTextLength); + } + fs.close(fd); + } +} + +// trim the given character if it exists in the end of string. +export function trimEnd(data: string, trimChar: string) { + if (!trimChar || !data) { + return data; + } + + if (str(data).endsWith(trimChar)) { + return data.substring(0, data.length - trimChar.length); + } else { + return data; + } +} + +export function readXmlFileAsJson(filePath: string): Q.Promise<any> { + tl.debug("Reading XML file: " + filePath); + return readFile(filePath, "utf-8") + .then(convertXmlStringToJson); +} + +export function readFile(filePath: string, encoding: string): Q.Promise<string> { + return Q.nfcall<string>(fs.readFile, filePath, encoding); +} + +export function convertXmlStringToJson(xmlContent: string): Q.Promise<any> { + tl.debug("Converting XML file to JSON"); + return Q.nfcall<any>(xml2js.parseString, xmlContent); +} + +export function writeJsonAsXmlFile(filePath: string, jsonContent: any): Q.Promise<void> { + let builder = new xml2js.Builder(); + tl.debug("Writing JSON as XML file: " + filePath); + let xml = builder.buildObject(jsonContent); + xml = str(xml).replaceAll("
", "").s; + return writeFile(filePath, xml); +} + +export function writeFile(filePath: string, fileContent: string): Q.Promise<void> { + tl.debug("Creating dir if not exists: " + path.dirname(filePath)); + fse.mkdirpSync(path.dirname(filePath)); + tl.debug("Check dir: " + fs.existsSync(path.dirname(filePath))); + return Q.nfcall<void>(fs.writeFile, filePath, fileContent); +} + +export function addPropToJson(obj: any, propName: string, value: any): void { + tl.debug("Adding property to JSON: " + propName); + if (obj === "undefined") { + obj = {}; + } + + if (obj instanceof Array) { + let propNode = obj.find(o => o[propName]); + if (propNode) { + obj = propNode; + } + } + + if (propName in obj) { + if (obj[propName] instanceof Array) { + obj[propName].push(value); + } else if (typeof obj[propName] !== "object") { + obj[propName] = [obj[propName], value]; + } + } else if (obj instanceof Array) { + let prop = {}; + prop[propName] = value; + obj.push(prop); + } else { + obj[propName] = value; + } +} + diff --git a/Tasks/Gradle/gradletask.ts b/Tasks/Gradle/gradletask.ts index b1e952021ce1..dea742619af9 100644 --- a/Tasks/Gradle/gradletask.ts +++ b/Tasks/Gradle/gradletask.ts @@ -1,25 +1,24 @@ /// <reference path="../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../definitions/codecoveragefactory.d.ts" /> import tl = require('vsts-task-lib/task'); import fs = require('fs'); import path = require('path'); +import * as Q from "q"; +import os = require('os'); -// Lowercased file names are to lessen the likelihood of xplat issues import sqCommon = require('./CodeAnalysis/SonarQube/common'); import sqGradle = require('./CodeAnalysis/gradlesonar'); - import {CodeAnalysisOrchestrator} from './CodeAnalysis/Common/CodeAnalysisOrchestrator'; import {BuildOutput, BuildEngine} from './CodeAnalysis/Common/BuildOutput'; import {PmdTool} from './CodeAnalysis/Common/PmdTool'; import {CheckstyleTool} from './CodeAnalysis/Common/CheckstyleTool'; - -import os = require('os'); +import {CodeCoverageEnablerFactory} from 'codecoverage-tools/codecoveragefactory'; var isWindows = os.type().match(/^Win/); // Set up localization resource file tl.setResourcePath(path.join(__dirname, 'task.json')); - var wrapperScript = tl.getPathInput('wrapperScript', true, true); if (isWindows) { @@ -43,7 +42,7 @@ if (!cwd) { } tl.cd(cwd); -var gb = tl.createToolRunner(wrapperScript); +var gb = tl.tool(wrapperScript); var javaHomeSelection = tl.getInput('javaHomeSelection', true); var specifiedJavaHome = null; var ccTool = tl.getInput('codeCoverageTool'); @@ -54,6 +53,7 @@ var summaryFile: string = null; var reportDirectory: string = null; var inputTasks: string[] = tl.getDelimitedInput('tasks', ' ', true); var isSonarQubeEnabled: boolean = sqCommon.isSonarQubeAnalysisEnabled(); +let reportingTaskName = ""; let buildOutput: BuildOutput = new BuildOutput(tl.getVariable('build.sourcesDirectory'), BuildEngine.Gradle); var codeAnalysisOrchestrator = new CodeAnalysisOrchestrator( @@ -64,7 +64,7 @@ if (isCodeCoverageOpted && inputTasks.indexOf('clean') == -1) { gb.arg('clean'); //if user opts for code coverage, we append clean functionality to make sure any uninstrumented class files are removed } -gb.argString(tl.getInput('options', false)); +gb.arg(tl.getInput('options', false)); gb.arg(inputTasks); // update JAVA_HOME if user selected specific JDK version or set path manually @@ -94,43 +94,44 @@ if (specifiedJavaHome) { tl.debug('Set JAVA_HOME to ' + specifiedJavaHome); process.env['JAVA_HOME'] = specifiedJavaHome; } - -if (isCodeCoverageOpted) { - tl.debug("Option to enable code coverage was selected and is being applied."); - enableCodeCoverage(); + +/* Actual execution of Build and further flows*/ +async function execBuild() { + await execEnableCodeCoverage(); + if (reportingTaskName && reportingTaskName != "") { + gb.arg(reportingTaskName); + } + + enableSonarQubeAnalysis(); + var gradleResult; + gb.exec() + .then(function (code) { + gradleResult = code; + publishTestResults(publishJUnitResults, testResultsFiles); + publishCodeCoverage(isCodeCoverageOpted); + return processCodeAnalysisResults(); + }) + .then(() => { + tl.exit(gradleResult); + }) + .fail(function (err) { + console.error(err); + tl.debug('taskRunner fail'); + tl.exit(1); + }); } -if (isSonarQubeEnabled) { - // Looks like: 'SonarQube analysis is enabled.' - console.log(tl.loc('codeAnalysis_ToolIsEnabled'), sqCommon.toolName); +function enableSonarQubeAnalysis() { + if (isSonarQubeEnabled) { + // Looks like: 'SonarQube analysis is enabled.' + console.log(tl.loc('codeAnalysis_ToolIsEnabled'), sqCommon.toolName); - gb = sqGradle.applyEnabledSonarQubeArguments(gb); - gb = sqGradle.applySonarQubeCodeCoverageArguments(gb, isCodeCoverageOpted, ccTool, summaryFile); + gb = sqGradle.applyEnabledSonarQubeArguments(gb); + gb = sqGradle.applySonarQubeCodeCoverageArguments(gb, isCodeCoverageOpted, ccTool, summaryFile); + } + gb = codeAnalysisOrchestrator.configureBuild(gb); } -gb = codeAnalysisOrchestrator.configureBuild(gb); - -setGradleOpts(); - -var gradleResult; -gb.exec() - .then(function (code) { - gradleResult = code; - - publishTestResults(publishJUnitResults, testResultsFiles); - publishCodeCoverage(isCodeCoverageOpted); - return processCodeAnalysisResults(); - }) - .then(() => { - tl.exit(gradleResult); - }) - .fail(function (err) { - publishTestResults(publishJUnitResults, testResultsFiles); - console.error(err); - tl.debug('taskRunner fail'); - tl.exit(1); - }); - function processCodeAnalysisResults(): Q.Promise<void> { tl.debug('Processing code analysis results'); @@ -174,7 +175,21 @@ function publishTestResults(publishJUnitResults, testResultsFiles: string) { } } -function enableCodeCoverage() { +function execEnableCodeCoverage(): Q.Promise<void> { + return enableCodeCoverage() + .then(function (resp) { + tl.debug("Enabled code coverage successfully"); + }).catch(function (err) { + tl.warning("Failed to enable code coverage: " + err); + }); +}; + +function enableCodeCoverage(): Q.Promise<any> { + if (!isCodeCoverageOpted) { + return Q.resolve(true); + } + + tl.debug("Option to enable code coverage was selected and is being applied."); var classFilter: string = tl.getInput('classFilter'); var classFilesDirectories: string = tl.getInput('classFilesDirectories'); var buildRootPath = cwd; @@ -186,15 +201,15 @@ function enableCodeCoverage() { var summaryFileName = "summary.xml"; if (isMultiModule) { - var reportingTaskName = "jacocoRootReport"; + reportingTaskName = "jacocoRootReport"; } else { - var reportingTaskName = "jacocoTestReport"; + reportingTaskName = "jacocoTestReport"; } } else if (ccTool.toLowerCase() == "cobertura") { var summaryFileName = "coverage.xml"; - var reportingTaskName = "cobertura"; + reportingTaskName = "cobertura"; } summaryFile = path.join(reportDirectory, summaryFileName); @@ -209,21 +224,14 @@ function enableCodeCoverage() { buildProps['reportdirectory'] = reportDirectoryName; buildProps['ismultimodule'] = String(isMultiModule); - try { - var codeCoverageEnabler = new tl.CodeCoverageEnabler('Gradle', ccTool); - codeCoverageEnabler.enableCodeCoverage(buildProps); - tl.debug("Code coverage is successfully enabled."); - } - catch (Error) { - tl.warning("Enabling code coverage failed. Check the build logs for errors."); - } - gb.arg(reportingTaskName); + let ccEnabler = new CodeCoverageEnablerFactory().getTool("gradle", ccTool.toLowerCase()); + return ccEnabler.enableCodeCoverage(buildProps); } function isMultiModuleProject(wrapperScript: string): boolean { - var gradleBuild = tl.createToolRunner(wrapperScript); + var gradleBuild = tl.tool(wrapperScript); gradleBuild.arg("properties"); - gradleBuild.argString(tl.getInput('options', false)); + gradleBuild.arg(tl.getInput('options', false)); var data = gradleBuild.execSync().stdout; if (typeof data != "undefined" && data) { @@ -254,3 +262,6 @@ function publishCodeCoverage(isCodeCoverageOpted: boolean) { } } } + +setGradleOpts(); +execBuild(); \ No newline at end of file diff --git a/Tasks/Gradle/package.json b/Tasks/Gradle/package.json index ec0b778db6e4..968773cf6e36 100644 --- a/Tasks/Gradle/package.json +++ b/Tasks/Gradle/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "xml2js": "^0.4.16", + "vsts-task-lib": "0.9.7", "request": "^2.74.0" } } \ No newline at end of file diff --git a/Tasks/Gradle/task.json b/Tasks/Gradle/task.json index 67bce37154f7..64d59b03cd06 100644 --- a/Tasks/Gradle/task.json +++ b/Tasks/Gradle/task.json @@ -12,7 +12,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 56 + "Patch": 57 }, "demands": [ "java" diff --git a/Tasks/Gradle/task.loc.json b/Tasks/Gradle/task.loc.json index 8301e2e85cb1..b2ed81bfe9c1 100644 --- a/Tasks/Gradle/task.loc.json +++ b/Tasks/Gradle/task.loc.json @@ -12,7 +12,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 56 + "Patch": 57 }, "demands": [ "java" diff --git a/Tasks/Maven/maventask.ts b/Tasks/Maven/maventask.ts index e138d7113579..2685b7ebeb6c 100644 --- a/Tasks/Maven/maventask.ts +++ b/Tasks/Maven/maventask.ts @@ -1,4 +1,5 @@ /// <reference path="../../definitions/vsts-task-lib.d.ts" /> +/// <reference path="../../definitions/codecoveragefactory.d.ts" /> import Q = require('q'); import os = require('os'); @@ -9,7 +10,7 @@ import tl = require('vsts-task-lib/task'); import {ToolRunner} from 'vsts-task-lib/toolrunner'; import sqCommon = require('./CodeAnalysis/SonarQube/common'); import sqMaven = require('./CodeAnalysis/mavensonar'); - +import {CodeCoverageEnablerFactory} from 'codecoverage-tools/codecoveragefactory'; import {CodeAnalysisOrchestrator} from "./CodeAnalysis/Common/CodeAnalysisOrchestrator"; import {BuildOutput, BuildEngine} from './CodeAnalysis/Common/BuildOutput'; import {PmdTool} from './CodeAnalysis/Common/PmdTool'; @@ -27,6 +28,12 @@ var publishJUnitResults: string = tl.getInput('publishJUnitResults'); var testResultsFiles: string = tl.getInput('testResultsFiles', true); var ccTool = tl.getInput('codeCoverageTool'); var isCodeCoverageOpted = (typeof ccTool != "undefined" && ccTool && ccTool.toLowerCase() != 'none'); +var isSonarQubeEnabled:boolean = false; +var summaryFile: string = null; +var reportDirectory: string = null; +var reportPOMFile: string = null; +var execFileJacoco: string = null; +var ccReportTask: string = null; let buildOutput: BuildOutput = new BuildOutput(tl.getVariable('build.sourcesDirectory'), BuildEngine.Maven); var codeAnalysisOrchestrator:CodeAnalysisOrchestrator = new CodeAnalysisOrchestrator( @@ -111,109 +118,100 @@ if (specifiedJavaHome) { tl.setVariable('JAVA_HOME', specifiedJavaHome); } -if (isCodeCoverageOpted) { - var summaryFile: string = null; - var reportDirectory: string = null; - var reportPOMFile: string = null; - var execFileJacoco: string = null; - var ccReportTask: string = null; - enableCodeCoverage(); -} -else { - tl.debug("Option to enable code coverage was not selected and is being skipped."); -} - -// Maven task orchestration occurs as follows: -// 1. Check that Maven exists by executing it to retrieve its version. -// 2. Apply any goals for static code analysis tools selected by the user. -// 3. Run Maven. Compilation or test errors will cause this to fail. -// In case the build has failed, the analysis will still succeed but the report will have less data. -// 4. Attempt to collate and upload static code analysis build summaries and artifacts. -// 5. Always publish test results even if tests fail, causing this task to fail. -// 6. If #3 or #4 above failed, exit with an error code to mark the entire step as failed. - -var userRunFailed: boolean = false; -var codeAnalysisFailed: boolean = false; - -// Setup tool runner that executes Maven only to retrieve its version -var mvnGetVersion = tl.createToolRunner(mvnExec); -mvnGetVersion.arg('-version'); - -configureMavenOpts(); - -// 1. Check that Maven exists by executing it to retrieve its version. -mvnGetVersion.exec() - .fail(function (err) { - console.error("Maven is not installed on the agent"); - tl.exit(1); // tl.exit sets the step result but does not stop execution - process.exit(1); - }) - .then(function (code) { - // Setup tool runner to execute Maven goals - var mvnRun = tl.createToolRunner(mvnExec); - mvnRun.arg('-f'); - mvnRun.pathArg(mavenPOMFile); - mvnRun.argString(mavenOptions); - if (isCodeCoverageOpted && mavenGoals.indexOf('clean') == -1) { - mvnRun.arg('clean'); - } - mvnRun.arg(mavenGoals); +async function execBuild() { + // Maven task orchestration occurs as follows: + // 1. Check that Maven exists by executing it to retrieve its version. + // 2. Apply any goals for static code analysis tools selected by the user. + // 3. Run Maven. Compilation or test errors will cause this to fail. + // In case the build has failed, the analysis will still succeed but the report will have less data. + // 4. Attempt to collate and upload static code analysis build summaries and artifacts. + // 5. Always publish test results even if tests fail, causing this task to fail. + // 6. If #3 or #4 above failed, exit with an error code to mark the entire step as failed. + + ccReportTask = await execEnableCodeCoverage(); + var userRunFailed: boolean = false; + var codeAnalysisFailed: boolean = false; + + // Setup tool runner that executes Maven only to retrieve its version + var mvnGetVersion = tl.tool(mvnExec); + mvnGetVersion.arg('-version'); + + configureMavenOpts(); + + // 1. Check that Maven exists by executing it to retrieve its version. + mvnGetVersion.exec() + .fail(function (err) { + console.error("Maven is not installed on the agent"); + tl.exit(1); // tl.exit sets the step result but does not stop execution + process.exit(1); + }) + .then(function (code) { + // Setup tool runner to execute Maven goals + var mvnRun = tl.tool(mvnExec); + mvnRun.arg('-f'); + mvnRun.arg(mavenPOMFile); + mvnRun.arg(mavenOptions); + if (isCodeCoverageOpted && mavenGoals.indexOf('clean') == -1) { + mvnRun.arg('clean'); + } + mvnRun.arg(mavenGoals); - // 2. Apply any goals for static code analysis tools selected by the user. - mvnRun = sqMaven.applySonarQubeArgs(mvnRun, execFileJacoco); - mvnRun = codeAnalysisOrchestrator.configureBuild(mvnRun); + // 2. Apply any goals for static code analysis tools selected by the user. + mvnRun = sqMaven.applySonarQubeArgs(mvnRun, execFileJacoco); + mvnRun = codeAnalysisOrchestrator.configureBuild(mvnRun); - // Read Maven standard output - mvnRun.on('stdout', function (data) { - processMavenOutput(data); - }); + // Read Maven standard output + mvnRun.on('stdout', function (data) { + processMavenOutput(data); + }); - // 3. Run Maven. Compilation or test errors will cause this to fail. - return mvnRun.exec(); // Run Maven with the user specified goals - }) - .fail(function (err) { - console.error(err.message); - userRunFailed = true; // Record the error and continue - }) - .then(function (code) { - // 4. Attempt to collate and upload static code analysis build summaries and artifacts. - - // The files won't be created if the build failed, and the user should probably fix their build first. - if (userRunFailed) { - console.error('Could not retrieve code analysis results - Maven run failed.'); - return; - } + // 3. Run Maven. Compilation or test errors will cause this to fail. + return mvnRun.exec(); // Run Maven with the user specified goals + }) + .fail(function (err) { + console.error(err.message); + userRunFailed = true; // Record the error and continue + }) + .then(function (code) { + // 4. Attempt to collate and upload static code analysis build summaries and artifacts. + + // The files won't be created if the build failed, and the user should probably fix their build first. + if (userRunFailed) { + console.error('Could not retrieve code analysis results - Maven run failed.'); + return; + } - // Otherwise, start uploading relevant build summaries. - tl.debug('Processing code analysis results'); - return sqMaven.processSonarQubeIntegration() - .then(() => { - return codeAnalysisOrchestrator.publishCodeAnalysisResults(); - }); - }) - .fail(function (err) { - console.error(err.message); - // Looks like: "Code analysis failed." - console.error(tl.loc('codeAnalysis_ToolFailed', 'Code')); - codeAnalysisFailed = true; - }) - .then(function () { - // 5. Always publish test results even if tests fail, causing this task to fail. - if (publishJUnitResults == 'true') { - publishJUnitTestResults(testResultsFiles); - } - publishCodeCoverage(isCodeCoverageOpted); + // Otherwise, start uploading relevant build summaries. + tl.debug('Processing code analysis results'); + return sqMaven.processSonarQubeIntegration() + .then(() => { + return codeAnalysisOrchestrator.publishCodeAnalysisResults(); + }); + }) + .fail(function (err) { + console.error(err.message); + // Looks like: "Code analysis failed." + console.error(tl.loc('codeAnalysis_ToolFailed', 'Code')); + codeAnalysisFailed = true; + }) + .then(function () { + // 5. Always publish test results even if tests fail, causing this task to fail. + if (publishJUnitResults == 'true') { + publishJUnitTestResults(testResultsFiles); + } + publishCodeCoverage(isCodeCoverageOpted); - // 6. If #3 or #4 above failed, exit with an error code to mark the entire step as failed. - if (userRunFailed || codeAnalysisFailed) { - tl.exit(1); // Set task failure - } - else { - tl.exit(0); // Set task success - } + // 6. If #3 or #4 above failed, exit with an error code to mark the entire step as failed. + if (userRunFailed || codeAnalysisFailed) { + tl.exit(1); // Set task failure + } + else { + tl.exit(0); // Set task success + } - // Do not force an exit as publishing results is async and it won't have finished - }); + // Do not force an exit as publishing results is async and it won't have finished + }); +} // Configure the JVM associated with this run. function configureMavenOpts() { @@ -253,7 +251,22 @@ function publishJUnitTestResults(testResultsFiles: string) { tp.publish(matchingJUnitResultFiles, true, "", "", "", true); } -function enableCodeCoverage() { +function execEnableCodeCoverage(): Q.Promise<string> { + return enableCodeCoverage() + .then(function (resp) { + tl.debug("Enabled code coverage successfully"); + return "CodeCoverage_9064e1d0"; + }).catch(function (err) { + tl.warning("Failed to enable code coverage: " + err); + return ""; + }); +}; + +function enableCodeCoverage() : Q.Promise<any> { + if(!isCodeCoverageOpted){ + return Q.resolve(true); + } + var classFilter: string = tl.getInput('classFilter'); var classFilesDirectories: string = tl.getInput('classFilesDirectories'); var sourceDirectories: string = tl.getInput('srcDirectories'); @@ -262,7 +275,6 @@ function enableCodeCoverage() { var reportPOMFileName = "CCReportPomA4D283EG.xml"; reportPOMFile = path.join(buildRootPath, reportPOMFileName); var targetDirectory = path.join(buildRootPath, "target"); - ccReportTask = "jacoco:report"; if (ccTool.toLowerCase() == "jacoco") { var reportDirectoryName = "CCReport43F6D5EF"; @@ -295,31 +307,25 @@ function enableCodeCoverage() { buildProps['reportdirectory'] = reportDirectory; buildProps['reportbuildfile'] = reportPOMFile; - try { - var codeCoverageEnabler = new tl.CodeCoverageEnabler('Maven', ccTool); - codeCoverageEnabler.enableCodeCoverage(buildProps); - tl.debug("Code coverage is successfully enabled."); - } - catch (Error) { - tl.warning("Enabling code coverage failed. Check the build logs for errors."); - } + let ccEnabler = new CodeCoverageEnablerFactory().getTool("maven", ccTool.toLowerCase()); + return ccEnabler.enableCodeCoverage(buildProps); } function publishCodeCoverage(isCodeCoverageOpted: boolean) { - if (isCodeCoverageOpted) { + if (isCodeCoverageOpted && ccReportTask) { tl.debug("Collecting code coverage reports"); if (ccTool.toLowerCase() == "jacoco") { - var mvnReport = tl.createToolRunner(mvnExec); + var mvnReport = tl.tool(mvnExec); mvnReport.arg('-f'); if (tl.exist(reportPOMFile)) { // multi module project - mvnReport.pathArg(reportPOMFile); + mvnReport.arg(reportPOMFile); mvnReport.arg("verify"); } else { - mvnReport.pathArg(mavenPOMFile); - mvnReport.arg(ccReportTask); + mvnReport.arg(mavenPOMFile); + mvnReport.arg("verify"); } mvnReport.exec().then(function (code) { publishCCToTfs(); @@ -397,3 +403,5 @@ function processMavenOutput(data) { } } } + +execBuild(); diff --git a/Tasks/Maven/package.json b/Tasks/Maven/package.json index 998cff10e139..d305e38081e0 100644 --- a/Tasks/Maven/package.json +++ b/Tasks/Maven/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "xml2js": "^0.4.16", + "vsts-task-lib": "0.9.7", "request": "^2.74.0" } } \ No newline at end of file diff --git a/Tasks/Maven/task.json b/Tasks/Maven/task.json index 2d3247e05909..847087945c01 100644 --- a/Tasks/Maven/task.json +++ b/Tasks/Maven/task.json @@ -16,7 +16,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 62 + "Patch": 63 }, "minimumAgentVersion": "1.89.0", "instanceNameFormat": "Maven $(mavenPOMFile)", diff --git a/Tasks/Maven/task.loc.json b/Tasks/Maven/task.loc.json index ff422068f882..69c9af9d5792 100644 --- a/Tasks/Maven/task.loc.json +++ b/Tasks/Maven/task.loc.json @@ -16,7 +16,7 @@ "version": { "Major": 1, "Minor": 0, - "Patch": 62 + "Patch": 63 }, "minimumAgentVersion": "1.89.0", "instanceNameFormat": "ms-resource:loc.instanceNameFormat", diff --git a/Tests/L0/ANT/_suite.ts b/Tests/L0/ANT/_suite.ts index 442c0d15b31f..969a718ecbc6 100644 --- a/Tests/L0/ANT/_suite.ts +++ b/Tests/L0/ANT/_suite.ts @@ -174,7 +174,7 @@ describe('ANT Suite', function() { .then(() => { // The response file will cause ANT to fail, but we are looking for the warning about ANT_HOME assert(tr.ran('/usr/local/bin/ANT -version'), 'it should have run ANT -version'); - assert(tr.invokedToolCount == 1, 'should have only run ANT 1 time'); + assert(tr.invokedToolCount == 2, 'should have only run ANT 2 times'); assert(tr.resultWasSet, 'task should have set a result'); assert(tr.stderr.length > 0, 'should have written to stderr'); assert(tr.failed, 'task should have failed'); @@ -264,121 +264,6 @@ describe('ANT Suite', function() { }); }) - it('Ant fails when code coverage tool is Jacoco but no class files directory input is provided.', (done) => { - setResponseFile('antCodeCoverage.json'); - - var tr = new trm.TaskRunner('Ant'); - tr.setInput('antBuildFile', '/build/build.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - - tr.run() - .then(() => { - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length > 0, 'should have written to stderr'); - assert(tr.failed, 'task should have failed'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Ant fails when code coverage tool is Cobertura but no class files directory input is provided.', (done) => { - setResponseFile('antCodeCoverage.json'); - - var tr = new trm.TaskRunner('Ant'); - tr.setInput('antBuildFile', '/build/build.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'Cobertura'); - - tr.run() - .then(() => { - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length > 0, 'should have written to stderr'); - assert(tr.failed, 'task should have failed'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Ant calls enable code coverage and publish code coverage when Cobertura is selected.', (done) => { - setResponseFile('antCodeCoverage.json'); - - var tr = new trm.TaskRunner('Ant'); - tr.setInput('antBuildFile', '/build/build.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'Cobertura'); - tr.setInput('classFilesDirectories', 'class1'); - - tr.run() - .then(() => { - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\\build\\CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\\build\\CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=Cobertura;\]/) >= 0 || tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\/build\/CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\/build\/CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=\\build\\CCReport43F6D5EF\\coverage.xml;reportdirectory=\\build\\CCReport43F6D5EF;\]/) >= 0 || - tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=\/build\/CCReport43F6D5EF\/coverage.xml;reportdirectory=\/build\/CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - done(); - }) - .fail((err) => { - assert.fail("task should not have failed"); - done(err); - }); - }) - - it('Ant calls enable code coverage and publish code coverage when Jacoco is selected.', (done) => { - setResponseFile('antCodeCoverage.json'); - - var tr = new trm.TaskRunner('Ant'); - tr.setInput('antBuildFile', '/build/build.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - tr.setInput('classFilesDirectories', 'class1'); - - tr.run() - .then(() => { - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\\build\\CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\\build\\CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=JaCoCo;\]/) >= 0 || tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\/build\/CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\/build\/CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=\\build\\CCReport43F6D5EF\\coverage.xml;reportdirectory=\\build\\CCReport43F6D5EF;\]/) >= 0 || - tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=\/build\/CCReport43F6D5EF\/coverage.xml;reportdirectory=\/build\/CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - done(); - }) - .fail((err) => { - assert.fail("task should not have failed"); - done(err); - }); - }) - - it('Ant calls enable code coverage but not publish code coverage when summary file is not generated.', (done) => { - setResponseFile('antGood.json'); - // antGood.json doesnt mock the stat for summary file. - var tr = new trm.TaskRunner('Ant'); - tr.setInput('antBuildFile', '/build/build.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - tr.setInput('classFilesDirectories', 'class1'); - - tr.run() - .then(() => { - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\\build\\CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\\build\\CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=JaCoCo;\]/) >= 0 || tr.stdout.search(/##vso\[codecoverage.enable buildfile=\/build\/build.xml;classfilesdirectories=class1;summaryfile=coverage.xml;reportdirectory=\/build\/CCReport43F6D5EF;ccreporttask=CodeCoverage_9064e1d0;reportbuildfile=\/build\/CCReportBuildA4D283EG.xml;buildtool=Ant;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish/) < 0, 'should have called publish code coverage.'); - done(); - }) - .fail((err) => { - assert.fail("task should not have failed"); - done(err); - }); - }) - it('Ant build with Publish Test Results.', (done) => { setResponseFile('antGood.json'); diff --git a/Tests/L0/Common-CodeCoverageEnabler/_suite.ts b/Tests/L0/Common-CodeCoverageEnabler/_suite.ts new file mode 100644 index 000000000000..69ee7967de42 --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/_suite.ts @@ -0,0 +1,208 @@ +/// <reference path="../../../definitions/mocha.d.ts"/> +/// <reference path="../../../definitions/node.d.ts"/> +/// <reference path="../../../definitions/Q.d.ts"/> +import Q = require('q'); +import assert = require('assert'); +import mockHelper = require('../../lib/mockHelper'); +import path = require('path'); +import fs = require('fs'); +import tl = require('../../lib/vsts-task-lib/toolRunner'); + +// Paths aren't the same between compile time and run time. This will need some work +let realrequire = require; +function myrequire(module: string): any { + return realrequire(path.join(__dirname, "../../../Tasks/Ant/node_modules", module)); +} +require = <typeof require>myrequire; +import {CodeCoverageEnablerFactory} from 'codecoverage-tools/codecoveragefactory'; + +function setResponseFile(name: string) { + process.env['MOCK_RESPONSES'] = path.join(__dirname, name); +} + +describe('Code Coverage enable tool tests', function () { + this.timeout(20000); + + let data = path.join(__dirname, "data"); + let buildProps: { [key: string]: string } = {}; + buildProps['classfilter'] = "+:com.abc,-:com.xyz" + buildProps['classfilesdirectories'] = "cfd"; + buildProps['sourcedirectories'] = "sd"; + buildProps['summaryfile'] = "coverage.xml"; + buildProps['reportdirectory'] = path.join(data, "CCReport43F6D5EF"); + buildProps['ccreporttask'] = "CodeCoverage_9064e1d0" + buildProps['reportbuildfile'] = path.join(data, "CCReportBuildA4D283EG.xml"); + + before((done) => { + Q.longStackSupport = true; + done(); + }); + + after(function () { + }); + + /* Maven build tool - Code Coverage */ + it('Maven single module build file with Jacoco CC', (done) => { + let buildFile = path.join(data, "single_module_pom.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("maven", "jacoco"); + ccEnabler.enableCodeCoverage(buildProps).then(function () { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`<include>**/com/abc.class</include>`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`<exclude>**/com/xyz.class</exclude>`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`jacoco-maven-plugin`), -1, "Jacoco maven plugin must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Maven multi module build file with Jacoco CC', (done) => { + let buildFile = path.join(data, "multi_module_pom.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("maven", "jacoco"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`<include>**/com/abc.class</include>`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`<exclude>**/com/xyz.class</exclude>`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`jacoco-maven-plugin`), -1, "Jacoco maven plugin must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Maven single module build file with Cobertura CC', (done) => { + let buildFile = path.join(data, "single_module_pom.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("maven", "cobertura"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`<include>com/abc.class</include>`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`<exclude>com/xyz.class</exclude>`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`cobertura-maven-plugin`), -1, "Cobertura maven plugin must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Maven multi module build file with Cobertura CC', (done) => { + let buildFile = path.join(data, "multi_module_pom.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("maven", "cobertura"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`<include>com/abc.class</include>`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`<exclude>com/xyz.class</exclude>`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`cobertura-maven-plugin`), -1, "Cobertura maven plugin must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + /* Gradle build tool - Code Coverage */ + it('Gradle single module build file with Jacoco CC', (done) => { + let buildFile = path.join(data, "single_module_build.gradle"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("gradle", "jacoco"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`def jacocoIncludes = ['com/abc.class']`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`def jacocoExcludes = ['com/xyz.class']`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`finalizedBy jacocoTestReport`), -1, "Jacoco report task must be present"); + assert.notEqual(content.indexOf(`apply plugin: 'jacoco'`), -1, "Jacoco gradle plugin must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Gradle multi module build file with Jacoco CC', (done) => { + let buildFile = path.join(data, "multi_module_build.gradle"); + buildProps['buildfile'] = buildFile; + buildProps['ismultimodule'] = "true"; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("gradle", "jacoco"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`def jacocoExcludes = ['com/xyz.class']`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`def jacocoIncludes = ['com/abc.class']`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`jacocoRootReport`), -1, "Jacoco task must be enabled"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Gradle single module build file with Cobertura CC', (done) => { + let buildFile = path.join(data, "single_module_build.gradle"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("gradle", "cobertura"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`cobertura.coverageIncludes = ['.*com.abc']`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`cobertura.coverageExcludes = ['.*com.xyz']`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`net.saliman:gradle-cobertura-plugin`), -1, "Cobertura Plugin must be present"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Gradle multi module build file with Cobertura CC', (done) => { + let buildFile = path.join(data, "multi_module_build.gradle"); + buildProps['buildfile'] = buildFile; + buildProps['ismultimodule'] = "true"; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("gradle", "cobertura"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`cobertura.coverageIncludes = ['.*com.abc']`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`cobertura.coverageExcludes = ['.*com.xyz']`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`net.saliman:gradle-cobertura-plugin`), -1, "Cobertura Plugin must be present"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + /* Ant build tool - Code Coverage */ + it('Ant build file with Jacoco CC', (done) => { + let buildFile = path.join(data, "ant_build.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("ant", "jacoco"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(content.indexOf(`excludes="**/com/xyz.class"`), -1, "Exclude filter must be present"); + assert.notEqual(content.indexOf(`includes="**/com/abc.class"`), -1, "Include filter must be present"); + assert.notEqual(content.indexOf(`jacoco:coverage destfile="jacoco.exec"`), -1, "Jacoco Plugin must be present"); + done(); + }).catch(function (err) { + done(err); + }); + }) + + it('Ant build file with Cobertura CC', (done) => { + let buildFile = path.join(data, "ant_build.xml"); + buildProps['buildfile'] = buildFile; + + let ccEnabler = new CodeCoverageEnablerFactory().getTool("ant", "cobertura"); + ccEnabler.enableCodeCoverage(buildProps).then(function (resp) { + let content = fs.readFileSync(buildFile, "utf-8"); + assert.notEqual(fs.existsSync(path.join(data, buildProps['reportbuildfile'])), true, "Report file must be present"); + assert.notEqual(content.indexOf(`cobertura-classpath`), -1, "Jacoco Plugin must be present"); + done(); + }).catch(function (err) { + done(err); + }); + }) +}); \ No newline at end of file diff --git a/Tests/L0/Common-CodeCoverageEnabler/data/ant_build.xml b/Tests/L0/Common-CodeCoverageEnabler/data/ant_build.xml new file mode 100644 index 000000000000..aca4cfce6d50 --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/data/ant_build.xml @@ -0,0 +1,38 @@ +<project name="JunitTest" default="test" basedir="."> + <property name="testdir" location="test" /> + <property name="srcdir" location="src" /> + <property name="libdir" value="lib"/> + <property name="reportdir" location="report" /> + <property name="full-compile" value="true" /> + + <path id="classpath.base"/> + <path id="classpath.test"> + <pathelement location="${testdir}" /> + <pathelement location="${srcdir}" /> + <path refid="classpath.base" /> + <pathelement location="lib/junit-4.12.jar" /> + </path> + + <target name="clean" > + <delete verbose="${full-compile}"> + <fileset dir="${testdir}" includes="**/*.class" /> + <fileset dir="." includes="**/Testresult*.xml" /> + </delete> + </target> + + <target name="compile" depends="clean"> + <javac srcdir="${srcdir}" destdir="${testdir}" verbose="${full-compile}"> + <classpath refid="classpath.test"/> + </javac> + </target> + + <target name="test" depends="compile"> + <junit fork="no" haltonfailure="false" printsummary="true" haltonerror="false" showoutput="false"> + <classpath refid="classpath.test" /> + <formatter type="plain" usefile="false" /> + <test name="app.test.UtilTest" outfile="Testresult_UtilTest"> + <formatter type="xml"/> + </test> + </junit> + </target> +</project> \ No newline at end of file diff --git a/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_build.gradle b/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_build.gradle new file mode 100644 index 000000000000..817ad287abbd --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_build.gradle @@ -0,0 +1,11 @@ +allprojects { + repositories { + mavenCentral() + } +} + +subprojects { + + apply plugin: 'java' + +} diff --git a/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_pom.xml b/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_pom.xml new file mode 100644 index 000000000000..db2de4b1ac49 --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/data/multi_module_pom.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.mlesniak.jacoco</groupId> + <artifactId>module-main</artifactId> + <version>1.0-SNAPSHOT</version> + <packaging>pom</packaging> + + <modules> + <module>module-1</module> + <module>module-2</module> + </modules> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <build> + <plugins> + </plugins> + </build> + + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/Tests/L0/Common-CodeCoverageEnabler/data/single_module_build.gradle b/Tests/L0/Common-CodeCoverageEnabler/data/single_module_build.gradle new file mode 100644 index 000000000000..b2440bd4e36f --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/data/single_module_build.gradle @@ -0,0 +1,10 @@ +repositories { + mavenCentral() +} + +apply plugin: 'java' + +dependencies { + compile 'log4j:log4j:1.2.17' + compile 'junit:junit:4.11' +} diff --git a/Tests/L0/Common-CodeCoverageEnabler/data/single_module_pom.xml b/Tests/L0/Common-CodeCoverageEnabler/data/single_module_pom.xml new file mode 100644 index 000000000000..421944719076 --- /dev/null +++ b/Tests/L0/Common-CodeCoverageEnabler/data/single_module_pom.xml @@ -0,0 +1,21 @@ +<!-- This was working file--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <packaging>jar</packaging> + <version>1.0-SNAPSHOT</version> + <name>my-app</name> + <url>http://maven.apache.org</url> + <build> + </build> + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>3.8.1</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/Tests/L0/Gradle/_suite.ts b/Tests/L0/Gradle/_suite.ts index 79468918b1ee..3f57697af842 100644 --- a/Tests/L0/Gradle/_suite.ts +++ b/Tests/L0/Gradle/_suite.ts @@ -183,6 +183,7 @@ describe('gradle Suite', function () { done(); }) .fail((err) => { + console.log(tr.stdout); done(err); }); }) @@ -554,159 +555,6 @@ describe('gradle Suite', function () { }); }) - it('Gradle with jacoco selected should call enable and publish code coverage for a single module project.', (done) => { - setResponseFile('gradleCCSingleModule.json'); - - var tr = new TaskRunner('Gradle'); - tr.setInput('wrapperScript', 'gradlew'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('tasks', 'build'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/build/test-results/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - - tr.run() - .then(() => { - assert(tr.ran(gradleWrapper + ' properties'), 'it should have run gradlew build'); - assert(tr.ran(gradleWrapper + ' clean build jacocoTestReport'), 'it should have run clean gradlew build'); - assert(tr.invokedToolCount == 2, 'should have only run gradle 2 times'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=build.gradle;summaryfile=summary.xml;reportdirectory=CCReport43F6D5EF;ismultimodule=false;buildtool=Gradle;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\\summary.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0 || - tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\/summary.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Gradle with jacoco selected should call enable and publish code coverage for a multi module project.', (done) => { - setResponseFile('gradleCCMultiModule.json'); - - var tr = new TaskRunner('Gradle'); - tr.setInput('wrapperScript', 'gradlew'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('tasks', 'build'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/build/test-results/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - - tr.run() - .then(() => { - assert(tr.ran(gradleWrapper + ' properties'), 'it should have run gradlew build'); - assert(tr.ran(gradleWrapper + ' clean build jacocoRootReport'), 'it should have run gradlew clean build'); - assert(tr.invokedToolCount == 2, 'should have only run gradle 2 times'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=build.gradle;summaryfile=summary.xml;reportdirectory=CCReport43F6D5EF;ismultimodule=true;buildtool=Gradle;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\\summary.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0 || - tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\/summary.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Gradle with cobertura selected should call enable and publish code coverage.', (done) => { - setResponseFile('gradleCCSingleModule.json'); - - var tr = new TaskRunner('Gradle'); - tr.setInput('wrapperScript', 'gradlew'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('tasks', 'build'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/build/test-results/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'Cobertura'); - - tr.run() - .then(() => { - assert(tr.ran(gradleWrapper + ' properties'), 'it should have run gradlew build'); - assert(tr.ran(gradleWrapper + ' clean build cobertura'), 'it should have run gradlew clean build'); - assert(tr.invokedToolCount == 2, 'should have only run gradle 2 times'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=build.gradle;summaryfile=coverage.xml;reportdirectory=CCReport43F6D5EF;ismultimodule=false;buildtool=Gradle;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=CCReport43F6D5EF\\coverage.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0 || - tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=CCReport43F6D5EF\/coverage.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Gradle with jacoco selected and report generation failed should call enable but not publish code coverage.', (done) => { - setResponseFile('gradleGood.json'); - - var tr = new TaskRunner('Gradle'); - tr.setInput('wrapperScript', 'gradlew'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('tasks', 'build'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/build/test-results/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - - tr.run() - .then(() => { - assert(tr.ran(gradleWrapper + ' properties'), 'it should have run gradlew build'); - assert(tr.ran(gradleWrapper + ' clean build jacocoTestReport'), 'it should have run gradlew clean build'); - assert(tr.invokedToolCount == 2, 'should have only run gradle 2 times'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=build.gradle;summaryfile=summary.xml;reportdirectory=CCReport43F6D5EF;ismultimodule=false;buildtool=Gradle;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish\]/) < 0, 'should not have called publish code coverage.'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - - it('Gradle with cobertura selected and report generation failed should call enable but not publish code coverage.', (done) => { - setResponseFile('gradleGood.json'); - - var tr = new TaskRunner('Gradle'); - tr.setInput('wrapperScript', 'gradlew'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('tasks', 'build'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/build/test-results/TEST-*.xml'); - tr.setInput('codeCoverageTool', 'Cobertura'); - - tr.run() - .then(() => { - assert(tr.ran(gradleWrapper + ' properties'), 'it should have run gradlew build'); - assert(tr.ran(gradleWrapper + ' clean build cobertura'), 'it should have run gradlew clean build'); - assert(tr.invokedToolCount == 2, 'should have only run gradle 2 times'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=build.gradle;summaryfile=coverage.xml;reportdirectory=CCReport43F6D5EF;ismultimodule=false;buildtool=Gradle;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish\]/) < 0, 'should not have called publish code coverage.'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - done(err); - }); - }) - it('Gradle build with publish test results.', (done) => { setResponseFile('gradleGood.json'); diff --git a/Tests/L0/Maven/_suite.ts b/Tests/L0/Maven/_suite.ts index 1eae9b65b47c..13e31c7b7aac 100644 --- a/Tests/L0/Maven/_suite.ts +++ b/Tests/L0/Maven/_suite.ts @@ -1028,239 +1028,6 @@ describe('Maven Suite', function () { }); }); - it('maven calls enable code coverage and publish code coverage when jacoco is selected', (done) => { - setResponseFile('responseCodeCoverage.json'); - var tr = new trm.TaskRunner('Maven', true); - tr.setInput('mavenVersionSelection', 'default'); - tr.setInput('mavenPOMFile', 'pom.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('goals', 'package'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package'), 'it should have run mvn -f pom.xml clean package'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml jacoco:report'), 'it should have run mvn -f pom.xml jacoco:report'); - // calls maven to generate cc report. - assert(tr.invokedToolCount == 3, 'should have only run maven 3 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=CCReport43F6D5EF\/jacoco.xml;reportdirectory=CCReport43F6D5EF;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\/jacoco.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - assert.fail("should not have thrown error"); - done(err); - }); - }) - - it('maven calls enable code coverage and publish code coverage and sonar analysis when jacoco and sonar analysis is selected', function (done) { - // Arrange - createTempDirsForSonarQubeTests(); - var testSrcDir: string = path.join(__dirname, 'data', 'taskreport-valid'); - var testStgDir: string = path.join(__dirname, '_temp'); - - // not a valid PR branch - mockHelper.setResponseAndBuildVars( - path.join(__dirname, 'responseCodeCoverage.json'), - path.join(__dirname, 'new_response.json'), - [["build.sourceBranch", "refspull/6/master"], ["build.repository.provider", "TFSGit"], - ['build.sourcesDirectory', testSrcDir], ['build.artifactStagingDirectory', testStgDir]]); - var responseJsonFilePath: string = path.join(__dirname, 'new_response.json'); - var responseJsonContent = JSON.parse(fs.readFileSync(responseJsonFilePath, 'utf-8')); - - // Add fields corresponding to responses for mock filesystem operations for the following paths - // Staging directories - responseJsonContent = mockHelper.setupMockResponsesForPaths(responseJsonContent, listFolderContents(testStgDir)); - // Test data files - responseJsonContent = mockHelper.setupMockResponsesForPaths(responseJsonContent, listFolderContents(testSrcDir)); - - // Write and set the newly-changed response file - var newResponseFilePath: string = path.join(__dirname, this.test.title + '_response.json'); - fs.writeFileSync(newResponseFilePath, JSON.stringify(responseJsonContent)); - setResponseFile(path.basename(newResponseFilePath)); - - var tr: trm.TaskRunner = setupDefaultMavenTaskRunner(); - tr.setInput('sqAnalysisEnabled', 'true'); - tr.setInput('sqConnectedServiceName', 'ID1'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - tr.setInput('publishJUnitResults', 'true'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml jacoco:report'), 'it should have run mvn -f pom.xml jacoco:report'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package -Dsonar.host.url=http://sonarqubeserver:9000 -Dsonar.login=uname -Dsonar.password=pword -Dsonar.jacoco.reportPath=CCReport43F6D5EF\/jacoco.exec sonar:sonar'), 'it should have run SQ analysis'); - // calls maven to generate cc report. - assert(tr.invokedToolCount == 3, 'should have only run maven 3 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=CCReport43F6D5EF\/jacoco.xml;reportdirectory=CCReport43F6D5EF;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=JaCoCo;summaryfile=CCReport43F6D5EF\/jacoco.xml;reportdirectory=CCReport43F6D5EF;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - console.log(tr.stdout); - console.log(tr.stderr); - console.log(err); - assert.fail("should not have thrown error"); - done(err); - }); - }) - - it('maven calls enable code coverage and not publish code coverage when jacoco is selected and report generation failed', (done) => { - setResponseFile('response.json'); - var tr = new trm.TaskRunner('Maven', true); - tr.setInput('mavenVersionSelection', 'default'); - tr.setInput('mavenPOMFile', 'pom.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('goals', 'package'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('codeCoverageTool', 'JaCoCo'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package'), 'it should have run mvn -f pom.xml clean package'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml jacoco:report'), 'it should have run mvn -f pom.xml jacoco:report'); - // calls maven to generate cc report. - assert(tr.invokedToolCount == 3, 'should have only run maven 3 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=CCReport43F6D5EF\/jacoco.xml;reportdirectory=CCReport43F6D5EF;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=JaCoCo;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish\]/) < 0, 'should not have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - assert.fail("should not have thrown error"); - done(err); - }); - }) - - it('maven calls enable code coverage and publish code coverage when cobertura is selected', (done) => { - setResponseFile('responseCodeCoverage.json'); - var tr = new trm.TaskRunner('Maven', true); - tr.setInput('mavenVersionSelection', 'default'); - tr.setInput('mavenPOMFile', 'pom.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('goals', 'package'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('codeCoverageTool', 'Cobertura'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package'), 'it should have run mvn -f pom.xml clean package'); - assert(tr.invokedToolCount == 2, 'should have only run maven 2 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=target\/site\/cobertura\/coverage.xml;reportdirectory=target\/site\/cobertura;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=target\/site\/cobertura\/coverage.xml;reportdirectory=target\/site\/cobertura;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - assert.fail("should not have thrown error"); - done(err); - }); - }) - - it('maven calls enable code coverage and publish code coverage and sonar analysis when cobertura is selected and sonar analysis enabled', function (done) { - // Arrange - createTempDirsForSonarQubeTests(); - var testSrcDir: string = path.join(__dirname, 'data', 'taskreport-valid'); - var testStgDir: string = path.join(__dirname, '_temp'); - - // not a valid PR branch - mockHelper.setResponseAndBuildVars( - path.join(__dirname, 'responseCodeCoverage.json'), - path.join(__dirname, 'new_response.json'), - [["build.sourceBranch", "refspull/6/master"], ["build.repository.provider", "TFSGit"], - ['build.sourcesDirectory', testSrcDir], ['build.artifactStagingDirectory', testStgDir]]); - var responseJsonFilePath: string = path.join(__dirname, 'new_response.json'); - var responseJsonContent = JSON.parse(fs.readFileSync(responseJsonFilePath, 'utf-8')); - - // Add fields corresponding to responses for mock filesystem operations for the following paths - // Staging directories - responseJsonContent = mockHelper.setupMockResponsesForPaths(responseJsonContent, listFolderContents(testStgDir)); - // Test data files - responseJsonContent = mockHelper.setupMockResponsesForPaths(responseJsonContent, listFolderContents(testSrcDir)); - - // Write and set the newly-changed response file - var newResponseFilePath: string = path.join(__dirname, this.test.title + '_response.json'); - fs.writeFileSync(newResponseFilePath, JSON.stringify(responseJsonContent)); - setResponseFile(path.basename(newResponseFilePath)); - - var tr: trm.TaskRunner = setupDefaultMavenTaskRunner(); - tr.setInput('sqAnalysisEnabled', 'true'); - tr.setInput('sqConnectedServiceName', 'ID1'); - tr.setInput('codeCoverageTool', 'Cobertura'); - tr.setInput('publishJUnitResults', 'true'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package -Dsonar.host.url=http://sonarqubeserver:9000 -Dsonar.login=uname -Dsonar.password=pword sonar:sonar'), 'it should have run SQ analysis'); - assert(tr.invokedToolCount == 2, 'should have only run maven 2 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=target\/site\/cobertura\/coverage.xml;reportdirectory=target\/site\/cobertura;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish codecoveragetool=Cobertura;summaryfile=target\/site\/cobertura\/coverage.xml;reportdirectory=target\/site\/cobertura;\]/) >= 0, 'should have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - assert.fail("should not have thrown error"); - done(err); - }); - }) - - it('maven calls enable code coverage and not publish code coverage when cobertura is selected and report generation failed', (done) => { - setResponseFile('response.json'); - var tr = new trm.TaskRunner('Maven', true); - tr.setInput('mavenVersionSelection', 'default'); - tr.setInput('mavenPOMFile', 'pom.xml'); // Make that checkPath returns true for this filename in the response file - tr.setInput('options', ''); - tr.setInput('goals', 'package'); - tr.setInput('javaHomeSelection', 'JDKVersion'); - tr.setInput('jdkVersion', 'default'); - tr.setInput('codeCoverageTool', 'Cobertura'); - tr.setInput('publishJUnitResults', 'true'); - tr.setInput('testResultsFiles', '**/TEST-*.xml'); - - tr.run() - .then(() => { - assert(tr.ran('/home/bin/maven/bin/mvn -version'), 'it should have run mvn -version'); - assert(tr.ran('/home/bin/maven/bin/mvn -f pom.xml clean package'), 'it should have run mvn -f pom.xml clean package'); - assert(tr.invokedToolCount == 2, 'should have only run maven 2 times'); - assert(tr.resultWasSet, 'task should have set a result'); - assert(tr.stdout.search(/##vso\[codecoverage.enable buildfile=pom.xml;summaryfile=target\/site\/cobertura\/coverage.xml;reportdirectory=target\/site\/cobertura;reportbuildfile=CCReportPomA4D283EG.xml;buildtool=Maven;codecoveragetool=Cobertura;\]/) >= 0, 'should have called enable code coverage.'); - assert(tr.stdout.search(/##vso\[codecoverage.publish\]/) < 0, 'should not have called publish code coverage.'); - assert(tr.stderr.length == 0, 'should not have written to stderr'); - assert(tr.succeeded, 'task should have succeeded'); - done(); - }) - .fail((err) => { - assert.fail("should not have thrown error"); - done(err); - }); - }) - it('Maven build with publish test results', (done) => { setResponseFile('response.json'); var tr = new trm.TaskRunner('Maven', true); diff --git a/common.json b/common.json index 253bff47b5ca..bd01012a82d6 100644 --- a/common.json +++ b/common.json @@ -96,5 +96,23 @@ "module": "nuget-task-common", "dest": "node_modules" } + ], + "Gradle" : [ + { + "module": "codecoverage-tools", + "dest": "node_modules" + } + ], + "Maven" : [ + { + "module": "codecoverage-tools", + "dest": "node_modules" + } + ], + "Ant" : [ + { + "module": "codecoverage-tools", + "dest": "node_modules" + } ] } diff --git a/definitions/codecoveragefactory.d.ts b/definitions/codecoveragefactory.d.ts new file mode 100644 index 000000000000..0161b27b2548 --- /dev/null +++ b/definitions/codecoveragefactory.d.ts @@ -0,0 +1,19 @@ +/// <reference path="./Q.d.ts" /> + +declare module 'codecoverage-tools/codecoveragefactory' { + import * as Q from "q"; + + export interface ICodeCoverageEnabler { + enableCodeCoverage(ccProps: { + [name: string]: string; + }): Q.Promise<boolean>; + } + + export interface ICodeCoverageEnablerFactory { + getTool(buildTool: string, ccTool: string): ICodeCoverageEnabler; + } + + export class CodeCoverageEnablerFactory implements ICodeCoverageEnablerFactory { + getTool(buildTool: string, ccTool: string): ICodeCoverageEnabler; + } +} diff --git a/definitions/fs-extra.d.ts b/definitions/fs-extra.d.ts new file mode 100644 index 000000000000..01a74d0e214d --- /dev/null +++ b/definitions/fs-extra.d.ts @@ -0,0 +1,88 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/2c5c3520d341526c54ca64ca3245d2569dce1d6e/fs-extra/fs-extra.d.ts +declare module "fs-extra" { + export * from "fs"; + + export function copy(src: string, dest: string, callback?: (err: Error) => void): void; + export function copy(src: string, dest: string, filter: CopyFilter, callback?: (err: Error) => void): void; + export function copy(src: string, dest: string, options: CopyOptions, callback?: (err: Error) => void): void; + + export function copySync(src: string, dest: string): void; + export function copySync(src: string, dest: string, filter: CopyFilter): void; + export function copySync(src: string, dest: string, options: CopyOptions): void; + + export function createFile(file: string, callback?: (err: Error) => void): void; + export function createFileSync(file: string): void; + + export function mkdirs(dir: string, callback?: (err: Error) => void): void; + export function mkdirp(dir: string, callback?: (err: Error) => void): void; + export function mkdirs(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void; + export function mkdirp(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void; + export function mkdirsSync(dir: string, options?: MkdirOptions): void; + export function mkdirpSync(dir: string, options?: MkdirOptions): void; + + export function outputFile(file: string, data: any, callback?: (err: Error) => void): void; + export function outputFileSync(file: string, data: any): void; + + export function outputJson(file: string, data: any, callback?: (err: Error) => void): void; + export function outputJSON(file: string, data: any, callback?: (err: Error) => void): void; + export function outputJsonSync(file: string, data: any): void; + export function outputJSONSync(file: string, data: any): void; + + export function readJson(file: string, callback: (err: Error, jsonObject: any) => void): void; + export function readJson(file: string, options: OpenOptions, callback: (err: Error, jsonObject: any) => void): void; + export function readJSON(file: string, callback: (err: Error, jsonObject: any) => void): void; + export function readJSON(file: string, options: OpenOptions, callback: (err: Error, jsonObject: any) => void): void; + + export function readJsonSync(file: string, options?: OpenOptions): any; + export function readJSONSync(file: string, options?: OpenOptions): any; + + export function remove(dir: string, callback?: (err: Error) => void): void; + export function removeSync(dir: string): void; + + export function writeJson(file: string, object: any, callback?: (err: Error) => void): void; + export function writeJson(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void; + export function writeJSON(file: string, object: any, callback?: (err: Error) => void): void; + export function writeJSON(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void; + + export function writeJsonSync(file: string, object: any, options?: OpenOptions): void; + export function writeJSONSync(file: string, object: any, options?: OpenOptions): void; + + export function ensureDir(path: string, cb: (err: Error) => void): void; + export function ensureDirSync(path: string): void; + + export function ensureFile(path: string, cb: (err: Error) => void): void; + export function ensureFileSync(path: string): void; + + export function ensureLink(path: string, cb: (err: Error) => void): void; + export function ensureLinkSync(path: string): void; + + export function ensureSymlink(path: string, cb: (err: Error) => void): void; + export function ensureSymlinkSync(path: string): void; + + export function emptyDir(path: string, callback?: (err: Error) => void): void; + export function emptyDirSync(path: string): boolean; + + export interface CopyFilterFunction { + (src: string): boolean + } + + export type CopyFilter = CopyFilterFunction | RegExp; + + export interface CopyOptions { + clobber?: boolean + preserveTimestamps?: boolean + dereference?: boolean + filter?: CopyFilter + } + + export interface OpenOptions { + encoding?: string; + flag?: string; + } + + export interface MkdirOptions { + fs?: any; + mode?: number; + } +} diff --git a/definitions/string.d.ts b/definitions/string.d.ts new file mode 100644 index 000000000000..65ab17c1c6af --- /dev/null +++ b/definitions/string.d.ts @@ -0,0 +1,131 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/string/string.d.ts +interface StringJS { + length: number; + + s: string; + + between(left: string, right?: string): StringJS; + + camelize(): StringJS; + + capitalize(): StringJS; + + chompLeft(prefix: string): StringJS; + + chompRight(suffix: string): StringJS; + + collapseWhitespace(): StringJS; + + contains(ss: string): boolean; + + count(substring: string): number; + + dasherize(): StringJS; + + decodeHTMLEntities(): StringJS; + + endsWith(ss: string): boolean; + + escapeHTML(): StringJS; + + ensureLeft(prefix: string): StringJS; + + ensureRight(suffix: string): StringJS; + + humanize(): StringJS; + + include(ss: string): boolean; + + isAlpha(): boolean; + + isAlphaNumeric(): boolean; + + isEmpty(): boolean; + + isLower(): boolean; + + isNumeric(): boolean; + + isUpper(): boolean; + + latinise(): StringJS; + + left(n: number): StringJS; + + lines(): string[]; + + pad(len: number, char?: string|number): StringJS; + + padLeft(len: number, char?: string|number): StringJS; + + padRight(len: number, char?: string|number): StringJS; + + parseCSV(delimiter?: string, qualifier?: string, escape?: string, lineDelimiter?: string): string[]; + + repeat(n: number): StringJS; + + replaceAll(ss: string, newStr: string): StringJS; + + strip(...strings: string[]): StringJS; + + right(n: number): StringJS; + + setValue(string: any): StringJS; + + slugify(): StringJS; + + startsWith(prefix: string): boolean; + + stripPunctuation(): StringJS; + + stripTags(...tags: string[]): StringJS; + + template(values: Object, open?: string, close?: string): StringJS; + + times(n: number): StringJS; + + toBoolean(): boolean; + + toCSV(delimiter?: string, qualifier?: string): StringJS; + toCSV(options: { + delimiter?: string, + qualifier?: string, + escape?: string, + encloseNumbers?: boolean, + keys?: boolean + }): StringJS; + + toFloat(precision?: number): number; + + toInt(): number; + + toInteger(): number; + + toString(): string; + + trim(): StringJS; + + trimLeft(): StringJS; + + trimRight(): StringJS; + + truncate(length: number, chars?: string): StringJS; + + underscore(): StringJS; + + unescapeHTML(): StringJS; + + wrapHTML(element?: string, attributes?: Object): StringJS; +} + +declare module "string" { + var S: { + (o: any): StringJS; + VERSION: string; + TMPL_OPEN: string; + TMPL_CLOSE: string; + } + + export = S; +}