PageRenderTime 60ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/scripts/_Events.groovy

https://bitbucket.org/atlassian/grails-clover-plugin
Groovy | 535 lines | 358 code | 96 blank | 81 comment | 55 complexity | a3c5fc1b00e47dc4ff22a78972f1431a MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1
  1. import org.apache.tools.ant.BuildLogger
  2. import org.apache.tools.ant.Project
  3. // Note: the GRAILS-5755 fix has solved problem with loading dependencies only partially:
  4. // - we don't have to load classes manually using 'classLoader.loadClass(name)' but
  5. // - during plugin installation via 'grails install-plugin' clover.jar is still not available
  6. // - note that after installation it is accessible
  7. // - note that it's also available when installation is made via BuildConfig.groovy)
  8. // Workaround:
  9. // - instead of 'import com.atlassian.clover.ant.tasks.AntInstrumentationConfig + new AntInstrumentationConfig()' we use
  10. // 'com.atlassian.clover.ant.tasks.AntInstrumentationConfig.newInstance()' (thanks to Groovy syntax it does not complain)
  11. // - FileOptimizable does not implement Optimizable interface and the "raw" method TestOptimizer.optimizeObjects() is used
  12. /* SOME CLOVER DEFAULT VALUES */
  13. defCloverSrcDirs = ["src/java", "src/groovy", "test/unit", "test/integration", "test/functional", "grails-app"]
  14. defCloverIncludes = ["**/*.groovy", "**/*.java"]
  15. defCloverExcludes = ["**/conf/**", "**/plugins/**"]
  16. defCloverReportDir = "${projectTargetDir}/clover/report" // flim-flamming between projectWorkDir and build. build is consistent
  17. defCloverHistoryDir = "${basedir}/.cloverhistory"
  18. defCloverReportStyle = "adg"
  19. defCloverReportTitle = metadata["app.name"]
  20. defCloverHistorical = true // by default, we will generate a historical report.
  21. defCloverSnapshotFile = new File("$projectWorkDir", "clover.snapshot") // this location can be overridden via the -clover.snapshotLocation argument
  22. defStoredTestTargetPatterns = []
  23. /* HELPER METHODS */
  24. /**
  25. * Return Class 'org.codehaus.groovy.grails.test.GrailsTestTargetPattern' (grails 2.4.3 and older) or
  26. * 'org.grails.test.GrailsTestTargetPattern' (grails 2.4.4 and newer)
  27. *
  28. * @return Class
  29. * @throws ClassNotFoundException if none of two names is found
  30. */
  31. Class getGrailsTestTargetPatternClass() {
  32. try {
  33. return Class.forName("org.codehaus.groovy.grails.test.GrailsTestTargetPattern")
  34. } catch (ClassNotFoundException ex) {
  35. return Class.forName("org.grails.test.GrailsTestTargetPattern")
  36. }
  37. }
  38. /* EVENT HANDLERS */
  39. eventCompileStart = {kind ->
  40. ConfigObject config = mergeConfig()
  41. // Ants Project is available via: kind.ant.project
  42. if (config.on && config.debug) {
  43. println "Clover: Compile start. Setting 'grover.ast.dump=" + config.dumpAST + "' system property."
  44. }
  45. System.setProperty("grover.ast.dump", "" + config.dumpAST)
  46. }
  47. eventSetClasspath = {URLClassLoader rootLoader ->
  48. // grailsSettings.compileDependencies.each { println it }
  49. ConfigObject config = mergeConfig()
  50. if (config.debug) {
  51. println "Clover: Dumping binding variables:"
  52. binding.variables.each { println it.key + " = " + it.value } // dumps all available vars and their values
  53. }
  54. toggleAntLogging(config)
  55. // automatically enable clover when optimizing
  56. if (config.on || config.optimize) {
  57. println "Clover: Clover is enabled. Configuration: ${config}"
  58. toggleCloverOn(config)
  59. // do not clean when optimizing or when user explicitly set clover.forceClean=false
  60. if ((!config.containsKey('forceClean') || config.forceClean) && !config.optimize) {
  61. // force a clean
  62. def webInf = "${basedir}/web-app/WEB-INF"
  63. def cleanDirs = ["${webInf}/classes", "${webInf}/lib", "${projectWorkDir}/gspcompile", classesDirPath, testDirPath, "${projectWorkDir}/clover"]
  64. println "Clover: Forcing a clean to ensure Clover instrumentation occurs. Disable by setting: clover.forceClean=false "
  65. cleanDirs.each {
  66. ant.delete(dir: it, failonerror: false)
  67. }
  68. }
  69. }
  70. }
  71. eventTestPhasesStart = {phase ->
  72. // binding.variables.each { println it.key + " = " + it.value } // dumps all available vars and their values
  73. defStoredTestTargetPatterns = testNames.collect {
  74. String it -> getGrailsTestTargetPatternClass().newInstance(it)
  75. }
  76. }
  77. class FileOptimizable /* Cannot declare 'implements com.atlassian.clover.api.optimization.Optimizable' due to
  78. problem with dependency resolution during 'grails install-plugin' */ {
  79. final File file
  80. final File baseDir
  81. FileOptimizable(file, baseDir) {
  82. this.file = file
  83. this.baseDir = baseDir
  84. }
  85. String getName() {
  86. sourceFileToClassName(baseDir, file)
  87. }
  88. @Override
  89. String toString() {
  90. return getName()
  91. }
  92. /**
  93. * Gets the corresponding class name for a source file of this test type.
  94. *
  95. * Copied from GrailsTestTypeSupport.groovy
  96. */
  97. String sourceFileToClassName(File sourceDir, File sourceFile) {
  98. String relativePath = getRelativePathName(sourceDir, sourceFile)
  99. def suffixPos = relativePath.lastIndexOf(".")
  100. relativePath[0..(suffixPos - 1)].replace(File.separatorChar, '.' as char)
  101. }
  102. String getRelativePathName(File sourceDir, File sourceFile) {
  103. def filePath = sourceFile.canonicalPath
  104. def basePath = sourceDir.canonicalPath
  105. if (!filePath.startsWith(basePath)) {
  106. throw new IllegalArgumentException("File path (${filePath}) is not descendent of base path (${basePath}).")
  107. }
  108. return filePath.substring(basePath.size() + 1)
  109. }
  110. }
  111. eventTestCompileStart = { type ->
  112. ConfigObject config = mergeConfig()
  113. if (config.optimize || config.on) {
  114. // GrailsProjectTestRunner has been introduced in grails 2.3 so let's check if we've got it here
  115. // using getVariable() instead of hasVariable() because the latter is not available in grails 1.3
  116. try {
  117. getVariable('projectTestRunner')
  118. // copy config from the current project to the testRunner's internal one
  119. def antConfig = com.atlassian.clover.ant.tasks.AntInstrumentationConfig.getFrom(ant.project)
  120. antConfig.setIn(projectTestRunner.projectTestCompiler.ant.project)
  121. // add GroovycSupport's build listener to this project, it will reconfigure groovyc tasks (since grails 2.3)
  122. com.atlassian.clover.ant.groovy.GroovycSupport.ensureAddedTo(projectTestRunner.projectTestCompiler.ant.project)
  123. } catch (MissingPropertyException ex) {
  124. // ignore
  125. }
  126. }
  127. }
  128. eventTestCompileEnd = { type ->
  129. def phasesToRun = [type.name]
  130. ConfigObject config = mergeConfig()
  131. if (config.optimize) {
  132. println "Clover: Test source compilation phase ended"
  133. def antInstrConfig = com.atlassian.clover.ant.tasks.AntInstrumentationConfig.getFrom(ant.project)
  134. def builder = com.atlassian.clover.api.optimization.OptimizationOptions.Builder.newInstance()
  135. def options = builder.enabled(true).
  136. debug(true).
  137. initString(antInstrConfig.initString).
  138. snapshot(defCloverSnapshotFile).build()
  139. println "Clover: Configuring test optimization with options " + options.toString()
  140. def optimizer = com.atlassian.clover.api.optimization.TestOptimizer.newInstance(options)
  141. // convert the testTargetPatterns into a list of optimizables...
  142. List optimizables = new ArrayList()
  143. // for each phase, gather source files and turn into optimizables
  144. phasesToRun.each {phaseName ->
  145. List<File> files = new LinkedList<File>()
  146. defStoredTestTargetPatterns.each { files.addAll(scanForSourceFiles(it, binding, phaseName.toString())) }
  147. files.each { optimizables << new FileOptimizable(it, new File("test", phaseName)) }
  148. if (config.verbose) {
  149. println("Clover: Tests to be optimized in ${phaseName} test phase: " + optimizables.toListString())
  150. }
  151. }
  152. List optimizedTests = optimizer.optimizeObjects(optimizables)
  153. final List/*<GrailsTestTargetPattern>*/ optimizedTestTargetPatterns = new LinkedList/*<GrailsTestTargetPattern>*/()
  154. optimizedTests.each {
  155. // String className = it.getName()
  156. final String className = (String)it.getClass().getMethod("getName").invoke(it)
  157. optimizedTestTargetPatterns << getGrailsTestTargetPatternClass().newInstance(createTestPattern(className))
  158. }
  159. println("Clover: Test Optimization selected " + optimizedTestTargetPatterns.size() + " out of " + optimizables.size() + " tests for execution")
  160. if (config.verbose) {
  161. println("Clover: Selected tests: " + optimizedTestTargetPatterns)
  162. }
  163. // Set variable read by _GrailsTest.groovy
  164. testTargetPatterns = optimizedTestTargetPatterns.toArray() /*as GrailsTestTargetPattern[]*/
  165. }
  166. }
  167. private String createTestPattern(String name) {
  168. return name.endsWith("Tests") ? name.substring(0, name.lastIndexOf("Tests")) : name
  169. }
  170. private List<File> scanForSourceFiles(Object/*GrailsTestTargetPattern*/ targetPattern, Binding binding, String phaseName) {
  171. def sourceFiles = []
  172. def resolveResources = binding['resolveResources']
  173. def testSuffixes = ['']
  174. def testExtensions = ["java", "groovy"]
  175. def sourceDir = new File("test", phaseName)
  176. testSuffixes.each { suffix ->
  177. testExtensions.each { extension ->
  178. def resources = resolveResources("file:${sourceDir.absolutePath}/${targetPattern.filePattern}${suffix}.${extension}".toString())
  179. sourceFiles.addAll(resources*.file.findAll { it.exists() }.toList())
  180. }
  181. }
  182. sourceFiles
  183. }
  184. eventTestPhasesEnd = {
  185. ConfigObject config = mergeConfig()
  186. if (!config.on && !config.optimize) {
  187. return
  188. }
  189. println "Clover: Tests ended. Generating reports"
  190. def historyDir = config.historydir ?: defCloverHistoryDir
  191. def reportLocation = config.reportdir ?: defCloverReportDir
  192. def reportStyle = config.reportStyle ?: defCloverReportStyle
  193. def historical = defCloverHistorical
  194. if (config.historical != null) {
  195. historical = config.historical
  196. }
  197. if (historical) {
  198. if (!config.historypointtask) {
  199. println "Clover: Generating history point using default 'clover-historypoint' task"
  200. ant.'clover-historypoint'(historyDir: historyDir)
  201. }
  202. else {
  203. println "Clover: Generating history point using custom 'config.historypointtask' closure"
  204. config.historypointtask(ant, binding)
  205. }
  206. }
  207. if (!config.reporttask) {
  208. println "Clover: Generating report using default 'clover-report' task"
  209. ant.'clover-report' {
  210. ant.current(outfile: reportLocation, title: config.title ?: defCloverReportTitle) {
  211. format(type: "html", reportStyle: reportStyle)
  212. ant.columns {
  213. lineCount()
  214. filteredElements()
  215. uncoveredElements()
  216. totalPercentageCovered()
  217. }
  218. }
  219. if (historical) {
  220. ant.historical(outfile: reportLocation, historyDir: historyDir)
  221. }
  222. ant.current(outfile: "${reportLocation}/clover.xml") {
  223. format(type: "xml")
  224. }
  225. if (config.json) {
  226. ant.current(outfile: reportLocation) {
  227. format(type: "json")
  228. }
  229. }
  230. }
  231. if (config.view) {
  232. launchReport(reportLocation)
  233. }
  234. } else {
  235. // reporttask is a user defined closure that takes a single parameter that is a reference to the org.codehaus.gant.GantBuilder instance.
  236. // this closure can be used to generate a custom html report.
  237. // see : http://groovy.codehaus.org/Using+Ant+from+Groovy
  238. println "Clover: Generating report using custom 'config.reporttask' closure"
  239. config.reporttask(ant, binding, this)
  240. }
  241. // TODO: if -clover.optimize, save a snapshot file to -clover.snapshotLocation
  242. if (config.optimize) {
  243. println "Clover: Saving optimization snapshot"
  244. ant.'clover-snapshot'(file: defCloverSnapshotFile)
  245. }
  246. }
  247. /**
  248. * Tries to launch a HTML report in your browser.
  249. *
  250. * If only a single test was run, then just that test's page will be shown.
  251. * Otherwise, the dashboard page is displayed. This is useful if using IDEA/Eclipse to run grails tests.
  252. *
  253. * @param reportLocation the directory containing the report to launch
  254. * @return
  255. */
  256. def launchReport(reportLocation) {
  257. File openFile = new File(reportLocation, "index.html")
  258. if (openFile.exists()) {
  259. // if there is a wildcard in the testname, we can't do anything...
  260. if (testNames) {
  261. String testName = testNames[0].replace((char) '.', File.separatorChar)
  262. String suffix = testName.toString().endsWith("Tests") ? "" : "Tests"
  263. File testFile = new File(reportLocation, testName + suffix + ".html")
  264. openFile = testFile.exists() ? testFile : openFile
  265. }
  266. String openLoc = openFile.toURI().toString()
  267. println "Clover: About to launch broswer: ${openLoc}"
  268. com.atlassian.clover.reporters.util.BrowserLaunch.openURL openLoc
  269. }
  270. }
  271. def toggleCloverOn(ConfigObject clover) {
  272. configureLicense(clover)
  273. ant.taskdef(resource: 'cloverlib.xml')
  274. ant.'clover-env'()
  275. // create an AntInstrumentationConfig object, and set this on the ant project
  276. def antConfig = com.atlassian.clover.ant.tasks.AntInstrumentationConfig.newInstance(ant.project)
  277. configureAntInstr(clover, antConfig)
  278. antConfig.setIn(ant.project)
  279. if (clover.setuptask) {
  280. println "Clover: using custom clover-setup configuration."
  281. clover.setuptask(ant, binding, this)
  282. } else {
  283. println "Clover: using default clover-setup configuration."
  284. final String initString = clover.get("initstring") != null ? clover.initstring : "${projectWorkDir}/clover/db/clover.db"
  285. antConfig.initstring = initString
  286. def cloverSrcDirs = clover.srcDirs ? clover.srcDirs : this.defCloverSrcDirs
  287. def cloverIncludes = clover.includes ? clover.includes : this.defCloverIncludes
  288. def cloverExcludes = clover.excludes ? clover.excludes : this.defCloverExcludes
  289. println """Clover:
  290. directories: ${cloverSrcDirs}
  291. includes: ${cloverIncludes}
  292. excludes ${cloverExcludes}"""
  293. ant.'clover-setup'(initString: initString, tmpDir: "${projectWorkDir}/clover/tmp") {
  294. cloverSrcDirs.each {dir ->
  295. if (new File(dir.toString()).exists()) {
  296. ant.fileset(dir: dir) {
  297. cloverExcludes.each { exclude(name: it) }
  298. cloverIncludes.each { include(name: it) }
  299. }
  300. }
  301. }
  302. }
  303. }
  304. if (clover.snapshotLocation) {
  305. defCloverSnapshotFile = new File(clover.snapshotLocation)
  306. }
  307. }
  308. /**
  309. * Populates an AntInstrumentationConfig instance with any matching properties in the ConfigObject.
  310. * Currently only primitive boolean, int and long are supported. As are String.
  311. */
  312. private configureAntInstr(ConfigObject clover, antConfig) {
  313. return clover.each {
  314. if (antConfig.getProperties().containsKey(it.key)) {
  315. String setter = MetaProperty.getSetterName(it.key)
  316. MetaProperty property = antConfig.metaClass.getMetaProperty(it.key.toString())
  317. final val
  318. switch (property.type) {
  319. case Integer.getPrimitiveClass("int"):
  320. val = it.value.toInteger()
  321. break
  322. case Long.getPrimitiveClass("long"):
  323. val = it.value.toLong()
  324. break
  325. case Boolean.getPrimitiveClass("boolean"):
  326. val = (it.value == null || Boolean.parseBoolean(it.value.toString()))
  327. break
  328. case File:
  329. val = new File(it.value.toString())
  330. break
  331. default:
  332. val = it.value
  333. }
  334. antConfig.invokeMethod(setter, val)
  335. }
  336. }
  337. }
  338. private configureLicense(ConfigObject clover) {
  339. // the directories to search for a clover.license file
  340. final String[] licenseSearchPaths = ["${userHome}", "${basedir}", "${basedir}/etc", "${grailsWorkDir}"]
  341. // the name of the system property that holds the clover license file
  342. final LICENSE_PROP = 'clover.license.path'
  343. final license
  344. if (clover.license.path) {
  345. license = clover.license.path
  346. } else {
  347. licenseSearchPaths.each {
  348. final String licensePath = "${it}/clover.license"
  349. if (new File(licensePath).exists()) {
  350. license = licensePath
  351. return
  352. }
  353. }
  354. }
  355. if (!license) {
  356. println "Clover: License is not configured, a built-in license will be used."
  357. } else {
  358. System.setProperty(LICENSE_PROP, license)
  359. println "Clover: Using Clover license path: ${System.getProperty LICENSE_PROP}"
  360. }
  361. }
  362. /**
  363. * Sets ant logging level to MSG_DEBUG or MSG_VERBOSE depending whether
  364. * clover.debug=true or clover.verbose=true properties are set.
  365. * @param clover
  366. */
  367. private void toggleAntLogging(ConfigObject clover) {
  368. // get any BuildListeners and turn logging on
  369. if (clover.debug) {
  370. ant.project.buildListeners.each {listener ->
  371. if (listener instanceof BuildLogger) {
  372. listener.messageOutputLevel = Project.MSG_DEBUG
  373. }
  374. }
  375. println "Clover: Ant task logging level set to DEBUG"
  376. } else if (clover.verbose) {
  377. ant.project.buildListeners.each {listener ->
  378. if (listener instanceof BuildLogger) {
  379. listener.messageOutputLevel = Project.MSG_VERBOSE
  380. }
  381. }
  382. println "Clover: Ant task logging level set to VERBOSE"
  383. }
  384. }
  385. /**
  386. * Takes any CLI arguments and merges them with any configuration defined in BuildConfig.groovy in the clover block.
  387. */
  388. private ConfigObject mergeConfig() {
  389. final Map argsMap = parseArguments()
  390. final ConfigObject config = buildConfig.clover == null ? new ConfigObject() : buildConfig.clover
  391. final ConfigSlurper slurper = new ConfigSlurper()
  392. final Properties props = new Properties()
  393. props.putAll(argsMap)
  394. final ConfigObject argsMapConfig = slurper.parse(props)
  395. config.merge(argsMapConfig.clover)
  396. return config
  397. }
  398. // Copied from _GrailsArgParsing.groovy since _GrailsCompile.groovy does not depend on parseArguments target
  399. // and the argsMap is not populated in time for the testStart event.
  400. // see: http://jira.codehaus.org/browse/GRAILS-2663
  401. private Map parseArguments() {
  402. // Only ever parse the arguments once. We also don't bother parsing
  403. // the arguments if the "args" string is empty.
  404. // if (argsMap.size() > 1 || argsMap["params"] || !args) return
  405. argsMap = [params: []]
  406. args?.tokenize().each { token ->
  407. def nameValueSwitch = token =~ "--?(.*)=(.*)"
  408. if (nameValueSwitch.matches()) { // this token is a name/value pair (ex: --foo=bar or -z=qux)
  409. final value = nameValueSwitch[0][2]
  410. argsMap[nameValueSwitch[0][1]] = "false".equalsIgnoreCase(value) ? false : value
  411. }
  412. else {
  413. def nameOnlySwitch = token =~ "--?(.*)"
  414. if (nameOnlySwitch.matches()) { // this token is just a switch (ex: -force or --help)
  415. argsMap[nameOnlySwitch[0][1]] = true
  416. }
  417. else { // single item tokens, append in order to an array of params
  418. argsMap["params"] << token
  419. }
  420. }
  421. }
  422. if (argsMap.containsKey('non-interactive')) {
  423. println "Clover: Setting non-interactive mode"
  424. isInteractive = !(argsMap.'non-interactive')
  425. }
  426. return argsMap
  427. }
  428. /**
  429. * Initialize Clover stuff as soon as plugin is installed. We don't do this in _Install.groovy script,
  430. * because we need access to project configuration object as well as global variables from _Events.groovy.
  431. */
  432. eventPluginInstalled = { fullPluginName ->
  433. if (((String) fullPluginName).startsWith("clover-")) {
  434. println "Clover: Plugin was installed, loading new Ant task definitions ..."
  435. // Call event callback internally in order to load clover task definitions,
  436. // toggle on Clover, set global variables like source paths etc
  437. eventSetClasspath(null)
  438. }
  439. }