1 package org.collectionspace.services.IntegrationTests.xmlreplay;
3 import org.apache.commons.cli.*;
5 import org.apache.commons.io.FileUtils;
6 import org.apache.commons.jexl2.JexlEngine;
7 import org.collectionspace.services.common.api.Tools;
9 import org.dom4j.io.SAXReader;
14 /** This class is used to replay a request to the Services layer, by sending the XML payload
15 * in an appropriate Multipart request.
16 * See example usage in calling class XmlReplayTest in services/IntegrationTests, and also in main() in this class.
17 * @author Laramie Crocker
19 public class XmlReplay {
21 public XmlReplay(String basedir, String reportsDir){
22 this.basedir = basedir;
23 this.serviceResultsMap = createResultsMap();
24 this.reportsList = new ArrayList<String>();
25 this.reportsDir = reportsDir;
28 public static final String DEFAULT_CONTROL = "xml-replay-control.xml";
29 public static final String DEFAULT_MASTER_CONTROL = "xml-replay-master.xml";
30 public static final String DEFAULT_DEV_MASTER_CONTROL = "dev-master.xml";
32 private String reportsDir = "";
33 public String getReportsDir(){
36 private String basedir = "."; //set from constructor.
37 public String getBaseDir(){
41 private String controlFileName = DEFAULT_CONTROL;
42 public String getControlFileName() {
43 return controlFileName;
45 public void setControlFileName(String controlFileName) {
46 this.controlFileName = controlFileName;
49 private String protoHostPort = "";
50 public String getProtoHostPort() {
53 public void setProtoHostPort(String protoHostPort) {
54 this.protoHostPort = protoHostPort;
57 private boolean autoDeletePOSTS = true;
58 public boolean isAutoDeletePOSTS() {
59 return autoDeletePOSTS;
61 public void setAutoDeletePOSTS(boolean autoDeletePOSTS) {
62 this.autoDeletePOSTS = autoDeletePOSTS;
66 public Dump getDump() {
69 public void setDump(Dump dump) {
73 AuthsMap defaultAuthsMap;
74 public AuthsMap getDefaultAuthsMap(){
75 return defaultAuthsMap;
77 public void setDefaultAuthsMap(AuthsMap authsMap){
78 defaultAuthsMap = authsMap;
81 private Map<String, ServiceResult> serviceResultsMap;
82 public Map<String, ServiceResult> getServiceResultsMap(){
83 return serviceResultsMap;
85 public static Map<String, ServiceResult> createResultsMap(){
86 return new HashMap<String, ServiceResult>();
89 private List<String> reportsList;
90 public List<String> getReportsList(){
94 public String toString(){
95 return "XmlReplay{"+this.basedir+", "+this.defaultAuthsMap+", "+this.dump+", "+this.reportsDir+'}';
98 // ============== METHODS ===========================================================
100 /** Optional information method: call this method after instantiating this class using the constructor XmlReplay(String), which sets the basedir. Then you
101 * pass in your relative masterFilename to that basedir to this method, which will return true if the file is readable, valid xml, etc.
102 * Do this in preference to just seeing if File.exists(), because there are rules to finding the file relative to the maven test dir, yada, yada.
103 * This method makes it easy to have a development test file that you don't check in, so that dev tests can be missing gracefully, etc.
105 public boolean masterConfigFileExists(String masterFilename){
107 org.dom4j.Document doc = openMasterConfigFile(masterFilename);
112 } catch (Throwable t){
117 public org.dom4j.Document openMasterConfigFile(String masterFilename) throws FileNotFoundException {
118 String fullPath = Tools.glue(basedir, "/", masterFilename);
119 File f = new File(fullPath);
123 org.dom4j.Document document = getDocument(fullPath); //will check full path first, then checks relative to PWD.
124 if (document == null){
125 throw new FileNotFoundException("XmlReplay master control file ("+masterFilename+") not found in basedir: "+basedir+". Exiting test.");
130 /** specify the master config file, relative to getBaseDir(), but ignore any tests or testGroups in the master.
131 * @return a Document object, which you don't need to use: all options will be stored in XmlReplay instance.
133 public org.dom4j.Document readOptionsFromMasterConfigFile(String masterFilename) throws FileNotFoundException {
134 org.dom4j.Document document = openMasterConfigFile(masterFilename);
135 if (document == null){
136 throw new FileNotFoundException(masterFilename);
138 protoHostPort = document.selectSingleNode("/xmlReplayMaster/protoHostPort").getText().trim();
139 AuthsMap authsMap = readAuths(document);
140 setDefaultAuthsMap(authsMap);
141 Dump dump = XmlReplay.readDumpOptions(document);
146 public List<List<ServiceResult>> runMaster(String masterFilename) throws Exception {
147 return runMaster(masterFilename, true);
150 /** Creates new instances of XmlReplay, one for each controlFile specified in the master,
151 * and setting defaults from this instance, but not sharing ServiceResult objects or maps. */
152 public List<List<ServiceResult>> runMaster(String masterFilename, boolean readOptionsFromMaster) throws Exception {
153 List<List<ServiceResult>> list = new ArrayList<List<ServiceResult>>();
154 org.dom4j.Document document;
155 if (readOptionsFromMaster){
156 document = readOptionsFromMasterConfigFile(masterFilename);
158 document = openMasterConfigFile(masterFilename);
161 throw new FileNotFoundException(masterFilename);
163 String controlFile, testGroup, test;
165 runNodes = document.selectNodes("/xmlReplayMaster/run");
166 for (Node runNode : runNodes) {
167 controlFile = runNode.valueOf("@controlFile");
168 testGroup = runNode.valueOf("@testGroup");
169 test = runNode.valueOf("@test"); //may be empty
171 //Create a new instance and clone only config values, not any results maps.
172 XmlReplay replay = new XmlReplay(basedir, this.reportsDir);
173 replay.setControlFileName(controlFile);
174 replay.setProtoHostPort(protoHostPort);
175 replay.setAutoDeletePOSTS(isAutoDeletePOSTS());
176 replay.setDump(dump);
177 replay.setDefaultAuthsMap(getDefaultAuthsMap());
179 //Now run *that* instance.
180 List<ServiceResult> results = replay.runTests(testGroup, test);
182 this.reportsList.addAll(replay.getReportsList()); //Add all the reports from the inner replay, to our master replay's reportsList, to generate the index.html file.
184 XmlReplayReport.saveIndexForMaster(basedir, reportsDir, masterFilename, this.reportsList);
188 /** Use this if you wish to run named tests within a testGroup, otherwise call runTestGroup(). */
189 public List<ServiceResult> runTests(String testGroupID, String testID) throws Exception {
190 List<ServiceResult> result = runXmlReplayFile(this.basedir,
191 this.controlFileName,
194 this.serviceResultsMap,
195 this.autoDeletePOSTS,
198 this.defaultAuthsMap,
204 /** Use this if you wish to specify just ONE test to run within a testGroup, otherwise call runTestGroup(). */
205 public ServiceResult runTest(String testGroupID, String testID) throws Exception {
206 List<ServiceResult> result = runXmlReplayFile(this.basedir,
207 this.controlFileName,
210 this.serviceResultsMap,
211 this.autoDeletePOSTS,
214 this.defaultAuthsMap,
217 if (result.size()>1){
218 throw new IndexOutOfBoundsException("Multiple ("+result.size()+") tests with ID='"+testID+"' were found within test group '"+testGroupID+"', but there should only be one test per ID attribute.");
220 return result.get(0);
223 /** Use this if you wish to run all tests within a testGroup.*/
224 public List<ServiceResult> runTestGroup(String testGroupID) throws Exception {
225 //NOTE: calling runTest with empty testID runs all tests in a test group, but don't expose this fact.
226 // Expose this method (runTestGroup) instead.
227 return runTests(testGroupID, "");
230 public List<ServiceResult> autoDelete(String logName){
231 return autoDelete(this.serviceResultsMap, logName);
234 /** Use this method to clean up resources created on the server that returned CSIDs, if you have
235 * specified autoDeletePOSTS==false, which means you are managing the cleanup yourself.
236 * @param serviceResultsMap a Map of ServiceResult objects, which will contain ServiceResult.deleteURL.
237 * @return a List<String> of debug info about which URLs could not be deleted.
239 public static List<ServiceResult> autoDelete(Map<String, ServiceResult> serviceResultsMap, String logName){
240 List<ServiceResult> results = new ArrayList<ServiceResult>();
241 for (ServiceResult pr : serviceResultsMap.values()){
243 if (Tools.notEmpty(pr.deleteURL)){
244 ServiceResult deleteResult = XmlReplayTransport.doDELETE(pr.deleteURL, pr.auth, pr.testID, "[autodelete:"+logName+"]");
245 results.add(deleteResult);
247 ServiceResult errorResult = new ServiceResult();
248 errorResult.fullURL = pr.fullURL;
249 errorResult.testGroupID = pr.testGroupID;
250 errorResult.fromTestID = pr.fromTestID;
251 errorResult.overrideGotExpectedResult();
252 results.add(errorResult);
254 } catch (Throwable t){
255 String s = (pr!=null) ? "ERROR while cleaning up ServiceResult map: "+pr+" for "+pr.deleteURL+" :: "+t
256 : "ERROR while cleaning up ServiceResult map (null ServiceResult): "+t;
257 System.err.println(s);
258 ServiceResult errorResult = new ServiceResult();
259 errorResult.fullURL = pr.fullURL;
260 errorResult.testGroupID = pr.testGroupID;
261 errorResult.fromTestID = pr.fromTestID;
262 errorResult.error = s;
263 results.add(errorResult);
269 public static class AuthsMap {
270 Map<String,String> map;
272 public String getDefaultAuth(){
273 return map.get(defaultID);
275 public String toString(){
276 return "AuthsMap: {default='"+defaultID+"'; "+map.keySet()+'}';
280 public static AuthsMap readAuths(org.dom4j.Document document){
281 Map<String, String> map = new HashMap<String, String>();
282 List<Node> authNodes = document.selectNodes("//auths/auth");
283 for (Node auth : authNodes) {
284 map.put(auth.valueOf("@ID"), auth.getStringValue());
286 AuthsMap authsMap = new AuthsMap();
287 Node auths = document.selectSingleNode("//auths");
288 String defaultID = "";
290 defaultID = auths.valueOf("@default");
293 authsMap.defaultID = defaultID;
297 public static class Dump {
298 public boolean payloads = false;
299 //public static final ServiceResult.DUMP_OPTIONS dumpServiceResultOptions = ServiceResult.DUMP_OPTIONS;
300 public ServiceResult.DUMP_OPTIONS dumpServiceResult = ServiceResult.DUMP_OPTIONS.minimal;
301 public String toString(){
302 return "payloads: "+payloads+" dumpServiceResult: "+dumpServiceResult;
306 public static Dump getDumpConfig(){
310 public static Dump readDumpOptions(org.dom4j.Document document){
311 Dump dump = getDumpConfig();
312 Node dumpNode = document.selectSingleNode("//dump");
313 if (dumpNode != null){
314 dump.payloads = Tools.isTrue(dumpNode.valueOf("@payloads"));
315 String dumpServiceResultStr = dumpNode.valueOf("@dumpServiceResult");
316 if (Tools.notEmpty(dumpServiceResultStr)){
317 dump.dumpServiceResult = ServiceResult.DUMP_OPTIONS.valueOf(dumpServiceResultStr);
323 private static class PartsStruct {
324 public List<Map<String,String>> varsList = new ArrayList<Map<String,String>>();
325 String responseFilename = "";
326 String overrideTestID = "";
327 String startElement = "";
330 public static PartsStruct readParts(Node testNode, final String testID, String xmlReplayBaseDir){
331 PartsStruct resultPartsStruct = new PartsStruct();
332 resultPartsStruct.responseFilename = testNode.valueOf("filename");
333 resultPartsStruct.startElement = testNode.valueOf("startElement");
334 resultPartsStruct.label = testNode.valueOf("label");
335 String responseFilename = testNode.valueOf("filename");
336 if (Tools.notEmpty(responseFilename)){
337 resultPartsStruct.responseFilename = xmlReplayBaseDir + '/' + responseFilename;
338 List<Node> varNodes = testNode.selectNodes("vars/var");
339 readVars(testNode, varNodes, resultPartsStruct);
341 return resultPartsStruct;
344 private static void readVars(Node testNode, List<Node> varNodes, PartsStruct resultPartsStruct){
345 Map<String,String> vars = new HashMap<String,String>();
346 resultPartsStruct.varsList.add(vars);
347 //System.out.println("### vars: "+vars.size()+" ########");
348 for (Node var: varNodes){
349 String ID = var.valueOf("@ID");
350 String value = var.getText();
351 //System.out.println("ID: "+ID+" value: "+value);
352 vars.put(ID, value); //vars is already part of resultPartsStruct.varsList
354 //System.out.println("### end-vars ########");
360 private static String fixupFullURL(String fullURL, String protoHostPort, String uri){
361 if ( ! uri.startsWith(protoHostPort)){
362 fullURL = Tools.glue(protoHostPort, "/", uri);
369 private static String fromTestID(String fullURL, Node testNode, Map<String, ServiceResult> serviceResultsMap){
370 String fromTestID = testNode.valueOf("fromTestID");
371 if (Tools.notEmpty(fromTestID)){
372 ServiceResult getPR = serviceResultsMap.get(fromTestID);
374 fullURL = Tools.glue(fullURL, "/", getPR.location);
380 private static String CSIDfromTestID(Node testNode, Map<String, ServiceResult> serviceResultsMap){
382 String fromTestID = testNode.valueOf("fromTestID");
383 if (Tools.notEmpty(fromTestID)){
384 ServiceResult getPR = serviceResultsMap.get(fromTestID);
386 result = getPR.location;
392 public static org.dom4j.Document getDocument(String xmlFileName) {
393 org.dom4j.Document document = null;
394 SAXReader reader = new SAXReader();
396 document = reader.read(xmlFileName);
397 } catch (DocumentException e) {
398 System.out.println("ERROR reading document: "+e);
399 //e.printStackTrace();
404 protected static String validateResponseSinglePayload(ServiceResult serviceResult,
405 Map<String, ServiceResult> serviceResultsMap,
406 PartsStruct expectedResponseParts,
407 XmlReplayEval evalStruct)
410 byte[] b = FileUtils.readFileToByteArray(new File(expectedResponseParts.responseFilename));
411 String expectedPartContent = new String(b);
412 Map<String,String> vars = expectedResponseParts.varsList.get(0); //just one part, so just one varsList. Must be there, even if empty.
413 expectedPartContent = evalStruct.eval(expectedPartContent, serviceResultsMap, vars, evalStruct.jexl, evalStruct.jc);
414 serviceResult.expectedContentExpanded = expectedPartContent;
415 String label = "NOLABEL";
416 String leftID = "{from expected part, label:"+label+" filename: "+expectedResponseParts.responseFilename+"}";
417 String rightID = "{from server, label:"+label
418 +" fromTestID: "+serviceResult.fromTestID
419 +" URL: "+serviceResult.fullURL
421 String startElement = expectedResponseParts.startElement;
422 String partLabel = expectedResponseParts.label;
423 if (Tools.isBlank(startElement)){
424 if (Tools.notBlank(partLabel))
425 startElement = "/document/*[local-name()='"+partLabel+"']";
427 TreeWalkResults.MatchSpec matchSpec = TreeWalkResults.MatchSpec.createDefault();
428 TreeWalkResults list =
429 XmlCompareJdom.compareParts(expectedPartContent,
431 serviceResult.result,
435 serviceResult.addPartSummary(label, list);
439 protected static String validateResponse(ServiceResult serviceResult,
440 Map<String, ServiceResult> serviceResultsMap,
441 PartsStruct expectedResponseParts,
442 XmlReplayEval evalStruct){
444 if (expectedResponseParts == null) return OK;
445 if (serviceResult == null) return OK;
446 if (serviceResult.result.length() == 0) return OK;
448 return validateResponseSinglePayload(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
449 } catch (Exception e){
450 String err = "ERROR in XmlReplay.validateResponse() : "+e;
455 //================= runXmlReplayFile ======================================================
457 public static List<ServiceResult> runXmlReplayFile(String xmlReplayBaseDir,
458 String controlFileName,
461 Map<String, ServiceResult> serviceResultsMap,
462 boolean param_autoDeletePOSTS,
464 String protoHostPortParam,
465 AuthsMap defaultAuths,
466 List<String> reportsList,
469 //Internally, we maintain two collections of ServiceResult:
470 // the first is the return value of this method.
471 // the second is the serviceResultsMap, which is used for keeping track of CSIDs created by POSTs, for later reference by DELETE, etc.
472 List<ServiceResult> results = new ArrayList<ServiceResult>();
474 XmlReplayReport report = new XmlReplayReport(reportsDir);
476 String controlFile = Tools.glue(xmlReplayBaseDir, "/", controlFileName);
477 org.dom4j.Document document;
478 document = getDocument(controlFile); //will check full path first, then checks relative to PWD.
480 throw new FileNotFoundException("XmlReplay control file ("+controlFileName+") not found in basedir: "+xmlReplayBaseDir+" Exiting test.");
482 String protoHostPort;
483 if (Tools.isEmpty(protoHostPortParam)){
484 protoHostPort = document.selectSingleNode("/xmlReplay/protoHostPort").getText().trim();
485 System.out.println("DEPRECATED: Using protoHostPort ('"+protoHostPort+"') from xmlReplay file ('"+controlFile+"'), not master.");
487 protoHostPort = protoHostPortParam;
489 if (Tools.isEmpty(protoHostPort)){
490 throw new Exception("XmlReplay control file must have a protoHostPort element");
494 AuthsMap authsMap = readAuths(document);
495 if (authsMap.map.size()==0){
496 authsMap = defaultAuths;
497 authsMapINFO = "Using defaultAuths from master file: "+defaultAuths;
499 authsMapINFO = "Using AuthsMap from control file: "+authsMap;
502 report.addTestGroup(testGroupID, controlFileName); //controlFileName is just the short name, without the full path.
503 String xmlReplayHeader = "========================================================================"
504 +"\r\nXmlReplay running:"
505 +"\r\n controlFile: "+ (new File(controlFile).getCanonicalPath())
506 +"\r\n protoHostPort: "+protoHostPort
507 +"\r\n testGroup: "+testGroupID
508 + (Tools.notEmpty(oneTestID) ? "\r\n oneTestID: "+oneTestID : "")
509 +"\r\n AuthsMap: "+authsMapINFO
510 +"\r\n param_autoDeletePOSTS: "+param_autoDeletePOSTS
511 +"\r\n Dump info: "+dump
512 +"\r\n========================================================================"
514 report.addRunInfo(xmlReplayHeader);
516 System.out.println(xmlReplayHeader);
518 String autoDeletePOSTS = "";
519 List<Node> testgroupNodes;
520 if (Tools.notEmpty(testGroupID)){
521 testgroupNodes = document.selectNodes("//testGroup[@ID='"+testGroupID+"']");
523 testgroupNodes = document.selectNodes("//testGroup");
526 JexlEngine jexl = new JexlEngine(); // Used for expression language expansion from uri field.
527 XmlReplayEval evalStruct = new XmlReplayEval();
528 evalStruct.serviceResultsMap = serviceResultsMap;
529 evalStruct.jexl = jexl;
531 for (Node testgroup : testgroupNodes) {
533 XmlReplayEval.MapContextWKeys jc = new XmlReplayEval.MapContextWKeys();//MapContext(); //Get a new JexlContext for each test group.
536 autoDeletePOSTS = testgroup.valueOf("@autoDeletePOSTS");
538 if (Tools.notEmpty(oneTestID)){
539 tests = testgroup.selectNodes("test[@ID='"+oneTestID+"']");
541 tests = testgroup.selectNodes("test");
543 String authForTest = "";
544 int testElementIndex = -1;
546 for (Node testNode : tests) {
547 long startTime = System.currentTimeMillis();
550 String testID = testNode.valueOf("@ID");
551 String testIDLabel = Tools.notEmpty(testID) ? (testGroupID+'.'+testID) : (testGroupID+'.'+testElementIndex);
552 String method = testNode.valueOf("method");
553 String uri = testNode.valueOf("uri");
554 String fullURL = Tools.glue(protoHostPort, "/", uri);
556 String authIDForTest = testNode.valueOf("@auth");
557 String currentAuthForTest = authsMap.map.get(authIDForTest);
558 if (Tools.notEmpty(currentAuthForTest)){
559 authForTest = currentAuthForTest; //else just run with current from last loop;
561 if (Tools.isEmpty(authForTest)){
562 authForTest = defaultAuths.getDefaultAuth();
565 if (uri.indexOf("$")>-1){
566 uri = evalStruct.eval(uri, serviceResultsMap, null, jexl, jc);
568 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
570 List<Integer> expectedCodes = new ArrayList<Integer>();
571 String expectedCodesStr = testNode.valueOf("expectedCodes");
572 if (Tools.notEmpty(expectedCodesStr)){
573 String[] codesArray = expectedCodesStr.split(",");
574 for (String code : codesArray){
575 expectedCodes.add(new Integer(code.trim()));
579 Node responseNode = testNode.selectSingleNode("response");
580 PartsStruct expectedResponseParts = null;
581 if (responseNode!=null){
582 expectedResponseParts = PartsStruct.readParts(responseNode, testID, xmlReplayBaseDir);
583 //System.out.println("reponse parts: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+expectedResponseParts);
586 ServiceResult serviceResult;
587 boolean isPOST = method.equalsIgnoreCase("POST");
588 boolean isPUT = method.equalsIgnoreCase("PUT");
589 if ( isPOST || isPUT ) {
590 PartsStruct parts = PartsStruct.readParts(testNode, testID, xmlReplayBaseDir);
591 if (Tools.notEmpty(parts.overrideTestID)) {
592 testID = parts.overrideTestID;
595 String csid = CSIDfromTestID(testNode, serviceResultsMap);
596 if (Tools.notEmpty(csid)) uri = Tools.glue(uri, "/", csid+"/items/");
598 uri = fromTestID(uri, testNode, serviceResultsMap);
600 //vars only make sense in two contexts: POST/PUT, because you are submitting another file with internal expressions,
601 // and in <response> nodes. For GET, DELETE, there is no payload, so all the URLs with potential expressions are right there in the testNode.
602 Map<String,String> vars = null;
603 if (parts.varsList.size()>0){
604 vars = parts.varsList.get(0);
606 serviceResult = XmlReplayTransport.doPOST_PUTFromXML(parts.responseFilename, vars, protoHostPort, uri, method, XmlReplayTransport.APPLICATION_XML, evalStruct, authForTest, testIDLabel);
608 serviceResult.addVars(vars);
610 results.add(serviceResult);
612 serviceResultsMap.put(testID, serviceResult); //PUTs do not return a Location, so don't add PUTs to serviceResultsMap.
614 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
615 } else if (method.equalsIgnoreCase("DELETE")){
616 String fromTestID = testNode.valueOf("fromTestID");
617 ServiceResult pr = serviceResultsMap.get(fromTestID);
619 serviceResult = XmlReplayTransport.doDELETE(pr.deleteURL, authForTest, testIDLabel, fromTestID);
620 serviceResult.fromTestID = fromTestID;
621 if (expectedCodes.size()>0){
622 serviceResult.expectedCodes = expectedCodes;
624 results.add(serviceResult);
625 if (serviceResult.codeInSuccessRange(serviceResult.responseCode)){ //gotExpectedResult depends on serviceResult.expectedCodes.
626 serviceResultsMap.remove(fromTestID);
629 if (Tools.notEmpty(fromTestID)){
630 serviceResult = new ServiceResult();
631 serviceResult.responseCode = 0;
632 serviceResult.error = "ID not found in element fromTestID: "+fromTestID;
633 System.err.println("****\r\nServiceResult: "+serviceResult.error+". SKIPPING TEST. Full URL: "+fullURL);
635 serviceResult = XmlReplayTransport.doDELETE(fullURL, authForTest, testID, fromTestID);
637 serviceResult.fromTestID = fromTestID;
638 results.add(serviceResult);
640 } else if (method.equalsIgnoreCase("GET")){
641 fullURL = fromTestID(fullURL, testNode, serviceResultsMap);
642 serviceResult = XmlReplayTransport.doGET(fullURL, authForTest, testIDLabel);
643 results.add(serviceResult);
644 serviceResultsMap.put(testID, serviceResult);
645 } else if (method.equalsIgnoreCase("LIST")){
646 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
647 String listQueryParams = ""; //TODO: empty for now, later may pick up from XML control file.
648 serviceResult = XmlReplayTransport.doLIST(fullURL, listQueryParams, authForTest, testIDLabel);
649 results.add(serviceResult);
650 serviceResultsMap.put(testID, serviceResult);
652 throw new Exception("HTTP method not supported by XmlReplay: "+method);
655 serviceResult.testID = testID;
656 serviceResult.fullURL = fullURL;
657 serviceResult.auth = authForTest;
658 serviceResult.method = method;
659 if (expectedCodes.size()>0){
660 serviceResult.expectedCodes = expectedCodes;
662 if (Tools.isEmpty(serviceResult.testID)) serviceResult.testID = testIDLabel;
663 if (Tools.isEmpty(serviceResult.testGroupID)) serviceResult.testGroupID = testGroupID;
665 Node expectedLevel = testNode.selectSingleNode("response/expected");
666 if (expectedLevel!=null){
667 String level = expectedLevel.valueOf("@level");
668 serviceResult.payloadStrictness = level;
670 //=====================================================
671 // ALL VALIDATION FOR ALL REQUESTS IS DONE HERE:
672 //=====================================================
673 boolean hasError = false;
674 String vError = validateResponse(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
675 if (Tools.notEmpty(vError)){
676 serviceResult.error = vError;
677 serviceResult.failureReason = " : VALIDATION ERROR; ";
680 if (hasError == false){
681 hasError = ! serviceResult.gotExpectedResult();
684 boolean doingAuto = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.auto);
685 String serviceResultRow = serviceResult.dump(dump.dumpServiceResult, hasError)+"; time:"+(System.currentTimeMillis()-startTime);
686 String leader = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) ? "XmlReplay:"+testIDLabel+": ": "";
688 report.addTestResult(serviceResult);
690 if ( (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed)
691 || (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.full) ){
692 System.out.println("\r\n#---------------------#");
694 System.out.println(timeString()+" "+leader+serviceResultRow+"\r\n");
695 if (dump.payloads || (doingAuto&&hasError) ) {
696 if (Tools.notBlank(serviceResult.requestPayload)){
697 System.out.println("\r\n========== request payload ===============");
698 System.out.println(serviceResult.requestPayload);
699 System.out.println("==========================================\r\n");
702 if (dump.payloads || (doingAuto&&hasError)) {
703 if (Tools.notBlank(serviceResult.result)){
704 System.out.println("\r\n========== response payload ==============");
705 System.out.println(serviceResult.result);
706 System.out.println("==========================================\r\n");
709 } catch (Throwable t) {
710 String msg = "ERROR: XmlReplay experienced an error in a test node: "+testNode+" Throwable: "+t;
711 System.out.println(msg);
712 System.out.println(Tools.getStackTrace(t));
713 ServiceResult serviceResult = new ServiceResult();
714 serviceResult.error = msg;
715 serviceResult.failureReason = " : SYSTEM ERROR; ";
716 results.add(serviceResult);
719 if (Tools.isTrue(autoDeletePOSTS)&¶m_autoDeletePOSTS){
720 autoDelete(serviceResultsMap, "default");
724 //=== Now spit out the HTML report file ===
725 File m = new File(controlFileName);
726 String localName = m.getName();//don't instantiate, just use File to extract file name without directory.
727 String reportName = localName+'-'+testGroupID+".html";
729 File resultFile = report.saveReport(xmlReplayBaseDir, reportsDir, reportName);
730 if (resultFile!=null) {
731 String toc = report.getTOC(reportName);
732 reportsList.add(toc);
734 //================================
739 private static String timeString() {
740 java.util.Date date= new java.util.Date();
741 java.sql.Timestamp ts = new java.sql.Timestamp(date.getTime());
742 return ts.toString();
746 //======================== MAIN ===================================================================
748 private static Options createOptions() {
749 Options options = new Options();
750 options.addOption("xmlReplayBaseDir", true, "default/basedir");
754 public static String usage(){
755 String result = "org.collectionspace.services.IntegrationTests.xmlreplay.XmlReplay {args}\r\n"
756 +" -xmlReplayBaseDir <dir> \r\n"
757 +" You may also override these with system args, e.g.: \r\n"
758 +" -DxmlReplayBaseDir=/path/to/dir \r\n"
759 +" These may also be passed in via the POM.\r\n"
760 +" You can also set these system args, e.g.: \r\n"
761 +" -DtestGroupID=<oneID> \r\n"
762 +" -DtestID=<one TestGroup ID>"
763 +" -DautoDeletePOSTS=<true|false> \r\n"
764 +" (note: -DautoDeletePOSTS won't force deletion if set to false in control file.";
768 private static String opt(CommandLine line, String option){
770 String fromProps = System.getProperty(option);
771 if (Tools.notEmpty(fromProps)){
777 result = line.getOptionValue(option);
784 public static void main(String[]args) throws Exception {
785 Options options = createOptions();
786 //System.out.println("System CLASSPATH: "+prop.getProperty("java.class.path", null));
787 CommandLineParser parser = new GnuParser();
789 // parse the command line arguments
790 CommandLine line = parser.parse(options, args);
792 String xmlReplayBaseDir = opt(line, "xmlReplayBaseDir");
793 String reportsDir = opt(line, "reportsDir");
794 String testGroupID = opt(line, "testGroupID");
795 String testID = opt(line, "testID");
796 String autoDeletePOSTS = opt(line, "autoDeletePOSTS");
797 String dumpResults = opt(line, "dumpResults");
798 String controlFilename = opt(line, "controlFilename");
799 String xmlReplayMaster = opt(line, "xmlReplayMaster");
801 if (Tools.isBlank(reportsDir)){
802 reportsDir = xmlReplayBaseDir + XmlReplayTest.REPORTS_DIRNAME;
804 reportsDir = Tools.fixFilename(reportsDir);
805 xmlReplayBaseDir = Tools.fixFilename(xmlReplayBaseDir);
806 controlFilename = Tools.fixFilename(controlFilename);
808 boolean bAutoDeletePOSTS = true;
809 if (Tools.notEmpty(autoDeletePOSTS)) {
810 bAutoDeletePOSTS = Tools.isTrue(autoDeletePOSTS);
812 boolean bDumpResults = false;
813 if (Tools.notEmpty(dumpResults)) {
814 bDumpResults = Tools.isTrue(autoDeletePOSTS);
816 if (Tools.isEmpty(xmlReplayBaseDir)){
817 System.err.println("ERROR: xmlReplayBaseDir was not specified.");
820 File f = new File(Tools.glue(xmlReplayBaseDir, "/", controlFilename));
821 if (Tools.isEmpty(xmlReplayMaster) && !f.exists()){
822 System.err.println("Control file not found: "+f.getCanonicalPath());
825 File fMaster = new File(Tools.glue(xmlReplayBaseDir, "/", xmlReplayMaster));
826 if (Tools.notEmpty(xmlReplayMaster) && !fMaster.exists()){
827 System.err.println("Master file not found: "+fMaster.getCanonicalPath());
831 String xmlReplayBaseDirResolved = (new File(xmlReplayBaseDir)).getCanonicalPath();
832 System.out.println("XmlReplay ::"
833 + "\r\n xmlReplayBaseDir: "+xmlReplayBaseDir
834 + "\r\n xmlReplayBaseDir(resolved): "+xmlReplayBaseDirResolved
835 + "\r\n controlFilename: "+controlFilename
836 + "\r\n xmlReplayMaster: "+xmlReplayMaster
837 + "\r\n testGroupID: "+testGroupID
838 + "\r\n testID: "+testID
839 + "\r\n autoDeletePOSTS: "+bAutoDeletePOSTS
840 + (Tools.notEmpty(xmlReplayMaster)
841 ? ("\r\n will use master file: "+fMaster.getCanonicalPath())
842 : ("\r\n will use control file: "+f.getCanonicalPath()) )
845 if (Tools.notEmpty(xmlReplayMaster)){
846 if (Tools.notEmpty(controlFilename)){
847 System.out.println("WARN: controlFilename: "+controlFilename+" will not be used because master was specified. Running master: "+xmlReplayMaster);
849 XmlReplay replay = new XmlReplay(xmlReplayBaseDirResolved, reportsDir);
850 replay.readOptionsFromMasterConfigFile(xmlReplayMaster);
851 replay.setAutoDeletePOSTS(bAutoDeletePOSTS);
852 Dump dumpFromMaster = replay.getDump();
853 dumpFromMaster.payloads = Tools.isTrue(dumpResults);
854 replay.setDump(dumpFromMaster);
855 replay.runMaster(xmlReplayMaster, false); //false, because we already just read the options, and override a few.
857 Dump dump = getDumpConfig();
858 dump.payloads = Tools.isTrue(dumpResults);
859 List<String> reportsList = new ArrayList<String>();
860 runXmlReplayFile(xmlReplayBaseDirResolved, controlFilename, testGroupID, testID, createResultsMap(), bAutoDeletePOSTS, dump, "", null, reportsList, reportsDir);
861 System.out.println("DEPRECATED: reportsList is generated, but not dumped: "+reportsList.toString());
863 } catch (ParseException exp) {
864 // oops, something went wrong
865 System.err.println("Cmd-line parsing failed. Reason: " + exp.getMessage());
866 System.err.println(usage());
867 } catch (Exception e) {
868 System.out.println("Error : " + e.getMessage());