1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package vogar.target; 18 19import java.io.File; 20import java.lang.reflect.Field; 21import java.lang.reflect.Modifier; 22import java.net.Authenticator; 23import java.net.CookieHandler; 24import java.net.ResponseCache; 25import java.text.DateFormat; 26import java.util.Locale; 27import java.util.HashMap; 28import java.util.TimeZone; 29import java.util.logging.ConsoleHandler; 30import java.util.logging.LogManager; 31import java.util.logging.Logger; 32import java.util.prefs.BackingStoreException; 33import java.util.prefs.Preferences; 34import javax.net.ssl.HostnameVerifier; 35import javax.net.ssl.HttpsURLConnection; 36import javax.net.ssl.SSLSocketFactory; 37import vogar.util.IoUtils; 38 39/** 40 * This class resets the VM to a relatively pristine state. Useful to defend 41 * against tests that muck with system properties and other global state. 42 */ 43public final class TestEnvironment { 44 45 private final HostnameVerifier defaultHostnameVerifier; 46 private final SSLSocketFactory defaultSSLSocketFactory; 47 48 /** The DateFormat.is24Hour field. Not present on older versions of Android or the RI. */ 49 private static final Field dateFormatIs24HourField; 50 static { 51 Field f; 52 try { 53 Class<?> dateFormatClass = Class.forName("java.text.DateFormat"); 54 f = dateFormatClass.getDeclaredField("is24Hour"); 55 } catch (ClassNotFoundException | NoSuchFieldException e) { 56 f = null; 57 } 58 dateFormatIs24HourField = f; 59 } 60 private final Boolean defaultDateFormatIs24Hour; 61 62 private static final String JAVA_RUNTIME_VERSION = System.getProperty("java.runtime.version"); 63 private static final String JAVA_VM_INFO = System.getProperty("java.vm.info"); 64 private static final String JAVA_VM_VERSION = System.getProperty("java.vm.version"); 65 private static final String JAVA_VM_VENDOR = System.getProperty("java.vm.vendor"); 66 private static final String JAVA_VM_NAME = System.getProperty("java.vm.name"); 67 68 private final String tmpDir; 69 70 public TestEnvironment() { 71 this.tmpDir = System.getProperty("java.io.tmpdir"); 72 if (tmpDir == null || tmpDir.length() == 0) { 73 throw new AssertionError("tmpDir is null or empty: " + tmpDir); 74 } 75 System.setProperties(null); // Reset. 76 77 // On android, behaviour around clearing "java.io.tmpdir" is inconsistent. 78 // Some release set it to "null" and others set it to "/tmp" both values are 79 // wrong for normal apps (mode=activity), where the value that the framework 80 // sets must be used. We unconditionally restore that value here. This code 81 // should be correct on the host and on the jvm too, since tmpdir is assumed 82 // to be immutable. 83 System.setProperty("java.io.tmpdir", tmpDir); 84 85 String userHome = System.getProperty("user.home"); 86 String userDir = System.getProperty("user.dir"); 87 if (userHome == null || userDir == null) { 88 throw new NullPointerException("user.home=" + userHome + ", user.dir=" + userDir); 89 } 90 91 defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); 92 defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); 93 defaultDateFormatIs24Hour = hasDateFormatIs24Hour() ? getDateFormatIs24Hour() : null; 94 95 disableSecurity(); 96 } 97 98 private String createTempDirectory(String subDirName) { 99 String dirName = tmpDir + "/" + subDirName; 100 IoUtils.safeMkdirs(new File(dirName)); 101 return dirName; 102 } 103 104 public void reset() { 105 // Reset system properties. 106 System.setProperties(null); 107 108 // On android, behaviour around clearing "java.io.tmpdir" is inconsistent. 109 // Some release set it to "null" and others set it to "/tmp" both values are 110 // wrong for normal apps (mode=activity), where the value that the framework 111 // sets must be used. We unconditionally restore that value here. This code 112 // should be correct on the host and on the jvm too, since tmpdir is assumed 113 // to be immutable. 114 System.setProperty("java.io.tmpdir", tmpDir); 115 116 // Require writable java.home and user.dir directories for preferences 117 if ("Dalvik".equals(System.getProperty("java.vm.name"))) { 118 setPropertyIfNull("java.home", createTempDirectory("java.home")); 119 setPropertyIfNull("dexmaker.dexcache", createTempDirectory("dexmaker.dexcache")); 120 } else { 121 // The mode --jvm has these properties writable. 122 if (JAVA_RUNTIME_VERSION != null) { 123 System.setProperty("java.runtime.version", JAVA_RUNTIME_VERSION); 124 } 125 if (JAVA_VM_INFO != null) { 126 System.setProperty("java.vm.info", JAVA_VM_INFO); 127 } 128 if (JAVA_VM_VERSION != null) { 129 System.setProperty("java.vm.version", JAVA_VM_VERSION); 130 } 131 if (JAVA_VM_VENDOR != null) { 132 System.setProperty("java.vm.vendor", JAVA_VM_VENDOR); 133 } 134 if (JAVA_VM_NAME != null) { 135 System.setProperty("java.vm.name", JAVA_VM_NAME); 136 } 137 } 138 String userHome = System.getProperty("user.home"); 139 if (userHome.length() == 0) { 140 userHome = tmpDir + "/user.home"; 141 IoUtils.safeMkdirs(new File(userHome)); 142 System.setProperty("user.home", userHome); 143 } 144 145 // Localization 146 Locale.setDefault(Locale.US); 147 TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); 148 if (hasDateFormatIs24Hour()) { 149 setDateFormatIs24Hour(defaultDateFormatIs24Hour); 150 } 151 152 // Preferences 153 // Temporarily silence the java.util.prefs logger, which otherwise emits 154 // an unactionable warning. See RI bug 4751540. 155 Logger loggerToMute = Logger.getLogger("java.util.prefs"); 156 boolean usedParentHandlers = loggerToMute.getUseParentHandlers(); 157 loggerToMute.setUseParentHandlers(false); 158 try { 159 // resetPreferences(Preferences.systemRoot()); 160 resetPreferences(Preferences.userRoot()); 161 } finally { 162 loggerToMute.setUseParentHandlers(usedParentHandlers); 163 } 164 165 // HttpURLConnection 166 Authenticator.setDefault(null); 167 CookieHandler.setDefault(null); 168 ResponseCache.setDefault(null); 169 HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier); 170 HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); 171 172 // Logging 173 LogManager.getLogManager().reset(); 174 Logger.getLogger("").addHandler(new ConsoleHandler()); 175 176 // Cleanup to force CloseGuard warnings etc 177 System.gc(); 178 System.runFinalization(); 179 } 180 181 private static void resetPreferences(Preferences root) { 182 try { 183 root.sync(); 184 } catch (BackingStoreException e) { 185 // Indicates that Preferences is probably not working. It's not really supported on 186 // Android so ignore. 187 return; 188 } 189 190 try { 191 for (String child : root.childrenNames()) { 192 root.node(child).removeNode(); 193 } 194 root.clear(); 195 root.flush(); 196 } catch (BackingStoreException e) { 197 throw new RuntimeException(e); 198 } 199 } 200 201 /** A class that always returns TRUE. */ 202 @SuppressWarnings("serial") 203 public static class LyingMap extends HashMap<Object, Boolean> { 204 @Override 205 public Boolean get(Object key) { 206 return Boolean.TRUE; 207 } 208 } 209 210 /** 211 * Does what is necessary to disable security checks for testing security-related classes. 212 */ 213 @SuppressWarnings("unchecked") 214 private static void disableSecurity() { 215 try { 216 Class<?> securityBrokerClass = Class.forName("javax.crypto.JceSecurity"); 217 218 Field modifiersField = Field.class.getDeclaredField("modifiers"); 219 modifiersField.setAccessible(true); 220 221 Field verifyMapField = securityBrokerClass.getDeclaredField("verificationResults"); 222 modifiersField.setInt(verifyMapField, verifyMapField.getModifiers() & ~Modifier.FINAL); 223 verifyMapField.setAccessible(true); 224 verifyMapField.set(null, new LyingMap()); 225 226 Field restrictedField = securityBrokerClass.getDeclaredField("isRestricted"); 227 restrictedField.setAccessible(true); 228 restrictedField.set(null, Boolean.FALSE); 229 } catch (Exception ignored) { 230 } 231 } 232 233 private static boolean hasDateFormatIs24Hour() { 234 return dateFormatIs24HourField != null; 235 } 236 237 private static Boolean getDateFormatIs24Hour() { 238 try { 239 return (Boolean) dateFormatIs24HourField.get(null); 240 } catch (IllegalAccessException e) { 241 Error e2 = new AssertionError("Unable to get java.text.DateFormat.is24Hour"); 242 e2.initCause(e); 243 throw e2; 244 } 245 } 246 247 private static void setDateFormatIs24Hour(Boolean value) { 248 try { 249 dateFormatIs24HourField.set(null, value); 250 } catch (IllegalAccessException e) { 251 Error e2 = new AssertionError("Unable to set java.text.DateFormat.is24Hour"); 252 e2.initCause(e); 253 throw e2; 254 } 255 } 256 257 private static void setPropertyIfNull(String property, String value) { 258 if (System.getProperty(property) == null) { 259 System.setProperty(property, value); 260 } 261 } 262} 263