diff --git a/app.js b/app.js index 16602180..f131b840 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -49,7 +48,8 @@ var express = require('express') , actionHistory = require('./routes/actionHistory') , versionControl = require('./routes/versionControl') , syncIDE = require('./routes/syncIDE') - , testcaseHistory = require('./routes/testcaseHistory'); + , testcaseHistory = require('./routes/testcaseHistory') + , remoteexecution = require('./routes/remoteexecution'); var realFs = require("fs"); var gracefulFs = require("graceful-fs"); @@ -164,6 +164,9 @@ app.post('/variableTags',auth.auth, variableTags.variableTagsPost); //start execution app.post('/executionengine/startexecution', executionengine.startexecutionPost); +// remote execution +app.get('/api/remoteexecution/startexecution', remoteexecution.startexecutionPost); + //stop app.post('/executionengine/stopexecution',auth.auth, executionengine.stopexecutionPost); app.post('/executionengine/actionresult',executionengine.actionresultPost); @@ -312,6 +315,7 @@ common.parseConfig(function(){ common.cleanUpExecutions(); common.cleanUpUserStatus(function(){ var server = require('http').createServer(app); + server.setTimeout(parseInt(common.Config.DefaultTimeout)); server.listen(common.Config.AppServerPort, function(){ realtime.initSocket(server); common.logger.log("Express server listening on port %d in %s mode", common.Config.AppServerPort, app.settings.env); diff --git a/properties.conf b/properties.conf index a72c8065..151bfa19 100644 --- a/properties.conf +++ b/properties.conf @@ -1,15 +1,18 @@ [Agent] -AgentVersion=1.00.01 +AgentVersion=2.40.20 AgentPort=5009 AgentVNCPort=3006 -Update=PASS +Update= OS=Windows -CloudAgent=true [Server] -ServerVersion= +ServerVersion=2.40.20 DBPort=27017 AppServerPort=3000 -AppServerIPHost=localhost -AppServerIPHost22=10.130.10.35 -AppServerIPHost22=192.168.10.107 \ No newline at end of file +AppServerIPHost=127.0.0.1 +# Default timeout for REST API in Milliseconds +DefaultTimeout=1200000 + +[Windows] +VNC=Yes +AppShortcutFolderName=RedwoodHQ diff --git a/public/controller/Executions.js b/public/controller/Executions.js index b47434b7..2527fc51 100644 --- a/public/controller/Executions.js +++ b/public/controller/Executions.js @@ -359,6 +359,15 @@ Ext.define("Redwood.controller.Executions", { executionView.up("executionsEditor").down("#runExecution").setDisabled(false); executionView.up("executionsEditor").down("#stopExecution").setDisabled(true); } + }, + failure: function(response) { + if (Ext.MessageBox.isVisible()) Ext.MessageBox.hide(); + var obj = Ext.decode(response.responseText); + if(obj.error != null){ + Ext.Msg.alert('Error', Ext.util.Format.htmlEncode(obj.error)); + executionView.up("executionsEditor").down("#runExecution").setDisabled(false); + executionView.up("executionsEditor").down("#stopExecution").setDisabled(true); + } } }); }) diff --git a/routes/executionengine.js b/routes/executionengine.js index 7ae772cf..ab91acbc 100644 --- a/routes/executionengine.js +++ b/routes/executionengine.js @@ -16,6 +16,7 @@ var nodemailer = require("nodemailer"); var spawn = require('child_process').spawn; var os = require('os'); var archiver = require('archiver'); +var elk = require('./elk'); var db; var compilations = {}; var fileSync = {}; @@ -61,13 +62,47 @@ exports.stopexecutionPost = function(req, res){ }); }; - exports.startexecutionPost = function(req, res){ + _startexecutionPost(req, res, function(exitcode){ + console.log("+++++++++++++++++++++++++EXECUTIONS STARTED ++++++++++++++++++++++++++++++++++++++++++++++++") + var executionId = req.body.executionID; + MonitorExecution(executionId, function(){ + // End the startexecution request + console.log('execution completed'); + elk.publishDataToElk(executionId, function(){ + console.log("Data published success: publishDataToElk"); + }); + }); + }); +} + +function MonitorExecution(execId,callback){ + db = common.getDB(); + var getStatus = function(callback){ + db.collection('executions', function(err, collection) { + collection.findOne({_id:execId}, {status:1}, function(err, dbexecution) { + callback(dbexecution.status); + }); + }); + }; + var verifyStatus = function(status){ + if(status == "Ready To Run"){ + setTimeout(function(){callback()},10000); + }else{ + setTimeout(function(){getStatus(verifyStatus)},10000) + } + }; + setTimeout(function(){getStatus(verifyStatus)},20000) +} + +var _startexecutionPost = function(req, res, callback){ db = common.getDB(); + res.contentType('json'); if (req.body.testcases.length == 0){ - res.contentType('json'); + res.status(403); res.json({error:"No Test Cases are selected for execution."}); + callback(0); // forbidden return; } var executionID = req.body.executionID; @@ -96,7 +131,10 @@ exports.startexecutionPost = function(req, res){ var machineConflict = false; var updatingConflict = false; + machines.forEach(function(machine){ + machine.threads = (machine.threads) ? machine.threads : 1; + console.log("validating thread count " + machine.threads); if (machine.state == "Running Test"){ machineConflict = true; } @@ -107,20 +145,27 @@ exports.startexecutionPost = function(req, res){ }); if(machineConflict == true){ - res.contentType('json'); + console.log("selected machines are running tests +++++++++"); + res.status(409); res.json({error:"Selected machines are currently running other tests."}); + callback(0); // conflict return; } if(updatingConflict == true){ - res.contentType('json'); + console.log("selected machines are running tests 2 +++++++++"); + res.status(409); res.json({error:"Selected machines are being updated."}); + callback(0); // conflict return; } if(executions[executionID]){ + console.log("selected machines are running tests 3 +++++++++"); res.contentType('json'); + res.status(409); res.json({error:"Execution is already running."}); + callback(0); // conflict return; } @@ -134,9 +179,11 @@ exports.startexecutionPost = function(req, res){ compileBuild(req.cookies.project,req.cookies.username,function(err){ if (err != null){ res.contentType('json'); + res.status(403); res.json({error:"Unable to compile scripts."}); updateExecution({_id:executionID},{$set:{status:"Ready To Run"}},true); delete executions[executionID]; + callback(0); // forbidden } else{ //copy files for each execution to prevent conflicts @@ -149,16 +196,22 @@ exports.startexecutionPost = function(req, res){ executions[executionID].sourceCache = sourceCache; } else{ + console.log("internal error +++++++++"); + /*res.status(500); + res.json({error:"Internal Error"});*/ + callback(0); return; } verifyMachineState(machines,function(err){ if(err){ + console.error("verifymachinestate failed +++++++++"); updateExecution({_id:executionID},{$set:{status:"Ready To Run"}},true); - res.contentType('json'); + res.status(403); res.json({error:err}); //git.deleteFiles(path.join(__dirname, '../public/automationscripts/'+req.cookies.project+"/"+req.cookies.username+"/build"),os.tmpDir()+"/jar_"+req.body.executionID); deleteDir(os.tmpDir()+"/jar_"+req.body.executionID); delete executions[executionID]; + callback(0); // forbidden return; } VerifyCloudCapacity(executions[executionID].template,function(response){ @@ -170,12 +223,15 @@ exports.startexecutionPost = function(req, res){ else{ message = "Cloud does not have the capacity to run this execution." } + console.error("cloud error: failed +++++++++"); updateExecution({_id:executionID},{$set:{status:"Ready To Run",cloudStatus:"Error: "+message}},true); res.contentType('json'); + res.status(500); res.json({error:"Cloud Error: "+message}); //git.deleteFiles(path.join(__dirname, '../public/automationscripts/'+req.cookies.project+"/"+req.cookies.username+"/build"),os.tmpDir()+"/jar_"+req.body.executionID); deleteDir(os.tmpDir()+"/jar_"+req.body.executionID); delete executions[executionID]; + callback(0); //internal error return; } res.contentType('json'); @@ -194,6 +250,8 @@ exports.startexecutionPost = function(req, res){ //git.deleteFiles(path.join(__dirname, '../public/automationscripts/'+req.cookies.project+"/"+req.cookies.username+"/build"),os.tmpDir()+"/jar_"+req.body.executionID); deleteDir(os.tmpDir()+"/jar_"+req.body.executionID); delete executions[executionID]; + console.error("startcloudmachine failed +++++++++"); + callback(0); //elk return; } if(executions[executionID].template){ @@ -223,6 +281,7 @@ exports.startexecutionPost = function(req, res){ }); }); }); + callback(0); // elk }); }); }); @@ -1937,6 +1996,8 @@ function updateExecution(query,update,finished,callback){ } realtime.emitMessage("UpdateExecutions",data); if(finished === true){ + console.log("FinishExecution +++++++++++++++ "); + //console.log(data); realtime.emitMessage("FinishExecution",data); } if (callback){ @@ -1995,7 +2056,7 @@ function verifyMachineState(machines,callback){ machines.forEach(function(machine){ db.collection('machines', function(err, collection) { collection.findOne({_id:new ObjectID(machine._id)}, {}, function(err, dbMachine) { - + //console.log("verifyMachineState : " + machine.threads ); if(dbMachine.maxThreads < dbMachine.takenThreads + machine.threads) { callback("Machine: "+ machine.host+" has reached thread limit."); @@ -2768,4 +2829,3 @@ function deleteDir(path,callback){ } }) } - diff --git a/routes/executions.js b/routes/executions.js index b304cebd..b42111f1 100644 --- a/routes/executions.js +++ b/routes/executions.js @@ -46,6 +46,14 @@ exports.executionsDelete = function(req, res){ Tags.CleanUpExecutionTags(req); }; +exports.deleteExecution = function(db, execution, callback) { + DeleteExecutions(db, execution, function() { + console.log("executions.deleteExecution") + realtime.emitMessage("DeleteExecutions",{id: execution._id}) + callback() + }) +} + exports.executionsPost = function(req, res){ var app = require('../common'); var data = req.body; @@ -162,4 +170,4 @@ function GetExecutions(db,query,callback){ }); }) }) -} \ No newline at end of file +} diff --git a/routes/remoteexecution.js b/routes/remoteexecution.js new file mode 100644 index 00000000..bc70c264 --- /dev/null +++ b/routes/remoteexecution.js @@ -0,0 +1,485 @@ +/* +Add ability to trigger test executions with a URL + +e.g. http://localhost:3000/api/remoteexecution/startexecution?name=Amazon%20Shopping&user=admin&testset=Amazon%20Shopping&machines=127.0.0.1&pullLatest=false&retryCount=1&project=Sample&ignoreScreenshots=true +http header +http body: { user=admin, + testset=Amazon Shopping, + machines=127.0.0.1&pullLatest=false&retryCount=1&project=Sample&ignoreScreenshots=true .... + +*/ +var common = require('../common'); +var app = require('../common'); +var xml = require('xml-writer'); +var fs = require('fs'); +var db; +var ObjectID = require('mongodb').ObjectID; +var http = require("http"); +//var elk = require("./elk"); +var execs = require('./executions'); + +common.initLogger("remoteexecution"); +common.parseConfig(function(){ + +}); + +function _sendStatus(finalResponse, exitDetails) { + console.log("_sendStatus start ----- ") + var statusCode = (!exitDetails || exitDetails.statusCode === 0) ? 200 : exitDetails.statusCode; + console.log("statuscode " + statusCode); + if(statusCode !== 200){ + if(exitDetails.executionId) { + console.log("REMOVING invalid execution records ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + db.collection('executions', function(err, collection) { + console.log("EXECUTION ID: " + exitDetails.executionId) + collection.findOne({_id:exitDetails.executionId}, {status:1}, function(err, dbexecution) { + console.log("Removing invalid record \n") + collection.remove({_id: exitDetails.executionId}); + execs.deleteExecution(db, dbexecution, function() { + console.log("REMOVED ~~~~~~~~~~~~") + }) + console.log("REMOVED: ~~~~~~~" + exitDetails.executionId + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \n") + }); + }); + } + finalResponse.contentType('json'); + finalResponse.status(statusCode); + finalResponse.json({error : exitDetails.error}); + } + finalResponse.end(); + console.log("_sendStatus end") +} + +exports.startexecutionPost = function(req, res){ + var execution = {}; + var query = require('url').parse(req.url,true).query; + execution.project = query.project; + execution.user = query.user; + execution.name = query.name; + execution.status = "Ready To Run"; + execution.locked = true; + execution.ignoreStatus = false; + execution.ignoreAfterState = false; + execution.lastRunDate = null; + execution.testsetname = query.testset; + execution.pullLatest = query.pullLatest; + + if(query.tags){ + execution.tag = query.tags.split(","); + } + if(query.retryCount) { + execution.retryCount = query.retryCount; + } else { + execution.retryCount = '0'; + } + if(query.ignoreScreenshots) { + if(query.ignoreScreenshots === 'true' || + query.ignoreScreenshots === 'false') { + execution.ignoreScreenshots = query.ignoreScreenshots; + } else{ + execution.ignoreScreenshots = false; + } + } + execution._id = new ObjectID().toString(); + if(!_validateQueryParams(query)) { + var exitDetails = {statusCode : 400, error : "Invalid Query Parameters"}; + _sendStatus(res, exitDetails); + return; + } + + if(!db) { + common.initDB(common.Config.DBPort,function(){ + db = app.getDB(); + console.log("call _prepareAndRunExecution 1") + _prepareAndRunExecution(execution, query, res, function() { + console.log("_prepareAndRunExecution completed 1") + }); + }); + } else { + console.log("call _prepareAndRunExecution 2") + _prepareAndRunExecution(execution, query, res, function() { + console.log("_prepareAndRunExecution completed 2") + }) + } +} + +function _prepareAndRunExecution(execution, query, res, callback){ + console.log("_prepareAndRunExecution") + PullLatest(execution,function(){ + formatMachines(query,function(machines) { + execution.machines = machines; + execution.templates = []; + if(query.cloudTemplate){ + execution.templates.push({name:query.cloudTemplate.split(":")[0], + result:"",description:"", + instances:parseInt(query.cloudTemplate.split(":")[1]), + threads:parseInt(query.cloudTemplate.split(":")[2])}); + } + formatTestSet(query.testset, query.project, function(testsetID){ + execution.testset = testsetID.toString(); + saveExecutionTestCases(testsetID,execution._id,function(testcases){ + if(query.variables){ + formatVariables(query.variables.split(","),query.project,function(variables){ + execution.variables = variables; + SaveAndRunExecution(query,execution,testcases,res,function(exitDetails){ + console.log("SaveAndRunExecution ----"); + exitDetails.executionId = execution._id; + _sendStatus(res, exitDetails); + callback() + //db.close(); + }); + }) + } + else{ + execution.variables = []; + SaveAndRunExecution(query,execution,testcases,res,function(exitDetails){ + console.log("SaveAndRunExecution +++++"); + exitDetails.executionId = execution._id; + _sendStatus(res, exitDetails); + callback() + //db.close(); + }); + } + }); + }); + }); + }); +} + +function _validateQueryParams(query) { + var valid = true; + if(!query.project || + !query.user || + !query.testset || + !query.machines || + !query.name) { + valid = false; + } + + return valid; +} + +function saveExecutionTestCases(testsetID,executionID,callback){ + var testcases = []; + db.collection('testsets', function(err, testSetCollection) { + db.collection('executiontestcases', function(err, ExeTCCollection) { + //console.log(testsetID); + testSetCollection.findOne({_id:db.bson_serializer.ObjectID(testsetID.toString())}, {testcases:1}, function(err, dbtestcases) { + dbtestcases.testcases.forEach(function(testcase,index){ + db.collection('testcases', function(err, tcCollection) { + tcCollection.findOne({_id:db.bson_serializer.ObjectID(testcase._id.toString())},{},function(err,dbtestcase){ + if(dbtestcase.tcData && dbtestcase.tcData.length > 0){ + var ddTCCount = 0; + dbtestcase.tcData.forEach(function(row,rowIndex){ + var insertTC = {executionID:executionID,name:dbtestcase.name,tag:dbtestcase.tag,status:"Not Run",testcaseID:testcase._id.toString(),_id: new ObjectID().toString()}; + insertTC.rowIndex = rowIndex+1; + insertTC.name = insertTC.name +"_"+(rowIndex+1); + insertTC.tcData = row; + testcases.push(insertTC); + ExeTCCollection.insert(insertTC, {safe:true},function(err,returnData){ + ddTCCount++; + if(ddTCCount == dbtestcase.tcData.length && index+1 == dbtestcases.testcases.length){ + callback(testcases); + } + }); + }) + } + else{ + var insertTC = {executionID:executionID,name:dbtestcase.name,tag:dbtestcase.tag,status:"Not Run",testcaseID:testcase._id.toString(),_id: new ObjectID().toString()}; + testcases.push(insertTC); + ExeTCCollection.insert(insertTC, {safe:true},function(err,returnData){ + if(index+1 == dbtestcases.testcases.length){ + callback(testcases); + } + }); + } + }); + }); + }); + }); + }); + }); +} + +function PullLatest(execution,callback){ + if(execution.pullLatest !== "true"){ + callback(); + return + } + var options = { + hostname: "localhost", + port: common.Config.AppServerPort, + path: '/scripts/pull', + method: 'POST', + agent:false, + headers: { + 'Content-Type': 'application/json', + 'Cookie': 'username='+execution.user+";project="+execution.project + } + }; + + var req = http.request(options, function(res) { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + //console.log('BODY: ' + chunk); + callback(); + }); + }); + + req.on('error', function(e) { + console.log('problem with request: ' + e.message); + }); + + // write data to request body + req.write(JSON.stringify({})); + req.end(); +} + +function StartExecution(execution,testcases,finalResponse,callback){ + var options = { + hostname: "localhost", + port: common.Config.AppServerPort, + path: '/executionengine/startexecution', + method: 'POST', + agent:false, + headers: { + 'Content-Type': 'application/json', + 'Cookie': 'username='+execution.user+";project="+execution.project + } + }; + + var req = http.request(options, function(res) { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + //console.log('BODY: ' + chunk); + var msg = JSON.parse(chunk); + var exitDetails = {statusCode : res.statusCode, error : ""}; + console.log("startexecution response " + res.statusCode); + if(msg.error){ + exitDetails.error = msg.error; + console.log(msg.error); + } + callback(exitDetails); + }); + }); + + req.on('error', function(e) { + console.log('problem with request: ' + e.message); + }); + + // write data to request body + req.write(JSON.stringify({retryCount:execution.retryCount,ignoreAfterState:false,ignoreStatus:execution.ignoreStatus,ignoreScreenshots:execution.ignoreScreenshots,testcases:testcases,variables:execution.variables,executionID:execution._id,machines:execution.machines,templates:execution.templates})); + req.end(); +} + +function SaveExecution(execution,callback){ + var options = { + hostname: "localhost", + port: common.Config.AppServerPort, + path: '/executions/'+execution._id, + method: 'POST', + agent:false, + headers: { + 'Content-Type': 'application/json', + 'Cookie': 'username='+execution.user+";project="+execution.project + } + }; + + var req = http.request(options, function(res) { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + //console.log('BODY: ' + chunk); + callback(); + }); + }); + + req.on('error', function(e) { + console.log('problem with request: ' + e.message); + }); + + // write data to request body + req.write(JSON.stringify(execution)); + req.end(); +} + +function MonitorExecution(execution,callback){ + var getStatus = function(callback){ + db.collection('executions', function(err, collection) { + collection.findOne({_id:execution._id}, {status:1}, function(err, dbexecution) { + callback(dbexecution.status); + }); + }); + }; + var verifyStatus = function(status){ + if(status == "Ready To Run"){ + setTimeout(function(){callback()},10000); + }else{ + setTimeout(function(){getStatus(verifyStatus)},10000) + } + }; + setTimeout(function(){getStatus(verifyStatus)},20000) +} + +function SaveAndRunExecution(queryParams,execution,testcases,finalResponse,callback){ + SaveExecution(execution,function(){ + StartExecution(execution,testcases,finalResponse,function(exitDetails){ + console.log("StartExecution callback"); + if(exitDetails.statusCode !== 0 && exitDetails.statusCode !== 200) { + callback(exitDetails); + console.log("StartExecution return"); + return; + } + + MonitorExecution(execution,function(){ + var xmlReport = new xml(); + GenerateReport(finalResponse, queryParams,execution,xmlReport,function(exitDetails){ + console.log("GenerateReport completed: ------------------------------"); + finalResponse.set('Content-Type', 'application/xml'); + finalResponse.write(xmlReport.toString()); + finalResponse.end(); + callback(exitDetails); + }) + }); + }) + }) +} + +function formatTestSet(testset,project,callback){ + db.collection('testsets', function(err, collection) { + collection.findOne({name:testset,project:project}, {_id:1}, function(err, dbtestset) { + callback(dbtestset._id); + }); + }); +} + +function formatVariables(clivariables,project,callback){ + var variables = []; + var count = 0; + db.collection('variables', function(err, collection) { + clivariables.forEach(function(variable){ + collection.findOne({name:variable.split("=")[0].trim(),project:project}, {}, function(err, dbvariable) { + dbvariable.value = variable.split("=")[1]; + variables.push(dbvariable); + count++; + if(count == clivariables.length){ + callback(variables); + } + }); + }); + }); +} + +function formatMachines(reqQueryParams,callback){ + if(!reqQueryParams.machines) callback([]); + var machines = reqQueryParams.machines.split(","); + var result = []; + var count = 0; + db.collection('machines', function(err, collection) { + machines.forEach(function(machine){ + collection.findOne({host:machine.split(":")[0]}, {}, function(err, dbmachine) { + if(!dbmachine){ + console.log("Machine: " +machine.split(":")[0]+" not found."); + callback([]); + return; + } + var threadsCount = machine.split(":")[1]; + dbmachine.threads = (threadsCount) ? threadsCount : 1; + console.log("Threads Count : " + threadsCount); + count++; + if(machine.split(":").length == 3){ + db.collection('actions', function(err, actionCollection) { + actionCollection.findOne({name:machine.split(":")[2]}, {}, function(err, action) { + if(action){ + dbmachine.baseState = action._id; + result.push(dbmachine); + if(count == machines.length){ + callback(result); + } + } + else{ + console.log("Suite Base state: " +machine.split(":")[2]+" not found."); + callback([]); + return; + } + }); + }); + } + else{ + result.push(dbmachine); + if(count == machines.length){ + callback(result); + } + } + }); + }); + }); +} + +function GenerateReport(finalResponse, queryParams,cliexecution,xw,callback){ + xw.startDocument(); + xw.startElement('testsuite'); + var exitDetails = {statusCode : 200, error: ""}; + db.collection('executions', function(err, collection) { + collection.findOne({_id:cliexecution._id}, {}, function(err, execution) { + cliexecution = execution; + console.log("GenerateReport started: ------------------------------") + //console.log(execution); + //finalResponse.set('Content-Type', 'application/json'); + //finalResponse.write(JSON.stringify(execution)); + //callback(exitDetails); + //finalResponse.end(); + xw.writeAttribute('errors', "0"); + xw.writeAttribute('failures', execution.failed.toString()); + xw.writeAttribute('tests', execution.total.toString()); + xw.writeAttribute('name',execution.name); + xw.writeAttribute('time',execution.runtime.toString()); + xw.writeAttribute('timestamp',execution.lastRunDate.toString()); + xw.startElement('properties'); + xw.endElement(); + + db.collection('executiontestcases', function(err, collection) { + var failed = false; + collection.find({executionID:execution._id}, {}, function(err, cursor) { + cursor.each(function(err, testcase) { + if(testcase == null) { + xw.endElement(); + xw.endDocument(); + console.log(xw.toString()); + callback(exitDetails); + return; + } + //console.log(testcase); + xw.startElement('testcase'); + xw.writeAttribute('classname',testcase.name); + xw.writeAttribute('name',testcase.name); + xw.writeAttribute('time',(testcase.runtime / 1000).toString()); + if (testcase.result == "Passed"){ + xw.endElement(); + } + else if (testcase.result == "Failed"){ + failed = true; + xw.startElement('failure'); + xw.writeAttribute('type',"Test Case Error"); + xw.writeAttribute('message',testcase.error); + + if(queryParams.resultURL){ + xw.writeCData(queryParams.resultURL+"/index.html?result="+testcase.resultID+"&project="+queryParams.project+" "+testcase.trace); + } + else{ + xw.writeCData(testcase.trace); + } + + xw.endElement(); + xw.endElement(); + } + else{ + xw.writeAttribute('executed',"false"); + xw.endElement(); + } + }); + }) + }); + }); + }); +} +