/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package uk.nhs.interoperability.payloads;
import static org.junit.Assert.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.custommonkey.xmlunit.DetailedDiff;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.NodeDetail;
import org.custommonkey.xmlunit.XMLTestCase;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xml.sax.SAXException;
import uk.nhs.interoperability.payloads.helpers.DocumentRenderer;
import uk.nhs.interoperability.payloads.notification.EventNotification;
import uk.nhs.interoperability.payloads.testutils.SchemaValidator;
import uk.nhs.interoperability.payloads.util.FileLoader;
import uk.nhs.interoperability.payloads.util.FileUtils;
import uk.nhs.interoperability.payloads.util.FileWriter;
import uk.nhs.interoperability.payloads.util.Logger;
import uk.nhs.interoperability.payloads.util.PropertyReader;
import uk.nhs.interoperability.payloads.util.TransformManager;
import uk.nhs.interoperability.payloads.util.xml.XML2HTML;
import uk.nhs.interoperability.payloads.util.xml.XMLNamespaceContext;
import uk.nhs.interoperability.payloads.vocabularies.internal.OrgIDType;
public abstract class AbstractTest extends XMLTestCase {
protected static XMLNamespaceContext parentNamespaces = Namespaces.parentNamespaces;
protected static XMLNamespaceContext fhirParentNamespaces = Namespaces.fhirParentNamespaces;
protected static String namespacesToAdd = Namespaces.namespacesToAdd;
protected static String fhirNamespacesToAdd = Namespaces.fhirNamespacesToAdd;
protected String content = "";
private String filename;
private String directory;
private String testName;
private boolean pass = true;
public String getFilename() {
return filename;
}
public String getDirectory() {
return directory;
}
public void init(String directory, String filename, String testName, String testDescription) {
content = FileLoader.loadFileOnClasspath("/reportTemplates/testReportTemplate.htm");
content = content.replaceAll("#TESTNAME#", testName);
content = content.replaceAll("#TESTDESCRIPTION#", testDescription);
this.filename = filename;
this.directory = directory;
this.testName = testName;
// Create output directory if it doesn't exist
FileUtils.createDirectory(PropertyReader.getProperty("testReportPath") + directory);
}
public void writeResults() {
// First, check if there is an index of results, if there isn't create it from the template.
// If there is, load it, and add our result to it (unless it is already listed)
System.out.println("Writing results for test: " + testName);
String indexContent;
String indexFilename = PropertyReader.getProperty("testReportPath") + "index.htm";
if (FileUtils.fileExists(indexFilename)) {
indexContent = FileLoader.loadFile(indexFilename);
} else {
indexContent = FileLoader.loadFileOnClasspath("/reportTemplates/index.htm");
}
String home = System.getProperty("user.home");
indexContent = indexContent.replaceAll("#FOOTER#", "Home folder: " + home.replace('\\', '/'));
String resultString = "<p id='" + testName + "' ";
if (this.pass) {
resultString = resultString + "class='pass'";
} else {
resultString = resultString + "class='fail'";
}
resultString = resultString + "><a target=\"results\" href=\"" + directory + filename + ".htm\">" + this.testName + "</a></p>";
if (!indexContent.contains(testName)) {
// See if we already have a heading for this test directory
String directoryHeading = "<h3>" + directory.substring(0, directory.length()-1) + "</h3><div class='testsInDirectory'>";
if (indexContent.contains(directoryHeading)) {
// Add this test under the existing directory heading
indexContent = indexContent.replace(directoryHeading, directoryHeading + resultString);
} else {
// Add the directory heading, and add the test under it
indexContent = indexContent.replace("<!--TESTLISTEND-->", directoryHeading + resultString + "</div><!--TESTLISTEND-->");
}
} else {
// Replace existing result line in file
indexContent = indexContent.replaceFirst("<p id='" + testName + "'.*?</p>", resultString);
}
FileWriter.writeFile(indexFilename, indexContent.getBytes());
// Now, write the actual result file
FileWriter.writeFile(PropertyReader.getProperty("testReportPath") + directory + filename + ".htm", content.getBytes());
}
public void testXMLisSimilar(String expectedResultFilename, String result, boolean addMissingDefaultNamespace) {
String expectedXML = FileLoader.loadFileOnClasspath(expectedResultFilename);
testXMLisSimilar(expectedXML, result, addMissingDefaultNamespace, true);
}
public String loadExpectedResult(String expectedResultFilename, boolean addMissingDefaultNamespace) {
String expectedXML = FileLoader.loadFileOnClasspath(expectedResultFilename);
if (addMissingDefaultNamespace) {
int idx = expectedXML.indexOf(">");
if (idx>-1) {
expectedXML = expectedXML.substring(0, idx) + namespacesToAdd + expectedXML.substring(idx);
}
}
return expectedXML;
}
public void testXMLisSimilar(String expectedXML, String result, boolean addMissingDefaultNamespace, boolean a) {
if (addMissingDefaultNamespace) {
int idx = expectedXML.indexOf(">");
if (idx>-1) {
expectedXML = expectedXML.substring(0, idx) + namespacesToAdd + expectedXML.substring(idx);
}
}
// Save expected and actual results to files
if (directory != null) {
/*String resultFile = filename + ".htm";
String filePrefix = PropertyReader.getProperty("testReportPath") + directory + filename;
// HTML Formatted Outputs
FileWriter.writeFile(filePrefix + "-expected-formatted.htm", XML2HTML.reformat(expectedXML, "<h1>Expected Result XML</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
FileWriter.writeFile(filePrefix + "-actual-formatted.htm", XML2HTML.reformat(result, "<h1>Actual Result XML</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
// Raw Outputs
FileWriter.writeFile(filePrefix + "-expected.xml", expectedXML.getBytes());
FileWriter.writeFile(filePrefix + "-actual.xml", result.getBytes());
content = content.replaceAll("#EXPECTED#", "<li><a href='" + filename + "-expected-formatted.htm'>Expected Result</a>" +
" [<a target=\"_blank\" href='" + filename + "-expected.xml'>Raw XML</a>]</li>");
content = content.replaceAll("#ACTUAL#", "<li><a href='" + filename + "-actual-formatted.htm'>Actual Result</a>" +
" [<a target=\"_blank\" href='" + filename + "-actual.xml'>Raw XML</a>]</li>");
*/
addExpectedResultWithXML(expectedXML);
addActualResultWithXML(result);
}
//XMLUnit.setXpathNamespaceContext(new XMLUnitNamespaceContext());
XMLUnit.setIgnoreWhitespace(true);
XMLUnit.setIgnoreAttributeOrder(true);
XMLUnit.setIgnoreComments(true);
//XMLUnit.setCompareUnmatched(false);
//assertXMLEqual("Comparing generated xml to example xml provided in DMS",expectedXML, result);
try {
Diff diff = XMLUnit.compareXML( expectedXML, result );
int differenceCount = outputCompareResults(diff);
assertTrue( "XML is similar", (differenceCount == 0) );
} catch (IOException e) {
fail("Error processing test");
e.printStackTrace();
} catch (SAXException e) {
fail("Error processing test");
e.printStackTrace();
}
}
public void testAgainstSchema(String schemaPath, String xmlTotest) {
Logger.info("Schema path: " + schemaPath);
File schema = new File(schemaPath);
String resultFile = filename + ".htm";
String filePrefix = PropertyReader.getProperty("testReportPath") + directory + filename;
content = content.replaceAll("#EXPECTED#", "<li>File valid against schema: " + schemaPath + "</li>");
try {
SchemaValidator.validate(xmlTotest, schema);
content = content.replaceAll("#TESTRESULT#", "<div class='pass'>PASS: Schema Validation Passed</div>");
/*FileWriter.writeFile(filePrefix + "-actual-formatted.htm", XML2HTML.reformat(xmlTotest, "<h1>XML tested against schema</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
FileWriter.writeFile(filePrefix + "-actual.xml", xmlTotest.getBytes());
content = content.replaceAll("#ACTUAL#", "<li>Valid. <a href='" + filename + "-actual-formatted.htm'>XML tested against schema</a>" +
" [<a target=\"_blank\" href='" + filename + "-actual.xml'>Raw XML</a>]</li>");*/
addActualResultWithXML(xmlTotest);
} catch (Exception e) {
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: Schema Validation Failed</div>" +
"<br/>Error: " + e.getMessage());
/*FileWriter.writeFile(filePrefix + "-actual-formatted.htm", XML2HTML.reformat(xmlTotest, "<h1>XML tested against schema</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
FileWriter.writeFile(filePrefix + "-actual.xml", xmlTotest.getBytes());
content = content.replaceAll("#ACTUAL#", "<li>Invalid. <a href='" + filename + "-actual-formatted.htm'>XML tested against schema</a>" +
" [<a target=\"_blank\" href='" + filename + "-actual.xml'>Raw XML</a>]</li>");*/
addActualResultWithXML(xmlTotest);
pass = false;
fail("Schema validation failed: " + e.getMessage());
}
}
public void testAgainstTemplatedSchema(String schemaPath, String xmlToTest) {
content = content.replaceAll("#EXPECTED#", "<li>Transformed file valid against templated schema: " + schemaPath + "</li>");
// HACK: The tansform from on-the-wire to templated CDA provided by the
// messaging team requires an XSLT 2.0 processor (Saxon to be specific). As this
// would break all the other transforms used in this library, we are forking
// a separate java command line process to do this transform. This means there
// has to be a writable directory to put the file into, and read the transformed
// file back from. By default we will use the working directory as this is only
// a unit test dependency. This should be the root project directory.
String tempPath = "";
String saxonPath = PropertyReader.getProperty("saxonJarPath");
String xslPath = tempPath + "src/main/resources/othertransforms/TrueCDAToCDALike_v2.xsl";
FileWriter.writeFile(tempPath+"temp.xml", xmlToTest.getBytes());
String command = "java -jar " + saxonPath + " -s:"+tempPath+"temp.xml -xsl:" + xslPath + " -o:templated.xml";
Logger.info(command);
Runtime rt = Runtime.getRuntime();
Process pr;
try {
pr = rt.exec(command);
// Wait until the process completes
pr.waitFor();
if (pr.exitValue() != 0) {
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: Templated Schema Validation Failed</div>" +
"<br/>Error running XSLT transformation using SAXON");
pass = false;
Logger.error("Error running XSLT transformation using SAXON", null);
fail("Error running XSLT transformation using SAXON");
}
} catch (IOException e) {
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: Templated Schema Validation Failed</div>" +
"<br/>Error: " + e.getMessage());
pass = false;
Logger.error("Error when trying to transform from on-the-wire to templated format", e);
fail("Error when trying to transform from on-the-wire to templated format: " + e.getMessage());
} catch (InterruptedException e) {
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: Templated Schema Validation Failed</div>" +
"<br/>Error: " + e.getMessage());
pass = false;
Logger.error("Error when trying to transform from on-the-wire to templated format", e);
fail("Error when trying to transform from on-the-wire to templated format: " + e.getMessage());
}
String templatedXML = FileLoader.loadFile("templated.xml");
testAgainstSchema(schemaPath, templatedXML);
}
private int outputCompareResults(Diff diff) {
String differenceSummary = "";
DetailedDiff detailedDiff = new DetailedDiff(diff);
int differenceCount = 0;
List<Difference> allDifferences = detailedDiff.getAllDifferences();
List<Difference> exceptions = new ArrayList<Difference>();
for (Difference diffItem : allDifferences) {
if (diffItem.isRecoverable()) {
// Similar but not identical - ignore
} else {
NodeDetail actualNodeDetail = diffItem.getTestNodeDetail();
NodeDetail expectedNodeDetail = diffItem.getControlNodeDetail();
// If we want any exceptions to the strict comparisons done by XMLUnit we can add them here
/*if ((expectedNodeDetail.getValue().startsWith("{urn:hl7-org:v3}")) &&
(actualNodeDetail.getValue().startsWith("{null}"))) {
exceptions.add(diffItem);
System.out.println("Difference ignored (exception): " + diffItem.toString());
} else {*/
System.out.println(diffItem.toString());
differenceSummary = differenceSummary + "<li>" + diffItem.toString() + "</li>";
differenceCount++;
//}
}
}
// Now remove any exceptions from the list of differences
for (Difference diffItem : exceptions) {
allDifferences.remove(diffItem);
}
if (differenceCount>0) {
System.out.println("== Found " + differenceCount + " significant differences in the XML content ==");
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: XML Does not match, "
+ differenceCount + " significant differences found.</div>" +
"<br/>Summary of differences:<ul>" + differenceSummary + "</ul>");
pass = false;
} else {
content = content.replaceAll("#TESTRESULT#", "<div class='pass'>PASS: XML Matches</div>");
}
return differenceCount;
}
public void render(String xml) {
String filePrefix = PropertyReader.getProperty("testReportPath") + directory + filename;
String html = DocumentRenderer.generateHTMLDocument(xml);
FileWriter.writeFile(filePrefix + "-rendered.htm", html.getBytes());
content = content.replaceAll("<!-- RENDERED OUTPUT CAN GO HERE IF APPLICABLE -->", "<li><a href='" + filename + "-rendered.htm'>Rendered Document</a>");
}
public void addActualResultWithXML(String xml) {
String filePrefix = PropertyReader.getProperty("testReportPath") + directory + filename;
String resultFile = filename + ".htm";
FileWriter.writeFile(filePrefix + "-actual-formatted.htm", XML2HTML.reformat(xml, "<h1>XML Output</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
FileWriter.writeFile(filePrefix + "-actual.xml", xml.getBytes());
content = content.replaceAll("#ACTUAL#", "<li>Valid. <a href='" + filename + "-actual-formatted.htm'>XML Output</a>" +
" [<a target=\"_blank\" href='" + filename + "-actual.xml'>Raw XML</a>]</li>");
}
public void addExpectedResultWithXML(String xml) {
String filePrefix = PropertyReader.getProperty("testReportPath") + directory + filename;
String resultFile = filename + ".htm";
FileWriter.writeFile(filePrefix + "-expected-formatted.htm", XML2HTML.reformat(xml, "<h1>Expected XML</h1><a href='" + resultFile + "'>Back to test result</a>").getBytes());
FileWriter.writeFile(filePrefix + "-expected.xml", xml.getBytes());
content = content.replaceAll("#EXPECTED#", "<li><a href='" + filename + "-expected-formatted.htm'>Expected XML</a>" +
" [<a target=\"_blank\" href='" + filename + "-expected.xml'>Raw XML</a>]</li>");
}
public void exception(Exception e) {
content = content.replaceAll("#TESTRESULT#", "<div class='fail'>FAIL: Exception thrown: " + e.getMessage() + "</div>");
content = content.replaceAll("#EXPECTED#", "");
content = content.replaceAll("#ACTUAL#", "");
e.printStackTrace();
fail("Exception thrown: " + e.toString());
}
public void setExpected(String expected) {
content = content.replaceAll("#EXPECTED#", expected);
}
public void setActual(String actual) {
int idx = content.indexOf("#ACTUAL#");
if (idx == -1) {
return;
}
StringBuilder out = new StringBuilder();
out.append(content.substring(0, idx));
out.append(actual);
out.append(content.substring(idx+8, content.length()));
content = out.toString();
}
public void setResult(String result) {
content = content.replaceAll("#TESTRESULT#", result);
}
public void setResultPass() {
content = content.replaceAll("#TESTRESULT#", "<div class='pass'>PASS</div>");
}
// Performance test methods
protected static String runPerfTest(Payload document) {
return runPerfTest(document, true, true, null, 15);
}
protected static String runPerfTest(Payload document, boolean serialise, boolean parse, String xmlDocument, int repeatCount) {
String result = "";
int sizeBytes = 0;
String testOutput = "<table width=500 border=1 cellpadding=2 cellspacing=0><tr><th>Serialise Tests</th><th>Parse Tests</th></tr><tr>";
long firstStartTime = System.currentTimeMillis();
if (serialise) {
// Serialise the objects
testOutput = testOutput + "<td>";
for (int n=0; n<repeatCount; n++) {
long startTime = System.currentTimeMillis();
result = document.serialise();
testOutput = testOutput + "<li>Test Run " + (n+1) + " = " + (System.currentTimeMillis() - startTime) + "ms</li>";
}
testOutput = testOutput + "</td>";
}
if (parse) {
// Now parse them
if (!serialise) {
// We haven't serialised a document, so we will have to use one loaded from a file and passed in
result = xmlDocument;
}
testOutput = testOutput + "<td>";
for (int n=0; n<repeatCount; n++) {
long startTime = System.currentTimeMillis();
EventNotification notification = new EventNotification();
notification.parse(result);
testOutput = testOutput + "<li>Test Run " + (n+1) + " = " + (System.currentTimeMillis() - startTime) + "ms</li>";
}
testOutput = testOutput + "</td>";
}
sizeBytes = result.getBytes().length;
testOutput = testOutput + "</tr></table><br/>";
testOutput = testOutput + "<li>Payload size = " + sizeBytes + " bytes</li>";
testOutput = testOutput + "<li>Total time = " + (System.currentTimeMillis() - firstStartTime) + "ms</li>";
return testOutput;
}
public static String exec(Class klass, boolean profile, String directory, String filename) throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
BufferedReader inStream = null;
StringBuilder output = new StringBuilder();
String javaHome = System.getProperty("java.home");
String javaBin = javaHome +
File.separator + "bin" +
File.separator + "java";
String classpath = System.getProperty("java.class.path");
String className = klass.getCanonicalName();
String jvmArgs = "";
if (profile) {
String profilerFilePath = PropertyReader.getProperty("testReportPath") + directory + filename + ".hprof";
jvmArgs = jvmArgs + "-agentlib:hprof=cpu=times,file="+profilerFilePath;
}
String cmd = "Running external JVM: " + javaBin + " " + jvmArgs + " -cp " + classpath + " " + className;
System.out.println(cmd);
FileWriter.writeFile(PropertyReader.getProperty("testReportPath") + directory + filename + "-CommandLine.txt", cmd.getBytes());
ProcessBuilder builder;
if (profile) {
builder = new ProcessBuilder(javaBin, jvmArgs, "-cp", classpath, className);
} else {
builder = new ProcessBuilder(javaBin, "-cp", classpath, className);
}
builder.redirectErrorStream(true);
Process process = builder.start();
// Read from standard output of process
try {
inStream = new BufferedReader( new InputStreamReader( process.getInputStream() ));
int i;
while ((i = inStream.read())>-1) {
output.append((char)i);
}
} catch(IOException e) {
output.append("Error on inStream.readLine()");
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
output.append(sw.toString());
}
process.waitFor();
System.out.println("External process exit value: " + process.exitValue());
output.append("<li>Time including JVM initialisation = ").append((System.currentTimeMillis() - startTime)).append("ms</li>");
return output.toString();
}
public static String encodeHprofOutputsForHTML(String s)
{
StringBuffer out = new StringBuffer();
out.append("<pre>");
for(int i=0; i<s.length(); i++)
{
char c = s.charAt(i);
if(c > 127 || c=='"' || c=='<' || c=='>') {
out.append("&#"+(int)c+";");
/*} else if (c == 13) {
out.append("<br/>");
} else if (c == 32) {
out.append(" ");*/
} else {
out.append(c);
}
}
out.append("</pre>");
return out.toString();
}
}