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