I am trying to retrieve a number of flightids from an xml and place them in an array but I keep getting no data displayed. It doesn't seem to find 'flights' but I am not sure why, is there anything wrong with the code below?
import groovy.xml.XmlUtil
def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context)
def response = context.expand( '${SOAP Request#Response}' )
def parsedxml = new XmlSlurper().parseText(response)
def tests= parsedxml.'**'.findAll { it.name() == 'b:TestId'}
log.info tests
//Get the rate plan codes
def testId = { option ->
def res = option.'**'.findAll {it.name() == 'b:TestId'}
if (res) return option.TestId.text()
null
}
def testIdArray = []
tests.each { if (testId(it)) testIdArray << testId(it) }
for (int i = 0; i < testIdArray.size(); i++) {
log.error "TestIds: " + testIdArray[i]
}
log.warn testIdArray.size()
Below is the xml:
<s:Envelope xxx="xxx" xxx="xxx">
<s:Header>
<a:Action s:mustUnderstand="1">xxx</a:Action>
</s:Header>
<s:Body>
<XML1 xmlns="xxx">
<XML2 xmlns:b="xxx" xmlns:i="xxx">
<XML3>
<b:TestId>000000</b:TestId>
</XML3>
<XML3>
<b:TestId>000000</b:TestId>
</XML3>
</XML2>
</XML1>
</s:Body>
</s:Envelope>
Pass the xml string response to below response variable
def xml = new XmlSlurper().parseText(response)
def getFlightIds = { type = '' ->
type ? xml.'**'.findAll { it.name() == type }.collect { it.FlightId.text() } : xml.'**'.findAll {it.FlightId.text()}
}
//Get the respective flight ids
//Use the desired one as you have mentioned none
def inFlightIds = getFlightIds('InboundFlightInformation')
def outFlightIds = getFlightIds('OutboundFlightInformation')
def allFlightIds = getFlightIds()
log.info "Inbound flight ids: ${inFlightIds}"
log.info "Outbound flight ids: ${outFlightIds}"
log.info "All flight ids: ${allFlightIds}"
You can quickly try online Demo
it.name() in your case it is a NodeChild
so, it.name() returns just a name without prefix. it means you should compare it to FlightId (without b: )
if you want to check a namespace (linked to a prefix) then your lookup must be like this:
def flights = parsedxml.'**'.findAll { it.name() == 'FlightId' && it.namespaceURI()=='xxx' }
Related
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def build_log = new URL (Build_Log_URL).getText()
def found_errors = null
for(knownError in knownErrorListbyLine) {
if (build_log.contains(knownError)) {
found_errors = build_log.readLines().findAll{ it.contains(knownError) }
for(error in found_errors) {
println "FOUND ERROR: " + error
}
}
}
I wrote this code to find listed errors in a string, but it takes about 20 seconds.
How can I improve the performance? I would love to learn from this.
Thanks a lot!
list.txt contains a string per line:
Step ... was FAILED
[ERROR] Pod-domainrouter call failed
#type":"ErrorExtender
[postDeploymentSteps] ... does not exist.
etc...
And build logs is where I need to find these errors.
Try this:
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def build_log = new URL (Build_Log_URL)
def found_errors = null
for(knownError in knownErrorListbyLine) {
build_log.eachLine{
if ( it.contains(knownError) ) {
println "FOUND ERROR: " + error
}
}
}
This might be even more performant:
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def build_log = new URL (Build_Log_URL)
def found_errors = null
build_log.eachLine{
for(knownError in knownErrorListbyLine) {
if ( it.contains(knownError) ) {
println "FOUND ERROR: " + error
}
}
}
Attempt using the last one relying on string eachLine instead.
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def build_log = new URL (Build_Log_URL).getText()
def found_errors = null
build_log.eachLine{
for(knownError in knownErrorListbyLine) {
if ( it.contains(knownError) ) {
println "FOUND ERROR: " + error
}
}
}
Try to move build_log.readLines() to the variable outside of the loop.
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def build_log = new URL (Build_Log_URL).getText()
def found_errors = null
def buildLogByLine = build_log.readLines()
for(knownError in knownErrorListbyLine) {
if (build_log.contains(knownError)) {
found_errors = buildLogByLine.findAll{ it.contains(knownError) }
for(error in found_errors) {
println "FOUND ERROR: " + error
}
}
}
Update: using multiple threads
Note: this may help in case errorList size is large enough. And also if the matching errors distributed evenly.
def sublists = knownErrorListbyLine.collate(x)
// int x - the sublist size,
// depends on the knownErrorListbyLine size, set the value to get e. g. 4 sublists (threads).
// Also do not use more than 2 threads per CPU. Start from 1 thread per CPU.
def logsWithErrors = []// list for store results per thread
def lock = new Object()
def threads = sublists.collect { errorSublist ->
Thread.start {
def logs = build_log.readLines()
errorSublist.findAll { build_log.contains(it) }.each { error ->
def results = logs.findAll { it.contains(error) }
synchronized(lock) {
logsWithErrors << results
}
}
}
}
threads*.join() // wait for all threads to finish
logsWithErrors.flatten().each {
println "FOUND ERROR: $it"
}
Also, as was suggested earlier by other user, try to measure the logs download time, it could be the bottleneck:
def errorList = readFile WORKSPACE + "/list.txt"
def knownErrorListbyLine = errorList.readLines()
def start = Calendar.getInstance().timeInMillis
def build_log = new URL(Build_Log_URL).getText()
def end = Calendar.getInstance().timeInMillis
println "Logs download time: ${(end-start)/1000} ms"
def found_errors = null
I am iterating through an XML file and want to print the gpath for each node with a value. I spent the day reading Groovy API docs and trying things, but it seems that what I think is simple, is not implemented in any obvious way.
Here is some code, showing the different things you can get from a NodeChild.
import groovy.util.XmlSlurper
def myXmlString = '''
<transaction>
<payment>
<txID>68246894</txID>
<customerName>Huey</customerName>
<accountNo type="Current">15778047</accountNo>
<txAmount>899</txAmount>
</payment>
<receipt>
<txID>68246895</txID>
<customerName>Dewey</customerName>
<accountNo type="Current">16288</accountNo>
<txAmount>120</txAmount>
</receipt>
<payment>
<txID>68246896</txID>
<customerName>Louie</customerName>
<accountNo type="Savings">89257067</accountNo>
<txAmount>210</txAmount>
</payment>
<payment>
<txID>68246897</txID>
<customerName>Dewey</customerName>
<accountNo type="Cheque">123321</accountNo>
<txAmount>500</txAmount>
</payment>
</transaction>
'''
def transaction = new XmlSlurper().parseText(myXmlString)
def nodes = transaction.'*'.depthFirst().findAll { it.name() != '' }
nodes.each { node ->
println node
println node.getClass()
println node.text()
println node.name()
println node.parent()
println node.children()
println node.innerText
println node.GPath
println node.getProperties()
println node.attributes()
node.iterator().each { println "${it.name()} : ${it}" }
println node.namespaceURI()
println node.getProperties().get('body').toString()
println node.getBody()[0].toString()
println node.attributes()
}
I found a post groovy Print path and value of elements in xml that came close to what I need, but it doesn't scale for deep nodes (see output below).
Example code from link:
transaction.'**'.inject([]) { acc, val ->
def localText = val.localText()
acc << val.name()
if( localText ) {
println "${acc.join('.')} : ${localText.join(',')}"
acc = acc.dropRight(1) // or acc = acc[0..-2]
}
acc
}
Output of example code :
transaction/payment/txID : 68246894
transaction/payment/customerName : Huey
transaction/payment/accountNo : 15778047
transaction/payment/txAmount : 899
transaction/payment/receipt/txID : 68246895
transaction/payment/receipt/customerName : Dewey
transaction/payment/receipt/accountNo : 16288
transaction/payment/receipt/txAmount : 120
transaction/payment/receipt/payment/txID : 68246896
transaction/payment/receipt/payment/customerName : Louie
transaction/payment/receipt/payment/accountNo : 89257067
transaction/payment/receipt/payment/txAmount : 210
transaction/payment/receipt/payment/payment/txID : 68246897
transaction/payment/receipt/payment/payment/customerName : Dewey
transaction/payment/receipt/payment/payment/accountNo : 123321
transaction/payment/receipt/payment/payment/txAmount : 500
Besides help getting it right, I also want to understand why there isn't a simple function like node.path or node.gpath that prints the absolute path to a node.
You could do this sort of thing:
import groovy.util.XmlSlurper
import groovy.util.slurpersupport.GPathResult
def transaction = new XmlSlurper().parseText(myXmlString)
def leaves = transaction.depthFirst().findAll { it.children().size() == 0 }
def path(GPathResult node) {
def result = [node.name()]
def pathWalker = [hasNext: { -> node.parent() != node }, next: { -> node = node.parent() }] as Iterator
(result + pathWalker.collect { it.name() }).reverse().join('/')
}
leaves.each { node ->
println "${path(node)} = ${node.text()}"
}
Which gives the output:
transaction/payment/txID = 68246894
transaction/payment/customerName = Huey
transaction/payment/accountNo = 15778047
transaction/payment/txAmount = 899
transaction/receipt/txID = 68246895
transaction/receipt/customerName = Dewey
transaction/receipt/accountNo = 16288
transaction/receipt/txAmount = 120
transaction/payment/txID = 68246896
transaction/payment/customerName = Louie
transaction/payment/accountNo = 89257067
transaction/payment/txAmount = 210
transaction/payment/txID = 68246897
transaction/payment/customerName = Dewey
transaction/payment/accountNo = 123321
transaction/payment/txAmount = 500
Not sure that's what you want though, as you don't say why it "doesn't scale for deep nodes"
Please help to print the assetNumber. Need to search for specific assetname and print its associated assetNumber using list in groovy.
Currently, it does not print value. It struck once search criteria is entered.
class Main
{
public static void main(String[] args)
{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in))
List list = new ArrayList()
Asset asset = new Asset()
def name
def assetNumber
def assigneeName
def assignedDate
def assetType
String userInput = "Yes"
while(userInput.equalsIgnoreCase("Yes"))
{
println "Enter the asset details:"
asset.name = br.readLine()
asset.assetNumber= Integer.parseInt(br.readLine())
asset.assigneeName = br.readLine()
asset.assignedDate = Date.parse('dd/MM/yyyy', br.readLine())
list.add(asset)
println "Do you want to continue(yes/no)"
userInput = br.readLine()
}
println "Enter the asset type:"
assetType = br.readLine()
println "Asserts with type "+assetType+":"
def items = list.findAll{p->p.name == assetType }
items.each { println it.assetNumber }
}
}
class Asset
{
def name
def assetNumber
def assigneeName
def assignedDate
}
You are overwriting the value in your Asset object, then adding the same object to the list every time
If you create a new asset inside the loop (rather than outside it), it should work...
Something like this:
import groovy.transform.*
class Main {
static main(args) {
def console = System.console()
def list = []
while (true) {
def name = console.readLine 'Enter the asset name:'
def number = console.readLine 'Enter the asset number:'
def assignee = console.readLine 'Assignee name:'
def date = console.readLine 'Date (dd/MM/yyyy):'
list << new Asset(
name: name,
assetNumber: Integer.parseInt(number),
assigneeName: assignee,
assignedDate: Date.parse('dd/MM/yyyy', date)
)
if ('Y' != console.readLine('Enter another (y/n):')?.take(1)?.toUpperCase()) {
break
}
}
def type = console.readLine 'Enter the asset type:'
println "Assets with type $type:"
list.findAll { p -> p.name == type }
.each { println it.assetNumber }
}
#Canonical
static class Asset {
def name
def assetNumber
def assigneeName
def assignedDate
}
}
How to save the test case properties if any of the assertions fail within this groovy script step?
Below is example code:
// define properties required for the script to run.
def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context)
def dataFolder = groovyUtils.projectPath
def vTIDAPI = testRunner.testCase.getPropertyValue("vTIDAPI")
def vTIDDB = testRunner.testCase.getPropertyValue("vTIDDB")
def RefAPI = testRunner.testCase.getPropertyValue("RefAPI")
def RefDB = testRunner.testCase.getPropertyValue("RefDB")
def AmountAPI = testRunner.testCase.getPropertyValue("AmountAPI")
def AmountDB = testRunner.testCase.getPropertyValue("AmountDB")
def CurrencyAPI = testRunner.testCase.getPropertyValue("CurrencyAPI")
def CurrencyDB = testRunner.testCase.getPropertyValue("CurrencyDB")
assert vTIDAPI == vTIDDB
assert RefAPI == RefDB
assert AmountAPI == AmountDB
assert CurrencyAPI == CurrencyDB
Here is the Groovy Script which does compare the given set of properties and on any of the assertion failure, writes the properties to a given file.
You need to change the value of property file name to be stored for variable propFileName variable.
Add more properties to be asserted in the form of key:value pairs format if needed
//Provide / edit the file name to store properties
def propFileName = '/tmp/testCase.properties'
//Define the properties to be matched or asserted ; add more properties if needed
def props = [ 'vTIDAPI':'vTIDDB', 'RefAPI':'RefDB', 'AmountAPI': 'AmountDB', 'CurrencyAPI': 'CurrencyDB']
/**
* Do not edit beyond this point
*/
def writeTestCasePropertiesToFile = {
//Get the test case properties as Properties object
def properties = context.testCase.properties.keySet().inject([:]){map, key -> map[key] = context.testCase.getPropertyValue(key); map as Properties}
log.info properties
assert properties instanceof Properties
properties?.store(new File(propFileName).newWriter(), null)
}
def myAssert = { arg1, arg2 ->
context.testCase.getPropertyValue(arg1) == context.testCase.getPropertyValue(arg2) ? null : "${arg1} value does not match with ${arg2}"
}
def failureMessage = new StringBuffer()
props.collect{ null == myAssert(it.key, it.value) ?: failureMessage.append(myAssert(it.key, it.value)).append('\n')}
if(failureMessage.toString()) {
log.error "Assertion failures:\n ${failureMessage.toString()}"
writeTestCasePropertiesToFile()
throw new Error(failureMessage.toString())
} else {
log.info 'Assertions passed'
}
EDIT: Based on the OP comments
Replace def myAssert = ... with below code fragment.
def myAssert = { arg1, arg2 ->
def actual = context.testCase.getPropertyValue(arg1)
def expected = context.testCase.getPropertyValue(arg2)
actual == expected ? null : "${arg1} value does not match with ${arg2} - api ${actual} vs db ${expected}"
}
I have generate a report based on an execution of a test suite where it creates a folder directory and insert a file displaying the report. This is compiled within a TearDown Script at Test Suite level. Below is the code:
def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context)
def dataFolder = groovyUtils.projectPath
def failedTestCases = 0
def succeedTestCases = 0
def totalTestCases = 0
def testCaseFailed = ""
def testCaseSucceed = ""
def date = new Date()
def folderTime = date.format("yyyy-MM-dd HH-mm-ss")
def hotelId = context.getProperty('hotelid')
def hotelname = context.getProperty('hotelname')
def hoteltype = context.getProperty('hoteltype')
//def propertyValues = ""
//def correlationid = messageExchange.modelItem.testStep.testCase.testSuite.Project.namegetPropertyValue("correlationid")
//Create a folder directory for the responses
RootResultFolder = dataFolder + "\\Test Reports" + "\\xxx_WebAPI - " + folderTime + "\\"
CreateResultFolder = new File(RootResultFolder)
CreateResultFolder.mkdir()
//context.setProperty("RootResultFolder", RootResultFolder)
def fileName = "WebAPI Test Report.txt"
def rootFolder = RootResultFolder + fileName
def logFile = new File(rootFolder)
if(logFile.exists())
{
log.info("Error a file named " + fileName + "already exisits")
}
else
{
runner.results.each { testCaseResult ->
def name = testCaseResult.testCase.name
totalTestCases++
if(testCaseResult.status.toString() == 'FAILED'){
failedTestCases ++
testCaseFailed += "- $name - HAS FAILED \n\n"
//propertyValues += "hotelid - $hotelid, hotelname - $hotelname, hoteltype - $hoteltype \n\n"
testCaseResult.results.each{ testStepResults ->
testStepResults.messages.each() { msg -> log.info msg }
}
}else{
succeedTestCases ++
testCaseSucceed += "- $name - SUCCEED \n\n"
testCaseResult.results.each{ testStepResults ->
testStepResults.messages.each() { msg -> log.info msg }
}
}
}
}
logFile.write "TOTAL TEST CASES SUCCEED: $succeedTestCases of $totalTestCases" + "\n\n" +
testCaseSucceed + "---\n\n" +
"TOTAL TEST CASES FAILED: $failedTestCases of $totalTestCases" + "\n\n" +
testCaseFailed + "\n\n"
What I actually want to do is move the code from Test Suite level and place it in the tear down script at Project level. Now when I run the code from there, it does not generate the file, I'm assuming I need to place the correct paths in as I am not moving to test suite to test case but from project to test suite to testcase to test steps.
My question is really on syntax, I want to develop a report when the whole project is run, it outputs the following results:
Project Name - is it success or failed. If one suite failed then project fails else it passes
Test Suite - Take name of each test suite in project and if passes then place 'Succeed' next to name of test suite else place 'Failed' next to name of test suite
Name of all test cases within test suite. Like the one in screenshot really, 'succeed' next to test cases that have passed and 'failed' next to those that haven't.
Finally the property values. If a test case has failed, capture the property values for that failed test case so we can track which values were entered that caused the failure of the test.
Can somebody help me with the relevant syntax to perform these so then I can peice it into my code and manipulate?
UPDATE:
def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context)
def dataFolder = groovyUtils.projectPath
def date = new Date()
def folderTime = date.format("yyyy-MM-dd HH-mm-ss")
//Create a folder directory for the responses
RootResultFolder = dataFolder + "\\Test Reports" + "\\xxx - " + folderTime + "\\"
CreateResultFolder = new File(RootResultFolder)
CreateResultFolder.mkdir()*/
//context.setProperty("RootResultFolder", RootResultFolder)
def reportFileName = "WebAPI Test Report.txt"
def rootFolder = RootResultFolder + reportFileName
def logFile = new File(rootFolder)
If you look at the TearDown Script of the project, it shows as below i.e., the variables already initialized by soapui.
Issue with your script
So if you look at it, there is runner variable. Also the same variable is available at TearDown script of test suite level. But, these are instances of different Objects. The script used in OP was of suite level which you aware and that is why you are not seeing in the result.
Here is the project level TearDown Script and following in line comments.
/**
*
* Below is the TearDown script for SoapUI Project level
* Which create a custom report in a given file
* Modify the variable "reportFileName" below
*
**/
//Modify the file as needed for report file
//def reportFileName = '/tmp/abctestreport.txt'
//Adding the below as user wants specific directory
//Get the project path
def dataFolder = new com.eviware.soapui.support.GroovyUtils(context).projectPath
//Create today's date for storing response
def today = new Date().format("yyyy-MM-dd")
def filePrefix = "${dataFolder}/TestReports/xxx_WebAPI_${today}" as String
def fileNamePart = new Date().format("yyyy-MM-dd'T'HH.mm.ss")
//creating filename dynamically.
def reportFileName = "${filePrefix}/xxx_WebAPI_TestReport_${fileNamePart}.txt" as String
//NOTE: Not required to edit beyond this point
/**
* This class holds the test case details
**/
class TestCaseResultHolder {
def log
Map<String, String> properties = [:]
boolean status
def createProperties(testCase) {
testCase.getPropertyNames().each { key ->
properties[key] = testCase.getPropertyValue(key)
}
}
def getCaseResult(caseRunner, caseName) {
log.info "Checking test case status ${caseName}"
if ( caseRunner.status.toString() == 'FAILED' ){
log.error "Test case $caseName has failed"
for ( stepResult in caseRunner?.results ){
stepResult.messages.each() { msg -> log.info msg }
}
return false
} else {
log.info "${caseName} is passed"
}
true
}
def buildCaseResult(caseRunner, caseName) {
status = getCaseResult(caseRunner, caseName)
if (!status) {
createProperties(caseRunner.testCase)
}
}
}
/**
* This class holds the test suite details
**/
class SuiteResultsHolder {
def log
Map<String, TestCaseResultHolder> casaeResults = [:]
int testCaseCount = 0
int passedCasesCount = 0
int failedCasesCount = 0
def buildSuiteResults(suiteRunner, suiteName){
log.info "Building results of test suite ${suiteName}"
for ( caseRunner in suiteRunner?.results ) {
def caseName = caseRunner.testCase.name
testCaseCount++
def tcHolder = new TestCaseResultHolder(log: log)
tcHolder.buildCaseResult(caseRunner, caseName)
casaeResults[caseName] = tcHolder
if (tcHolder.status) {
passedCasesCount++
} else {
failedCasesCount++
}
}
}
def getStatus() {
(0 < failedCasesCount) ? false : true
}
}
/**
* This class holds the project details
**/
class ProjectResultsHolder {
def log
Map<String, SuiteResultsHolder> suiteResults = [:]
int suiteCount = 0
int passedSuitecount = 0
int failedSuiteCount = 0
def buildProjectResults(projectRunner, projectName) {
log.info "Building results of test project ${projectName}"
for(suiteRunner in projectRunner?.results) {
def suiteName = suiteRunner.testSuite.name
suiteCount++
def suiteResultsHolder = new SuiteResultsHolder(log: log)
suiteResultsHolder.buildSuiteResults(suiteRunner, suiteName)
suiteResults[suiteName] = suiteResultsHolder
if (suiteResultsHolder.status) {
passedSuitecount++
} else {
failedSuiteCount++
}
}
}
def getStatus() {
(0 < failedSuiteCount) ? false : true
}
}
//Get the status string based on boolean
def getResult(status){ status == true ? 'SUCCEED' : 'FAILED'}
//Draws a line
def drawLine(def letter = '=', def count = 70) { letter.multiply(count)}
//Gets the summary report
def getSummaryReport(project, projectResultHolder) {
def report = new StringBuffer()
report.append(drawLine()).append('\n')
report.append("\t\t\tTest Execution Summary\n")
report.append(drawLine('-', 60)).append('\n')
report.append("Project : ${project.name}\n")
report.append("Result : ${getResult(projectResultHolder.status)}\n")
report.append("Total test suites executed: ${projectResultHolder.suiteCount}\n")
report.append("Test suites passed: ${projectResultHolder.passedSuitecount}\n")
report.append("Test suites failed: ${projectResultHolder.failedSuiteCount}\n")
report.append(drawLine()).append('\n')
report
}
//Gets the test case report
def getTestCaseReport(testCaseReport) {
def report = new StringBuffer()
report.append(drawLine('-', 60)).append('\n')
report.append("\t\tTest Case Details:\n")
report.append(drawLine('-', 60)).append('\n')
testCaseReport.each { kase, tcReport ->
report.append("Name : ${kase}\n")
report.append("Status : ${getResult(tcReport.status)}\n")
if (!tcReport.status) {
report.append("Properties : ${tcReport.properties.toString()}\n")
}
}
report
}
//Get the detailed report
def getDetailedReport(projectResultHolder) {
def report = new StringBuffer()
report.append(drawLine()).append('\n')
report.append("\t\t\tTest Execution Detailed Report\n")
report.append(drawLine()).append('\n')
projectResultHolder.suiteResults.each { suite, details ->
report.append("Test Suite : ${suite}\n")
report.append("Result : ${getResult(details.status)}\n")
report.append("Total Cases : ${details.testCaseCount}\n")
report.append("Cases Passed : ${details.passedCasesCount}\n")
report.append("Cases Failed: ${details.failedCasesCount}\n")
report.append(getTestCaseReport(details.casaeResults))
report.append(drawLine()).append('\n')
report.append(drawLine()).append('\n')
}
report
}
//Save the contents to a file
def saveToFile(file, content) {
if (!file.parentFile.exists()) {
file.parentFile.mkdirs()
log.info "Directory did not exist, created"
}
file.write(content)
assert file.exists(), "${file.name} not created"
}
def holder = new ProjectResultsHolder(log: log)
holder.buildProjectResults(runner, project.name)
def finalReport = new StringBuffer()
finalReport.append(getSummaryReport(project, holder))
finalReport.append(getDetailedReport(holder))
def reportFile = new File(reportFileName)
saveToFile(reportFile, finalReport.toString())
And here is the generated output: