1 /* 2 * Copyright (C) 2020 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 package com.android.tradefed.invoker.logger; 17 18 import com.android.tradefed.invoker.ExecutionFiles; 19 import com.android.tradefed.invoker.IInvocationContext; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.result.ActionInProgress; 22 import com.android.tradefed.result.FailureDescription; 23 import com.android.tradefed.result.error.ErrorIdentifier; 24 25 import java.io.File; 26 import java.lang.StackWalker.Option; 27 import java.util.HashMap; 28 import java.util.Map; 29 import java.util.Optional; 30 import java.util.concurrent.ConcurrentHashMap; 31 32 import javax.annotation.Nullable; 33 34 /** 35 * A class that tracks and provides the current invocation information useful anywhere inside the 36 * invocation. 37 */ 38 public class CurrentInvocation { 39 40 /** Some special named key that we will always populate for the invocation. */ 41 public enum InvocationInfo { 42 WORK_FOLDER("work_folder"); 43 44 private final String mKeyName; 45 InvocationInfo(String key)46 private InvocationInfo(String key) { 47 mKeyName = key; 48 } 49 50 @Override toString()51 public String toString() { 52 return mKeyName; 53 } 54 } 55 56 /** Describes the level of isolation */ 57 public enum IsolationGrade { 58 NOT_ISOLATED, // No action were taken to isolate the test. 59 REBOOT_ISOLATED, // Reboot was done before the test. 60 FULLY_ISOLATED; // Test received a fresh device. 61 } 62 CurrentInvocation()63 private CurrentInvocation() {} 64 65 /** Internal storage of the invocation values. */ 66 private static class InternalInvocationTracking { 67 public Map<InvocationInfo, File> mInvocationInfoFiles = new HashMap<>(); 68 public ExecutionFiles mExecutionFiles; 69 public ActionInProgress mActionInProgress = ActionInProgress.UNSET; 70 public IsolationGrade mIsModuleIsolated = IsolationGrade.FULLY_ISOLATED; 71 public IsolationGrade mIsRunIsolated = IsolationGrade.FULLY_ISOLATED; 72 public IInvocationContext mContext; 73 public IInvocationContext mModuleContext; 74 } 75 76 /** 77 * Track info per ThreadGroup as a proxy to invocation since an invocation run within one 78 * threadgroup. 79 */ 80 private static final Map<ThreadGroup, InternalInvocationTracking> mPerGroupInfo = 81 new ConcurrentHashMap<ThreadGroup, CurrentInvocation.InternalInvocationTracking>(); 82 83 private static final Map<ThreadGroup, Map<InvocationLocal<?>, Optional<?>>> mInvocationLocals = 84 new ConcurrentHashMap<>(); 85 86 private static ThreadLocal<ThreadGroup> sLocal = new ThreadLocal<>(); 87 88 /** Tracks a localized context when using the properties inside the gRPC server */ setLocalGroup(ThreadGroup tg)89 public static void setLocalGroup(ThreadGroup tg) { 90 sLocal.set(tg); 91 } 92 93 /** Resets the localized context. */ resetLocalGroup()94 public static void resetLocalGroup() { 95 sLocal.remove(); 96 } 97 98 /** 99 * Add one key-value to be tracked at the invocation level. 100 * 101 * @param key The key under which the invocation info will be tracked. 102 * @param value The value of the invocation metric. 103 */ addInvocationInfo(InvocationInfo key, File value)104 public static void addInvocationInfo(InvocationInfo key, File value) { 105 ThreadGroup group = Thread.currentThread().getThreadGroup(); 106 synchronized (mPerGroupInfo) { 107 if (mPerGroupInfo.get(group) == null) { 108 mPerGroupInfo.put(group, new InternalInvocationTracking()); 109 } 110 mPerGroupInfo.get(group).mInvocationInfoFiles.put(key, value); 111 } 112 } 113 114 /** Returns the Map of invocation metrics for the invocation in progress. */ getInfo(InvocationInfo key)115 public static File getInfo(InvocationInfo key) { 116 ThreadGroup group = Thread.currentThread().getThreadGroup(); 117 synchronized (mPerGroupInfo) { 118 if (sLocal.get() != null) { 119 group = sLocal.get(); 120 } 121 if (mPerGroupInfo.get(group) == null) { 122 mPerGroupInfo.put(group, new InternalInvocationTracking()); 123 } 124 return mPerGroupInfo.get(group).mInvocationInfoFiles.get(key); 125 } 126 } 127 128 /** Returns the current work folder for the invocation or null if none set yet. */ getWorkFolder()129 public static File getWorkFolder() { 130 File workfolder = getInfo(InvocationInfo.WORK_FOLDER); 131 if (workfolder == null || !workfolder.exists()) { 132 return null; 133 } 134 return workfolder; 135 } 136 137 /** Clear the invocation info for an invocation. */ clearInvocationInfos()138 public static void clearInvocationInfos() { 139 ThreadGroup group = Thread.currentThread().getThreadGroup(); 140 synchronized (mPerGroupInfo) { 141 mPerGroupInfo.remove(group); 142 } 143 mInvocationLocals.remove(group); 144 } 145 146 /** 147 * One-time registration of the {@link ExecutionFiles}. This is done by the Test Harness. 148 * 149 * @param invocFiles The registered {@link ExecutionFiles}. 150 */ registerExecutionFiles(ExecutionFiles invocFiles)151 public static void registerExecutionFiles(ExecutionFiles invocFiles) { 152 ThreadGroup group = Thread.currentThread().getThreadGroup(); 153 synchronized (mPerGroupInfo) { 154 if (mPerGroupInfo.get(group) == null) { 155 mPerGroupInfo.put(group, new InternalInvocationTracking()); 156 } 157 if (mPerGroupInfo.get(group).mExecutionFiles == null) { 158 mPerGroupInfo.get(group).mExecutionFiles = invocFiles; 159 } else { 160 CLog.w( 161 "CurrentInvocation#registerExecutionFiles should only be called once " 162 + "per invocation."); 163 } 164 } 165 } 166 167 /** Returns the {@link ExecutionFiles} for the invocation. */ getInvocationFiles()168 public static ExecutionFiles getInvocationFiles() { 169 ThreadGroup group = Thread.currentThread().getThreadGroup(); 170 synchronized (mPerGroupInfo) { 171 if (mPerGroupInfo.get(group) == null) { 172 mPerGroupInfo.put(group, new InternalInvocationTracking()); 173 } 174 return mPerGroupInfo.get(group).mExecutionFiles; 175 } 176 } 177 178 /** Sets the {@link ActionInProgress} for the invocation. */ setActionInProgress(ActionInProgress action)179 public static void setActionInProgress(ActionInProgress action) { 180 ThreadGroup group = Thread.currentThread().getThreadGroup(); 181 synchronized (mPerGroupInfo) { 182 if (mPerGroupInfo.get(group) == null) { 183 mPerGroupInfo.put(group, new InternalInvocationTracking()); 184 } 185 mPerGroupInfo.get(group).mActionInProgress = action; 186 } 187 } 188 189 /** Returns the current {@link ActionInProgress} for the invocation. Can be null. */ getActionInProgress()190 public static @Nullable ActionInProgress getActionInProgress() { 191 ThreadGroup group = Thread.currentThread().getThreadGroup(); 192 synchronized (mPerGroupInfo) { 193 if (mPerGroupInfo.get(group) == null) { 194 return null; 195 } 196 return mPerGroupInfo.get(group).mActionInProgress; 197 } 198 } 199 200 /** Sets the {@link IInvocationContext} for the invocation. */ setInvocationContext(IInvocationContext context)201 public static void setInvocationContext(IInvocationContext context) { 202 ThreadGroup group = Thread.currentThread().getThreadGroup(); 203 synchronized (mPerGroupInfo) { 204 if (mPerGroupInfo.get(group) == null) { 205 mPerGroupInfo.put(group, new InternalInvocationTracking()); 206 } 207 mPerGroupInfo.get(group).mContext = context; 208 } 209 } 210 211 /** Returns the current {@link IInvocationContext} for the invocation. Can be null. */ getInvocationContext()212 public static @Nullable IInvocationContext getInvocationContext() { 213 ThreadGroup group = Thread.currentThread().getThreadGroup(); 214 synchronized (mPerGroupInfo) { 215 if (mPerGroupInfo.get(group) == null) { 216 return null; 217 } 218 return mPerGroupInfo.get(group).mContext; 219 } 220 } 221 222 /** Sets the module {@link IInvocationContext} of the currently running module. */ setModuleContext(IInvocationContext moduleContext)223 public static void setModuleContext(IInvocationContext moduleContext) { 224 ThreadGroup group = Thread.currentThread().getThreadGroup(); 225 synchronized (mPerGroupInfo) { 226 if (mPerGroupInfo.get(group) == null) { 227 mPerGroupInfo.put(group, new InternalInvocationTracking()); 228 } 229 mPerGroupInfo.get(group).mModuleContext = moduleContext; 230 } 231 } 232 233 /** 234 * Returns the module {@link IInvocationContext} for the current module. Can be null if out of 235 * scope of a module run. 236 */ getModuleContext()237 public static @Nullable IInvocationContext getModuleContext() { 238 ThreadGroup group = Thread.currentThread().getThreadGroup(); 239 synchronized (mPerGroupInfo) { 240 if (mPerGroupInfo.get(group) == null) { 241 return null; 242 } 243 return mPerGroupInfo.get(group).mModuleContext; 244 } 245 } 246 247 /** Returns whether the current suite module executed was isolated or not. */ moduleCurrentIsolation()248 public static IsolationGrade moduleCurrentIsolation() { 249 ThreadGroup group = Thread.currentThread().getThreadGroup(); 250 synchronized (mPerGroupInfo) { 251 if (mPerGroupInfo.get(group) == null) { 252 return IsolationGrade.NOT_ISOLATED; 253 } 254 return mPerGroupInfo.get(group).mIsModuleIsolated; 255 } 256 } 257 258 /** Update whether the suite module is isolated or not. */ setModuleIsolation(IsolationGrade isolation)259 public static void setModuleIsolation(IsolationGrade isolation) { 260 ThreadGroup group = Thread.currentThread().getThreadGroup(); 261 synchronized (mPerGroupInfo) { 262 if (mPerGroupInfo.get(group) == null) { 263 mPerGroupInfo.put(group, new InternalInvocationTracking()); 264 } 265 mPerGroupInfo.get(group).mIsModuleIsolated = isolation; 266 } 267 } 268 269 /** Returns whether the current test run executed was isolated or not. */ runCurrentIsolation()270 public static IsolationGrade runCurrentIsolation() { 271 ThreadGroup group = Thread.currentThread().getThreadGroup(); 272 synchronized (mPerGroupInfo) { 273 if (mPerGroupInfo.get(group) == null) { 274 return IsolationGrade.NOT_ISOLATED; 275 } 276 return mPerGroupInfo.get(group).mIsRunIsolated; 277 } 278 } 279 280 /** Update whether the test run is isolated or not. */ setRunIsolation(IsolationGrade isolation)281 public static void setRunIsolation(IsolationGrade isolation) { 282 ThreadGroup group = Thread.currentThread().getThreadGroup(); 283 synchronized (mPerGroupInfo) { 284 if (mPerGroupInfo.get(group) == null) { 285 mPerGroupInfo.put(group, new InternalInvocationTracking()); 286 } 287 mPerGroupInfo.get(group).mIsRunIsolated = isolation; 288 } 289 } 290 291 /** 292 * Create a failure associated with the invocation action in progress. Convenience utility to 293 * avoid calling {@link FailureDescription#setActionInProgress(ActionInProgress)}. 294 */ createFailure( String errorMessage, ErrorIdentifier errorIdentifier)295 public static FailureDescription createFailure( 296 String errorMessage, ErrorIdentifier errorIdentifier) { 297 FailureDescription failure = FailureDescription.create(errorMessage); 298 ActionInProgress action = getActionInProgress(); 299 if (action != null) { 300 failure.setActionInProgress(action); 301 } 302 if (errorIdentifier != null) { 303 failure.setErrorIdentifier(errorIdentifier); 304 failure.setFailureStatus(errorIdentifier.status()); 305 } 306 // Automatically populate the origin 307 Class<?> clazz = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass(); 308 failure.setOrigin(clazz.getCanonicalName()); 309 return failure; 310 } 311 getLocal(InvocationLocal<T> local)312 static <T> T getLocal(InvocationLocal<T> local) { 313 ThreadGroup group = Thread.currentThread().getThreadGroup(); 314 Map<InvocationLocal<?>, Optional<?>> locals = 315 mInvocationLocals.computeIfAbsent(group, unused -> new ConcurrentHashMap<>()); 316 317 // Note that ConcurrentHashMap guarantees that the function is atomic and called at-most 318 // once. 319 Optional<?> holder = 320 locals.computeIfAbsent(local, unused -> Optional.ofNullable(local.initialValue())); 321 322 @SuppressWarnings("unchecked") 323 T value = (T) holder.orElse(null); 324 return value; 325 } 326 } 327