Coverage Report - org.jbehave.core.reporters.CrossReference
 
Classes in this File Line Coverage Branch Coverage Complexity
CrossReference
90%
69/76
83%
5/6
1.767
CrossReference$1
94%
16/17
50%
1/2
1.767
CrossReference$StepMatch
35%
7/20
0%
0/18
1.767
CrossReference$StepUsage
100%
5/5
N/A
1.767
CrossReference$StoryHolder
100%
5/5
N/A
1.767
CrossReference$XRefRoot
96%
32/33
66%
8/12
1.767
CrossReference$XRefStepMonitor
75%
12/16
50%
3/6
1.767
CrossReference$XRefStory
82%
28/34
60%
6/10
1.767
CrossReference$XrefOutputFailed
0%
0/2
N/A
1.767
 
 1  
 package org.jbehave.core.reporters;
 2  
 
 3  
 import java.io.File;
 4  
 import java.io.FileWriter;
 5  
 import java.io.IOException;
 6  
 import java.io.Writer;
 7  
 import java.lang.reflect.Method;
 8  
 import java.util.ArrayList;
 9  
 import java.util.HashMap;
 10  
 import java.util.HashSet;
 11  
 import java.util.List;
 12  
 import java.util.Map;
 13  
 import java.util.Set;
 14  
 
 15  
 import org.jbehave.core.io.StoryLocation;
 16  
 import org.jbehave.core.model.ExamplesTable;
 17  
 import org.jbehave.core.model.Meta;
 18  
 import org.jbehave.core.model.Narrative;
 19  
 import org.jbehave.core.model.Scenario;
 20  
 import org.jbehave.core.model.StepPattern;
 21  
 import org.jbehave.core.model.Story;
 22  
 import org.jbehave.core.steps.NullStepMonitor;
 23  
 import org.jbehave.core.steps.StepMonitor;
 24  
 import org.jbehave.core.steps.StepType;
 25  
 
 26  
 import com.thoughtworks.xstream.XStream;
 27  
 import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
 28  
 
 29  56
 public class CrossReference extends Format {
 30  
 
 31  5
     private final XStream XSTREAM_FOR_XML = new XStream();
 32  5
     private final XStream XSTREAM_FOR_JSON = new XStream(new JsonHierarchicalStreamDriver());
 33  5
     private ThreadLocal<Story> currentStory = new ThreadLocal<Story>();
 34  5
     private ThreadLocal<Long> currentStoryStart = new ThreadLocal<Long>();
 35  5
     private ThreadLocal<String> currentScenarioTitle = new ThreadLocal<String>();
 36  5
     private List<StoryHolder> stories = new ArrayList<StoryHolder>();
 37  5
     private Map<String, Long> times = new HashMap<String, Long>();
 38  5
     private Map<String, StepMatch> stepMatches = new HashMap<String, StepMatch>();
 39  5
     private StepMonitor stepMonitor = new XRefStepMonitor();
 40  5
     private Set<String> failingStories = new HashSet<String>();
 41  5
     private Set<String> stepsPerformed = new HashSet<String>();
 42  5
     private boolean doJson = true;
 43  5
     private boolean doXml = true;
 44  5
     private boolean excludeStoriesWithNoExecutedScenarios = false;
 45  5
     private boolean outputAfterEachStory = false;
 46  
     private Format threadSafeDelegateFormat;
 47  
 
 48  
     public CrossReference() {
 49  5
         this("XREF");
 50  5
     }
 51  
 
 52  
     public CrossReference(String name) {
 53  5
         super(name);
 54  5
         configure(XSTREAM_FOR_XML);
 55  5
         configure(XSTREAM_FOR_JSON);
 56  5
     }
 57  
 
 58  
     public CrossReference withJsonOnly() {
 59  1
         doJson = true;
 60  1
         doXml = false;
 61  1
         return this;
 62  
     }
 63  
 
 64  
     public CrossReference withXmlOnly() {
 65  1
         doJson = false;
 66  1
         doXml = true;
 67  1
         return this;
 68  
     }
 69  
 
 70  
     public CrossReference withOutputAfterEachStory(boolean outputAfterEachStory) {
 71  0
         this.outputAfterEachStory = outputAfterEachStory;
 72  0
         return this;
 73  
     }
 74  
 
 75  
     public CrossReference withThreadSafeDelegateFormat(Format format) {
 76  0
         this.threadSafeDelegateFormat = format;
 77  0
         return this;
 78  
     }
 79  
 
 80  
     public CrossReference excludingStoriesWithNoExecutedScenarios(boolean exclude) {
 81  4
         this.excludeStoriesWithNoExecutedScenarios = exclude;
 82  4
         return this;
 83  
     }
 84  
 
 85  
     public StepMonitor getStepMonitor() {
 86  4
         return stepMonitor;
 87  
     }
 88  
 
 89  
     /**
 90  
      * Output to JSON and/or XML files.  Could be at the end of the suite, or per story
 91  
      * In the case of the latter, synchronization is needed as two stories (on two threads) could
 92  
      * be completing concurrently, and we need to guard against ConcurrentModificationException
 93  
      * @param storyReporterBuilder the reporter to use
 94  
      */
 95  
     public synchronized void outputToFiles(StoryReporterBuilder storyReporterBuilder) {
 96  5
         XRefRoot root = createXRefRoot(storyReporterBuilder, stories, failingStories);
 97  5
         root.addStepMatches(stepMatches);
 98  5
         if (doXml) {
 99  4
             outputFile(fileName("xml"), XSTREAM_FOR_XML, root, storyReporterBuilder);
 100  
         }
 101  5
         if (doJson) {
 102  4
             outputFile(fileName("json"), XSTREAM_FOR_JSON, root, storyReporterBuilder);
 103  
         }
 104  5
     }
 105  
 
 106  
     protected String fileName(String extension) {
 107  8
         return name().toLowerCase() + "." + extension;
 108  
     }
 109  
 
 110  
     protected final XRefRoot createXRefRoot(StoryReporterBuilder storyReporterBuilder, List<StoryHolder> stories,
 111  
             Set<String> failingStories) {
 112  5
         XRefRoot xrefRoot = newXRefRoot();
 113  5
         xrefRoot.setExcludeStoriesWithNoExecutedScenarios(excludeStoriesWithNoExecutedScenarios);
 114  5
         xrefRoot.processStories(stories, stepsPerformed,  times, storyReporterBuilder, failingStories);
 115  5
         return xrefRoot;
 116  
     }
 117  
 
 118  
     protected XRefRoot newXRefRoot() {
 119  1
         return new XRefRoot();
 120  
     }
 121  
 
 122  
     private void outputFile(String name, XStream xstream, XRefRoot root, StoryReporterBuilder storyReporterBuilder) {
 123  
 
 124  8
         File outputDir = new File(storyReporterBuilder.outputDirectory(), "view");
 125  8
         outputDir.mkdirs();
 126  
         try {
 127  8
             Writer writer = makeWriter(new File(outputDir, name));
 128  8
             writer.write(xstream.toXML(root));
 129  8
             writer.flush();
 130  8
             writer.close();
 131  0
         } catch (IOException e) {
 132  0
             throw new XrefOutputFailed(name, e);
 133  8
         }
 134  
 
 135  8
     }
 136  
 
 137  
     @SuppressWarnings("serial")
 138  
     public static class XrefOutputFailed extends RuntimeException {
 139  
 
 140  
         public XrefOutputFailed(String name, Throwable cause) {
 141  0
             super(name, cause);
 142  0
         }
 143  
 
 144  
     }
 145  
 
 146  
     protected Writer makeWriter(File file) throws IOException {
 147  2
         return new FileWriter(file);
 148  
     }
 149  
 
 150  
     private void configure(XStream xstream) {
 151  10
         xstream.setMode(XStream.NO_REFERENCES);
 152  10
         aliasForXRefRoot(xstream);
 153  10
         aliasForXRefStory(xstream);
 154  10
         xstream.alias("stepMatch", StepMatch.class);
 155  10
         xstream.alias("pattern", StepPattern.class);
 156  10
         xstream.alias("use", StepUsage.class);
 157  10
         xstream.omitField(ExamplesTable.class, "parameterConverters");
 158  10
         xstream.omitField(ExamplesTable.class, "defaults");
 159  10
     }
 160  
 
 161  
     protected void aliasForXRefStory(XStream xstream) {
 162  8
         xstream.alias("story", XRefStory.class);
 163  8
     }
 164  
 
 165  
     protected void aliasForXRefRoot(XStream xstream) {
 166  2
         xstream.alias("xref", XRefRoot.class);
 167  2
     }
 168  
 
 169  
     @Override
 170  
     public StoryReporter createStoryReporter(FilePrintStreamFactory factory, final StoryReporterBuilder storyReporterBuilder) {
 171  
         StoryReporter delegate;
 172  4
         if (threadSafeDelegateFormat == null) {
 173  4
             delegate = new NullStoryReporter();
 174  
         } else {
 175  0
             delegate = threadSafeDelegateFormat.createStoryReporter(factory, storyReporterBuilder);
 176  
         }
 177  4
         return new DelegatingStoryReporter(delegate) {
 178  
 
 179  
             @Override
 180  
             public void beforeStory(Story story, boolean givenStory) {
 181  4
                 stories.add(new StoryHolder(story));
 182  4
                 currentStory.set(story);
 183  4
                 currentStoryStart.set(System.currentTimeMillis());
 184  4
                 super.beforeStory(story, givenStory);
 185  4
             }
 186  
 
 187  
             @Override
 188  
             public void failed(String step, Throwable cause) {
 189  4
                 failingStories.add(currentStory.get().getPath());
 190  4
                 super.failed(step, cause);
 191  4
             }
 192  
 
 193  
             @Override
 194  
             public void afterStory(boolean givenStory) {
 195  4
                 times.put(currentStory.get().getPath(), System.currentTimeMillis() - currentStoryStart.get());
 196  4
                 if (outputAfterEachStory) {
 197  0
                     outputToFiles(storyReporterBuilder);
 198  
                 }
 199  4
                 super.afterStory(givenStory);
 200  4
             }
 201  
 
 202  
             @Override
 203  
             public void beforeScenario(String title) {
 204  4
                 currentScenarioTitle.set(title);
 205  4
                 super.beforeScenario(title);
 206  4
             }
 207  
         };
 208  
     }
 209  
 
 210  10
     private class XRefStepMonitor extends NullStepMonitor {
 211  
         @Override
 212  
         public void performing(String step, boolean dryRun) {
 213  0
             super.performing(step, dryRun);
 214  0
             stepsPerformed.add(currentStory.get().getPath());
 215  0
         }
 216  
 
 217  
         public void stepMatchesPattern(String step, boolean matches, StepPattern pattern, Method method,
 218  
                 Object stepsInstance) {
 219  4
             Story story = currentStory.get();
 220  4
             if (story == null) {
 221  0
                 throw new NullPointerException("story not setup for CrossReference");
 222  
             }
 223  
 
 224  4
             if (matches) {
 225  4
                 String key = pattern.type() + pattern.annotated();
 226  4
                 StepMatch stepMatch = stepMatches.get(key);
 227  4
                 if (stepMatch == null) {
 228  4
                     stepMatch = new StepMatch(pattern.type(), pattern.annotated(), pattern.resolved());
 229  4
                     stepMatches.put(key, stepMatch);
 230  
                 }
 231  
                 // find canonical ref for same stepMatch
 232  4
                 stepMatch.usages.add(new StepUsage(story.getPath(), currentScenarioTitle.get(), step));
 233  
             }
 234  4
             super.stepMatchesPattern(step, matches, pattern, method, stepsInstance);
 235  4
         }
 236  
     }
 237  
 
 238  13
     public static class XRefRoot {
 239  5
         protected long whenMade = System.currentTimeMillis();
 240  5
         protected String createdBy = createdBy();
 241  
 
 242  5
         private Set<String> meta = new HashSet<String>();
 243  5
         private List<XRefStory> stories = new ArrayList<XRefStory>();
 244  5
         private List<StepMatch> stepMatches = new ArrayList<StepMatch>();
 245  
 
 246  
         private transient boolean excludeStoriesWithNoExecutedScenarios;
 247  
 
 248  
         public void setExcludeStoriesWithNoExecutedScenarios(boolean exclude) {
 249  5
             this.excludeStoriesWithNoExecutedScenarios = exclude;
 250  5
         }
 251  
 
 252  
         protected String createdBy() {
 253  5
             return "JBehave";
 254  
         }
 255  
 
 256  
         protected void processStories(List<StoryHolder> stories, Set<String> stepsPerformed, Map<String, Long> times, StoryReporterBuilder builder, Set<String> failures) {
 257  5
             for (StoryHolder storyHolder : stories) {
 258  4
                 Story story = storyHolder.story;
 259  4
                 if (someScenarios(story, stepsPerformed) || !excludeStoriesWithNoExecutedScenarios) {
 260  4
                     XRefStory xRefStory = createXRefStory(builder, story, !failures.contains(story.getPath()), this);
 261  4
                     xRefStory.started = storyHolder.when;
 262  4
                     xRefStory.duration = getTime(times, story);
 263  4
                     this.stories.add(xRefStory);
 264  
                 }
 265  4
             }
 266  5
         }
 267  
 
 268  
         protected Long getTime(Map<String, Long> times, Story story) {
 269  4
             Long time = times.get(story.getPath());
 270  4
             if (time == null) {
 271  0
                 return 0L;
 272  
             }
 273  4
             return time;
 274  
         }
 275  
 
 276  
         protected boolean someScenarios(Story story, Set<String> stepsPerformed) {
 277  4
             return stepsPerformed.contains(story.getPath());
 278  
         }
 279  
 
 280  
         /**
 281  
          * Ensure that XRefStory is instantiated completely, before secondary
 282  
          * methods are invoked (or overridden)
 283  
          */
 284  
         protected final XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story,
 285  
                 boolean passed, XRefRoot root) {
 286  4
             XRefStory xrefStory = createXRefStory(storyReporterBuilder, story, passed);
 287  4
             xrefStory.processMetaTags(root);
 288  4
             xrefStory.processScenarios();
 289  4
             return xrefStory;
 290  
         }
 291  
 
 292  
         /**
 293  
          * Override this is you want to add fields to the JSON. Specifically,
 294  
          * create a subclass of XRefStory to return.
 295  
          * 
 296  
          * @param storyReporterBuilder the story reporter builder
 297  
          * @param story the story
 298  
          * @param passed the story passed (or failed)
 299  
          * @return An XRefStory
 300  
          */
 301  
         protected XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story, boolean passed) {
 302  3
             return new XRefStory(story, storyReporterBuilder, passed);
 303  
         }
 304  
 
 305  
         protected void addStepMatches(Map<String, StepMatch> stepMatchMap) {
 306  5
             for (String key : stepMatchMap.keySet()) {
 307  4
                 StepMatch stepMatch = stepMatchMap.get(key);
 308  4
                 stepMatches.add(stepMatch);
 309  4
             }
 310  5
         }
 311  
     }
 312  
 
 313  
     @SuppressWarnings("unused")
 314  
     public static class XRefStory {
 315  
         private transient Story story; // don't turn into JSON.
 316  
         private String description;
 317  4
         private String narrative = "";
 318  
         private String name;
 319  
         private String path;
 320  
         private String html;
 321  4
         private String meta = "";
 322  4
         private String scenarios = "";
 323  
         private boolean passed;
 324  
         public long started;
 325  
         public long duration;
 326  
 
 327  4
         public XRefStory(Story story, StoryReporterBuilder storyReporterBuilder, boolean passed) {
 328  4
             this.story = story;
 329  4
             Narrative narrative = story.getNarrative();
 330  4
             if (!narrative.isEmpty()) {
 331  4
                 this.narrative = "In order to " + narrative.inOrderTo() + "\n" + "As a " + narrative.asA() + "\n"
 332  
                         + "I want to " + narrative.iWantTo() + "\n";
 333  
             }
 334  4
             this.description = story.getDescription().asString();
 335  4
             this.name = story.getName();
 336  4
             this.path = story.getPath();
 337  4
             this.passed = passed;
 338  4
             this.html = storyReporterBuilder.pathResolver().resolveName(new StoryLocation(storyReporterBuilder.codeLocation(), story.getPath()),
 339  
                     "html");
 340  4
         }
 341  
 
 342  
         protected void processScenarios() {
 343  4
             for (Scenario scenario : story.getScenarios()) {
 344  0
                 String body = "Scenario:" + scenario.getTitle() + "\n";
 345  0
                 List<String> steps = scenario.getSteps();
 346  0
                 for (String step : steps) {
 347  0
                     body = body + step + "\n";
 348  
                 }
 349  0
                 scenarios = scenarios + body + "\n\n";
 350  0
             }
 351  4
         }
 352  
 
 353  
         protected void processMetaTags(XRefRoot root) {
 354  4
             Meta storyMeta = story.getMeta();
 355  4
             for (String next : storyMeta.getPropertyNames()) {
 356  8
                 String property = next + "=" + storyMeta.getProperty(next);
 357  8
                 addMetaProperty(property, root.meta);
 358  8
                 String newMeta = appendMetaProperty(property, this.meta);
 359  8
                 if (newMeta != null) {
 360  7
                     this.meta = newMeta;
 361  
                 }
 362  8
             }
 363  4
         }
 364  
 
 365  
         protected String appendMetaProperty(String property, String meta) {
 366  7
             return meta + property + "\n";
 367  
         }
 368  
 
 369  
         protected void addMetaProperty(String property, Set<String> meta) {
 370  7
             meta.add(property);
 371  7
         }
 372  
     }
 373  
 
 374  
     @SuppressWarnings("unused")
 375  
     public static class StepUsage {
 376  
         private final String story;
 377  
         private final String scenario;
 378  
         private final String step;
 379  
 
 380  4
         public StepUsage(String story, String scenario, String step) {
 381  4
             this.story = story;
 382  4
             this.scenario = scenario;
 383  4
             this.step = step;
 384  4
         }
 385  
     }
 386  
 
 387  4
     public static class StepMatch {
 388  
         private final StepType type; // key
 389  
         private final String annotatedPattern; // key
 390  
         // these not in hashcode or equals()
 391  
         @SuppressWarnings("unused")
 392  
         private final String resolvedPattern;
 393  4
         private final Set<StepUsage> usages = new HashSet<StepUsage>();
 394  
 
 395  4
         public StepMatch(StepType type, String annotatedPattern, String resolvedPattern) {
 396  4
             this.type = type;
 397  4
             this.annotatedPattern = annotatedPattern;
 398  4
             this.resolvedPattern = resolvedPattern;
 399  4
         }
 400  
 
 401  
         @Override
 402  
         public boolean equals(Object o) {
 403  0
             if (this == o)
 404  0
                 return true;
 405  0
             if (o == null || getClass() != o.getClass())
 406  0
                 return false;
 407  
 
 408  0
             StepMatch stepMatch = (StepMatch) o;
 409  
 
 410  0
             if (annotatedPattern != null ? !annotatedPattern.equals(stepMatch.annotatedPattern)
 411  
                     : stepMatch.annotatedPattern != null)
 412  0
                 return false;
 413  0
             if (type != stepMatch.type)
 414  0
                 return false;
 415  
 
 416  0
             return true;
 417  
         }
 418  
 
 419  
         @Override
 420  
         public int hashCode() {
 421  0
             int result = type != null ? type.hashCode() : 0;
 422  0
             result = 31 * result + (annotatedPattern != null ? annotatedPattern.hashCode() : 0);
 423  0
             return result;
 424  
         }
 425  
     }
 426  
 
 427  4
     private class StoryHolder {
 428  
         Story story;
 429  
         long when;
 430  
 
 431  4
         private StoryHolder(Story story) {
 432  4
             this.story = story;
 433  4
             this.when = System.currentTimeMillis();
 434  4
         }
 435  
 
 436  
 
 437  
     }
 438  
 }