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     }
74 
75     /**
76      * Track info per ThreadGroup as a proxy to invocation since an invocation run within one
77      * threadgroup.
78      */
79     private static final Map<ThreadGroup, InternalInvocationTracking> mPerGroupInfo =
80             new ConcurrentHashMap<ThreadGroup, CurrentInvocation.InternalInvocationTracking>();
81 
82     private static final Map<ThreadGroup, Map<InvocationLocal<?>, Optional<?>>> mInvocationLocals =
83             new ConcurrentHashMap<>();
84 
85     private static ThreadLocal<ThreadGroup> sLocal = new ThreadLocal<>();
86 
87     /** Tracks a localized context when using the properties inside the gRPC server */
setLocalGroup(ThreadGroup tg)88     public static void setLocalGroup(ThreadGroup tg) {
89         sLocal.set(tg);
90     }
91 
92     /** Resets the localized context. */
resetLocalGroup()93     public static void resetLocalGroup() {
94         sLocal.remove();
95     }
96 
97     /**
98      * Add one key-value to be tracked at the invocation level.
99      *
100      * @param key The key under which the invocation info will be tracked.
101      * @param value The value of the invocation metric.
102      */
addInvocationInfo(InvocationInfo key, File value)103     public static void addInvocationInfo(InvocationInfo key, File value) {
104         ThreadGroup group = Thread.currentThread().getThreadGroup();
105         synchronized (mPerGroupInfo) {
106             if (mPerGroupInfo.get(group) == null) {
107                 mPerGroupInfo.put(group, new InternalInvocationTracking());
108             }
109             mPerGroupInfo.get(group).mInvocationInfoFiles.put(key, value);
110         }
111     }
112 
113     /** Returns the Map of invocation metrics for the invocation in progress. */
getInfo(InvocationInfo key)114     public static File getInfo(InvocationInfo key) {
115         ThreadGroup group = Thread.currentThread().getThreadGroup();
116         synchronized (mPerGroupInfo) {
117             if (sLocal.get() != null) {
118                 group = sLocal.get();
119             }
120             if (mPerGroupInfo.get(group) == null) {
121                 mPerGroupInfo.put(group, new InternalInvocationTracking());
122             }
123             return mPerGroupInfo.get(group).mInvocationInfoFiles.get(key);
124         }
125     }
126 
127     /** Returns the current work folder for the invocation or null if none set yet. */
getWorkFolder()128     public static File getWorkFolder() {
129         File workfolder = getInfo(InvocationInfo.WORK_FOLDER);
130         if (workfolder == null || !workfolder.exists()) {
131             return null;
132         }
133         return workfolder;
134     }
135 
136     /** Clear the invocation info for an invocation. */
clearInvocationInfos()137     public static void clearInvocationInfos() {
138         ThreadGroup group = Thread.currentThread().getThreadGroup();
139         synchronized (mPerGroupInfo) {
140             mPerGroupInfo.remove(group);
141         }
142         mInvocationLocals.remove(group);
143     }
144 
145     /**
146      * One-time registration of the {@link ExecutionFiles}. This is done by the Test Harness.
147      *
148      * @param invocFiles The registered {@link ExecutionFiles}.
149      */
registerExecutionFiles(ExecutionFiles invocFiles)150     public static void registerExecutionFiles(ExecutionFiles invocFiles) {
151         ThreadGroup group = Thread.currentThread().getThreadGroup();
152         synchronized (mPerGroupInfo) {
153             if (mPerGroupInfo.get(group) == null) {
154                 mPerGroupInfo.put(group, new InternalInvocationTracking());
155             }
156             if (mPerGroupInfo.get(group).mExecutionFiles == null) {
157                 mPerGroupInfo.get(group).mExecutionFiles = invocFiles;
158             } else {
159                 CLog.w(
160                         "CurrentInvocation#registerExecutionFiles should only be called once "
161                                 + "per invocation.");
162             }
163         }
164     }
165 
166     /** Returns the {@link ExecutionFiles} for the invocation. */
getInvocationFiles()167     public static ExecutionFiles getInvocationFiles() {
168         ThreadGroup group = Thread.currentThread().getThreadGroup();
169         synchronized (mPerGroupInfo) {
170             if (mPerGroupInfo.get(group) == null) {
171                 mPerGroupInfo.put(group, new InternalInvocationTracking());
172             }
173             return mPerGroupInfo.get(group).mExecutionFiles;
174         }
175     }
176 
177     /** Sets the {@link ActionInProgress} for the invocation. */
setActionInProgress(ActionInProgress action)178     public static void setActionInProgress(ActionInProgress action) {
179         ThreadGroup group = Thread.currentThread().getThreadGroup();
180         synchronized (mPerGroupInfo) {
181             if (mPerGroupInfo.get(group) == null) {
182                 mPerGroupInfo.put(group, new InternalInvocationTracking());
183             }
184             mPerGroupInfo.get(group).mActionInProgress = action;
185         }
186     }
187 
188     /** Returns the current {@link ActionInProgress} for the invocation. Can be null. */
getActionInProgress()189     public static @Nullable ActionInProgress getActionInProgress() {
190         ThreadGroup group = Thread.currentThread().getThreadGroup();
191         synchronized (mPerGroupInfo) {
192             if (mPerGroupInfo.get(group) == null) {
193                 return null;
194             }
195             return mPerGroupInfo.get(group).mActionInProgress;
196         }
197     }
198 
199     /** Sets the {@link IInvocationContext} for the invocation. */
setInvocationContext(IInvocationContext context)200     public static void setInvocationContext(IInvocationContext context) {
201         ThreadGroup group = Thread.currentThread().getThreadGroup();
202         synchronized (mPerGroupInfo) {
203             if (mPerGroupInfo.get(group) == null) {
204                 mPerGroupInfo.put(group, new InternalInvocationTracking());
205             }
206             mPerGroupInfo.get(group).mContext = context;
207         }
208     }
209 
210     /** Returns the current {@link IInvocationContext} for the invocation. Can be null. */
getInvocationContext()211     public static @Nullable IInvocationContext getInvocationContext() {
212         ThreadGroup group = Thread.currentThread().getThreadGroup();
213         synchronized (mPerGroupInfo) {
214             if (mPerGroupInfo.get(group) == null) {
215                 return null;
216             }
217             return mPerGroupInfo.get(group).mContext;
218         }
219     }
220 
221     /** Returns whether the current suite module executed was isolated or not. */
moduleCurrentIsolation()222     public static IsolationGrade moduleCurrentIsolation() {
223         ThreadGroup group = Thread.currentThread().getThreadGroup();
224         synchronized (mPerGroupInfo) {
225             if (mPerGroupInfo.get(group) == null) {
226                 return IsolationGrade.NOT_ISOLATED;
227             }
228             return mPerGroupInfo.get(group).mIsModuleIsolated;
229         }
230     }
231 
232     /** Update whether the suite module is isolated or not. */
setModuleIsolation(IsolationGrade isolation)233     public static void setModuleIsolation(IsolationGrade isolation) {
234         ThreadGroup group = Thread.currentThread().getThreadGroup();
235         synchronized (mPerGroupInfo) {
236             if (mPerGroupInfo.get(group) == null) {
237                 mPerGroupInfo.put(group, new InternalInvocationTracking());
238             }
239             mPerGroupInfo.get(group).mIsModuleIsolated = isolation;
240         }
241     }
242 
243     /** Returns whether the current test run executed was isolated or not. */
runCurrentIsolation()244     public static IsolationGrade runCurrentIsolation() {
245         ThreadGroup group = Thread.currentThread().getThreadGroup();
246         synchronized (mPerGroupInfo) {
247             if (mPerGroupInfo.get(group) == null) {
248                 return IsolationGrade.NOT_ISOLATED;
249             }
250             return mPerGroupInfo.get(group).mIsRunIsolated;
251         }
252     }
253 
254     /** Update whether the test run is isolated or not. */
setRunIsolation(IsolationGrade isolation)255     public static void setRunIsolation(IsolationGrade isolation) {
256         ThreadGroup group = Thread.currentThread().getThreadGroup();
257         synchronized (mPerGroupInfo) {
258             if (mPerGroupInfo.get(group) == null) {
259                 mPerGroupInfo.put(group, new InternalInvocationTracking());
260             }
261             mPerGroupInfo.get(group).mIsRunIsolated = isolation;
262         }
263     }
264 
265     /**
266      * Create a failure associated with the invocation action in progress. Convenience utility to
267      * avoid calling {@link FailureDescription#setActionInProgress(ActionInProgress)}.
268      */
createFailure( String errorMessage, ErrorIdentifier errorIdentifier)269     public static FailureDescription createFailure(
270             String errorMessage, ErrorIdentifier errorIdentifier) {
271         FailureDescription failure = FailureDescription.create(errorMessage);
272         ActionInProgress action = getActionInProgress();
273         if (action != null) {
274             failure.setActionInProgress(action);
275         }
276         if (errorIdentifier != null) {
277             failure.setErrorIdentifier(errorIdentifier);
278             failure.setFailureStatus(errorIdentifier.status());
279         }
280         // Automatically populate the origin
281         Class<?> clazz = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass();
282         failure.setOrigin(clazz.getCanonicalName());
283         return failure;
284     }
285 
getLocal(InvocationLocal<T> local)286     static <T> T getLocal(InvocationLocal<T> local) {
287         ThreadGroup group = Thread.currentThread().getThreadGroup();
288         Map<InvocationLocal<?>, Optional<?>> locals =
289                 mInvocationLocals.computeIfAbsent(group, unused -> new ConcurrentHashMap<>());
290 
291         // Note that ConcurrentHashMap guarantees that the function is atomic and called at-most
292         // once.
293         Optional<?> holder =
294                 locals.computeIfAbsent(local, unused -> Optional.ofNullable(local.initialValue()));
295 
296         @SuppressWarnings("unchecked")
297         T value = (T) holder.orElse(null);
298         return value;
299     }
300 }
301