blob: 31401ca32ff43ba87571bed98971ebfea9aa0334 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.cts.statsd.subscriber;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import com.android.compatibility.common.util.CpuFeatures;
import com.android.internal.os.StatsdConfigProto;
import com.android.os.AtomsProto.Atom;
import com.android.os.AtomsProto.SystemUptime;
import com.android.os.ShellConfig;
import com.android.os.statsd.ShellDataProto;
import com.android.tradefed.device.CollectingByteOutputReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil;
import com.android.tradefed.testtype.DeviceTestCase;
import com.google.common.io.Files;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* Statsd shell data subscription test.
*/
public class ShellSubscriberTest extends DeviceTestCase {
private int sizetBytes;
@Override
protected void setUp() throws Exception {
super.setUp();
sizetBytes = getSizetBytes();
}
public void testShellSubscription() {
if (sizetBytes < 0) {
return;
}
ShellConfig.ShellSubscription config = createConfig();
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
startSubscription(config, receiver, /*maxTimeoutForCommandSec=*/5,
/*subscriptionTimeSec=*/5);
checkOutput(receiver);
}
public void testShellSubscriptionReconnect() {
if (sizetBytes < 0) {
return;
}
ShellConfig.ShellSubscription config = createConfig();
for (int i = 0; i < 5; i++) {
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
// A subscription time of -1 means that statsd will not impose a timeout on the
// subscription. Thus, the client will exit before statsd ends the subscription.
startSubscription(config, receiver, /*maxTimeoutForCommandSec=*/5,
/*subscriptionTimeSec=*/-1);
checkOutput(receiver);
}
}
private int getSizetBytes() {
try {
ITestDevice device = getDevice();
if (CpuFeatures.isArm64(device)) {
return 8;
}
if (CpuFeatures.isArm32(device)) {
return 4;
}
return -1;
} catch (DeviceNotAvailableException e) {
return -1;
}
}
// Choose a pulled atom that is likely to be supported on all devices (SYSTEM_UPTIME). Testing
// pushed atoms is trickier because executeShellCommand() is blocking, so we cannot push a
// breadcrumb event while the shell subscription is running.
private ShellConfig.ShellSubscription createConfig() {
return ShellConfig.ShellSubscription.newBuilder()
.addPulled(ShellConfig.PulledAtomSubscription.newBuilder()
.setMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
.setAtomId(Atom.SYSTEM_UPTIME_FIELD_NUMBER))
.setFreqMillis(2000))
.build();
}
/**
* @param maxTimeoutForCommandSec maximum time imposed by adb that the command will run
* @param subscriptionTimeSec maximum time imposed by statsd that the subscription will last
*/
private void startSubscription(
ShellConfig.ShellSubscription config,
CollectingByteOutputReceiver receiver,
int maxTimeoutForCommandSec,
int subscriptionTimeSec) {
LogUtil.CLog.d("Uploading the following config:\n" + config.toString());
try {
File configFile = File.createTempFile("shellconfig", ".config");
configFile.deleteOnExit();
int length = config.toByteArray().length;
byte[] combined = new byte[sizetBytes + config.toByteArray().length];
System.arraycopy(IntToByteArrayLittleEndian(length), 0, combined, 0, sizetBytes);
System.arraycopy(config.toByteArray(), 0, combined, sizetBytes, length);
Files.write(combined, configFile);
String remotePath = "/data/local/tmp/" + configFile.getName();
getDevice().pushFile(configFile, remotePath);
LogUtil.CLog.d("waiting....................");
String cmd = String.join(" ", "cat", remotePath, "|", "cmd stats data-subscribe",
String.valueOf(subscriptionTimeSec));
getDevice().executeShellCommand(cmd, receiver, maxTimeoutForCommandSec,
/*maxTimeToOutputShellResponse=*/maxTimeoutForCommandSec, TimeUnit.SECONDS,
/*retryAttempts=*/0);
getDevice().executeShellCommand("rm " + remotePath);
} catch (Exception e) {
fail(e.getMessage());
}
}
private byte[] IntToByteArrayLittleEndian(int length) {
ByteBuffer b = ByteBuffer.allocate(sizetBytes);
b.order(ByteOrder.LITTLE_ENDIAN);
b.putInt(length);
return b.array();
}
// We do not know how much data will be returned, but we can check the data format.
private void checkOutput(CollectingByteOutputReceiver receiver) {
int atomCount = 0;
int startIndex = 0;
byte[] output = receiver.getOutput();
assertThat(output.length).isGreaterThan(0);
while (output.length > startIndex) {
assertThat(output.length).isAtLeast(startIndex + sizetBytes);
int dataLength = readSizetFromByteArray(output, startIndex);
assertThat(output.length).isAtLeast(startIndex + sizetBytes + dataLength);
ShellDataProto.ShellData data = null;
try {
int dataStart = startIndex + sizetBytes;
int dataEnd = dataStart + dataLength;
data = ShellDataProto.ShellData.parseFrom(
Arrays.copyOfRange(output, dataStart, dataEnd));
} catch (InvalidProtocolBufferException e) {
fail("Failed to parse proto");
}
assertThat(data.getAtomCount()).isEqualTo(1);
assertThat(data.getAtom(0).hasSystemUptime()).isTrue();
assertThat(data.getAtom(0).getSystemUptime().getUptimeMillis()).isGreaterThan(0L);
atomCount++;
startIndex += sizetBytes + dataLength;
}
assertThat(atomCount).isGreaterThan(0);
}
// Converts the bytes in range [startIndex, startIndex + sizetBytes) from a little-endian array
// into an integer. Even though sizetBytes could be greater than 4, we assume that the result
// will fit within an int.
private int readSizetFromByteArray(byte[] arr, int startIndex) {
int value = 0;
for (int j = 0; j < sizetBytes; j++) {
value += ((int) arr[j + startIndex] & 0xffL) << (8 * j);
}
return value;
}
}