View Javadoc
1   package net.avcompris.base.testutil.processes;
2   
3   import static com.google.common.base.Preconditions.checkNotNull;
4   import static com.google.common.collect.Iterables.toArray;
5   import static org.apache.commons.lang3.CharEncoding.UTF_8;
6   import static org.junit.Assert.assertTrue;
7   
8   import java.io.File;
9   import java.io.FileOutputStream;
10  import java.io.IOException;
11  import java.io.OutputStream;
12  import java.io.OutputStreamWriter;
13  import java.io.PrintWriter;
14  import java.lang.reflect.Constructor;
15  import java.lang.reflect.GenericArrayType;
16  import java.lang.reflect.InvocationTargetException;
17  import java.lang.reflect.Method;
18  import java.lang.reflect.ParameterizedType;
19  import java.lang.reflect.Type;
20  import java.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.TreeSet;
27  
28  import org.apache.commons.io.FileUtils;
29  import org.joda.time.DateTime;
30  import org.junit.Before;
31  import org.junit.Ignore;
32  import org.junit.Test;
33  
34  /**
35   * This is a base class for JUnit test classes that want to run a or several
36   * processes before or while running tests. Mainly, processes consist in reading
37   * an entire database or a large
38   * 
39   * @author David Andrianavalontsalama
40   */
41  public abstract class TestsWithProcessesBefore {
42  
43      private final Method[] testMethods;
44  
45      protected TestsWithProcessesBefore() {
46  
47          final Class<? extends TestsWithProcessesBefore> thisClass = getClass();
48  
49          final RequiresProcesses processBefore = thisClass.getAnnotation(RequiresProcesses.class);
50  
51          if (processBefore == null) {
52  
53              throw new RuntimeException("Test class should have annotation @ProcessBefore: " + thisClass.getName());
54          }
55  
56          final Class<?>[] processClasses = processBefore.value();
57  
58          for (final Class<?> processClass : processClasses) {
59  
60              if (!AbstractProcess.class.isAssignableFrom(processClass)) {
61  
62                  throw new IllegalArgumentException("Declared process class should extend AbstractProcess: "
63                          + processClass.getName());
64              }
65  
66              @SuppressWarnings("unchecked")
67              final Class<? extends AbstractProcess<?, ?>> typedClass = (Class<? extends AbstractProcess<?, ?>>) processClass;
68  
69              this.processClasses.add(typedClass);
70          }
71  
72          final List<Method> methods = new ArrayList<Method>();
73  
74          for (final Method method : thisClass.getMethods()) {
75  
76              if (method.isAnnotationPresent(Test.class) && !method.isAnnotationPresent(Ignore.class)) {
77  
78                  final Class<?>[] paramTypes = method.getParameterTypes();
79  
80                  if (paramTypes == null || paramTypes.length == 0) {
81  
82                      methods.add(method);
83                  }
84              }
85          }
86  
87          testMethods = toArray(methods, Method.class);
88  
89          storeInstance();
90      }
91  
92      private void storeInstance() {
93  
94          final TestsWithProcessesBefore cachedInstance = instances.get(getClass());
95  
96          if (cachedInstance == null) {
97  
98              try {
99  
100                 createInstance(this);
101 
102             } catch (final IllegalAccessException e) {
103                 throw new RuntimeException(e);
104             } catch (final InstantiationException e) {
105                 throw new RuntimeException(e);
106             }
107         }
108     }
109 
110     private static synchronized void createInstance(final TestsWithProcessesBefore instance)
111             throws InstantiationException, IllegalAccessException {
112 
113         final Class<? extends TestsWithProcessesBefore> instanceClass = instance.getClass();
114 
115         final TestsWithProcessesBefore cachedInstance = instances.get(instanceClass);
116 
117         if (cachedInstance != null) {
118 
119             return;
120         }
121 
122         instances.put(instanceClass, instance);
123 
124         try {
125 
126             instance.initProcess();
127 
128         } catch (final Throwable e) {
129 
130             e.printStackTrace();
131 
132             instance.releaseProcess();
133         }
134     }
135 
136     private void initProcess() throws Exception {
137 
138         for (final Class<? extends AbstractProcess<?, ?>> processClass : processClasses) {
139 
140             final String processName = processClass.getSimpleName();
141 
142             System.out.println("Creating process instance: " + processName + "...");
143 
144             final AbstractProcess<?, ?> process = processClass.newInstance();
145 
146             System.out.println("Initializing process: " + processName + "...");
147 
148             process.setTests(this);
149 
150             process.init();
151 
152             System.out.println("Process " + processName + " initialized.");
153 
154             final ProcessInstance processInstance = new ProcessInstance(process);
155 
156             processInstances.put(processClass, processInstance);
157 
158             for (final Method testMethod : testMethods) {
159 
160                 final String methodName = testMethod.getName();
161 
162                 processInstance.methodErrors.put(methodName, new ArrayList<Throwable>());
163 
164                 processInstance.methodFailures.put(methodName, new ArrayList<AssertionError>());
165 
166                 processInstance.processEntryResults.put(methodName, new ArrayList<ProcessEntryResult>());
167             }
168         }
169     }
170 
171     private synchronized void releaseProcess() {
172 
173         final List<Class<? extends AbstractProcess<?, ?>>> processClassesCopy = new ArrayList<Class<? extends AbstractProcess<?, ?>>>(
174                 processClasses);
175 
176         for (final Class<? extends AbstractProcess<?, ?>> processClass : processClassesCopy) {
177 
178             final ProcessInstance processInstance = processInstances.get(processClass);
179 
180             if (processInstance == null) {
181 
182                 continue;
183             }
184 
185             final AbstractProcess<?, ?> process = processInstance.process;
186 
187             final String processName = processClass.getSimpleName();
188 
189             System.out.println("Releasing process: " + processName + "...");
190 
191             boolean released = true;
192 
193             try {
194 
195                 process.release();
196 
197             } catch (final Throwable e) {
198 
199                 if (processInstance.errors != null) {
200 
201                     processInstance.errors.add(e);
202                 }
203 
204                 e.printStackTrace();
205 
206                 released = false;
207 
208                 System.out.println("ERROR. While releasing process " + processName + ".");
209             }
210 
211             if (released) {
212 
213                 System.out.println("Process " + processName + " released.");
214 
215                 processClasses.remove(processClass);
216             }
217         }
218     }
219 
220     private final List<Class<? extends AbstractProcess<?, ?>>> processClasses = new ArrayList<Class<? extends AbstractProcess<?, ?>>>();
221 
222     private static Map<Class<? extends TestsWithProcessesBefore>, TestsWithProcessesBefore> instances = new HashMap<Class<? extends TestsWithProcessesBefore>, TestsWithProcessesBefore>();
223 
224     private final Map<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> processInstances = new HashMap<Class<? extends AbstractProcess<?, ?>>, ProcessInstance>();
225 
226     protected final boolean isProcessRunning() {
227 
228         return running;
229     }
230 
231     private static ParameterizedType getParameterizedTypeSuperclass(final Class<? extends AbstractProcess<?, ?>> c) {
232 
233         for (Type k = c.getGenericSuperclass(); k != null;) {
234 
235             if (ParameterizedType.class.isInstance(k)) {
236 
237                 return (ParameterizedType) k;
238             }
239 
240             k = ((Class<?>) k).getGenericSuperclass();
241         }
242 
243         throw new RuntimeException("Not implemented.");
244     }
245 
246     protected final <X> X getProcessCurrentOfType(final Class<X> currentClass) throws Exception {
247 
248         checkNotNull(currentClass, "currentClass");
249 
250         for (final Class<? extends AbstractProcess<?, ?>> c : getProcessInstances().keySet()) {
251 
252             final ParameterizedType parameterizedType = getParameterizedTypeSuperclass(c);
253 
254             final Type currentType = parameterizedType.getActualTypeArguments()[0];
255 
256             if (areSameTypes(currentClass, currentType)) {
257 
258                 @SuppressWarnings("unchecked")
259                 final X x = getProcessCurrent((Class<? extends AbstractProcess<X, ?>>) c);
260 
261                 return x;
262             }
263         }
264 
265         throw new IllegalArgumentException("Cannot find process class for currentClass: " + currentClass.getName());
266     }
267 
268     protected final void ignoreThisOneInProcess() {
269 
270         ignoreThisOnesThreadLocal.set(true);
271     }
272 
273     private String getCurrentTestMethodName() {
274 
275         for (final StackTraceElement ste : new Exception().getStackTrace()) {
276 
277             final String methodName = ste.getMethodName();
278 
279             // TODO optimize this! With methodNames put in cache.
280             for (final Method testMethod : testMethods) {
281 
282                 if (methodName.equals(testMethod.getName())) {
283 
284                     return methodName;
285                 }
286             }
287         }
288 
289         return null;
290     }
291 
292     private static String composeTestSummary(final ProcessInstance processInstance, final String testMethodName) {
293 
294         final int count = processInstance.process.getCurrentIndex();
295 
296         final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);
297 
298         final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);
299 
300         final String summary = "Runs: " + count //
301                 + ", Failures: " + methodFailures.size() //
302                 + ", Errors: " + methodErrors.size() //
303                 + " (+" + processInstance.errors.size() + ")";
304 
305         return summary;
306     }
307 
308     private static <T extends Throwable> T recomposeThrowable(final T error, final String summary) {
309 
310         final StackTraceElement[] stackTrace = error.getStackTrace();
311 
312         T newError = error;
313 
314         final Class<? extends Throwable> errorClass = error.getClass();
315 
316         Constructor<? extends Throwable> constructor;
317 
318         try {
319 
320             constructor = errorClass.getConstructor(String.class);
321 
322         } catch (final Throwable e1) {
323 
324             try {
325 
326                 constructor = errorClass.getConstructor(Object.class);
327 
328             } catch (final Throwable e2) {
329 
330                 try {
331 
332                     constructor = errorClass.getConstructor(String.class, String.class, String.class);
333 
334                 } catch (final Throwable e3) {
335 
336                     constructor = null;
337                 }
338             }
339         }
340 
341         if (constructor != null) {
342 
343             final String message = summary + ", Sample: " + error.getMessage();
344 
345             try {
346 
347                 final Object o;
348 
349                 if (constructor.getParameterTypes().length == 1) {
350 
351                     o = constructor.newInstance(message);
352 
353                 } else {
354 
355                     o = constructor.newInstance(message, "", "");
356                 }
357 
358                 @SuppressWarnings("unchecked")
359                 final T e = (T) o;
360 
361                 newError = e;
362 
363             } catch (final Throwable e) {
364 
365                 // do nothing
366             }
367         }
368 
369         newError.setStackTrace(stackTrace);
370 
371         return newError;
372     }
373 
374     private final ProcessInstance getUniqueProcessInstance() {
375 
376         final int instanceCount = processInstances.size();
377 
378         switch (instanceCount) {
379         case 0:
380             throw new IllegalStateException("Cannot call getUniqueProcessInstance()" //
381                     + " with zero process.");
382 
383         case 1:
384             return processInstances.values().iterator().next();
385 
386         default:
387             break;
388         }
389 
390         for (final Method testMethod : testMethods) {
391 
392             final WhileProcessing processing = testMethod.getAnnotation(WhileProcessing.class);
393 
394             if (processing != null) {
395 
396                 final Class<? extends AbstractProcess<?, ?>>[] processClasses = processing.value();
397 
398                 if (processClasses != null && processClasses.length == 1) {
399 
400                     return getInitializedProcessInstance(processClasses[0]);
401                 }
402             }
403         }
404 
405         throw new IllegalStateException("Cannot call getUniqueProcessInstance()" + " with more than one process: "
406                 + processInstances.keySet().iterator().next().getSimpleName() + ", etc.");
407     }
408 
409     protected final int getProcessCurrentIndex() throws Exception {
410 
411         final ProcessInstance processInstance = getUniqueProcessInstance();
412 
413         return getProcessCurrentIndex(processInstance);
414     }
415 
416     protected final int getProcessCurrentIndex(final Class<? extends AbstractProcess<?, ?>> processClass)
417             throws Exception {
418 
419         final ProcessInstance processInstance = getInitializedProcessInstance(processClass);
420 
421         return getProcessCurrentIndex(processInstance);
422     }
423 
424     private final int getProcessCurrentIndex(final ProcessInstance processInstance) throws Exception {
425 
426         return processInstance.process.getCurrentIndex();
427     }
428 
429     protected final <X> X getProcessCurrent(final Class<? extends AbstractProcess<X, ?>> processClass) throws Exception {
430 
431         checkNotNull(processClass, "processClass");
432 
433         final ProcessInstance processInstance = getInitializedProcessInstance(processClass);
434 
435         if (!running) {
436 
437             final String testMethodName = getCurrentTestMethodName();
438 
439             if (testMethodName == null) {
440                 throw new IllegalStateException("Calling getProcessCurrent() from outside a test method.");
441             }
442 
443             final String summary = composeTestSummary(processInstance, testMethodName);
444 
445             Throwable error = null;
446 
447             final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);
448 
449             if (!methodErrors.isEmpty()) {
450 
451                 error = methodErrors.get(0);
452             }
453 
454             if (error == null) {
455 
456                 final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);
457 
458                 if (!methodFailures.isEmpty()) {
459 
460                     final AssertionError failure = methodFailures.get(0);
461 
462                     throw recomposeThrowable(failure, summary);
463                 }
464 
465                 if (!processInstance.errors.isEmpty()) {
466 
467                     error = processInstance.errors.get(0);
468                 }
469             }
470 
471             if (error != null) {
472 
473                 if (Error.class.isInstance(error)) {
474 
475                     throw recomposeThrowable((Error) error, summary);
476 
477                 } else if (Exception.class.isInstance(error)) {
478 
479                     throw recomposeThrowable((Exception) error, summary);
480 
481                 } else {
482 
483                     throw new RuntimeException(summary, error);
484                 }
485             }
486         }
487 
488         @SuppressWarnings("unchecked")
489         final X current = (X) processInstance.process.getCurrent();
490 
491         return current;
492     }
493 
494     private static boolean areSameTypes(final Class<?> c, final Type t) {
495 
496         if (c.equals(t)) {
497 
498             return true;
499         }
500 
501         if (c.isArray() && GenericArrayType.class.isInstance(t)) {
502 
503             if (c.getComponentType().equals(((GenericArrayType) t).getGenericComponentType())) {
504 
505                 return true;
506             }
507         }
508 
509         return false;
510     }
511 
512     protected final <X> X getProcessResultOfType(final Class<?> resultClass) throws Exception {
513 
514         checkNotNull(resultClass, "resultClass");
515 
516         for (final Class<? extends AbstractProcess<?, ?>> c : getProcessInstances().keySet()) {
517 
518             final ParameterizedType parameterizedType = getParameterizedTypeSuperclass(c);
519 
520             final Type resultType = parameterizedType.getActualTypeArguments()[1];
521 
522             if (areSameTypes(resultClass, resultType)) {
523 
524                 @SuppressWarnings("unchecked")
525                 final X x = getProcessResult((Class<? extends AbstractProcess<?, X>>) c);
526 
527                 return x;
528             }
529         }
530 
531         throw new IllegalArgumentException("Cannot find process class for resultClass: " + resultClass.getName());
532     }
533 
534     protected final <X> X getProcessResult(final Class<? extends AbstractProcess<?, X>> processClass) throws Exception {
535 
536         checkNotNull(processClass, "processClass");
537 
538         if (running || lockFile.exists()) {
539 
540             throw new IllegalStateException("Process are still running.");
541         }
542 
543         final ProcessInstance processInstance = getInitializedProcessInstance(processClass);
544 
545         if (!processInstance.errors.isEmpty()) {
546 
547             final Throwable error = processInstance.errors.get(0);
548 
549             if (Error.class.isInstance(error)) {
550 
551                 throw (Error) error;
552 
553             } else if (Exception.class.isInstance(error)) {
554 
555                 throw (Exception) error;
556 
557             } else {
558 
559                 throw new RuntimeException(error);
560             }
561         }
562 
563         @SuppressWarnings("unchecked")
564         final X result = (X) processInstance.process.getResult();
565 
566         return result;
567     }
568 
569     private static boolean running = false;
570 
571     @Before
572     public final void setUpProcess() throws Exception {
573 
574         if (!running) {
575 
576             runProcess();
577         }
578     }
579 
580     final File lockFile = new File("target", getClass().getSimpleName() + ".lock");
581 
582     private static Set<Class<? extends TestsWithProcessesBefore>> haveBeenRun = new HashSet<Class<? extends TestsWithProcessesBefore>>();
583 
584     private synchronized void runProcess() throws IOException, IllegalArgumentException, SecurityException,
585             InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
586 
587         if (running) {
588 
589             return;
590         }
591 
592         if (lockFile.exists()) {
593 
594             throw new IllegalStateException("Lock file exists (" + new DateTime(lockFile.lastModified()) + "): "
595                     + lockFile.getCanonicalPath());
596         }
597 
598         final Class<? extends TestsWithProcessesBefore> thisClass = getClass();
599 
600         if (haveBeenRun.contains(thisClass)) {
601 
602             return;
603         }
604 
605         FileUtils.touch(lockFile);
606 
607         running = true;
608 
609         haveBeenRun.add(thisClass);
610 
611         runInstanceProcess();
612 
613         lockFile.delete();
614 
615         running = false;
616     }
617 
618     private ProcessInstance getInitializedProcessInstance(final Class<? extends AbstractProcess<?, ?>> processClass) {
619 
620         final String processName = processClass.getSimpleName();
621 
622         final ProcessInstance processInstance = getProcessInstances().get(processClass);
623 
624         if (processInstance == null) {
625             throw new IllegalStateException("Process has not been initialized: " + processName);
626         }
627 
628         return processInstance;
629     }
630 
631     private Map<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> getProcessInstances() {
632 
633         final Class<? extends TestsWithProcessesBefore> thisClass = getClass();
634 
635         final TestsWithProcessesBefore instance = instances.get(thisClass);
636 
637         if (instance == null) {
638             throw new IllegalStateException("Instance has not been initialized for: " + thisClass);
639         }
640 
641         return instance.processInstances;
642     }
643 
644     private ProcessInstance getInitializedProcessInstance(final AbstractProcess<?, ?> process) {
645 
646         final String processName = process.getClass().getSimpleName();
647 
648         for (final Map.Entry<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> entry : processInstances
649                 .entrySet()) {
650 
651             final Class<?> c = entry.getKey();
652 
653             if (c.equals(process.getClass())) {
654 
655                 return entry.getValue();
656             }
657         }
658 
659         throw new IllegalStateException("Process has not been initialized: " + processName);
660     }
661 
662     private void runInstanceProcess() throws IOException, IllegalArgumentException, SecurityException,
663             InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
664 
665         try {
666 
667             for (final Class<? extends AbstractProcess<?, ?>> processClass : processClasses) {
668 
669                 final String processName = processClass.getSimpleName();
670 
671                 final ProcessInstance processInstance = getInitializedProcessInstance(processClass);
672 
673                 System.out.println("Executing process: " + processName + "...");
674 
675                 final long start = System.currentTimeMillis();
676 
677                 assertTrue("Error list is not empty.", processInstance.errors.isEmpty());
678 
679                 for (final Method testMethod : testMethods) {
680 
681                     final String testMethodName = testMethod.getName();
682 
683                     assertTrue("Error list is not empty for: " + testMethodName + "()", processInstance.methodErrors
684                             .get(testMethodName).isEmpty());
685 
686                     assertTrue("Failure list is not empty for: " + testMethodName + "()",
687                             processInstance.methodFailures.get(testMethodName).isEmpty());
688                 }
689 
690                 boolean success = false;
691 
692                 try {
693 
694                     processInstance.process.execute();
695 
696                     success = true;
697 
698                 } catch (final Throwable e) {
699 
700                     success = false;
701 
702                     processInstance.errors.add(e);
703 
704                     e.printStackTrace(System.out);
705                 }
706 
707                 final long elapsed = System.currentTimeMillis() - start;
708 
709                 System.out.println();
710 
711                 if (success) {
712 
713                     System.out.print("SUCCESS. Process " + processName + " done in " + elapsed + " ms.");
714 
715                 } else {
716 
717                     System.out.print("ERROR. In process " + processName + " after " + elapsed + " ms.");
718                 }
719 
720                 processInstance.process.cliProgress.end();
721 
722                 // System.out.println();
723 
724                 emitProcessEntryReport(processInstance);
725 
726                 emitStandardReport(processInstance);
727             }
728 
729         } finally {
730 
731             releaseProcess();
732         }
733     }
734 
735     private static final ThreadLocal<Boolean> ignoreThisOnesThreadLocal = new ThreadLocal<Boolean>();
736 
737     final void runTests(final AbstractProcess<?, ?> process, final ProcessEntry processEntry) {
738 
739         checkNotNull(process);
740 
741         final ProcessInstance processInstance = getInitializedProcessInstance(process);
742 
743         for (final Method testMethod : testMethods) {
744 
745             final WhileProcessing whileProcessing = testMethod.getAnnotation(WhileProcessing.class);
746 
747             final WillReportAfterProcesses willReportAfterProcesses = testMethod
748                     .getAnnotation(WillReportAfterProcesses.class);
749 
750             if (willReportAfterProcesses != null || whileProcessing == null) {
751                 continue;
752             }
753 
754             final Class<? extends AbstractProcess<?, ?>>[] whileProcessingProcesses = whileProcessing.value();
755 
756             if (whileProcessingProcesses != null && whileProcessingProcesses.length != 0) {
757 
758                 boolean found = false;
759 
760                 for (final Class<? extends AbstractProcess<?, ?>> c : whileProcessingProcesses) {
761                     if (c.isAssignableFrom(process.getClass())) {
762                         found = true;
763                         break;
764                     }
765                 }
766 
767                 if (!found) {
768                     continue;
769                 }
770             }
771 
772             final String testMethodName = testMethod.getName();
773 
774             final List<Throwable> errors = processInstance.methodErrors.get(testMethodName);
775 
776             final List<AssertionError> failures = processInstance.methodFailures.get(testMethodName);
777 
778             final List<ProcessEntryResult> results = processInstance.processEntryResults.get(testMethodName);
779 
780             final ProcessEntryResult result = (processEntry == null) ? null : new ProcessEntryResult(processEntry);
781 
782             final long start = System.currentTimeMillis();
783 
784             ignoreThisOnesThreadLocal.set(false);
785 
786             try {
787 
788                 testMethod.invoke(this);
789 
790             } catch (final InvocationTargetException e) {
791 
792                 final Throwable cause = e.getTargetException();
793 
794                 if (cause == null) {
795 
796                     setResultError(result, e);
797 
798                     errors.add(e);
799 
800                 } else if (AssertionError.class.isInstance(cause)) {
801 
802                     setResultFailure(result, (AssertionError) cause);
803 
804                     failures.add((AssertionError) cause);
805 
806                 } else {
807 
808                     setResultError(result, e);
809 
810                     errors.add(cause);
811                 }
812 
813             } catch (final AssertionError e) {
814 
815                 setResultFailure(result, (AssertionError) e);
816 
817                 failures.add(e);
818 
819             } catch (final Throwable e) {
820 
821                 setResultError(result, e);
822 
823                 errors.add(e);
824             }
825 
826             if (result != null && !ignoreThisOnesThreadLocal.get()) {
827 
828                 results.add(result);
829             }
830 
831             final long elapsed = System.currentTimeMillis() - start;
832 
833             setResultElapsedMs(result, elapsed);
834         }
835     }
836 
837     private static void setResultError(final ProcessEntryResult result, final Throwable e) {
838 
839         if (result != null) {
840 
841             result.setError(e);
842         }
843     }
844 
845     private static void setResultFailure(final ProcessEntryResult result, final AssertionError e) {
846 
847         if (result != null) {
848 
849             result.setFailure(e);
850         }
851     }
852 
853     private static void setResultElapsedMs(final ProcessEntryResult result, final long elapsed) {
854 
855         if (result != null) {
856 
857             result.setElapsedMs(elapsed);
858         }
859     }
860 
861     private void emitStandardReport(final ProcessInstance processInstance) throws IllegalArgumentException,
862             SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException,
863             NoSuchMethodException {
864 
865         final String processName = processInstance.process.getClass().getSimpleName();
866 
867         final Report report = instantiateReport(processName, 1, getReportCount());
868 
869         try {
870 
871             final int errorCount = processInstance.errors.size();
872 
873             switch (errorCount) {
874             case 0:
875                 report.info("No error at process level.");
876                 break;
877             case 1:
878                 report.error("One error at process level.");
879                 break;
880             default:
881                 report.error(errorCount + " errors at process level.");
882                 break;
883             }
884 
885             final List<Throwable> totalMethodErrors = new ArrayList<Throwable>();
886             final List<AssertionError> totalMethodFailures = new ArrayList<AssertionError>();
887 
888             final Set<String> testMethodNames = processInstance.methodErrors.keySet();
889 
890             for (final String methodName : testMethodNames) {
891 
892                 totalMethodErrors.addAll(processInstance.methodErrors.get(methodName));
893                 totalMethodFailures.addAll(processInstance.methodFailures.get(methodName));
894             }
895 
896             final int totalMethodErrorCount = totalMethodErrors.size();
897             final int totalMethodFailureCount = totalMethodFailures.size();
898 
899             switch (totalMethodFailureCount) {
900             case 0:
901                 report.info("No failure at test level.");
902                 break;
903             case 1:
904                 report.error("One failure at test level.");
905                 break;
906             default:
907                 report.error(totalMethodFailureCount + " failures at test level.");
908                 break;
909             }
910 
911             switch (totalMethodErrorCount) {
912             case 0:
913                 report.info("No error at test level.");
914                 break;
915             case 1:
916                 report.error("One error at test level.");
917                 break;
918             default:
919                 report.error(totalMethodErrorCount + " errors at test level.");
920                 break;
921             }
922 
923             // SUMMARY BY TEST METHOD
924 
925             report.info("Summary:");
926 
927             report.info("Test Method", "Runs", "Failures", "Errors");
928 
929             final int count = processInstance.process.getCurrentIndex();
930 
931             for (final String testMethodName : new TreeSet<String>(testMethodNames)) {
932 
933                 final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);
934 
935                 final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);
936 
937                 report.infoDetail(testMethodName + "()", count, methodFailures.size(), methodErrors.size());
938             }
939 
940             // DETAIL AT THE GLOBAL LEVEL
941 
942             // report.info("Errors at process level: " + errorCount);
943             // for (int i = 0; i < errorCount && i < MAX; ++i) {
944             // report.errorDetail(processInstance.errors.get(i));
945             // }
946             // if (errorCount > MAX) {
947             // report.errorDetail("(...)");
948             // }
949             //
950             // report.info("Failures at test level: " +
951             // totalMethodFailureCount);
952             // for (int i = 0; i < totalMethodFailureCount && i < MAX; ++i) {
953             // report.errorDetail(totalMethodFailures.get(i));
954             // }
955             // if (totalMethodFailureCount > MAX) {
956             // report.errorDetail("(...)");
957             // }
958             //
959             // report.info("Errors at test level: " + totalMethodErrorCount);
960             // for (int i = 0; i < totalMethodErrorCount && i < MAX; ++i) {
961             // report.errorDetail(totalMethodErrors.get(i));
962             // }
963             // if (totalMethodErrorCount > MAX) {
964             // report.errorDetail("(...)");
965             // }
966 
967             // DETAIL BY TEST METHOD
968 
969             for (final String testMethodName : new TreeSet<String>(testMethodNames)) {
970 
971                 final String summary = composeTestSummary(processInstance, testMethodName);
972 
973                 final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);
974 
975                 final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);
976 
977                 if (methodErrors.isEmpty() && methodFailures.isEmpty()) {
978 
979                     // report.info(testMethodName + "(): " + summary);
980 
981                 } else {
982 
983                     report.error(testMethodName + "(): " + summary);
984                     for (int i = 0; i < methodFailures.size() && i < MAX; ++i) {
985                         report.errorDetail(methodFailures.get(i));
986                     }
987                     if (methodFailures.size() > MAX) {
988                         report.errorDetail("(...)");
989                     }
990 
991                     for (int i = 0; i < methodErrors.size() && i < MAX; ++i) {
992                         report.errorDetail(methodErrors.get(i));
993                     }
994                     if (methodErrors.size() > MAX) {
995                         report.errorDetail("(...)");
996                     }
997                 }
998             }
999 
1000         } finally {
1001 
1002             report.send();
1003         }
1004     }
1005 
1006     private static void emitProcessEntryReport(final ProcessInstance processInstance) throws IOException {
1007 
1008         final File file = new File("target/processentry-report/" + processInstance.process.getClass().getSimpleName()
1009                 + ".xml");
1010 
1011         FileUtils.forceMkdir(file.getParentFile());
1012 
1013         final OutputStream os = new FileOutputStream(file);
1014         try {
1015 
1016             final PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, UTF_8));
1017 
1018             pw.println("<processentry-report>");
1019 
1020             for (final Map.Entry<String, List<ProcessEntryResult>> e : processInstance.processEntryResults.entrySet()) {
1021 
1022                 final String testMethodName = e.getKey();
1023 
1024                 final List<ProcessEntryResult> results = e.getValue();
1025 
1026                 pw.println("<testMethod name=\"" + testMethodName + "\">");
1027 
1028                 if (results != null) {
1029 
1030                     for (final ProcessEntryResult result : results) {
1031 
1032                         pw.print("\t<processentry id=\"" + xmlEncode(result.getProcessEntryId()) + "\" elapsed=\""
1033                                 + result.getElapsedMs() + "\"");
1034 
1035                         printReportXmlAttribute(pw, "errorClassName", result.getErrorClassName());
1036                         printReportXmlAttribute(pw, "errorMessage", result.getErrorMessage());
1037                         printReportXmlAttribute(pw, "failureClassName", result.getFailureClassName());
1038                         printReportXmlAttribute(pw, "failureMessage", result.getFailureMessage());
1039 
1040                         pw.println("/>");
1041                     }
1042                 }
1043 
1044                 pw.println("</testMethod>");
1045             }
1046 
1047             pw.println("</processentry-report>");
1048 
1049             pw.flush();
1050 
1051         } finally {
1052             os.close();
1053         }
1054     }
1055 
1056     private static void printReportXmlAttribute(final PrintWriter pw, final String name, final String value) {
1057 
1058         if (value != null) {
1059             pw.print(" " + name + "=\"" + xmlEncode(value) + "\"");
1060         }
1061     }
1062 
1063     private static String xmlEncode(final String s) {
1064 
1065         return s.replace("&", "&amp;").replace("\"", "&quot;").replace("<", "&lt;").replace(">", "&gt;");
1066     }
1067 
1068     private static final int MAX = 10;
1069 
1070     private int reportIndex = 2;
1071 
1072     protected final Report createReport() {
1073 
1074         final String testMethodName = getCurrentTestMethodName();
1075 
1076         final Report report;
1077 
1078         try {
1079 
1080             report = instantiateReport(getClass().getSimpleName() + "." + testMethodName + "()", reportIndex,
1081                     getReportCount());
1082 
1083         } catch (final RuntimeException e) {
1084             throw e;
1085         } catch (final Exception e) {
1086             throw new RuntimeException(e);
1087         }
1088 
1089         ++reportIndex;
1090 
1091         return report;
1092     }
1093 
1094     private final int getReportCount() {
1095 
1096         int reportCount = 1;
1097 
1098         for (final Method testMethod : testMethods) {
1099 
1100             if (testMethod.isAnnotationPresent(WillReportAfterProcesses.class)) {
1101 
1102                 ++reportCount;
1103             }
1104         }
1105 
1106         return reportCount;
1107     }
1108 
1109     private static boolean hasConstructor(final Class<?> c, final Class<?>... params) {
1110 
1111         try {
1112 
1113             c.getConstructor(params);
1114 
1115             return true;
1116 
1117         } catch (final Exception e) {
1118 
1119             // do nothing
1120         }
1121 
1122         return false;
1123     }
1124 
1125     private Report instantiateReport(final String name, final int index, final int count)
1126             throws IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException,
1127             InvocationTargetException, NoSuchMethodException {
1128 
1129         final Class<? extends TestsWithProcessesBefore> thisClass = getClass();
1130 
1131         final ReportTo reportTo = thisClass.getAnnotation(ReportTo.class);
1132 
1133         if (reportTo == null) {
1134 
1135             return new SystemOutReport(name, index, count);
1136 
1137         } else {
1138 
1139             final List<Report> reports = new ArrayList<Report>();
1140 
1141             for (final Class<? extends Report> reportClass : reportTo.value()) {
1142 
1143                 final Report r;
1144 
1145                 if (hasConstructor(reportClass, String.class, int.class, int.class)) {
1146 
1147                     r = reportClass.getConstructor(String.class, int.class, int.class).newInstance(name, index, count);
1148 
1149                 } else if (hasConstructor(reportClass, String.class)) {
1150 
1151                     r = reportClass.getConstructor(String.class).newInstance(name);
1152 
1153                 } else {
1154 
1155                     r = reportClass.newInstance();
1156                 }
1157 
1158                 reports.add(r);
1159             }
1160 
1161             return new CompositeReport(reports);
1162         }
1163     }
1164 }