1 /*
2  * Copyright (C) 2016 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 
17 package com.android.server.telecom.tests;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertTrue;
24 
25 import android.telecom.Logging.Session;
26 import android.telecom.Logging.SessionManager;
27 
28 import androidx.test.filters.SmallTest;
29 
30 import org.junit.After;
31 import org.junit.Before;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.junit.runners.JUnit4;
35 
36 import java.lang.ref.WeakReference;
37 
38 /**
39  * Unit tests for android.telecom.Logging.SessionManager
40  */
41 
42 @RunWith(JUnit4.class)
43 public class SessionManagerTest extends TelecomTestCase {
44 
45     private static final String TEST_PARENT_NAME = "testParent";
46     private static final int TEST_PARENT_THREAD_ID = 0;
47     private static final String TEST_CHILD_NAME = "testChild";
48     private static final int TEST_CHILD_THREAD_ID = 1;
49     private static final int TEST_DELAY_TIME = 100; // ms
50 
51     private SessionManager mTestSessionManager;
52     // Used to verify sessionComplete callback
53     private long mfullSessionCompleteTime = Session.UNDEFINED;
54     private String mFullSessionMethodName = "";
55 
56     @Override
57     @Before
setUp()58     public void setUp() throws Exception {
59         super.setUp();
60         mTestSessionManager = new SessionManager();
61         mTestSessionManager.registerSessionListener(((sessionName, timeMs) -> {
62             mfullSessionCompleteTime = timeMs;
63             mFullSessionMethodName = sessionName;
64         }));
65         // Remove automatic stale session cleanup for testing
66         mTestSessionManager.mCleanStaleSessions = null;
67     }
68 
69     @Override
70     @After
tearDown()71     public void tearDown() throws Exception {
72         mFullSessionMethodName = "";
73         mfullSessionCompleteTime = Session.UNDEFINED;
74         mTestSessionManager = null;
75         super.tearDown();
76     }
77 
78     /**
79      * Starts a Session on the current thread and verifies that it exists in the HashMap
80      */
81     @SmallTest
82     @Test
testStartSession()83     public void testStartSession() {
84         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
85 
86         // Set the thread Id to 0
87         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
88         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
89 
90         Session testSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
91         assertEquals(TEST_PARENT_NAME, testSession.getShortMethodName());
92         assertFalse(testSession.isSessionCompleted());
93         assertFalse(testSession.isStartedFromActiveSession());
94     }
95 
96     /**
97      * Starts two sessions in the same thread. The first session will be parented to the second
98      * session and the second session will be attached to that thread ID.
99      */
100     @SmallTest
101     @Test
testStartInvisibleChildSession()102     public void testStartInvisibleChildSession() {
103         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
104 
105         // Set the thread Id to 0 for the parent
106         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
107         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
108         // Create invisible child session - same Thread ID as parent
109         mTestSessionManager.startSession(TEST_CHILD_NAME, null);
110 
111         // There should only be one session in the mapper (the child)
112         assertEquals(1, mTestSessionManager.mSessionMapper.size());
113         Session testChildSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
114         assertEquals( TEST_CHILD_NAME, testChildSession.getShortMethodName());
115         assertTrue(testChildSession.isStartedFromActiveSession());
116         assertNotNull(testChildSession.getParentSession());
117         assertEquals(TEST_PARENT_NAME, testChildSession.getParentSession().getShortMethodName());
118         assertFalse(testChildSession.isSessionCompleted());
119         assertFalse(testChildSession.getParentSession().isSessionCompleted());
120     }
121 
122     /**
123      * End the active Session and verify that it is completed and removed from mSessionMapper.
124      */
125     @SmallTest
126     @Test
testEndSession()127     public void testEndSession() {
128         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
129         // Set the thread Id to 0
130         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
131         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
132         Session testSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
133 
134         assertEquals(1, mTestSessionManager.mSessionMapper.size());
135         try {
136             // Make sure execution time is > 0
137             Thread.sleep(1);
138         } catch (InterruptedException ignored) {}
139         mTestSessionManager.endSession();
140 
141         assertTrue(testSession.isSessionCompleted());
142         assertTrue(testSession.getLocalExecutionTime() > 0);
143         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
144     }
145 
146     /**
147      * Ends an active invisible child session and verifies that the parent session is moved back
148      * into mSessionMapper.
149      */
150     @SmallTest
151     @Test
testEndInvisibleChildSession()152     public void testEndInvisibleChildSession() {
153         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
154         // Set the thread Id to 0 for the parent
155         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
156         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
157         // Create invisible child session - same Thread ID as parent
158         mTestSessionManager.startSession(TEST_CHILD_NAME, null);
159         Session testChildSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
160 
161         mTestSessionManager.endSession();
162 
163         // There should only be one session in the mapper (the parent)
164         assertEquals(1, mTestSessionManager.mSessionMapper.size());
165         Session testParentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
166         assertEquals(TEST_PARENT_NAME, testParentSession.getShortMethodName());
167         assertFalse(testParentSession.isStartedFromActiveSession());
168         assertTrue(testChildSession.isSessionCompleted());
169         assertFalse(testParentSession.isSessionCompleted());
170     }
171 
172     /**
173      * Creates a subsession (child Session) of the current session and prepares it to be continued
174      * in a different thread.
175      */
176     @SmallTest
177     @Test
testCreateSubsession()178     public void testCreateSubsession() {
179         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
180         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
181 
182         Session testSession = mTestSessionManager.createSubsession();
183 
184         assertEquals(1, mTestSessionManager.mSessionMapper.size());
185         Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
186         assertNotNull(testSession.getParentSession());
187         assertEquals(TEST_PARENT_NAME, testSession.getParentSession().getShortMethodName());
188         assertEquals(TEST_PARENT_NAME, parentSession.getShortMethodName());
189         assertTrue(parentSession.getChildSessions().contains(testSession));
190         assertFalse(testSession.isSessionCompleted());
191         assertFalse(testSession.isStartedFromActiveSession());
192         assertTrue(testSession.getChildSessions().isEmpty());
193     }
194 
195     /**
196      * Cancels a subsession that was started before it was continued and verifies that it is
197      * marked as completed and never added to mSessionMapper.
198      */
199     @SmallTest
200     @Test
testCancelSubsession()201     public void testCancelSubsession() {
202         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
203         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
204         Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
205         Session testSession = mTestSessionManager.createSubsession();
206 
207         mTestSessionManager.cancelSubsession(testSession);
208 
209         assertTrue(testSession.isSessionCompleted());
210         assertFalse(parentSession.isSessionCompleted());
211         assertEquals(Session.UNDEFINED, testSession.getLocalExecutionTime());
212         assertNull(testSession.getParentSession());
213     }
214 
215 
216     /**
217      * Continues a subsession in a different thread and verifies that both the new subsession and
218      * its parent are in mSessionMapper.
219      */
220     @SmallTest
221     @Test
testContinueSubsession()222     public void testContinueSubsession() {
223         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
224         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
225         Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
226         Session testSession = mTestSessionManager.createSubsession();
227 
228         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
229         mTestSessionManager.continueSession(testSession, TEST_CHILD_NAME);
230 
231         assertEquals(2, mTestSessionManager.mSessionMapper.size());
232         assertEquals(testSession, mTestSessionManager.mSessionMapper.get(TEST_CHILD_THREAD_ID));
233         assertEquals(parentSession, testSession.getParentSession());
234         assertFalse(parentSession.isStartedFromActiveSession());
235         assertFalse(parentSession.isSessionCompleted());
236         assertFalse(testSession.isSessionCompleted());
237         assertFalse(testSession.isStartedFromActiveSession());
238     }
239 
240     /**
241      * Ends a subsession that exists in a different thread and verifies that it is completed and
242      * no longer exists in mSessionMapper.
243      */
244     @SmallTest
245     @Test
testEndSubsession()246     public void testEndSubsession() {
247         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
248         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
249         Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
250         Session testSession = mTestSessionManager.createSubsession();
251         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
252         mTestSessionManager.continueSession(testSession, TEST_CHILD_NAME);
253 
254         mTestSessionManager.endSession();
255 
256         assertTrue(testSession.isSessionCompleted());
257         assertNull(mTestSessionManager.mSessionMapper.get(TEST_CHILD_THREAD_ID));
258         assertFalse(parentSession.isSessionCompleted());
259         assertEquals(parentSession, mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID));
260     }
261 
262     /**
263      * When there are subsessions in multiple threads, the parent session may end before the
264      * subsessions themselves. When the subsession ends, we need to recursively clean up the parent
265      * sessions that are complete as well and note the completion time of the entire chain.
266      */
267     @SmallTest
268     @Test
testEndSubsessionWithParentComplete()269     public void testEndSubsessionWithParentComplete() {
270         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
271         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
272         Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
273         Session childSession = mTestSessionManager.createSubsession();
274         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
275         mTestSessionManager.continueSession(childSession, TEST_CHILD_NAME);
276         // Switch to the parent session ID and end the session.
277         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
278         mTestSessionManager.endSession();
279         assertTrue(parentSession.isSessionCompleted());
280         assertFalse(childSession.isSessionCompleted());
281 
282         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
283         try {
284             Thread.sleep(TEST_DELAY_TIME);
285         } catch (InterruptedException ignored) {}
286         mTestSessionManager.endSession();
287 
288         assertEquals(0, mTestSessionManager.mSessionMapper.size());
289         assertTrue(parentSession.getChildSessions().isEmpty());
290         assertNull(childSession.getParentSession());
291         assertTrue(childSession.isSessionCompleted());
292         assertEquals(TEST_PARENT_NAME, mFullSessionMethodName);
293         // Reduce flakiness by assuming that the true completion time is within a threshold of
294         // +-50 ms
295         assertTrue(mfullSessionCompleteTime >= TEST_DELAY_TIME / 2);
296         assertTrue(mfullSessionCompleteTime <= TEST_DELAY_TIME * 1.5);
297     }
298 
299     /**
300      * Tests that starting an external session packages up the parent session information and
301      * correctly generates the child session.
302      */
303     @SmallTest
304     @Test
testStartExternalSession()305     public void testStartExternalSession() {
306         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
307         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
308         Session.Info sessionInfo =
309                 mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID).getInfo();
310         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
311 
312         mTestSessionManager.startExternalSession(sessionInfo, TEST_CHILD_NAME);
313 
314         Session externalSession = mTestSessionManager.mSessionMapper.get(TEST_CHILD_THREAD_ID);
315         assertNotNull(externalSession);
316         assertFalse(externalSession.isSessionCompleted());
317         assertEquals(TEST_CHILD_NAME, externalSession.getShortMethodName());
318         // First subsession of the parent external Session, so the session will be _0.
319         assertEquals("0", externalSession.getSessionId());
320     }
321 
322     /**
323      * Verifies that ending an external session tears down the session correctly and removes the
324      * external session from mSessionMapper.
325      */
326     @SmallTest
327     @Test
testEndExternalSession()328     public void testEndExternalSession() {
329         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
330         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
331         Session.Info sessionInfo =
332                 mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID).getInfo();
333         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
334         mTestSessionManager.startExternalSession(sessionInfo, TEST_CHILD_NAME);
335         Session externalSession = mTestSessionManager.mSessionMapper.get(TEST_CHILD_THREAD_ID);
336 
337         try {
338             // Make sure execution time is > 0
339             Thread.sleep(1);
340         } catch (InterruptedException ignored) {}
341         mTestSessionManager.endSession();
342 
343         assertTrue(externalSession.isSessionCompleted());
344         assertTrue(externalSession.getLocalExecutionTime() > 0);
345         assertNull(mTestSessionManager.mSessionMapper.get(TEST_CHILD_THREAD_ID));
346     }
347 
348     /**
349      * Verifies that the callback to inform that the top level parent Session has completed is not
350      * the external Session, but the one subsession underneath.
351      */
352     @SmallTest
353     @Test
testEndExternalSessionListenerCallback()354     public void testEndExternalSessionListenerCallback() {
355         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
356         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
357         Session.Info sessionInfo =
358                 mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID).getInfo();
359         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
360         mTestSessionManager.startExternalSession(sessionInfo, TEST_CHILD_NAME);
361 
362         try {
363             // Make sure execution time is recorded correctly
364             Thread.sleep(TEST_DELAY_TIME);
365         } catch (InterruptedException ignored) {}
366         mTestSessionManager.endSession();
367 
368         assertEquals(TEST_CHILD_NAME, mFullSessionMethodName);
369         assertTrue(mfullSessionCompleteTime >= TEST_DELAY_TIME / 2);
370         assertTrue(mfullSessionCompleteTime <= TEST_DELAY_TIME * 1.5);
371     }
372 
373     /**
374      * Verifies that the recursive method for getting the full ID works correctly.
375      */
376     @SmallTest
377     @Test
testFullMethodPath()378     public void testFullMethodPath() {
379         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
380         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
381         Session testSession = mTestSessionManager.createSubsession();
382         mTestSessionManager.mCurrentThreadId = () -> TEST_CHILD_THREAD_ID;
383         mTestSessionManager.continueSession(testSession, TEST_CHILD_NAME);
384 
385         String fullId = mTestSessionManager.getSessionId();
386 
387         assertTrue(fullId.contains(TEST_PARENT_NAME + Session.SUBSESSION_SEPARATION_CHAR
388                 + TEST_CHILD_NAME));
389     }
390 
391     /**
392      * Make sure that the cleanup timer runs correctly and the GC collects the stale sessions
393      * correctly to ensure that there are no dangling sessions.
394      */
395     @SmallTest
396     @Test
testStaleSessionCleanupTimer()397     public void testStaleSessionCleanupTimer() {
398         mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
399         mTestSessionManager.startSession(TEST_PARENT_NAME, null);
400         WeakReference<Session> sessionRef = new WeakReference<>(
401                 mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID));
402         try {
403             // Make sure that the sleep time is always > delay time.
404             Thread.sleep(2 * TEST_DELAY_TIME);
405             mTestSessionManager.cleanupStaleSessions(TEST_DELAY_TIME);
406             Runtime.getRuntime().gc();
407             // Give it a second for GC to run.
408             Thread.sleep(1000);
409         } catch (InterruptedException ignored) {}
410 
411         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
412         assertNull(sessionRef.get());
413     }
414 }
415