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 }