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.JexlContext;
7 import org.apache.commons.jexl2.JexlEngine;
8 import org.collectionspace.services.common.api.Tools;
10 import org.dom4j.io.SAXReader;
15 /** This class is used to replay a request to the Services layer, by sending the XML payload
16 * in an appropriate Multipart request.
17 * See example usage in calling class XmlReplayTest in services/IntegrationTests, and also in main() in this class.
18 * @author Laramie Crocker
20 @SuppressWarnings("unchecked")
21 public class XmlReplay {
23 public XmlReplay(String basedir, String reportsDir){
24 this.basedir = basedir;
25 this.serviceResultsMap = createResultsMap();
26 this.reportsList = new ArrayList<String>();
27 this.reportsDir = reportsDir;
30 public static final String DEFAULT_CONTROL = "xml-replay-control.xml";
31 public static final String DEFAULT_MASTER_CONTROL = "xml-replay-master.xml";
32 public static final String DEFAULT_DEV_MASTER_CONTROL = "dev-master.xml";
33 private static final int MAX_REATTEMPTS = 10;
34 private static final String REATTEMPT_KEY = "REATTEMPT_KEY";
36 private String reportsDir = "";
37 public String getReportsDir(){
40 private String basedir = "."; //set from constructor.
41 public String getBaseDir(){
45 private String controlFileName = DEFAULT_CONTROL;
46 public String getControlFileName() {
47 return controlFileName;
49 public void setControlFileName(String controlFileName) {
50 this.controlFileName = controlFileName;
53 private String protoHostPort = "";
54 public String getProtoHostPort() {
57 public void setProtoHostPort(String protoHostPort) {
58 this.protoHostPort = protoHostPort;
61 private boolean autoDeletePOSTS = true;
62 public boolean isAutoDeletePOSTS() {
63 return autoDeletePOSTS;
65 public void setAutoDeletePOSTS(boolean autoDeletePOSTS) {
66 this.autoDeletePOSTS = autoDeletePOSTS;
70 public Dump getDump() {
73 public void setDump(Dump dump) {
77 AuthsMap defaultAuthsMap;
78 public AuthsMap getDefaultAuthsMap(){
79 return defaultAuthsMap;
81 public void setDefaultAuthsMap(AuthsMap authsMap){
82 defaultAuthsMap = authsMap;
85 private Map<String, ServiceResult> serviceResultsMap;
86 public Map<String, ServiceResult> getServiceResultsMap(){
87 return serviceResultsMap;
89 public static Map<String, ServiceResult> createResultsMap(){
90 return new HashMap<String, ServiceResult>();
93 private List<String> reportsList;
94 public List<String> getReportsList(){
99 public String toString(){
100 return "XmlReplay{"+this.basedir+", "+this.defaultAuthsMap+", "+this.dump+", "+this.reportsDir+'}';
103 // ============== METHODS ===========================================================
105 /** Optional information method: call this method after instantiating this class using the constructor XmlReplay(String), which sets the basedir. Then you
106 * pass in your relative masterFilename to that basedir to this method, which will return true if the file is readable, valid xml, etc.
107 * 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.
108 * 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.
110 public boolean masterConfigFileExists(String masterFilename){
112 org.dom4j.Document doc = openMasterConfigFile(masterFilename);
117 } catch (Throwable t){
122 public org.dom4j.Document openMasterConfigFile(String masterFilename) throws FileNotFoundException {
123 String fullPath = Tools.glue(basedir, "/", masterFilename);
124 File f = new File(fullPath);
128 org.dom4j.Document document = getDocument(fullPath); //will check full path first, then checks relative to PWD.
129 if (document == null){
130 throw new FileNotFoundException("XmlReplay master control file ("+masterFilename+") not found in basedir: "+basedir+". Exiting test.");
135 /** specify the master config file, relative to getBaseDir(), but ignore any tests or testGroups in the master.
136 * @return a Document object, which you don't need to use: all options will be stored in XmlReplay instance.
138 public org.dom4j.Document readOptionsFromMasterConfigFile(String masterFilename) throws FileNotFoundException {
139 org.dom4j.Document document = openMasterConfigFile(masterFilename);
140 if (document == null){
141 throw new FileNotFoundException(masterFilename);
143 protoHostPort = document.selectSingleNode("/xmlReplayMaster/protoHostPort").getText().trim();
144 AuthsMap authsMap = readAuths(document);
145 setDefaultAuthsMap(authsMap);
146 Dump dump = XmlReplay.readDumpOptions(document);
151 public List<List<ServiceResult>> runMaster(String masterFilename) throws Exception {
152 return runMaster(masterFilename, true);
155 /** Creates new instances of XmlReplay, one for each controlFile specified in the master,
156 * and setting defaults from this instance, but not sharing ServiceResult objects or maps. */
157 public List<List<ServiceResult>> runMaster(String masterFilename, boolean readOptionsFromMaster) throws Exception {
158 List<List<ServiceResult>> list = new ArrayList<List<ServiceResult>>();
159 org.dom4j.Document document;
160 if (readOptionsFromMaster){
161 document = readOptionsFromMasterConfigFile(masterFilename);
163 document = openMasterConfigFile(masterFilename);
166 throw new FileNotFoundException(masterFilename);
168 String controlFile, testGroup, test;
170 runNodes = document.selectNodes("/xmlReplayMaster/run");
171 for (Node runNode : runNodes) {
172 controlFile = runNode.valueOf("@controlFile");
173 testGroup = runNode.valueOf("@testGroup");
174 test = runNode.valueOf("@test"); //may be empty
176 //Create a new instance and clone only config values, not any results maps.
177 XmlReplay replay = new XmlReplay(basedir, this.reportsDir);
178 replay.setControlFileName(controlFile);
179 replay.setProtoHostPort(protoHostPort);
180 replay.setAutoDeletePOSTS(isAutoDeletePOSTS());
181 replay.setDump(dump);
182 replay.setDefaultAuthsMap(getDefaultAuthsMap());
184 //Now run *that* instance.
185 List<ServiceResult> results = replay.runTests(testGroup, test);
187 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.
189 XmlReplayReport.saveIndexForMaster(basedir, reportsDir, masterFilename, this.reportsList);
193 /** Use this if you wish to run named tests within a testGroup, otherwise call runTestGroup(). */
194 public List<ServiceResult> runTests(String testGroupID, String testID) throws Exception {
195 List<ServiceResult> result = runXmlReplayFile(this.basedir,
196 this.controlFileName,
199 this.serviceResultsMap,
200 this.autoDeletePOSTS,
203 this.defaultAuthsMap,
209 /** Use this if you wish to specify just ONE test to run within a testGroup, otherwise call runTestGroup(). */
210 public ServiceResult runTest(String testGroupID, String testID) throws Exception {
211 List<ServiceResult> result = runXmlReplayFile(this.basedir,
212 this.controlFileName,
215 this.serviceResultsMap,
216 this.autoDeletePOSTS,
219 this.defaultAuthsMap,
222 if (result.size()>1){
223 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.");
225 return result.get(0);
228 /** Use this if you wish to run all tests within a testGroup.*/
229 public List<ServiceResult> runTestGroup(String testGroupID) throws Exception {
230 //NOTE: calling runTest with empty testID runs all tests in a test group, but don't expose this fact.
231 // Expose this method (runTestGroup) instead.
232 return runTests(testGroupID, "");
235 public List<ServiceResult> autoDelete(String logName){
236 return autoDelete(this.defaultAuthsMap, this.serviceResultsMap, logName, 0);
239 /** Use this method to clean up resources created on the server that returned CSIDs, if you have
240 * specified autoDeletePOSTS==false, which means you are managing the cleanup yourself.
241 * @param serviceResultsMap a Map of ServiceResult objects, which will contain ServiceResult.deleteURL.
242 * @return a List<String> of debug info about which URLs could not be deleted.
244 private static List<ServiceResult> autoDelete(AuthsMap defaultAuths, Map<String, ServiceResult> serviceResultsMap, String logName, int reattempt) {
245 List<ServiceResult> results = new ArrayList<ServiceResult>();
246 HashMap<String, ServiceResult> reattemptList = new HashMap<String, ServiceResult>();
247 int deleteFailures = 0;
248 for (ServiceResult pr : serviceResultsMap.values()) {
250 if (pr.autoDelete == true && Tools.notEmpty(pr.deleteURL)){
251 ServiceResult deleteResult = XmlReplayTransport.doDELETE(pr.deleteURL, pr.adminAuth, pr.testID, "[autodelete:"+logName+"]");
252 if (deleteResult.gotExpectedResult() == false || deleteResult.responseCode != 200) {
253 reattemptList.put(REATTEMPT_KEY + deleteFailures++, pr); // We need to try again after our dependents have been deleted. cow()
255 results.add(deleteResult);
257 ServiceResult errorResult = new ServiceResult();
258 errorResult.fullURL = pr.fullURL;
259 errorResult.testGroupID = pr.testGroupID;
260 errorResult.fromTestID = pr.fromTestID;
261 errorResult.overrideGotExpectedResult();
262 results.add(errorResult);
264 } catch (Throwable t){
265 String s = (pr!=null) ? "ERROR while cleaning up ServiceResult map: "+pr+" for "+pr.deleteURL+" :: "+t
266 : "ERROR while cleaning up ServiceResult map (null ServiceResult): "+t;
267 System.err.println(s);
268 ServiceResult errorResult = new ServiceResult();
269 errorResult.fullURL = pr.fullURL;
270 errorResult.testGroupID = pr.testGroupID;
271 errorResult.fromTestID = pr.fromTestID;
272 errorResult.error = s;
273 results.add(errorResult);
277 // If there were things we had trouble deleting, it might have been because they had dependents that
278 // needed to be deleted first. Therefore, we're going to try again and again (recursively) up until we reach
279 // our MAX_REATTEMPTS limit.
281 if (reattemptList.size() > 0 && reattempt < MAX_REATTEMPTS) {
282 return autoDelete(defaultAuths, reattemptList, logName, ++reattempt); // recursive call
288 public static class AuthsMap {
289 Map<String,String> map;
291 public String getDefaultAuth(){
292 return map.get(defaultID);
295 public String toString(){
296 return "AuthsMap: {default='"+defaultID+"'; "+map.keySet()+'}';
300 public static AuthsMap readAuths(org.dom4j.Document document){
301 Map<String, String> map = new HashMap<String, String>();
302 List<Node> authNodes = document.selectNodes("//auths/auth");
303 for (Node auth : authNodes) {
304 map.put(auth.valueOf("@ID"), auth.getStringValue());
306 AuthsMap authsMap = new AuthsMap();
307 Node auths = document.selectSingleNode("//auths");
308 String defaultID = "";
310 defaultID = auths.valueOf("@default");
313 authsMap.defaultID = defaultID;
317 public static class Dump {
318 public boolean payloads = false;
319 //public static final ServiceResult.DUMP_OPTIONS dumpServiceResultOptions = ServiceResult.DUMP_OPTIONS;
320 public ServiceResult.DUMP_OPTIONS dumpServiceResult = ServiceResult.DUMP_OPTIONS.minimal;
322 public String toString(){
323 return "payloads: "+payloads+" dumpServiceResult: "+dumpServiceResult;
327 public static Dump getDumpConfig(){
331 public static Dump readDumpOptions(org.dom4j.Document document){
332 Dump dump = getDumpConfig();
333 Node dumpNode = document.selectSingleNode("//dump");
334 if (dumpNode != null){
335 dump.payloads = Tools.isTrue(dumpNode.valueOf("@payloads"));
336 String dumpServiceResultStr = dumpNode.valueOf("@dumpServiceResult");
337 if (Tools.notEmpty(dumpServiceResultStr)){
338 dump.dumpServiceResult = ServiceResult.DUMP_OPTIONS.valueOf(dumpServiceResultStr);
344 private static class PartsStruct {
345 public List<Map<String,String>> varsList = new ArrayList<Map<String,String>>();
346 String responseFilename = "";
347 String overrideTestID = "";
348 String startElement = "";
351 public static PartsStruct readParts(Node testNode, final String testID, String xmlReplayBaseDir) {
352 PartsStruct resultPartsStruct = new PartsStruct();
353 resultPartsStruct.responseFilename = testNode.valueOf("filename");
354 resultPartsStruct.startElement = testNode.valueOf("startElement");
355 resultPartsStruct.label = testNode.valueOf("label");
356 String responseFilename = testNode.valueOf("filename");
357 if (Tools.notEmpty(responseFilename)) {
358 resultPartsStruct.responseFilename = xmlReplayBaseDir + '/' + responseFilename;
359 List<Node> varNodes = testNode.selectNodes("vars/var");
360 readVars(testNode, varNodes, resultPartsStruct);
362 return resultPartsStruct;
365 private static void readVars(Node testNode, List<Node> varNodes, PartsStruct resultPartsStruct) {
366 Map<String,String> vars = new HashMap<String,String>();
367 resultPartsStruct.varsList.add(vars);
368 //System.out.println("### vars: "+vars.size()+" ########");
369 for (Node var: varNodes){
370 String ID = var.valueOf("@ID");
371 String value = var.getText();
372 //System.out.println("ID: "+ID+" value: "+value);
373 vars.put(ID, value); //vars is already part of resultPartsStruct.varsList
375 //System.out.println("### end-vars ########");
381 private static String fixupFullURL(String fullURL, String protoHostPort, String uri){
382 if ( ! uri.startsWith(protoHostPort)){
383 fullURL = Tools.glue(protoHostPort, "/", uri);
390 private static String fromTestID(String fullURL, Node testNode, Map<String, ServiceResult> serviceResultsMap){
391 String fromTestID = testNode.valueOf("fromTestID");
392 if (Tools.notEmpty(fromTestID)){
393 ServiceResult getPR = serviceResultsMap.get(fromTestID);
395 fullURL = Tools.glue(fullURL, "/", getPR.location);
401 private static String CSIDfromTestID(Node testNode, Map<String, ServiceResult> serviceResultsMap){
403 String fromTestID = testNode.valueOf("fromTestID");
404 if (Tools.notEmpty(fromTestID)){
405 ServiceResult getPR = serviceResultsMap.get(fromTestID);
407 result = getPR.location;
413 public static org.dom4j.Document getDocument(String xmlFileName) {
414 org.dom4j.Document document = null;
415 SAXReader reader = new SAXReader();
417 document = reader.read(xmlFileName);
418 } catch (DocumentException e) {
419 System.out.println("ERROR reading document: "+e);
420 //e.printStackTrace();
425 protected static String validateResponseSinglePayload(ServiceResult serviceResult,
426 Map<String, ServiceResult> serviceResultsMap,
427 PartsStruct expectedResponseParts,
428 XmlReplayEval evalStruct)
431 byte[] b = FileUtils.readFileToByteArray(new File(expectedResponseParts.responseFilename));
432 String expectedPartContent = new String(b);
433 Map<String,String> vars = expectedResponseParts.varsList.get(0); //just one part, so just one varsList. Must be there, even if empty.
434 expectedPartContent = XmlReplayEval.eval(expectedPartContent, serviceResultsMap, vars, evalStruct.jexl, evalStruct.jc);
435 serviceResult.expectedContentExpanded = expectedPartContent;
436 String label = "NOLABEL";
437 String leftID = "{from expected part, label:"+label+" filename: "+expectedResponseParts.responseFilename+"}";
438 String rightID = "{from server, label:"+label
439 +" fromTestID: "+serviceResult.fromTestID
440 +" URL: "+serviceResult.fullURL
442 String startElement = expectedResponseParts.startElement;
443 String partLabel = expectedResponseParts.label;
444 if (Tools.isBlank(startElement)){
445 if (Tools.notBlank(partLabel))
446 startElement = "/document/*[local-name()='"+partLabel+"']";
448 TreeWalkResults.MatchSpec matchSpec = TreeWalkResults.MatchSpec.createDefault();
449 TreeWalkResults list =
450 XmlCompareJdom.compareParts(expectedPartContent,
452 serviceResult.result,
456 serviceResult.addPartSummary(label, list);
460 protected static String validateResponse(ServiceResult serviceResult,
461 Map<String, ServiceResult> serviceResultsMap,
462 PartsStruct expectedResponseParts,
463 XmlReplayEval evalStruct){
465 if (expectedResponseParts == null) return OK;
466 if (serviceResult == null) return OK;
467 if (serviceResult.result.length() == 0) return OK;
469 return validateResponseSinglePayload(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
470 } catch (Exception e){
471 String err = "ERROR in XmlReplay.validateResponse() : "+e;
476 //================= runXmlReplayFile ======================================================
478 public static List<ServiceResult> runXmlReplayFile(String xmlReplayBaseDir,
479 String controlFileName,
480 String targetedTestGroupID,
482 Map<String, ServiceResult> serviceResultsMap,
483 boolean param_autoDeletePOSTS,
485 String protoHostPortParam,
486 AuthsMap defaultAuths,
487 List<String> reportsList,
490 //Internally, we maintain two collections of ServiceResult:
491 // the first is the return value of this method.
492 // the second is the serviceResultsMap, which is used for keeping track of CSIDs created by POSTs, for later reference by DELETE, etc.
493 List<ServiceResult> results = new ArrayList<ServiceResult>();
495 XmlReplayReport report = new XmlReplayReport(reportsDir);
497 String controlFile = Tools.glue(xmlReplayBaseDir, "/", controlFileName);
498 org.dom4j.Document document;
499 document = getDocument(controlFile); //will check full path first, then checks relative to PWD.
501 throw new FileNotFoundException("XmlReplay control file ("+controlFileName+") not found in basedir: "+xmlReplayBaseDir+" Exiting test.");
503 String protoHostPort;
504 if (Tools.isEmpty(protoHostPortParam)){
505 protoHostPort = document.selectSingleNode("/xmlReplay/protoHostPort").getText().trim();
506 System.out.println("DEPRECATED: Using protoHostPort ('"+protoHostPort+"') from xmlReplay file ('"+controlFile+"'), not master.");
508 protoHostPort = protoHostPortParam;
510 if (Tools.isEmpty(protoHostPort)){
511 throw new Exception("XmlReplay control file must have a protoHostPort element");
515 AuthsMap authsMap = readAuths(document);
516 if (authsMap.map.size()==0){
517 authsMap = defaultAuths;
518 authsMapINFO = "Using defaultAuths from master file: "+defaultAuths;
520 authsMapINFO = "Using AuthsMap from control file: "+authsMap;
523 report.addTestGroup(targetedTestGroupID, controlFileName); //controlFileName is just the short name, without the full path.
524 String xmlReplayHeader = "========================================================================"
525 +"\r\nXmlReplay running:"
526 +"\r\n controlFile: "+ (new File(controlFile).getCanonicalPath())
527 +"\r\n protoHostPort: "+protoHostPort
528 +"\r\n testGroup: "+targetedTestGroupID
529 + (Tools.notEmpty(oneTestID) ? "\r\n oneTestID: "+oneTestID : "")
530 +"\r\n AuthsMap: "+authsMapINFO
531 +"\r\n param_autoDeletePOSTS: "+param_autoDeletePOSTS
532 +"\r\n Dump info: "+dump
533 +"\r\n========================================================================"
535 report.addRunInfo(xmlReplayHeader);
537 System.out.println(xmlReplayHeader);
539 String autoDeletePOSTS = "";
540 String authIDForCleanup = "";
541 List<Node> testgroupNodes;
542 if (Tools.notEmpty(targetedTestGroupID)){
543 testgroupNodes = document.selectNodes("//testGroup[@ID='"+targetedTestGroupID+"']");
545 testgroupNodes = document.selectNodes("//testGroup");
548 JexlEngine jexl = new JexlEngine(); // Used for expression language expansion from uri field.
549 XmlReplayEval evalStruct = new XmlReplayEval();
550 evalStruct.serviceResultsMap = serviceResultsMap;
551 evalStruct.jexl = jexl;
553 for (Node testgroup : testgroupNodes) {
554 String testGroupID = testgroup.valueOf("@ID");
555 XmlReplayEval.MapContextWKeys jc = new XmlReplayEval.MapContextWKeys();//MapContext(); //Get a new JexlContext for each test group.
557 autoDeletePOSTS = testgroup.valueOf("@autoDeletePOSTS");
559 // Decide which auth to use for POST request cleanups
561 String authForCleanup = null;
562 authIDForCleanup = testgroup.valueOf("@authForCleanup");
563 if (Tools.isEmpty(authIDForCleanup) == false) {
564 authForCleanup = authsMap.map.get(authIDForCleanup);
565 if (Tools.isEmpty(authForCleanup)) {
566 String msg = String.format("The 'authForCleanup' attribute value '%s' declared for test group '%s' is not defined.",
567 authIDForCleanup, testGroupID);
568 throw new Exception(msg);
571 authForCleanup = authForCleanup != null ? authForCleanup : defaultAuths.getDefaultAuth();
574 if (Tools.notEmpty(oneTestID)){
575 tests = testgroup.selectNodes("test[@ID='"+oneTestID+"']");
577 tests = testgroup.selectNodes("test");
579 String authForTest = ""; // reset auth for each test group
580 int testElementIndex = -1;
582 for (Node testNode : tests) {
583 long startTime = System.currentTimeMillis();
586 String testID = testNode.valueOf("@ID");
588 // Figure out if we will auto delete resources
589 boolean autoDelete = param_autoDeletePOSTS;
590 String autoDeleteValue = testNode.valueOf("@autoDeletePOSTS");
591 if (autoDeleteValue != null && !autoDeleteValue.trim().isEmpty()) {
592 autoDelete = Boolean.valueOf(autoDeleteValue).booleanValue();
595 String testIDLabel = Tools.notEmpty(testID) ? (testGroupID+'.'+testID) : (testGroupID+'.'+testElementIndex);
596 String method = testNode.valueOf("method");
597 String contentType = testNode.valueOf("contentType");
598 String uri = testNode.valueOf("uri");
599 String fullURL = Tools.glue(protoHostPort, "/", uri);
601 if (contentType == null || contentType.equals("")) {
602 contentType = XmlReplayTransport.APPLICATION_XML;
605 String currentAuthForTest = null;
606 String authIDForTest = testNode.valueOf("@auth");
607 if (Tools.notEmpty(authIDForTest)) {
608 currentAuthForTest = authsMap.map.get(authIDForTest);
609 if (currentAuthForTest == null) {
610 String msg = String.format("The 'auth' attribute value '%s' declared for test '%s' is not defined.",
611 authIDForTest, testIDLabel);
612 throw new Exception(msg);
615 String tokenAuthExpression = testNode.valueOf("@tokenauth");
616 if (Tools.notEmpty(tokenAuthExpression)){
617 currentAuthForTest = "Bearer " + XmlReplayEval.eval(tokenAuthExpression, serviceResultsMap, null, jexl, jc);
621 if (Tools.notEmpty(currentAuthForTest)) {
622 authForTest = currentAuthForTest; //else just run with current from last loop;
624 if (Tools.isEmpty(authForTest)) {
625 authForTest = defaultAuths.getDefaultAuth();
628 if (uri.indexOf("$")>-1){
629 uri = XmlReplayEval.eval(uri, serviceResultsMap, null, jexl, jc);
631 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
633 List<Integer> expectedCodes = new ArrayList<Integer>();
634 String expectedCodesStr = testNode.valueOf("expectedCodes");
635 if (Tools.notEmpty(expectedCodesStr)){
636 String[] codesArray = expectedCodesStr.split(",");
637 for (String code : codesArray){
638 expectedCodes.add(new Integer(code.trim()));
642 Node responseNode = testNode.selectSingleNode("response");
643 PartsStruct expectedResponseParts = null;
644 if (responseNode!=null){
645 expectedResponseParts = PartsStruct.readParts(responseNode, testID, xmlReplayBaseDir);
646 //System.out.println("reponse parts: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+expectedResponseParts);
649 ServiceResult serviceResult;
650 boolean isPOST = method.equalsIgnoreCase("POST");
651 boolean isPUT = method.equalsIgnoreCase("PUT");
652 if ( isPOST || isPUT ) {
653 PartsStruct parts = PartsStruct.readParts(testNode, testID, xmlReplayBaseDir);
654 if (Tools.notEmpty(parts.overrideTestID)) {
655 testID = parts.overrideTestID;
658 String csid = CSIDfromTestID(testNode, serviceResultsMap);
659 if (Tools.notEmpty(csid)) uri = Tools.glue(uri, "/", csid+"/items/");
661 uri = fromTestID(uri, testNode, serviceResultsMap);
663 //vars only make sense in two contexts: POST/PUT, because you are submitting another file with internal expressions,
664 // and in <response> nodes. For GET, DELETE, there is no payload, so all the URLs with potential expressions are right there in the testNode.
665 Map<String,String> vars = null;
666 if (parts.varsList.size()>0){
667 vars = parts.varsList.get(0);
669 serviceResult = XmlReplayTransport.doPOST_PUTFromXML(parts.responseFilename, vars, protoHostPort, uri, method, contentType, evalStruct, authForTest, testIDLabel);
670 serviceResult.autoDelete = autoDelete;
672 serviceResult.addVars(vars);
674 results.add(serviceResult);
676 serviceResultsMap.put(testID, serviceResult); //PUTs do not return a Location, so don't add PUTs to serviceResultsMap.
678 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
679 } else if (method.equalsIgnoreCase("DELETE")){
680 String fromTestID = testNode.valueOf("fromTestID");
681 ServiceResult pr = Tools.notBlank(fromTestID) ? serviceResultsMap.get(fromTestID) : null;
683 serviceResult = XmlReplayTransport.doDELETE(pr.deleteURL, authForTest, testIDLabel, fromTestID);
684 serviceResult.fromTestID = fromTestID;
685 if (expectedCodes.size()>0){
686 serviceResult.expectedCodes = expectedCodes;
688 results.add(serviceResult);
689 if (serviceResult.codeInSuccessRange(serviceResult.responseCode)){ //gotExpectedResult depends on serviceResult.expectedCodes.
690 serviceResultsMap.remove(fromTestID);
693 if (Tools.notEmpty(fromTestID)){
694 serviceResult = new ServiceResult();
695 serviceResult.responseCode = 0;
696 serviceResult.error = "ID not found in element fromTestID: "+fromTestID;
697 System.err.println("****\r\nServiceResult: "+serviceResult.error+". SKIPPING TEST. Full URL: "+fullURL);
699 serviceResult = XmlReplayTransport.doDELETE(fullURL, authForTest, testID, fromTestID);
701 serviceResult.fromTestID = fromTestID;
702 results.add(serviceResult);
704 } else if (method.equalsIgnoreCase("GET")){
705 fullURL = fromTestID(fullURL, testNode, serviceResultsMap);
706 serviceResult = XmlReplayTransport.doGET(fullURL, authForTest, testIDLabel);
707 results.add(serviceResult);
708 serviceResultsMap.put(testID, serviceResult);
709 } else if (method.equalsIgnoreCase("LIST")){
710 fullURL = fixupFullURL(fullURL, protoHostPort, uri);
711 String listQueryParams = ""; //TODO: empty for now, later may pick up from XML control file.
712 serviceResult = XmlReplayTransport.doLIST(fullURL, listQueryParams, authForTest, testIDLabel);
713 results.add(serviceResult);
714 serviceResultsMap.put(testID, serviceResult);
716 throw new Exception("HTTP method not supported by XmlReplay: "+method);
719 serviceResult.testID = testID;
720 serviceResult.fullURL = fullURL;
721 serviceResult.auth = authForTest;
722 serviceResult.adminAuth = authForCleanup;
723 serviceResult.method = method;
724 if (expectedCodes.size()>0){
725 serviceResult.expectedCodes = expectedCodes;
727 if (Tools.isEmpty(serviceResult.testID)) serviceResult.testID = testIDLabel;
728 if (Tools.isEmpty(serviceResult.testGroupID)) serviceResult.testGroupID = testGroupID;
730 Node expectedLevel = testNode.selectSingleNode("response/expected");
731 if (expectedLevel!=null){
732 String level = expectedLevel.valueOf("@level");
733 serviceResult.payloadStrictness = level;
735 //=====================================================
736 // ALL VALIDATION FOR ALL REQUESTS IS DONE HERE:
737 //=====================================================
738 boolean hasError = false;
739 String vError = validateResponse(serviceResult, serviceResultsMap, expectedResponseParts, evalStruct);
740 if (Tools.notEmpty(vError)){
741 serviceResult.error = vError;
742 serviceResult.failureReason = " : VALIDATION ERROR; ";
745 if (hasError == false){
746 hasError = ! serviceResult.gotExpectedResult();
749 report.addTestResult(serviceResult);
751 if ((dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) || (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.full)) {
752 System.out.println("\r\n#---------------------#");
755 String leader = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.detailed) ? "XmlReplay:" + testIDLabel +": ": "";
756 String serviceResultRow = serviceResult.dump(dump.dumpServiceResult, hasError) + "; time:" + (System.currentTimeMillis()-startTime);
757 //System.out.println(timeString() + "\n" + leader + serviceResultRow + "\r\n");
758 System.out.println(String.format("Time: %s\nLeader: %s\nAuth: %s\nResult: %s\r\n",
759 timeString(), testIDLabel, Tools.notEmpty(authIDForTest) ? authIDForTest : "<inferred>", serviceResultRow));
761 boolean doingAuto = (dump.dumpServiceResult == ServiceResult.DUMP_OPTIONS.auto);
762 if (dump.payloads || (doingAuto&&hasError) ) {
763 if (Tools.notBlank(serviceResult.requestPayload)){
764 System.out.println("\r\n========== request payload ===============");
765 System.out.println(serviceResult.requestPayload);
766 System.out.println("==========================================\r\n");
769 if (dump.payloads || (doingAuto&&hasError)) {
770 if (Tools.notBlank(serviceResult.result)){
771 System.out.println("\r\n========== response payload ==============");
772 System.out.println(serviceResult.result);
773 System.out.println("==========================================\r\n");
776 } catch (Throwable t) {
777 String msg = "ERROR: XmlReplay experienced an error in a test node: "+testNode+" Throwable: "+t;
778 System.out.println(msg);
779 System.out.println(Tools.getStackTrace(t));
780 ServiceResult serviceResult = new ServiceResult();
781 serviceResult.error = msg;
782 serviceResult.failureReason = " : SYSTEM ERROR; ";
783 results.add(serviceResult);
790 if (Tools.isTrue(autoDeletePOSTS) && param_autoDeletePOSTS){
791 autoDelete(defaultAuths, serviceResultsMap, "default", 0);
795 //=== Now spit out the HTML report file ===
796 File m = new File(controlFileName);
797 String localName = m.getName();//don't instantiate, just use File to extract file name without directory.
798 String reportName = localName+'-'+targetedTestGroupID+".html";
800 File resultFile = report.saveReport(xmlReplayBaseDir, reportsDir, reportName);
801 if (resultFile!=null) {
802 String toc = report.getTOC(reportName);
803 reportsList.add(toc);
809 private static String timeString() {
810 java.util.Date date = new java.util.Date();
811 java.sql.Timestamp ts = new java.sql.Timestamp(date.getTime());
812 return ts.toString();
815 //======================== MAIN ===================================================================
817 private static Options createOptions() {
818 Options options = new Options();
819 options.addOption("xmlReplayBaseDir", true, "default/basedir");
823 public static String usage(){
824 String result = "org.collectionspace.services.IntegrationTests.xmlreplay.XmlReplay {args}\r\n"
825 +" -xmlReplayBaseDir <dir> \r\n"
826 +" You may also override these with system args, e.g.: \r\n"
827 +" -DxmlReplayBaseDir=/path/to/dir \r\n"
828 +" These may also be passed in via the POM.\r\n"
829 +" You can also set these system args, e.g.: \r\n"
830 +" -DtestGroupID=<oneID> \r\n"
831 +" -DtestID=<one TestGroup ID>"
832 +" -DautoDeletePOSTS=<true|false> \r\n"
833 +" (note: -DautoDeletePOSTS won't force deletion if set to false in control file.";
837 private static String opt(CommandLine line, String option){
839 String fromProps = System.getProperty(option);
840 if (Tools.notEmpty(fromProps)){
846 result = line.getOptionValue(option);
853 public static void main(String[]args) throws Exception {
854 Options options = createOptions();
855 //System.out.println("System CLASSPATH: "+prop.getProperty("java.class.path", null));
856 CommandLineParser parser = new GnuParser();
858 // parse the command line arguments
859 CommandLine line = parser.parse(options, args);
861 String xmlReplayBaseDir = opt(line, "xmlReplayBaseDir");
862 String reportsDir = opt(line, "reportsDir");
863 String testGroupID = opt(line, "testGroupID");
864 String testID = opt(line, "testID");
865 String autoDeletePOSTS = opt(line, "autoDeletePOSTS");
866 String dumpResults = opt(line, "dumpResults");
867 String controlFilename = opt(line, "controlFilename");
868 String xmlReplayMaster = opt(line, "xmlReplayMaster");
870 if (Tools.isBlank(reportsDir)){
871 reportsDir = xmlReplayBaseDir + XmlReplayTest.REPORTS_DIRNAME;
873 reportsDir = Tools.fixFilename(reportsDir);
874 xmlReplayBaseDir = Tools.fixFilename(xmlReplayBaseDir);
875 controlFilename = Tools.fixFilename(controlFilename);
877 boolean bAutoDeletePOSTS = true;
878 if (Tools.notEmpty(autoDeletePOSTS)) {
879 bAutoDeletePOSTS = Tools.isTrue(autoDeletePOSTS);
881 boolean bDumpResults = false;
882 if (Tools.notEmpty(dumpResults)) {
883 bDumpResults = Tools.isTrue(autoDeletePOSTS);
885 if (Tools.isEmpty(xmlReplayBaseDir)){
886 System.err.println("ERROR: xmlReplayBaseDir was not specified.");
889 File f = new File(Tools.glue(xmlReplayBaseDir, "/", controlFilename));
890 if (Tools.isEmpty(xmlReplayMaster) && !f.exists()){
891 System.err.println("Control file not found: "+f.getCanonicalPath());
894 File fMaster = new File(Tools.glue(xmlReplayBaseDir, "/", xmlReplayMaster));
895 if (Tools.notEmpty(xmlReplayMaster) && !fMaster.exists()){
896 System.err.println("Master file not found: "+fMaster.getCanonicalPath());
900 String xmlReplayBaseDirResolved = (new File(xmlReplayBaseDir)).getCanonicalPath();
901 System.out.println("XmlReplay ::"
902 + "\r\n xmlReplayBaseDir: "+xmlReplayBaseDir
903 + "\r\n xmlReplayBaseDir(resolved): "+xmlReplayBaseDirResolved
904 + "\r\n controlFilename: "+controlFilename
905 + "\r\n xmlReplayMaster: "+xmlReplayMaster
906 + "\r\n testGroupID: "+testGroupID
907 + "\r\n testID: "+testID
908 + "\r\n autoDeletePOSTS: "+bAutoDeletePOSTS
909 + (Tools.notEmpty(xmlReplayMaster)
910 ? ("\r\n will use master file: "+fMaster.getCanonicalPath())
911 : ("\r\n will use control file: "+f.getCanonicalPath()) )
914 if (Tools.notEmpty(xmlReplayMaster)){
915 if (Tools.notEmpty(controlFilename)){
916 System.out.println("WARN: controlFilename: "+controlFilename+" will not be used because master was specified. Running master: "+xmlReplayMaster);
918 XmlReplay replay = new XmlReplay(xmlReplayBaseDirResolved, reportsDir);
919 replay.readOptionsFromMasterConfigFile(xmlReplayMaster);
920 replay.setAutoDeletePOSTS(bAutoDeletePOSTS);
921 Dump dumpFromMaster = replay.getDump();
922 dumpFromMaster.payloads = Tools.isTrue(dumpResults);
923 replay.setDump(dumpFromMaster);
924 replay.runMaster(xmlReplayMaster, false); //false, because we already just read the options, and override a few.
926 Dump dump = getDumpConfig();
927 dump.payloads = Tools.isTrue(dumpResults);
928 List<String> reportsList = new ArrayList<String>();
929 runXmlReplayFile(xmlReplayBaseDirResolved, controlFilename, testGroupID, testID, createResultsMap(), bAutoDeletePOSTS, dump, "", null, reportsList, reportsDir);
930 System.out.println("DEPRECATED: reportsList is generated, but not dumped: "+reportsList.toString());
932 } catch (ParseException exp) {
933 // oops, something went wrong
934 System.err.println("Cmd-line parsing failed. Reason: " + exp.getMessage());
935 System.err.println(usage());
936 } catch (Exception e) {
937 System.out.println("Error : " + e.getMessage());