![](/hapi-fhir/images/logos/raccoon-forwards.png)
001package org.hl7.fhir.r5.test.utils; 002 003import org.apache.commons.codec.binary.Base64; 004import org.apache.commons.lang3.StringUtils; 005import org.hl7.fhir.exceptions.FHIRException; 006import org.hl7.fhir.utilities.*; 007import org.hl7.fhir.utilities.filesystem.CSFile; 008import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 009import org.hl7.fhir.utilities.json.JsonUtilities; 010import org.hl7.fhir.utilities.json.model.JsonArray; 011import org.hl7.fhir.utilities.json.model.JsonElement; 012import org.hl7.fhir.utilities.json.model.JsonNull; 013import org.hl7.fhir.utilities.json.model.JsonObject; 014import org.hl7.fhir.utilities.json.model.JsonPrimitive; 015import org.hl7.fhir.utilities.json.model.JsonProperty; 016import org.hl7.fhir.utilities.json.parser.JsonParser; 017import org.hl7.fhir.utilities.settings.FhirSettings; 018import org.w3c.dom.Document; 019import org.w3c.dom.Element; 020import org.w3c.dom.NamedNodeMap; 021import org.w3c.dom.Node; 022 023import org.hl7.fhir.utilities.tests.BaseTestingUtilities; 024 025import javax.xml.parsers.DocumentBuilder; 026import javax.xml.parsers.DocumentBuilderFactory; 027import java.io.*; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Map; 031 032public class CompareUtilities extends BaseTestingUtilities { 033 034 private static final boolean SHOW_DIFF = false; 035 private JsonObject externals; 036 037 public String createNotEqualMessage(String id, final String message, final String expected, final String actual) { 038 return new StringBuilder() 039 .append(message).append('\n') 040 .append("Expected:").append(presentExpected(expected)).append(" for "+id).append('\n') 041 .append("Actual :").append("\""+actual+"\"").toString(); 042 } 043 044 private String presentExpected(String expected) { 045 if (expected == null) { 046 return "null"; 047 } else if (expected.startsWith("$") && expected.endsWith("$")) { 048 if (expected.startsWith("$choice:")) { 049 return "Contains one of "+readChoices(expected.substring(8, expected.length()-1)).toString(); 050 } else if (expected.startsWith("$fragments:")) { 051 List<String> fragments = readChoices(expected.substring(11, expected.length()-1)); 052 return "Contains all of "+fragments.toString(); 053 } else if (expected.startsWith("$external:")) { 054 String[] cmd = expected.substring(1, expected.length() - 1).split(":"); 055 if (externals != null) { 056 String s = externals.asString(cmd[1]); 057 return "\""+s+"\" (Ext)"; 058 } else { 059 List<String> fragments = readChoices(cmd[2]); 060 return "Contains all of "+fragments.toString()+" (because no external string provided for "+cmd[1]+")"; 061 } 062 } else { 063 switch (expected) { 064 case "$$" : return "$$"; 065 case "$instant$": return "\"An Instant\""; 066 case "$uuid$": return "\"A Uuid\""; 067 default: return "Unhandled template: "+expected; 068 } 069 } 070 } else { 071 return "\""+expected+"\""; 072 } 073 } 074 075 public static String checkXMLIsSame(String id, InputStream expected, InputStream actual) throws Exception { 076 CompareUtilities self = new CompareUtilities(); 077 String result = self.compareXml(id, expected, actual); 078 return result; 079 } 080 081 public static String checkXMLIsSame(String id, String expected, String actual) throws Exception { 082 CompareUtilities self = new CompareUtilities(); 083 String result = self.compareXml(id, expected, actual); 084 if (result != null && SHOW_DIFF) { 085 String diff = getDiffTool(); 086 if (diff != null && ManagedFileAccess.file(diff).exists() || Utilities.isToken(diff)) { 087 Runtime.getRuntime().exec(new String[]{diff, expected, actual}); 088 } 089 } 090 return result; 091 } 092 093 private static String getDiffTool() throws IOException { 094 if (FhirSettings.hasDiffToolPath()) { 095 return FhirSettings.getDiffToolPath(); 096 } else if (System.getenv("ProgramFiles") != null) { 097 return Utilities.path(System.getenv("ProgramFiles"), "WinMerge", "WinMergeU.exe"); 098 } else { 099 return null; 100 } 101 } 102 103 private String compareXml(String id, InputStream expected, InputStream actual) throws Exception { 104 return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement()); 105 } 106 107 private String compareXml(String id, String expected, String actual) throws Exception { 108 return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement()); 109 } 110 111 private String compareElements(String id, String path, Element expectedElement, Element actualElement) { 112 if (!namespacesMatch(expectedElement.getNamespaceURI(), actualElement.getNamespaceURI())) 113 return createNotEqualMessage(id, "Namespaces differ at " + path, expectedElement.getNamespaceURI(), actualElement.getNamespaceURI()); 114 if (!expectedElement.getLocalName().equals(actualElement.getLocalName())) 115 return createNotEqualMessage(id, "Names differ at " + path , expectedElement.getLocalName(), actualElement.getLocalName()); 116 path = path + "/" + expectedElement.getLocalName(); 117 String s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes()); 118 if (!Utilities.noString(s)) 119 return s; 120 s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes()); 121 if (!Utilities.noString(s)) 122 return s; 123 124 Node expectedChild = expectedElement.getFirstChild(); 125 Node actualChild = actualElement.getFirstChild(); 126 expectedChild = skipBlankText(expectedChild); 127 actualChild = skipBlankText(actualChild); 128 while (expectedChild != null && actualChild != null) { 129 if (expectedChild.getNodeType() != actualChild.getNodeType()) 130 return createNotEqualMessage(id, "node type mismatch in children of " + path, Short.toString(expectedElement.getNodeType()), Short.toString(actualElement.getNodeType())); 131 if (expectedChild.getNodeType() == Node.TEXT_NODE) { 132 if (!normalise(expectedChild.getTextContent()).equals(normalise(actualChild.getTextContent()))) 133 return createNotEqualMessage(id, "Text differs at " + path, normalise(expectedChild.getTextContent()).toString(), normalise(actualChild.getTextContent()).toString()); 134 } else if (expectedChild.getNodeType() == Node.ELEMENT_NODE) { 135 s = compareElements(id, path, (Element) expectedChild, (Element) actualChild); 136 if (!Utilities.noString(s)) 137 return s; 138 } 139 140 expectedChild = skipBlankText(expectedChild.getNextSibling()); 141 actualChild = skipBlankText(actualChild.getNextSibling()); 142 } 143 if (expectedChild != null) 144 return "node mismatch - more nodes in actual in children of " + path; 145 if (actualChild != null) 146 return "node mismatch - more nodes in expected in children of " + path; 147 return null; 148 } 149 150 private boolean namespacesMatch(String ns1, String ns2) { 151 return ns1 == null ? ns2 == null : ns1.equals(ns2); 152 } 153 154 private String normalise(String text) { 155 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 156 while (result.contains(" ")) 157 result = result.replace(" ", " "); 158 return result; 159 } 160 161 private String compareAttributes(String id, String path, NamedNodeMap expected, NamedNodeMap actual) { 162 for (int i = 0; i < expected.getLength(); i++) { 163 164 Node expectedNode = expected.item(i); 165 String expectedNodeName = expectedNode.getNodeName(); 166 if (!(expectedNodeName.equals("xmlns") || expectedNodeName.startsWith("xmlns:"))) { 167 Node actualNode = actual.getNamedItem(expectedNodeName); 168 if (actualNode == null) 169 return "Attributes differ at " + path + ": missing attribute " + expectedNodeName; 170 if (!normalise(expectedNode.getTextContent()).equals(normalise(actualNode.getTextContent()))) { 171 byte[] b1 = unBase64(expectedNode.getTextContent()); 172 byte[] b2 = unBase64(actualNode.getTextContent()); 173 if (!sameBytes(b1, b2)) 174 return createNotEqualMessage(id, "Attributes differ at " + path, normalise(expectedNode.getTextContent()).toString(), normalise(actualNode.getTextContent()).toString()) ; 175 } 176 } 177 } 178 return null; 179 } 180 181 private boolean sameBytes(byte[] b1, byte[] b2) { 182 if (b1.length == 0 || b2.length == 0) 183 return false; 184 if (b1.length != b2.length) 185 return false; 186 for (int i = 0; i < b1.length; i++) 187 if (b1[i] != b2[i]) 188 return false; 189 return true; 190 } 191 192 private byte[] unBase64(String text) { 193 return Base64.decodeBase64(text); 194 } 195 196 private Node skipBlankText(Node node) { 197 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && StringUtils.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 198 node = node.getNextSibling(); 199 return node; 200 } 201 202 private Document loadXml(String fn) throws Exception { 203 return loadXml(ManagedFileAccess.inStream(fn)); 204 } 205 206 private Document loadXml(InputStream fn) throws Exception { 207 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 208 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 209 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 210 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 211 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 212 factory.setXIncludeAware(false); 213 factory.setExpandEntityReferences(false); 214 215 factory.setNamespaceAware(true); 216 DocumentBuilder builder = factory.newDocumentBuilder(); 217 return builder.parse(fn); 218 } 219 220 public static String checkJsonSrcIsSame(String id, String expected, String actual, JsonObject externals) throws FileNotFoundException, IOException { 221 return checkJsonSrcIsSame(id, expected, actual, true, externals); 222 } 223 224 public static String checkJsonSrcIsSame(String id, String expectedString, String actualString, boolean showDiff, JsonObject externals) throws FileNotFoundException, IOException { 225 CompareUtilities self = new CompareUtilities(); 226 self.externals = externals; 227 String result = self.compareJsonSrc(id, expectedString, actualString); 228 if (result != null && SHOW_DIFF && showDiff) { 229 String diff = null; 230 if (System.getProperty("os.name").contains("Linux")) 231 diff = Utilities.path("/", "usr", "bin", "meld"); 232 else if (System.getenv("ProgramFiles(X86)") != null) { 233 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 234 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 235 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 236 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 237 } 238 if (diff == null || diff.isEmpty()) 239 return result; 240 241 List<String> command = new ArrayList<String>(); 242 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 243 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 244 TextFile.stringToFile(expectedString, expected); 245 TextFile.stringToFile(actualString, actual); 246 command.add(diff); 247 if (diff.toLowerCase().contains("meld")) 248 command.add("--newtab"); 249 command.add(expected); 250 command.add(actual); 251 252 ProcessBuilder builder = new ProcessBuilder(command); 253 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 254 builder.start(); 255 256 } 257 return result; 258 } 259 260 public static String checkJsonIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 261 CompareUtilities self = new CompareUtilities(); 262 String result = self.compareJson(id, expected, actual); 263 if (result != null && SHOW_DIFF) { 264 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 265 List<String> command = new ArrayList<String>(); 266 command.add("\"" + diff + "\" \"" + expected + "\" \"" + actual + "\""); 267 268 ProcessBuilder builder = new ProcessBuilder(command); 269 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 270 builder.start(); 271 272 } 273 return result; 274 } 275 276 private String compareJsonSrc(String id, String expected, String actual) throws FileNotFoundException, IOException { 277 JsonObject actualJsonObject = JsonParser.parseObject(actual); 278 JsonObject expectedJsonObject = JsonParser.parseObject(expected); 279 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 280 } 281 282 private String compareJson(String id, String expected, String actual) throws FileNotFoundException, IOException { 283 JsonObject actualJsonObject = JsonParser.parseObject(TextFile.fileToString(actual)); 284 JsonObject expectedJsonObject = JsonParser.parseObject(TextFile.fileToString(expected)); 285 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 286 } 287 288 private String compareObjects(String id, String path, JsonObject expectedJsonObject, JsonObject actualJsonObject) { 289 List<String> optionals = listOptionals(expectedJsonObject); 290 List<String> countOnlys = listCountOnlys(expectedJsonObject); 291 for (JsonProperty en : actualJsonObject.getProperties()) { 292 String n = en.getName(); 293 if (!n.equals("fhir_comments")) { 294 if (expectedJsonObject.has(n)) { 295 String s = compareNodes(id, path + '.' + n, expectedJsonObject.get(n), en.getValue(), countOnlys.contains(n)); 296 if (!Utilities.noString(s)) 297 return s; 298 } else 299 return "properties differ at " + path + ": missing property " + n; 300 } 301 } 302 for (JsonProperty en : expectedJsonObject.getProperties()) { 303 String n = en.getName(); 304 if (!n.equals("fhir_comments") && !n.equals("$optional$") && !optionals.contains(n)) { 305 if (!actualJsonObject.has(n)) 306 return "properties differ at " + path + ": missing property " + n; 307 } 308 } 309 return null; 310 } 311 312 private List<String> listOptionals(JsonObject expectedJsonObject) { 313 List<String> res = new ArrayList<>(); 314 if (expectedJsonObject.has("$optional-properties$")) { 315 res.add("$optional-properties$"); 316 res.add("$count-arrays$"); 317 for (String s : expectedJsonObject.getStrings("$optional-properties$")) { 318 res.add(s); 319 } 320 } 321 return res; 322 } 323 324 private List<String> listCountOnlys(JsonObject expectedJsonObject) { 325 List<String> res = new ArrayList<>(); 326 if (expectedJsonObject.has("$count-arrays$")) { 327 for (String s : expectedJsonObject.getStrings("$count-arrays$")) { 328 res.add(s); 329 } 330 } 331 return res; 332 } 333 334 private String compareNodes(String id, String path, JsonElement expectedJsonElement, JsonElement actualJsonElement, boolean countOnly) { 335 if (!(expectedJsonElement instanceof JsonPrimitive && actualJsonElement instanceof JsonPrimitive)) { 336 if (actualJsonElement.getClass() != expectedJsonElement.getClass()) { 337 return createNotEqualMessage(id, "properties differ at " + path, expectedJsonElement.getClass().getName(), actualJsonElement.getClass().getName()); 338 } 339 } 340 if (actualJsonElement instanceof JsonPrimitive) { 341 JsonPrimitive actualJsonPrimitive = (JsonPrimitive) actualJsonElement; 342 JsonPrimitive expectedJsonPrimitive = (JsonPrimitive) expectedJsonElement; 343 if (actualJsonPrimitive.isJsonBoolean() && expectedJsonPrimitive.isJsonBoolean()) { 344 if (actualJsonPrimitive.asBoolean() != expectedJsonPrimitive.asBoolean()) 345 return createNotEqualMessage(id, "boolean property values differ at " + path , expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 346 } else if (actualJsonPrimitive.isJsonString() && expectedJsonPrimitive.isJsonString()) { 347 String actualJsonString = actualJsonPrimitive.asString(); 348 String expectedJsonString = expectedJsonPrimitive.asString(); 349 if (!(actualJsonString.contains("<div") && expectedJsonString.contains("<div"))) 350 if (!matches(actualJsonString, expectedJsonString)) 351 if (!sameBytes(unBase64(actualJsonString), unBase64(expectedJsonString))) 352 return createNotEqualMessage(id, "string property values differ at " + path, expectedJsonString, actualJsonString); 353 } else if (actualJsonPrimitive.isJsonNumber() && expectedJsonPrimitive.isJsonNumber()) { 354 if (!actualJsonPrimitive.asString().equals(expectedJsonPrimitive.asString())) 355 return createNotEqualMessage(id, "number property values differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 356 } else if (expectedJsonElement instanceof JsonNull) { 357 return actualJsonPrimitive instanceof JsonNull ? null : createNotEqualMessage(id, "null Properties differ at " + path, "null", actualJsonPrimitive.asString()); 358 } else { 359 return createNotEqualMessage(id, "property types differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 360 } 361 } else if (actualJsonElement instanceof JsonObject) { 362 String s = compareObjects(id, path, (JsonObject) expectedJsonElement, (JsonObject) actualJsonElement); 363 if (!Utilities.noString(s)) 364 return s; 365 } else if (actualJsonElement instanceof JsonArray) { 366 JsonArray actualArray = (JsonArray) actualJsonElement; 367 JsonArray expectedArray = (JsonArray) expectedJsonElement; 368 369 int as = actualArray.size(); 370 int es = expectedArray.size(); 371 if (countOnly) { 372 if (as != es) { 373 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 374 } 375 } else { 376 int expectedMin = countExpectedMin(expectedArray); 377 int oc = optionalCount(expectedArray); 378 379 if (as > es || as < expectedMin) 380 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 381 int c = 0; 382 for (int i = 0; i < es; i++) { 383 if (c >= as) { 384 if (i >= es - oc && isOptional(expectedArray.get(i))) { 385 return null; // this is OK 386 } else { 387 return "One or more array items did not match at "+path+" starting at index "+i; 388 } 389 } 390 String s = compareNodes(id, path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(c), false); 391 if (!Utilities.noString(s) && !isOptional(expectedArray.get(i))) { 392 return s; 393 } 394 if (Utilities.noString(s)) { 395 c++; 396 } 397 } 398 if (c < as) { 399 return "Unexpected Node found in array at '"+path+"' at index "+c; 400 } 401 } 402 } else 403 return "unhandled property " + actualJsonElement.getClass().getName(); 404 return null; 405 } 406 407 private int optionalCount(JsonArray arr) { 408 int c = 0; 409 for (JsonElement e : arr) { 410 if (e.isJsonObject()) { 411 JsonObject j = e.asJsonObject(); 412 if (j.has("$optional$") && j.asBoolean("$optional$")) { 413 c++; 414 } 415 } 416 } 417 return c; 418 } 419 420 private boolean isOptional(JsonElement e) { 421 return e.isJsonObject() && e.asJsonObject().has("$optional$"); 422 } 423 424 private int countExpectedMin(JsonArray array) { 425 int count = array.size(); 426 for (JsonElement e : array) { 427 if (isOptional(e)) { 428 count--; 429 } 430 } 431 return count; 432 } 433 434 private boolean matches(String actualJsonString, String expectedJsonString) { 435 if (expectedJsonString.startsWith("$") && expectedJsonString.endsWith("$")) { 436 if (expectedJsonString.startsWith("$choice:")) { 437 return Utilities.existsInList(actualJsonString, readChoices(expectedJsonString.substring(8, expectedJsonString.length()-1))); 438 439 } else if (expectedJsonString.startsWith("$fragments:")) { 440 List<String> fragments = readChoices(expectedJsonString.substring(11, expectedJsonString.length()-1)); 441 for (String f : fragments) { 442 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 443 return false; 444 } 445 } 446 return true; 447 } else if (expectedJsonString.startsWith("$external:")) { 448 String[] cmd = expectedJsonString.substring(1, expectedJsonString.length() - 1).split("\\:"); 449 if (externals != null) { 450 String s = externals.asString(cmd[1]); 451 return actualJsonString.equals(s); 452 } else if (cmd.length <= 2) { 453 return true; 454 } else { 455 List<String> fragments = readChoices(cmd[2]); 456 for (String f : fragments) { 457 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 458 return false; 459 } 460 } 461 return true; 462 } 463 } else { 464 switch (expectedJsonString) { 465 case "$$" : return true; 466 case "$instant$": return actualJsonString.matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"); 467 case "$uuid$": return actualJsonString.matches("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); 468 default: 469 throw new Error("Unhandled template: "+expectedJsonString); 470 } 471 } 472 } else { 473 return actualJsonString.equals(expectedJsonString); 474 } 475 } 476 477 private List<String> readChoices(String s) { 478 List<String> list = new ArrayList<>(); 479 for (String p : s.split("\\|")) { 480 list.add(p); 481 } 482 return list; 483 } 484 485 public static String checkTextIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 486 return checkTextIsSame(id, expected, actual, true); 487 } 488 489 public static String checkTextIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException { 490 CompareUtilities self = new CompareUtilities(); 491 String result = self.compareText(id, expectedString, actualString); 492 if (result != null && SHOW_DIFF && showDiff) { 493 String diff = null; 494 if (System.getProperty("os.name").contains("Linux")) 495 diff = Utilities.path("/", "usr", "bin", "meld"); 496 else { 497 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 498 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 499 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 500 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 501 } 502 if (diff == null || diff.isEmpty()) 503 return result; 504 505 List<String> command = new ArrayList<String>(); 506 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 507 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 508 TextFile.stringToFile(expectedString, expected); 509 TextFile.stringToFile(actualString, actual); 510 command.add(diff); 511 if (diff.toLowerCase().contains("meld")) 512 command.add("--newtab"); 513 command.add(expected); 514 command.add(actual); 515 516 ProcessBuilder builder = new ProcessBuilder(command); 517 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 518 builder.start(); 519 520 } 521 return result; 522 } 523 524 525 private String compareText(String id, String expectedString, String actualString) { 526 for (int i = 0; i < Integer.min(expectedString.length(), actualString.length()); i++) { 527 if (expectedString.charAt(i) != actualString.charAt(i)) 528 return createNotEqualMessage(id, "Strings differ at character " + Integer.toString(i), charWithContext(expectedString, i), charWithContext(actualString, i)); 529 } 530 if (expectedString.length() != actualString.length()) 531 return createNotEqualMessage(id, "Strings differ in length but match to the end of the shortest.", Integer.toString(expectedString.length()), Integer.toString(actualString.length())); 532 return null; 533 } 534 535 private String charWithContext(String s, int i) { 536 String result = s.substring(i, i+1); 537 if (i > 7) { 538 i = i - 7; 539 } 540 int e = i + 20; 541 if (e > s.length()) { 542 e = s.length(); 543 } 544 if (e > i+1) { 545 result = result + " with context '"+s.substring(i, e)+"'"; 546 } 547 return result; 548 } 549 550}