1 | |
package org.jbehave.core.embedder; |
2 | |
|
3 | |
import java.io.File; |
4 | |
import java.io.FileWriter; |
5 | |
import java.io.IOException; |
6 | |
import java.io.Writer; |
7 | |
import java.util.ArrayList; |
8 | |
import java.util.HashMap; |
9 | |
import java.util.List; |
10 | |
import java.util.Map; |
11 | |
import java.util.Properties; |
12 | |
import java.util.concurrent.Callable; |
13 | |
import java.util.concurrent.ExecutionException; |
14 | |
import java.util.concurrent.ExecutorService; |
15 | |
import java.util.concurrent.Future; |
16 | |
import java.util.regex.Pattern; |
17 | |
import java.util.regex.PatternSyntaxException; |
18 | |
|
19 | |
import org.codehaus.plexus.util.StringUtils; |
20 | |
import org.jbehave.core.configuration.Configuration; |
21 | |
import org.jbehave.core.embedder.PerformableTree.PerformableRoot; |
22 | |
import org.jbehave.core.embedder.PerformableTree.RunContext; |
23 | |
import org.jbehave.core.failures.BatchFailures; |
24 | |
import org.jbehave.core.model.Story; |
25 | |
import org.jbehave.core.model.StoryDuration; |
26 | |
import org.jbehave.core.steps.InjectableStepsFactory; |
27 | |
import org.jbehave.core.steps.StepCollector.Stage; |
28 | |
|
29 | |
|
30 | |
|
31 | |
|
32 | |
|
33 | |
|
34 | |
public class StoryManager { |
35 | |
|
36 | |
private final Configuration configuration; |
37 | |
private final EmbedderControls embedderControls; |
38 | |
private final EmbedderMonitor embedderMonitor; |
39 | |
private final ExecutorService executorService; |
40 | |
private final InjectableStepsFactory stepsFactory; |
41 | |
private final PerformableTree performableTree; |
42 | 39 | private final Map<String, RunningStory> runningStories = new HashMap<String, RunningStory>(); |
43 | 39 | private final Map<MetaFilter, List<Story>> excludedStories = new HashMap<MetaFilter, List<Story>>(); |
44 | |
private RunContext context; |
45 | |
|
46 | |
public StoryManager(Configuration configuration, |
47 | |
InjectableStepsFactory stepsFactory, |
48 | |
EmbedderControls embedderControls, EmbedderMonitor embedderMonitor, |
49 | 39 | ExecutorService executorService, PerformableTree performableTree) { |
50 | 39 | this.configuration = configuration; |
51 | 39 | this.embedderControls = embedderControls; |
52 | 39 | this.embedderMonitor = embedderMonitor; |
53 | 39 | this.executorService = executorService; |
54 | 39 | this.stepsFactory = stepsFactory; |
55 | 39 | this.performableTree = performableTree; |
56 | 39 | } |
57 | |
|
58 | |
public Story storyOfPath(String storyPath) { |
59 | 60 | return performableTree.storyOfPath(configuration, storyPath); |
60 | |
} |
61 | |
|
62 | |
public Story storyOfText(String storyAsText, String storyId) { |
63 | 0 | return performableTree.storyOfText(configuration, storyAsText, storyId); |
64 | |
} |
65 | |
|
66 | |
public void clear() { |
67 | 0 | runningStories.clear(); |
68 | 0 | } |
69 | |
|
70 | |
public PerformableRoot performableRoot() { |
71 | 17 | return performableTree.getRoot(); |
72 | |
} |
73 | |
|
74 | |
public List<StoryOutcome> outcomes() { |
75 | 0 | List<StoryOutcome> outcomes = new ArrayList<StoryOutcome>(); |
76 | 0 | for (RunningStory story : runningStories.values()) { |
77 | 0 | outcomes.add(new StoryOutcome(story)); |
78 | 0 | } |
79 | 0 | return outcomes; |
80 | |
} |
81 | |
|
82 | |
public void runStoriesAsPaths(List<String> storyPaths, MetaFilter filter, |
83 | |
BatchFailures failures) { |
84 | 22 | runStories(storiesOf(storyPaths), filter, failures); |
85 | 15 | } |
86 | |
|
87 | |
private List<Story> storiesOf(List<String> storyPaths) { |
88 | 22 | List<Story> stories = new ArrayList<Story>(); |
89 | 22 | for (String storyPath : storyPaths) { |
90 | 58 | stories.add(storyOfPath(storyPath)); |
91 | 58 | } |
92 | 22 | return stories; |
93 | |
} |
94 | |
|
95 | |
public void runStories(List<Story> stories, MetaFilter filter, |
96 | |
BatchFailures failures) { |
97 | |
|
98 | 22 | context = performableTree.newRunContext(configuration, stepsFactory, |
99 | |
embedderMonitor, filter, failures); |
100 | |
|
101 | |
|
102 | 22 | performableTree.addStories(context, stories); |
103 | |
|
104 | |
|
105 | 22 | performStories(context, performableTree, stories); |
106 | |
|
107 | |
|
108 | 15 | failures.putAll(context.getFailures()); |
109 | |
|
110 | 15 | } |
111 | |
|
112 | |
private void performStories(RunContext context, |
113 | |
PerformableTree performableTree, List<Story> stories) { |
114 | |
|
115 | 22 | performableTree.performBeforeOrAfterStories(context, Stage.BEFORE); |
116 | |
|
117 | |
|
118 | 22 | runningStories(context, stories); |
119 | 22 | waitUntilAllDoneOrFailed(context); |
120 | 15 | MetaFilter filter = context.filter(); |
121 | 15 | List<Story> notAllowed = notAllowedBy(filter); |
122 | 15 | if (!notAllowed.isEmpty()) { |
123 | 2 | embedderMonitor.storiesNotAllowed(notAllowed, filter, |
124 | 1 | embedderControls.verboseFiltering()); |
125 | |
} |
126 | |
|
127 | |
|
128 | 15 | performableTree.performBeforeOrAfterStories(context, Stage.AFTER); |
129 | 15 | } |
130 | |
|
131 | |
public Map<String, RunningStory> runningStories(RunContext context, |
132 | |
List<Story> stories) { |
133 | 22 | for (Story story : stories) { |
134 | 58 | filterRunning(context, story); |
135 | 58 | } |
136 | 22 | return runningStories; |
137 | |
} |
138 | |
|
139 | |
private void filterRunning(RunContext context, Story story) { |
140 | 58 | FilteredStory filteredStory = context.filter(story); |
141 | 58 | if (filteredStory.allowed()) { |
142 | 56 | runningStories.put(story.getPath(), runningStory(story)); |
143 | |
} else { |
144 | 2 | notAllowedBy(context.getFilter()).add(story); |
145 | |
} |
146 | 58 | } |
147 | |
|
148 | |
public List<Story> notAllowedBy(MetaFilter filter) { |
149 | 17 | List<Story> stories = excludedStories.get(filter); |
150 | 17 | if (stories == null) { |
151 | 15 | stories = new ArrayList<Story>(); |
152 | 15 | excludedStories.put(filter, stories); |
153 | |
} |
154 | 17 | return stories; |
155 | |
} |
156 | |
|
157 | |
public RunningStory runningStory(Story story) { |
158 | 56 | return submit(new EnqueuedStory(performableTree, context, |
159 | |
embedderControls, embedderMonitor, story)); |
160 | |
} |
161 | |
|
162 | |
public void waitUntilAllDoneOrFailed(RunContext context) { |
163 | 22 | if ( runningStories.values().isEmpty() ) { |
164 | 1 | return; |
165 | |
} |
166 | 21 | boolean allDone = false; |
167 | 21 | boolean started = false; |
168 | 716 | while (!allDone || !started) { |
169 | 702 | allDone = true; |
170 | 702 | for (RunningStory runningStory : runningStories.values()) { |
171 | 2549 | if ( runningStory.isStarted() ){ |
172 | 1394 | started = true; |
173 | 1394 | Story story = runningStory.getStory(); |
174 | 1394 | Future<ThrowableStory> future = runningStory.getFuture(); |
175 | 1394 | if (!future.isDone()) { |
176 | 736 | allDone = false; |
177 | 736 | StoryDuration duration = runningStory.getDuration(); |
178 | 734 | runningStory.updateDuration(); |
179 | 734 | if (duration.timedOut()) { |
180 | 10 | embedderMonitor.storyTimeout(story, duration); |
181 | 10 | context.cancelStory(story, duration); |
182 | 10 | future.cancel(true); |
183 | 10 | if (embedderControls.failOnStoryTimeout()) { |
184 | 5 | throw new StoryExecutionFailed(story.getPath(), |
185 | |
new StoryTimeout(duration)); |
186 | |
} |
187 | |
continue; |
188 | |
} |
189 | 724 | } else { |
190 | |
try { |
191 | 658 | ThrowableStory throwableStory = future.get(); |
192 | 593 | Throwable throwable = throwableStory.getThrowable(); |
193 | 593 | if (throwable != null) { |
194 | 0 | context.addFailure(story.getPath(), throwable); |
195 | 0 | if (!embedderControls.ignoreFailureInStories()) { |
196 | 0 | continue; |
197 | |
} |
198 | |
} |
199 | 65 | } catch (Throwable e) { |
200 | 65 | context.addFailure(story.getPath(), e); |
201 | 65 | if (!embedderControls.ignoreFailureInStories()) { |
202 | 65 | continue; |
203 | |
} |
204 | 593 | } |
205 | |
} |
206 | 1317 | } else { |
207 | 1155 | started = false; |
208 | |
} |
209 | 2472 | } |
210 | 695 | tickTock(); |
211 | |
} |
212 | |
|
213 | 14 | Properties storyDurations = new Properties(); |
214 | 14 | long total = 0; |
215 | 14 | for (RunningStory runningStory : runningStories.values()) { |
216 | 31 | long durationInMillis = runningStory.getDurationInMillis(); |
217 | 31 | total += durationInMillis; |
218 | 31 | storyDurations.setProperty(runningStory.getStory().getPath(), Long.toString(durationInMillis)); |
219 | 31 | Future<ThrowableStory> future = runningStory.getFuture(); |
220 | 31 | if (!future.isDone()) { |
221 | 0 | future.cancel(true); |
222 | |
} |
223 | 31 | } |
224 | 14 | int threads = embedderControls.threads(); |
225 | 14 | long threadAverage = total / threads; |
226 | 14 | storyDurations.setProperty("total", Long.toString(total)); |
227 | 14 | storyDurations.setProperty("threads", Long.toString(threads)); |
228 | 14 | storyDurations.setProperty("threadAverage", Long.toString(threadAverage)); |
229 | 14 | write(storyDurations, "storyDurations.props"); |
230 | 14 | } |
231 | |
|
232 | |
private void write(Properties p, String name) { |
233 | 14 | File outputDirectory = configuration.storyReporterBuilder() |
234 | 14 | .outputDirectory(); |
235 | |
try { |
236 | 14 | Writer output = new FileWriter(new File(outputDirectory, name)); |
237 | 14 | p.store(output, this.getClass().getName()); |
238 | 14 | output.close(); |
239 | 0 | } catch (IOException e) { |
240 | 0 | e.printStackTrace(); |
241 | 14 | } |
242 | 14 | } |
243 | |
|
244 | |
private void tickTock() { |
245 | |
try { |
246 | 695 | Thread.sleep(100); |
247 | 0 | } catch (InterruptedException e) { |
248 | 695 | } |
249 | 695 | } |
250 | |
|
251 | |
private synchronized RunningStory submit(EnqueuedStory enqueuedStory) { |
252 | 56 | return new RunningStory(enqueuedStory, executorService.submit(enqueuedStory)); |
253 | |
} |
254 | |
|
255 | 46 | static class EnqueuedStory implements Callable<ThrowableStory> { |
256 | |
|
257 | |
private final PerformableTree performableTree; |
258 | |
private final RunContext context; |
259 | |
private final EmbedderControls embedderControls; |
260 | |
private final EmbedderMonitor embedderMonitor; |
261 | |
private final Story story; |
262 | |
private long startedAtMillis; |
263 | |
|
264 | |
public EnqueuedStory(PerformableTree performableTree, |
265 | |
RunContext context, EmbedderControls embedderControls, |
266 | 63 | EmbedderMonitor embedderMonitor, Story story) { |
267 | 63 | this.performableTree = performableTree; |
268 | 63 | this.context = context; |
269 | 63 | this.embedderControls = embedderControls; |
270 | 63 | this.embedderMonitor = embedderMonitor; |
271 | 63 | this.story = story; |
272 | 63 | } |
273 | |
|
274 | |
public ThrowableStory call() throws Exception { |
275 | 46 | String storyPath = story.getPath(); |
276 | |
try { |
277 | 46 | embedderMonitor.runningStory(storyPath); |
278 | 46 | startedAtMillis = System.currentTimeMillis(); |
279 | 46 | performableTree.perform(context, story); |
280 | 13 | } catch (Throwable e) { |
281 | 13 | if (embedderControls.ignoreFailureInStories()) { |
282 | 4 | embedderMonitor.storyFailed(storyPath, e); |
283 | |
} else { |
284 | 9 | return new ThrowableStory(story, new StoryExecutionFailed( |
285 | |
storyPath, e)); |
286 | |
} |
287 | 33 | } |
288 | 37 | return new ThrowableStory(story, null); |
289 | |
} |
290 | |
|
291 | |
public Story getStory() { |
292 | 1425 | return story; |
293 | |
} |
294 | |
|
295 | |
public long getStartedAtMillis() { |
296 | 2577 | return startedAtMillis; |
297 | |
} |
298 | |
|
299 | |
public long getTimeoutInSecs() { |
300 | 35 | String storyTimeoutsByPath = embedderControls.storyTimeoutInSecsByPath(); |
301 | |
|
302 | 35 | if( StringUtils.isNotBlank(storyTimeoutsByPath) ){ |
303 | 24 | Map<String,Long> storyTimeouts = new HashMap<String, Long>(); |
304 | 24 | Pattern validStoryTimeout = Pattern.compile("[\\S*]:[\\d+]"); |
305 | |
|
306 | 62 | for(String storyTimeout: storyTimeoutsByPath.split(",")) { |
307 | 40 | if(!validStoryTimeout.matcher(storyTimeout).find()) { |
308 | 2 | embedderMonitor.invalidTimeoutFormat(storyTimeout); |
309 | |
} |
310 | 40 | storyTimeouts.put(storyTimeout.split(":")[0], Long.parseLong(storyTimeout.split(":")[1])); |
311 | |
} |
312 | |
|
313 | 22 | for(String storyPattern: storyTimeouts.keySet()) { |
314 | 31 | if( story.getPath().matches(regexOf(storyPattern)) ){ |
315 | 18 | Long timeout = storyTimeouts.get(storyPattern); |
316 | 18 | embedderMonitor.usingTimeout(story.getName(), timeout); |
317 | 18 | return timeout; |
318 | |
} |
319 | 13 | } |
320 | |
|
321 | |
} |
322 | 15 | embedderMonitor.usingTimeout(story.getName(), embedderControls.storyTimeoutInSecs()); |
323 | 15 | return embedderControls.storyTimeoutInSecs(); |
324 | |
} |
325 | |
|
326 | |
private String regexOf(String storyPattern) { |
327 | |
try { |
328 | |
|
329 | 31 | Pattern.compile(storyPattern); |
330 | 5 | return storyPattern; |
331 | 26 | } catch (PatternSyntaxException e) { |
332 | |
|
333 | 26 | return storyPattern.replace("*", ".*"); |
334 | |
} |
335 | |
} |
336 | |
} |
337 | |
|
338 | |
@SuppressWarnings("serial") |
339 | |
public static class StoryExecutionFailed extends RuntimeException { |
340 | |
|
341 | |
public StoryExecutionFailed(String storyPath, Throwable failure) { |
342 | 14 | super(storyPath, failure); |
343 | 14 | } |
344 | |
|
345 | |
} |
346 | |
|
347 | |
@SuppressWarnings("serial") |
348 | |
public static class StoryTimeout extends RuntimeException { |
349 | |
|
350 | |
public StoryTimeout(StoryDuration storyDuration) { |
351 | 10 | super(storyDuration.getDurationInSecs() + "s > " |
352 | 5 | + storyDuration.getTimeoutInSecs() + "s"); |
353 | 5 | } |
354 | |
|
355 | |
} |
356 | |
|
357 | |
public static class ThrowableStory { |
358 | |
private Story story; |
359 | |
private Throwable throwable; |
360 | |
|
361 | 46 | public ThrowableStory(Story story, Throwable throwable) { |
362 | 46 | this.story = story; |
363 | 46 | this.throwable = throwable; |
364 | 46 | } |
365 | |
|
366 | |
public Story getStory() { |
367 | 0 | return story; |
368 | |
} |
369 | |
|
370 | |
public Throwable getThrowable() { |
371 | 593 | return throwable; |
372 | |
} |
373 | |
} |
374 | |
|
375 | |
public static class RunningStory { |
376 | |
private EnqueuedStory enqueuedStory; |
377 | |
private Future<ThrowableStory> future; |
378 | |
private StoryDuration duration; |
379 | |
|
380 | |
public RunningStory(EnqueuedStory enqueuedStory, |
381 | 56 | Future<ThrowableStory> future) { |
382 | 56 | this.enqueuedStory = enqueuedStory; |
383 | 56 | this.future = future; |
384 | 56 | } |
385 | |
|
386 | |
public Future<ThrowableStory> getFuture() { |
387 | 1425 | return future; |
388 | |
} |
389 | |
|
390 | |
public Story getStory() { |
391 | 1425 | return enqueuedStory.getStory(); |
392 | |
} |
393 | |
|
394 | |
public long getDurationInMillis() { |
395 | 31 | if ( duration == null ){ |
396 | 14 | return 0; |
397 | |
} |
398 | 17 | return duration.getDurationInSecs() * 1000; |
399 | |
} |
400 | |
|
401 | |
public StoryDuration getDuration() { |
402 | 736 | if (duration == null) { |
403 | 28 | duration = new StoryDuration(enqueuedStory.getStartedAtMillis(), enqueuedStory.getTimeoutInSecs()); |
404 | |
} |
405 | 734 | return duration; |
406 | |
} |
407 | |
|
408 | |
public void updateDuration() { |
409 | 734 | duration.update(); |
410 | 734 | } |
411 | |
|
412 | |
public boolean isDone() { |
413 | 0 | return future.isDone(); |
414 | |
} |
415 | |
|
416 | |
public boolean isFailed() { |
417 | 0 | if (isDone()) { |
418 | |
try { |
419 | 0 | return future.get().getThrowable() != null; |
420 | 0 | } catch (InterruptedException e) { |
421 | 0 | } catch (ExecutionException e) { |
422 | 0 | } |
423 | |
} |
424 | 0 | return false; |
425 | |
} |
426 | |
|
427 | |
public boolean isStarted(){ |
428 | 2549 | return enqueuedStory.getStartedAtMillis() != 0; |
429 | |
} |
430 | |
} |
431 | |
|
432 | |
public static class StoryOutcome { |
433 | |
private String path; |
434 | |
private Boolean done; |
435 | |
private Boolean failed; |
436 | |
|
437 | 0 | public StoryOutcome(RunningStory story) { |
438 | 0 | this.path = story.getStory().getPath(); |
439 | 0 | this.done = story.isDone(); |
440 | 0 | this.failed = story.isFailed(); |
441 | 0 | } |
442 | |
|
443 | |
public String getPath() { |
444 | 0 | return path; |
445 | |
} |
446 | |
|
447 | |
public Boolean isDone() { |
448 | 0 | return done; |
449 | |
} |
450 | |
|
451 | |
public Boolean isFailed() { |
452 | 0 | return failed; |
453 | |
} |
454 | |
|
455 | |
} |
456 | |
|
457 | |
} |