1 /*
2  * Copyright (C) 2019 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 android.cts.statsd.subscriber;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import com.android.compatibility.common.util.CpuFeatures;
22 import com.android.internal.os.StatsdConfigProto;
23 import com.android.os.AtomsProto.Atom;
24 import com.android.os.AtomsProto.SystemUptime;
25 import com.android.os.ShellConfig;
26 import com.android.os.statsd.ShellDataProto;
27 import com.android.tradefed.device.CollectingByteOutputReceiver;
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.log.LogUtil;
31 import com.android.tradefed.testtype.DeviceTestCase;
32 import com.google.common.io.Files;
33 import com.google.protobuf.InvalidProtocolBufferException;
34 
35 import java.io.File;
36 import java.nio.ByteBuffer;
37 import java.nio.ByteOrder;
38 import java.util.Arrays;
39 import java.util.concurrent.TimeUnit;
40 
41 /**
42  * Statsd shell data subscription test.
43  */
44 public class ShellSubscriberTest extends DeviceTestCase {
45     private int sizetBytes;
46 
47     @Override
setUp()48     protected void setUp() throws Exception {
49         super.setUp();
50         sizetBytes = getSizetBytes();
51     }
52 
testShellSubscription()53     public void testShellSubscription() {
54         if (sizetBytes < 0) {
55             return;
56         }
57 
58         ShellConfig.ShellSubscription config = createConfig();
59         CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
60         startSubscription(config, receiver, /*maxTimeoutForCommandSec=*/5,
61                 /*subscriptionTimeSec=*/5);
62         checkOutput(receiver);
63     }
64 
testShellSubscriptionReconnect()65     public void testShellSubscriptionReconnect() {
66         if (sizetBytes < 0) {
67             return;
68         }
69 
70         ShellConfig.ShellSubscription config = createConfig();
71         for (int i = 0; i < 5; i++) {
72             CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
73             // A subscription time of -1 means that statsd will not impose a timeout on the
74             // subscription. Thus, the client will exit before statsd ends the subscription.
75             startSubscription(config, receiver, /*maxTimeoutForCommandSec=*/5,
76                     /*subscriptionTimeSec=*/-1);
77             checkOutput(receiver);
78         }
79     }
80 
getSizetBytes()81     private int getSizetBytes() {
82         try {
83             ITestDevice device = getDevice();
84             if (CpuFeatures.isArm64(device)) {
85                 return 8;
86             }
87             if (CpuFeatures.isArm32(device)) {
88                 return 4;
89             }
90             return -1;
91         } catch (DeviceNotAvailableException e) {
92             return -1;
93         }
94     }
95 
96     // Choose a pulled atom that is likely to be supported on all devices (SYSTEM_UPTIME). Testing
97     // pushed atoms is trickier because executeShellCommand() is blocking, so we cannot push a
98     // breadcrumb event while the shell subscription is running.
createConfig()99     private ShellConfig.ShellSubscription createConfig() {
100         return ShellConfig.ShellSubscription.newBuilder()
101                 .addPulled(ShellConfig.PulledAtomSubscription.newBuilder()
102                         .setMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
103                                 .setAtomId(Atom.SYSTEM_UPTIME_FIELD_NUMBER))
104                         .setFreqMillis(2000))
105                 .build();
106     }
107 
108     /**
109      * @param maxTimeoutForCommandSec maximum time imposed by adb that the command will run
110      * @param subscriptionTimeSec maximum time imposed by statsd that the subscription will last
111      */
startSubscription( ShellConfig.ShellSubscription config, CollectingByteOutputReceiver receiver, int maxTimeoutForCommandSec, int subscriptionTimeSec)112     private void startSubscription(
113             ShellConfig.ShellSubscription config,
114             CollectingByteOutputReceiver receiver,
115             int maxTimeoutForCommandSec,
116             int subscriptionTimeSec) {
117         LogUtil.CLog.d("Uploading the following config:\n" + config.toString());
118         try {
119             File configFile = File.createTempFile("shellconfig", ".config");
120             configFile.deleteOnExit();
121             int length = config.toByteArray().length;
122             byte[] combined = new byte[sizetBytes + config.toByteArray().length];
123 
124             System.arraycopy(IntToByteArrayLittleEndian(length), 0, combined, 0, sizetBytes);
125             System.arraycopy(config.toByteArray(), 0, combined, sizetBytes, length);
126 
127             Files.write(combined, configFile);
128             String remotePath = "/data/local/tmp/" + configFile.getName();
129             getDevice().pushFile(configFile, remotePath);
130             LogUtil.CLog.d("waiting....................");
131 
132             String cmd = String.join(" ", "cat", remotePath, "|", "cmd stats data-subscribe",
133                   String.valueOf(subscriptionTimeSec));
134 
135 
136             getDevice().executeShellCommand(cmd, receiver, maxTimeoutForCommandSec,
137                     /*maxTimeToOutputShellResponse=*/maxTimeoutForCommandSec, TimeUnit.SECONDS,
138                     /*retryAttempts=*/0);
139             getDevice().executeShellCommand("rm " + remotePath);
140         } catch (Exception e) {
141             fail(e.getMessage());
142         }
143     }
144 
IntToByteArrayLittleEndian(int length)145     private byte[] IntToByteArrayLittleEndian(int length) {
146         ByteBuffer b = ByteBuffer.allocate(sizetBytes);
147         b.order(ByteOrder.LITTLE_ENDIAN);
148         b.putInt(length);
149         return b.array();
150     }
151 
152     // We do not know how much data will be returned, but we can check the data format.
checkOutput(CollectingByteOutputReceiver receiver)153     private void checkOutput(CollectingByteOutputReceiver receiver) {
154         int atomCount = 0;
155         int startIndex = 0;
156 
157         byte[] output = receiver.getOutput();
158         assertThat(output.length).isGreaterThan(0);
159         while (output.length > startIndex) {
160             assertThat(output.length).isAtLeast(startIndex + sizetBytes);
161             int dataLength = readSizetFromByteArray(output, startIndex);
162             if (dataLength == 0) {
163                 // We have received a heartbeat from statsd. This heartbeat isn't accompanied by any
164                 // atoms so return to top of while loop.
165                 startIndex += sizetBytes;
166                 continue;
167             }
168             assertThat(output.length).isAtLeast(startIndex + sizetBytes + dataLength);
169 
170             ShellDataProto.ShellData data = null;
171             try {
172                 int dataStart = startIndex + sizetBytes;
173                 int dataEnd = dataStart + dataLength;
174                 data = ShellDataProto.ShellData.parseFrom(
175                         Arrays.copyOfRange(output, dataStart, dataEnd));
176             } catch (InvalidProtocolBufferException e) {
177                 fail("Failed to parse proto");
178             }
179 
180             assertThat(data.getAtomCount()).isEqualTo(1);
181             assertThat(data.getAtom(0).hasSystemUptime()).isTrue();
182             assertThat(data.getAtom(0).getSystemUptime().getUptimeMillis()).isGreaterThan(0L);
183             atomCount++;
184             startIndex += sizetBytes + dataLength;
185         }
186         assertThat(atomCount).isGreaterThan(0);
187     }
188 
189     // Converts the bytes in range [startIndex, startIndex + sizetBytes) from a little-endian array
190     // into an integer. Even though sizetBytes could be greater than 4, we assume that the result
191     // will fit within an int.
readSizetFromByteArray(byte[] arr, int startIndex)192     private int readSizetFromByteArray(byte[] arr, int startIndex) {
193         int value = 0;
194         for (int j = 0; j < sizetBytes; j++) {
195             value += ((int) arr[j + startIndex] & 0xffL) << (8 * j);
196         }
197         return value;
198     }
199 }
200