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 * @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 * @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 * @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 }