View Javadoc

1   /*
2    * This file is part of hyphenType. hyphenType is free software: you can
3    * redistribute it and/or modify it under the terms of the GNU General Public
4    * License as published by the Free Software Foundation, either version 3 of the
5    * License, or (at your option) any later version. hyphenType is distributed in
6    * the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
8    * the GNU General Public License for more details. You should have received a
9    * copy of the GNU General Public License along with hyphenType. If not, see
10   * <http://www.gnu.org/licenses/>.
11   */
12  package org.hyphenType.unittesting;
13  
14  import org.hyphenType.datastructure.Options;
15  
16  /**
17   * A (strange) exception that is thrown to simulate JVM exit when we utilize
18   * {@link MockArgumentsInvocationHandler}. Behind the scenes, the
19   * {@link MockArgumentsInvocationHandler} will utilize a
20   * {@link UnitTestingOptionExtractor}, which in turn is responsible for throwing
21   * exceptions of type {@link NonExceptionalExit}. There are cases in which
22   * programmers may chose to design their algorithms under the assumption that a
23   * call to methods {@link Options#exit(Enum)} or {@link Options#exit(int)} will
24   * prevent subsequent code to be executed. This is the case, for instance, in
25   * the following code:
26   * 
27   * <pre>
28   * <code>
29   * public static int x(Opt opt) {
30   * 	if (!opt.a())
31   * 		opt.exit(17);
32   * 	return 12;
33   * }
34   * </code>
35   * </pre>
36   * 
37   * Assume that {@code opt.a()} returns {@code true}. In such case, execution of
38   * the method {@code x(Opt opt)} should never reach the {@code return}
39   * statement, even when we execute this method during unit testing. Not
40   * suspending the execution of the method {@code x(Opt opt)} when we reach the
41   * statement {@code opt.exit(1)} may cause the behavior of the code under test
42   * to change during unit testing. To prevent such behavior change, this
43   * exception ( {@link NonExceptionalExit}) is silently thrown every time to
44   * methods {@link Options#exit(Enum)} or {@link Options#exit(int)} are called.<br/>
45   * <br/>
46   * Since {@link NonExceptionalExit} is a {@link RuntimeException}, procedures
47   * that call {@link Options#exit(Enum)} or {@link Options#exit(int)} directly do
48   * not need to catch the exception for the source code to compile. Note that
49   * there is no exception handling in the method {@code x(Opt opt)} above. On the
50   * other hand, unit tests that cause the execution of methods such as {@code
51   * x(Opt opt)} above will need to catch this exception. This is demonstrated in
52   * the following code:
53   * 
54   * <pre>
55   * <code>
56   * &#64;Test
57   * public void testX() {
58   * 	UnitTestingOptionExtractor<Opt> unitTestingOptionExtractor = new UnitTestingOptionExtractor<Opt>(Opt.class);
59   * 	Opt opt = oe.options("-b");
60   * 	try {
61   * 		x(opt); // Calling x(Opt opt) as defined above
62   * 		assertFail("We should not reach this line.");
63   * 	}
64   * 	catch(NonExceptionalExit e) {
65   * 		assertTrue(unitTestingOptionExtractor.exitIntCalled(opt));
66   * 	}
67   * }
68   * </code>
69   * </pre>
70   * 
71   * In short, throwing this exception does not mean a failure (that's why this
72   * exception is called {@link NonExceptionalExit}), but is only a means to
73   * interrupt execution of a procedure under test. To prevent
74   * {@link UnitTestingOptionExtractor} to throw this exception (strictly
75   * speaking, to prevent {@link MockArgumentsInvocationHandler} to throw this
76   * exception), use the constructor
77   * {@link UnitTestingOptionExtractor#UnitTestingOptionExtractor(Class, boolean)}
78   * and pass {@code false} as the second argument.<br/>
79   * <br/>
80   * This class also provides the following methods to check how
81   * {@link Options#exit(int)} or {@link Options#exit(Enum)} was called, and which
82   * of them was called:
83   * <ul>
84   * <li> {@link NonExceptionalExit#getStatusCodeEnum()}</li>
85   * <li> {@link NonExceptionalExit#exitEnumCalled()}</li>
86   * <li> {@link NonExceptionalExit#getStatusCodeInt()}</li>
87   * <li> {@link NonExceptionalExit#exitIntCalled()}</li>
88   * </ul>
89   * Those methods are equivalent to:
90   * <ul>
91   * <li> {@link UnitTestingOptionExtractor#getStatusCodeEnum(Options)}</li>
92   * <li> {@link UnitTestingOptionExtractor#exitEnumCalled(Options)}</li>
93   * <li> {@link UnitTestingOptionExtractor#getStatusCodeInt(Options)}</li>
94   * <li> {@link UnitTestingOptionExtractor#exitIntCalled(Options)}</li>
95   * </ul>
96   * Here is an example on how to use those methods in unit testing:
97   * 
98   * <pre>
99   * <code>
100  * &#64;Test
101  * public void testX() {
102  * 	UnitTestingOptionExtractor<Opt> unitTestingOptionExtractor = new UnitTestingOptionExtractor<Opt>(Opt.class);
103  * 	Opt opt = oe.options("-b");
104  * 	try {
105  * 		x(opt); // Calling x(Opt opt) as defined above
106  * 		assertFail("We should not reach this line.");
107  * 	}
108  * 	catch(NonExceptionalExit e) {
109  * 		assertTrue(e.exitIntCalled());
110  * 		assertEquals(17, getStatusCodeInt());
111  * 	}
112  * }
113  * </code>
114  * </pre>
115  * 
116  * Additionally, the method {@link NonExceptionalExit#exitCallPoint()} can be
117  * used to find in which class, file, and line is the statement that called
118  * {@link Options#exit(Enum)} or {@link Options#exit(int)}:
119  * 
120  * <pre>
121  * <code>
122  * &#64;Test
123  * public void sample1() throws InvalidOptionsInterfaceException, InvalidOptionException {
124  * 	UnitTestingOptionExtractor<AnotherOptions> optionExtractor = new UnitTestingOptionExtractor<AnotherOptions>(AnotherOptions.class);
125  * 	try {
126  * 		AnotherApplication.main(optionExtractor.options("-x"));
127  * 		fail("Should have thrown an exception on the line above.");
128  * 	}
129  * 	catch (NonExceptionalExit e) {
130  * 		assertEquals(AnotherApplication.class.getName(), e.exitCallPoint().getClassName());
131  * 		// Line number detection won't work unless classes were compiled using debug mode.
132  * 		assertEquals(6, e.exitCallPoint().getLineNumber());
133  * 		assertEquals("main", e.exitCallPoint().getMethodName());
134  * 	}
135  * }
136  * </code>
137  * </pre>
138  * 
139  * The return type of {@link NonExceptionalExit#exitCallPoint()} is
140  * {@link StackTraceElement}, which is obtained via
141  * {@link Exception#getStackTrace()}. Therefore, the stack trace element
142  * returned by {@link NonExceptionalExit#exitCallPoint()} will only have the
143  * line number data if the classes were compiled using debug mode. If classes
144  * were not compiled with debug mode, the standard behavior is that those line
145  * numbers will be all set to -1.
146  * 
147  * @author Aurelio Akira M. Matsui
148  * @see UnitTestingOptionExtractor
149  * @see MockArgumentsInvocationHandler
150  * @see Options#exit(Enum)
151  * @see Options#exit(int)
152  */
153 public final class NonExceptionalExit extends RuntimeException {
154 
155     private static final long serialVersionUID = 1450961275714733458L;
156 
157     private final boolean intConstuctorCalled;
158     private final int exitCode;
159     private final Enum<?> exitCodeEnum;
160     private StackTraceElement exitCallPoint;
161 
162     /**
163      * This constructor is not visible from outside of this package to control which class can throw this exception.
164      * 
165      * @param exitCode
166      */
167     NonExceptionalExit(int exitCode) {
168         this.intConstuctorCalled = true;
169         this.exitCode = exitCode;
170         this.exitCodeEnum = null;
171         findExitCallPoint();
172     }
173 
174     /**
175      * This constructor is not visible from outside of this package to control which class can throw this exception.
176      * 
177      * @param exitCodeEnum
178      */
179     NonExceptionalExit(Enum<?> exitCodeEnum) {
180         this.intConstuctorCalled = false;
181         this.exitCode = exitCodeEnum.ordinal();
182         this.exitCodeEnum = exitCodeEnum;
183         findExitCallPoint();
184     }
185 
186     private void findExitCallPoint() {
187         exitCallPoint = null;
188         StackTraceElement[] elements = this.getStackTrace();
189         for (int i = 0; i < elements.length; i++) {
190             if (elements[i].getLineNumber() < 0 && elements[i].getFileName() == null && elements[i].getClassName().startsWith("$Proxy") && elements[i].getMethodName().equals("exit") && i + 1 < elements.length) {
191                 exitCallPoint = elements[i + 1];
192             }
193         }
194     }
195 
196     public Enum<?> getStatusCodeEnum() {
197         return exitCodeEnum;
198     }
199 
200     public boolean exitEnumCalled() {
201         return !intConstuctorCalled;
202     }
203 
204     public int getStatusCodeInt() {
205         return exitCode;
206     }
207 
208     public boolean exitIntCalled() {
209         return intConstuctorCalled;
210     }
211 
212     public StackTraceElement exitCallPoint() {
213         return exitCallPoint;
214     }
215 }