View Javadoc

1   /*
2    * Copyright 2006-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.kuali.rice.vc.test;
18  
19  import com.predic8.schema.ComplexType;
20  import com.predic8.schema.Sequence;
21  import com.predic8.soamodel.Difference;
22  import com.predic8.wsdl.Definitions;
23  import com.predic8.wsdl.Operation;
24  import com.predic8.wsdl.PortType;
25  import com.predic8.wsdl.WSDLParser;
26  import com.predic8.wsdl.diff.WsdlDiffGenerator;
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.log4j.Logger;
29  import org.codehaus.jackson.JsonNode;
30  import org.codehaus.jackson.map.ObjectMapper;
31  import org.kuali.rice.core.api.config.property.Config;
32  import org.kuali.rice.core.api.config.property.ConfigContext;
33  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
34  import org.kuali.rice.core.api.lifecycle.Lifecycle;
35  import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
36  import org.kuali.rice.test.BaselineTestCase;
37  
38  import javax.xml.namespace.QName;
39  import java.io.BufferedReader;
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.InputStreamReader;
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.Collections;
48  import java.util.Iterator;
49  import java.util.LinkedList;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.regex.Pattern;
53  
54  import static org.junit.Assert.assertTrue;
55  import static org.junit.Assert.fail;
56  
57  /*
58  *  Compatible Changes
59  *   - adding a new WSDL operation definition and associated message definitions
60  *   - adding a new WSDL port type definition and associated operation definitions
61  *   - adding new WSDL binding and service definitions
62  *   - adding a new optional XML Schema element or attribute declaration to a message definition
63  *   - reducing the constraint granularity of an XML Schema element or attribute of a message definition type
64  *   - adding a new XML Schema wildcard to a message definition type
65  *   - adding a new optional WS-Policy assertion
66  *   - adding a new WS-Policy alternative
67  *
68  * Incompatible Changes
69  *   - renaming an existing WSDL operation definition
70  *   - removing an existing WSDL operation definition
71  *   - changing the MEP of an existing WSDL operation definition
72  *   - adding a fault message to an existing WSDL operation definition
73  *   - adding a new required XML Schema element or attribute declaration to a message definition
74  *   - increasing the constraint granularity of an XML Schema element or attribute declaration of a message definition
75  *   - renaming an optional or required XML Schema element or attribute in a message definition
76  *   - removing an optional or required XML Schema element or attribute or wildcard from a message definition
77  *   - adding a new required WS-Policy assertion or expression
78  *   - adding a new ignorable WS-Policy expression (most of the time)
79  */
80  @BaselineTestCase.BaselineMode(BaselineTestCase.Mode.ROLLBACK)
81  public abstract class WsdlCompareTestCase extends BaselineTestCase {
82      private static final Logger LOG = Logger.getLogger(WsdlCompareTestCase.class);
83      private static final String WSDL_URL = "wsdl.test.previous.url";
84      private static final String WSDL_PREVIOUS_VERSION = "wsdl.test.previous.version";
85      private static final String LINE_SEPARATOR = System.getProperty("line.separator");
86  
87      private String previousVersion;
88  
89      private static final List<String> ignoreBreakageRegexps = Arrays.asList(
90              ".*Position of any null changed.$", // change in position of an 'any' doesn't indicate a breakage for us
91              ".*Position of element null changed.$" // this also indicates an 'any' changing position, ignore it too
92      );
93  
94      public WsdlCompareTestCase(String moduleName) {
95          super(moduleName);
96      }
97  
98      protected List<String> verifyWsdlDifferences(Difference diff, String level) {
99          List<String> results = new ArrayList<String>();
100 
101         if (diff.isBreaks()) {
102             boolean ignore = false;
103             for (String ignoreBreakageRegexp : ignoreBreakageRegexps) {
104                 if (diff.getDescription().matches(ignoreBreakageRegexp)) {
105                     ignore = true;
106                     break;
107                 }
108             }
109 
110             if (ignore) {
111                 LOG.info(level + "non-breaking change" + diff.getDescription());
112             } else {
113                 LOG.error(level + "breaking change: " + diff.getType() + diff.getDescription());
114                 results.add(level + diff.getDescription());
115             }
116         }
117 
118 
119         //check for operation based sequence changes
120         String opBreakageString = checkForOperationBasedChanges(diff);
121         if (opBreakageString != null) {
122             results.add(level + opBreakageString);
123         }
124 
125         for (Difference moreDiff : diff.getDiffs())  {
126             List<String> childBreakages = verifyWsdlDifferences(moreDiff, level + "  ");
127             for (String childBreakage : childBreakages) {
128                 if (!diff.getDescription().trim().startsWith("Schema ")) {
129                     results.add(level + diff.getDescription() + LINE_SEPARATOR + childBreakage);
130                 } else {
131                     results.add(childBreakage);
132                 }
133             }
134         }
135 
136         return results;
137     }
138 
139     /*
140      * This method is essentially an extra check because java2ws marks parameters on methods as minOccurs=0, which means
141      * as far as the wsdl comparison, adding a new parameter is ok, because it isn't required.
142      *
143      * Unfortunately, that adding the parameter breaks compatibility for us because it invalidates the java interface.
144      *
145      * So, This method goes through, and checks to see if the sequence change is on one of the services Operators.  If it
146      * is on an operator, and there is a difference in type of the operator, we've broken compatibility and should fail.
147      *
148      * returns a string if there is a breakage, null otherwise
149      */
150     private String checkForOperationBasedChanges(Difference diff) {
151         if ("sequence".equals(diff.getType())
152                 && diff.getA() != null
153                 && diff.getB() != null) {
154             Sequence oldSequence = (Sequence)diff.getA();
155             Sequence newSequence = (Sequence)diff.getB();
156             if (newSequence.getParent() instanceof ComplexType) {
157                 ComplexType parent = (ComplexType)newSequence.getParent();
158                 String serviceName = newSequence.getSchema().getDefinitions().getName();
159                 PortType portType = newSequence.getSchema().getDefinitions().getPortType(serviceName);
160                 if (portType != null) {
161                     Operation operation = portType.getOperation(parent.getName());
162 
163                     if (operation != null) {
164                         return "Element cannot be added to a sequence if sequence is an Operation " +
165                                 diff.getDescription();
166                     }
167 //                    assertTrue("Element cannot be added to a sequence if sequence is an Operation " + diff
168 //                            .getDescription(), operation == null);
169                 }
170             }
171         }
172         return null;
173     }
174 
175     protected List<Difference> compareWsdlDefinitions(String oldWsdl, String newWsdl) {
176         WSDLParser parser = new WSDLParser();
177 
178         Definitions wsdl1;
179         Definitions wsdl2;
180         try {
181             wsdl1 = parser.parse(oldWsdl);
182         } catch (com.predic8.xml.util.ResourceDownloadException e) {
183             LOG.error("COULDN'T PARSE " + oldWsdl);
184             return Collections.emptyList();
185         }
186         try {
187             wsdl2 = parser.parse(newWsdl);
188         } catch (com.predic8.xml.util.ResourceDownloadException e) {
189             LOG.error("COULDN'T PARSE " + newWsdl);
190             return Collections.emptyList();
191         }
192 
193         WsdlDiffGenerator diffGen = new WsdlDiffGenerator(wsdl1, wsdl2);
194         return diffGen.compare();
195     }
196 
197     protected String getPreviousVersionWsdlUrl(String wsdlFile, MavenVersion previousVersion) {
198 
199         StringBuilder oldWsdl = new StringBuilder(buildWsdlUrlPrefix(previousVersion.getOriginalForm()));
200         oldWsdl.append("rice-");
201         oldWsdl.append(getModuleName());
202         oldWsdl.append("-api-");
203         oldWsdl.append(previousVersion.getOriginalForm());
204         oldWsdl.append("-");
205         oldWsdl.append(wsdlFile);
206 
207         return oldWsdl.toString();
208     }
209 
210     //String oldWsdl = MAVEN_REPO_PREFIX + MODULE + "-api/" + PREVIOUS_VERSION + "/rice-" + MODULE + "-api-" + PREVIOUS_VERSION + "-" + file.getName();
211     private String buildWsdlUrlPrefix(String previousVersion) {
212         String wsdlUrl = ConfigContext.getCurrentContextConfig().getProperty(WSDL_URL);
213 
214         if (StringUtils.isNotBlank(wsdlUrl)
215                 && StringUtils.isNotBlank(previousVersion)) {
216             StringBuilder urlBuilder = new StringBuilder(wsdlUrl);
217             if (!wsdlUrl.endsWith("/")) {
218                 urlBuilder.append("/");
219             }
220             urlBuilder.append("rice-");
221             urlBuilder.append(getModuleName());
222             urlBuilder.append("-api/");
223             urlBuilder.append(previousVersion);
224             urlBuilder.append("/");
225 
226             return urlBuilder.toString();
227 
228         } else {
229             throw new RuntimeException("Couldn't build wsdl url prefix");
230         }
231     }
232 
233     /**
234      * Allows an extending test to specify versions of specific wsdls to omit from testing.  This can be useful for
235      * ignoring version compatibility issues that have already been addressed in previous versions.
236      *
237      * @return a Map from wsdl file name (e.g. "DocumentTypeService.wsdl") to a list of {@link MavenVersion}s to filter
238      */
239     protected Map<String, List<MavenVersion>> getWsdlVersionBlacklists() {
240         return null;
241     }
242 
243     protected void compareWsdlFiles(File[] files) {
244         List<VersionCompatibilityBreakage> breakages = new ArrayList<VersionCompatibilityBreakage>();
245 
246         assertTrue("There should be wsdls to compare", files != null  && files.length > 0);
247 
248         MavenVersion currentVersion = getCurrentMavenVersion();
249         List<MavenVersion> versions = getInterveningVersions();
250 
251         for (File file : files) {
252             if (file.getName().endsWith(".wsdl")) {
253                 LOG.info("new wsdl: " + file.getAbsolutePath());
254                 String newWsdl = file.getAbsolutePath();
255 
256                 // do filtering to avoid comparing with blacklisted versions of wsdls
257 
258                 List<MavenVersion> filteredVersions = new ArrayList<MavenVersion>(versions);
259                 Map<String, List<MavenVersion>> wsdlVersionBlacklists = getWsdlVersionBlacklists();
260 
261                 if (wsdlVersionBlacklists != null) {
262                     for (Map.Entry<String, List<MavenVersion>> wsdlVersionBlacklist : wsdlVersionBlacklists.entrySet()) {
263                         if (file.getName().equals(wsdlVersionBlacklist.getKey())) {
264                             LOG.info("filtering blacklisted versions of " + wsdlVersionBlacklist.getKey() + ": " +
265                                     StringUtils.join(wsdlVersionBlacklist.getValue(), ","));
266                             filteredVersions.removeAll(wsdlVersionBlacklist.getValue());
267                         }
268                     }
269                 }
270 
271                 Iterator<MavenVersion> versionsIter = filteredVersions.iterator();
272 
273                 boolean processedCurrent = false;
274 
275                 MavenVersion v1;
276                 MavenVersion v2 = versionsIter.next();
277 
278                 // walk the versions, checking diffs between each consecutive pair
279                 while (versionsIter.hasNext() || !processedCurrent) {
280                     v1 = v2; // march down the list
281 
282                     String v1Wsdl = getPreviousVersionWsdlUrl(file.getName(), v1);
283                     String v2Wsdl;
284 
285                     if (versionsIter.hasNext()) {
286                         v2 = versionsIter.next();
287                         v2Wsdl = getPreviousVersionWsdlUrl(file.getName(), v2);
288                     } else {
289                         v2 = currentVersion;
290                         v2Wsdl = file.getAbsolutePath();
291                         processedCurrent = true;
292                     }
293 
294                     LOG.info("checking version transition: " + v1.getOriginalForm() + " -> " + v2.getOriginalForm());
295 
296                     if (v1Wsdl == null) {
297                         LOG.warn("SKIPPING check, wsdl not found for " + v1Wsdl);
298                     } else if (v2Wsdl == null) {
299                         LOG.warn("SKIPPING check, wsdl not found for " + v2Wsdl);
300                     } else {
301 
302                         List<Difference> differences = compareWsdlDefinitions(v1Wsdl, v2Wsdl);
303                         for (Difference diff : differences) {
304                             List<String> breakageStrings = verifyWsdlDifferences(diff, "");
305 
306                             for (String breakage : breakageStrings) {
307                                 breakages.add(new VersionCompatibilityBreakage(v1, v2, v1Wsdl, v2Wsdl, breakage));
308                             }
309                         }
310                     }
311                 }
312 
313             }
314 
315 
316         }
317 
318         if (!breakages.isEmpty()) {
319             fail(buildBreakagesSummary(breakages));
320         }
321     }
322 
323     protected String buildBreakagesSummary(List<VersionCompatibilityBreakage> breakages) {
324         StringBuilder errorsStringBuilder =
325                 new StringBuilder(LINE_SEPARATOR + "!!!!! Detected " + breakages.size() + " VC Breakages !!!!!"
326                         + LINE_SEPARATOR);
327 
328         MavenVersion lastOldVersion = null;
329         String lastOldWsdlUrl = "";
330 
331         for (VersionCompatibilityBreakage breakage : breakages) {
332             // being lazy and using '!=' instead of '!lastOldVersion.equals(...)' to avoid NPEs and extra checks
333             if (lastOldVersion != breakage.oldMavenVersion || lastOldWsdlUrl != breakage.oldWsdlUrl) {
334                 lastOldVersion = breakage.oldMavenVersion;
335                 lastOldWsdlUrl = breakage.oldWsdlUrl;
336 
337                 errorsStringBuilder.append(LINE_SEPARATOR + "Old Version: " + lastOldVersion.getOriginalForm()
338                         +", wsdl: " + lastOldWsdlUrl);
339                 errorsStringBuilder.append(LINE_SEPARATOR + "New Version: " + breakage.newMavenVersion.getOriginalForm()
340                         +", wsdl: " + breakage.newWsdlUrl + LINE_SEPARATOR + LINE_SEPARATOR);
341             }
342             errorsStringBuilder.append(breakage.breakageMessage + LINE_SEPARATOR);
343         }
344         return errorsStringBuilder.toString();
345     }
346 
347     public String getPreviousVersion() {
348         if (StringUtils.isEmpty(this.previousVersion)) {
349             this.previousVersion = ConfigContext.getCurrentContextConfig().getProperty(WSDL_PREVIOUS_VERSION);
350         }
351         return this.previousVersion;
352     }
353 
354     public void setPreviousVersion(String previousVersion) {
355         this.previousVersion = previousVersion;
356     }
357 
358     @Override
359     protected Lifecycle getLoadApplicationLifecycle() {
360         SpringResourceLoader springResourceLoader = new SpringResourceLoader(new QName("VCTestHarnessResourceLoader"), "classpath:VCTestHarnessSpringBeans.xml", null);
361         springResourceLoader.setParentSpringResourceLoader(getTestHarnessSpringResourceLoader());
362         return springResourceLoader;
363     }
364 
365     @Override
366     protected List<Lifecycle> getPerTestLifecycles() {
367         return new ArrayList<Lifecycle>();
368     }
369 
370     @Override
371     protected List<Lifecycle> getSuiteLifecycles() {
372         List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
373 
374         /**
375          * Initializes Rice configuration from the test harness configuration file.
376          */
377         lifecycles.add(new BaseLifecycle() {
378             @Override
379             public void start() throws Exception {
380                 Config config = getTestHarnessConfig();
381                 ConfigContext.init(config);
382                 super.start();
383             }
384         });
385 
386         return lifecycles;
387     }
388 
389 
390     protected List<MavenVersion> getInterveningVersions() {
391         ArrayList<MavenVersion> results = new ArrayList<MavenVersion>();
392 
393         MavenVersion previousVersion = new MavenVersion(getPreviousVersion());
394 
395         MavenVersion currentVersion = getCurrentMavenVersion();
396 
397         if (currentVersion.compareTo(previousVersion) <= 0) {
398             throw new IllegalStateException("currentVersion " + currentVersion +
399                     "  is <= previousVersion " + previousVersion);
400         }
401         String searchContent = getMavenSearchResults();
402 
403         LinkedList<MavenVersion> riceVersions = parseSearchResults(searchContent);
404 
405         for (MavenVersion riceVersion : riceVersions) {
406             if ( currentVersion.compareTo(riceVersion) > 0 &&
407                     previousVersion.compareTo(riceVersion) <= 0 &&
408                     "".equals(riceVersion.getQualifier()) ) {
409                 results.add(riceVersion);
410             }
411         }
412 
413         return results;
414     }
415 
416     private MavenVersion getCurrentMavenVersion() {
417         return new MavenVersion(ConfigContext.getCurrentContextConfig().getProperty("rice.version"));
418     }
419 
420     private LinkedList<MavenVersion> parseSearchResults(String searchContent) {
421         LinkedList<MavenVersion> riceVersions = new LinkedList<MavenVersion>();
422 
423         ObjectMapper mapper = new ObjectMapper();
424         JsonNode rootNode;
425         try {
426             rootNode = mapper.readTree(searchContent);
427         } catch (IOException e) {
428             throw new RuntimeException("Can't parse maven search results", e);
429         }
430         JsonNode docsNode = rootNode.get("response").get("docs");
431 
432         for (JsonNode node : docsNode) {
433             String versionStr = node.get("v").toString();
434             // System.out.println(versionStr);
435             riceVersions.add(new MavenVersion(versionStr.replace(/* strip out surrounding quotes */ "\"","")));
436         }
437 
438         Collections.sort(riceVersions);
439         return riceVersions;
440     }
441 
442     private String getMavenSearchResults() {
443         // using the maven search REST api specified here: http://search.maven.org/#api
444         // this query gets all versions of Rice from maven central
445         final String mavenSearchUrlString =
446                 "http://search.maven.org/solrsearch/select?q=g:%22org.kuali.rice%22+AND+a:%22rice%22&core=gav&rows=20&wt=json";
447 
448         URL mavenSearchUrl;
449 
450         try {
451             mavenSearchUrl = new URL(mavenSearchUrlString);
452         } catch (MalformedURLException e) {
453             throw new RuntimeException("can't parse maven search url", e);
454         }
455 
456         StringBuilder contentBuilder = new StringBuilder();
457         BufferedReader contentReader;
458         try {
459             contentReader = new BufferedReader(new InputStreamReader(mavenSearchUrl.openStream()));
460             String line;
461             while (null != (line = contentReader.readLine())) {
462                 contentBuilder.append(line + LINE_SEPARATOR);
463             }
464         } catch (IOException e) {
465             throw new RuntimeException("Unable to read search results", e);
466         }
467         return contentBuilder.toString();
468     }
469 
470     /**
471      * Utility class for parsing and comparing maven versions
472      */
473     protected static class MavenVersion implements Comparable<MavenVersion> {
474         private static final Pattern PERIOD_PATTERN = Pattern.compile("\\.");
475         private final List<Integer> numbers;
476         private final String originalForm;
477         private final String qualifier;
478 
479         public MavenVersion(String versionString) {
480             originalForm = versionString;
481             if (versionString == null || "".equals(versionString.trim())) {
482                 throw new IllegalArgumentException("empty or null version string");
483             }
484             String versionPart;
485             int dashIndex = versionString.indexOf('-');
486             if (dashIndex != -1 && versionString.length()-1 > dashIndex) {
487                 qualifier = versionString.substring(dashIndex+1).trim();
488                 versionPart = versionString.substring(0,dashIndex);
489             } else {
490                 versionPart = versionString;
491                 qualifier = "";
492             }
493             String [] versionArray = PERIOD_PATTERN.split(versionPart);
494 
495             List<Integer> numbersBuilder = new ArrayList<Integer>(versionArray.length);
496 
497             for (String versionParticle : versionArray) {
498                 numbersBuilder.add(Integer.valueOf(versionParticle));
499             }
500 
501             numbers = Collections.unmodifiableList(numbersBuilder);
502         }
503 
504         @Override
505         public int compareTo(MavenVersion that) {
506             Iterator<Integer> thisNumbersIter = this.numbers.iterator();
507             Iterator<Integer> thatNumbersIter = that.numbers.iterator();
508 
509             while (thisNumbersIter.hasNext()) {
510                 // all else being equal, he/she who has the most digits wins
511                 if (!thatNumbersIter.hasNext()) return 1;
512 
513                 int numberComparison = thisNumbersIter.next().compareTo(thatNumbersIter.next());
514 
515                 // if one is greater than the other, we've established primacy
516                 if (numberComparison != 0) return numberComparison;
517             }
518             // all else being equal, he/she who has the most digits wins
519             if (thatNumbersIter.hasNext()) return -1;
520 
521             return compareQualifiers(this.qualifier, that.qualifier);
522         }
523 
524         private static int compareQualifiers(String thisQ, String thatQ) {
525             // no qualifier is considered greater than a qualifier (e.g. 1.0-SNAPSHOT is less than 1.0)
526             if ("".equals(thisQ)) {
527                 if ("".equals(thatQ)) {
528                     return 0;
529                 }
530                 return 1;
531             } else if ("".equals(thatQ)) {
532                 return -1;
533             }
534 
535             return thisQ.compareTo(thatQ);
536         }
537 
538         public List<Integer> getNumbers() {
539             return Collections.unmodifiableList(numbers);
540         }
541 
542         public String getQualifier() {
543             return qualifier;
544         }
545 
546         public String getOriginalForm() {
547             return originalForm;
548         }
549 
550         @Override
551         public String toString() {
552             return "MavenVersion{" +
553                     originalForm +
554                     '}';
555         }
556 
557         @Override
558         public boolean equals(Object o) {
559             if (this == o) {
560                 return true;
561             }
562             if (o == null || getClass() != o.getClass()) {
563                 return false;
564             }
565 
566             final MavenVersion that = (MavenVersion) o;
567 
568             if (!originalForm.equals(that.originalForm)) {
569                 return false;
570             }
571 
572             return true;
573         }
574 
575         @Override
576         public int hashCode() {
577             return originalForm.hashCode();
578         }
579     }
580 
581     /**
582      * struct-ish class to hold data about a VC breakage
583      */
584     protected static class VersionCompatibilityBreakage {
585         private final MavenVersion oldMavenVersion;
586         private final MavenVersion newMavenVersion;
587         private final String oldWsdlUrl;
588         private final String newWsdlUrl;
589         private final String breakageMessage;
590 
591         public VersionCompatibilityBreakage(MavenVersion oldMavenVersion, MavenVersion newMavenVersion, String oldWsdlUrl, String newWsdlUrl, String breakageMessage) {
592             if (oldMavenVersion == null) throw new IllegalArgumentException("oldMavenVersion must not be null");
593             if (newMavenVersion == null) throw new IllegalArgumentException("newMavenVersion must not be null");
594             if (StringUtils.isEmpty(oldWsdlUrl)) throw new IllegalArgumentException("oldWsdlUrl must not be empty/null");
595             if (StringUtils.isEmpty(newWsdlUrl)) throw new IllegalArgumentException("newWsdlUrl must not be empty/null");
596             if (StringUtils.isEmpty(breakageMessage)) throw new IllegalArgumentException("breakageMessage must not be empty/null");
597             this.oldWsdlUrl = oldWsdlUrl;
598             this.newWsdlUrl = newWsdlUrl;
599             this.oldMavenVersion = oldMavenVersion;
600             this.newMavenVersion = newMavenVersion;
601             this.breakageMessage = breakageMessage;
602         }
603 
604         @Override
605         public String toString() {
606             return "VersionCompatibilityBreakage{" +
607                     "oldMavenVersion=" + oldMavenVersion +
608                     ", newMavenVersion=" + newMavenVersion +
609                     ", oldWsdlUrl='" + oldWsdlUrl + '\'' +
610                     ", newWsdlUrl='" + newWsdlUrl + '\'' +
611                     ", breakageMessage='" + breakageMessage + '\'' +
612                     '}';
613         }
614     }
615 
616 }