blob: f24ffba54bdeeb9f136b4c724fbfc29dc5e42e55 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test.services;
import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.SINGLE_PROCESS;
import android.os.ParcelFileDescriptor;
import androidx.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.common.PlatformServiceBridge;
import org.chromium.android_webview.common.crash.SystemWideCrashDirectories;
import org.chromium.android_webview.services.AwMinidumpUploaderDelegate;
import org.chromium.android_webview.services.AwMinidumpUploaderDelegate.SamplingDelegate;
import org.chromium.android_webview.services.CrashReceiverService;
import org.chromium.android_webview.test.AwJUnit4ClassRunner;
import org.chromium.android_webview.test.OnlyRunIn;
import org.chromium.base.Callback;
import org.chromium.base.FileUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.minidump_uploader.CrashTestRule;
import org.chromium.components.minidump_uploader.CrashTestRule.MockCrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.MinidumpUploadJob;
import org.chromium.components.minidump_uploader.MinidumpUploadJobImpl;
import org.chromium.components.minidump_uploader.MinidumpUploadTestUtility;
import org.chromium.components.minidump_uploader.MinidumpUploaderDelegate;
import org.chromium.components.minidump_uploader.TestMinidumpUploadJobImpl;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import org.chromium.components.version_info.Channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Instrumentation tests for WebView's implementation of MinidumpUploaderDelegate, and the
* interoperability of WebView's minidump-copying and minidump-uploading logic.
*
* These tests loads native library and mark the process as a browser process, it's safer to
* leave them unbatched to avoid possible state leaking between tests.
*/
@RunWith(AwJUnit4ClassRunner.class)
@OnlyRunIn(SINGLE_PROCESS)
public class MinidumpUploadJobTest {
@Rule
public CrashTestRule mTestRule = new CrashTestRule() {
@Override
public File getExistingCacheDir() {
return SystemWideCrashDirectories.getOrCreateWebViewCrashDir();
}
};
private static final String BOUNDARY = "TESTBOUNDARY";
private static class TestPlatformServiceBridge extends PlatformServiceBridge {
private final boolean mEnabled;
public TestPlatformServiceBridge(boolean enabled) {
mEnabled = enabled;
}
@Override
public boolean canUseGms() {
return true;
}
@Override
public void queryMetricsSetting(Callback<Boolean> callback) {
ThreadUtils.assertOnUiThread();
callback.onResult(mEnabled);
}
}
private static class TestSamplingDelegate implements SamplingDelegate {
private final int mChannel;
private final int mRandomSampling;
TestSamplingDelegate(int channel, int randomSampling) {
mChannel = channel;
mRandomSampling = randomSampling;
}
@Override
public int getChannel() {
return mChannel;
}
@Override
public int getRandomSample() {
return mRandomSampling;
}
}
// randomSampl < CRASH_DUMP_PERCENTAGE_FOR_STABLE to always sample-in crashes.
private static final SamplingDelegate TEST_SAMPLING_DELEGATE =
new TestSamplingDelegate(Channel.UNKNOWN, 0);
/**
* Ensure MinidumpUploadJobImpl doesn't crash even if the WebView Crash dir doesn't exist (could
* happen e.g. if a Job persists across WebView-updates?
*
* MinidumpUploadJobImpl should automatically recreate the directory.
*/
@Test
@MediumTest
public void testUploadingWithoutCrashDir() {
File webviewCrashDir = mTestRule.getExistingCacheDir();
// Delete the WebView crash directory to ensure MinidumpUploadJob doesn't crash without it.
FileUtils.recursivelyDeleteFile(webviewCrashDir, FileUtils.DELETE_ALL);
Assert.assertFalse(webviewCrashDir.exists());
PlatformServiceBridge.injectInstance(new TestPlatformServiceBridge(true));
final CrashReportingPermissionManager permManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
MinidumpUploadJob minidumpUploadJob =
// Use AwMinidumpUploaderDelegate instead of TestMinidumpUploaderDelegate here
// since AwMinidumpUploaderDelegate defines the WebView crash directory.
new TestMinidumpUploadJobImpl(new AwMinidumpUploaderDelegate(
TEST_SAMPLING_DELEGATE) {
@Override
public CrashReportingPermissionManager createCrashReportingPermissionManager() {
return permManager;
}
});
// Ensure that we don't crash when trying to upload minidumps without a crash directory.
MinidumpUploadTestUtility.uploadMinidumpsSync(
minidumpUploadJob, false /* expectReschedule */);
}
/**
* Ensures that the minidump copying works together with the minidump uploading.
*/
@Test
@MediumTest
public void testCopyAndUploadWebViewMinidump() throws IOException {
final CrashFileManager fileManager =
new CrashFileManager(SystemWideCrashDirectories.getWebViewCrashDir());
// Note that these minidump files are set up directly in the cache dir - not in the WebView
// crash dir. This is to ensure the CrashFileManager doesn't see these minidumps without us
// first copying them.
File minidumpToCopy = new File(mTestRule.getExistingCacheDir(), "toCopy.dmp.try0");
CrashTestRule.setUpMinidumpFile(minidumpToCopy, BOUNDARY, "browser");
final String expectedFileContent = readEntireFile(minidumpToCopy);
File[] uploadedFiles = copyAndUploadMinidumpsSync(
fileManager, new File[][] {{minidumpToCopy}}, new int[] {0});
// CrashReceiverService will rename the minidumps to some globally unique file name
// meaning that we have to check the contents of the minidump rather than the file
// name.
try {
Assert.assertEquals(expectedFileContent, readEntireFile(uploadedFiles[0]));
} catch (IOException e) {
throw new RuntimeException(e);
}
File webviewTmpDir = SystemWideCrashDirectories.getWebViewTmpCrashDir();
Assert.assertEquals(0, webviewTmpDir.listFiles().length);
}
/**
* Ensure that crash files are sampled-out for STABLE channel.
*/
@Test
@MediumTest
public void testSampledOutCrashesForStableChannel() throws IOException {
// samplingPercentage >= CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes are never sampled-in.
testSampleCrashesByChannel(Channel.STABLE, 1, false);
}
/**
* Ensure that crash files are sampled-in for STABLE channel.
*/
@Test
@MediumTest
public void testSampledInCrashesForStableChannel() throws IOException {
// samplingPercentage < CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes are always sampled-in.
testSampleCrashesByChannel(Channel.STABLE, 0, true);
}
/**
* Ensure that crash files are sampled-out for STABLE channel.
*/
@Test
@MediumTest
public void testSampledOutCrashesForDefaultChannel() throws IOException {
// samplingPercentage >= CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes are never sampled-in.
testSampleCrashesByChannel(Channel.DEFAULT, 1, false);
}
/**
* Ensure that crash files are sampled-in for STABLE channel.
*/
@Test
@MediumTest
public void testSampledInCrashesForDefaultChannel() throws IOException {
// samplingPercentage < CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes are always sampled-in.
testSampleCrashesByChannel(Channel.DEFAULT, 0, true);
}
/**
* Ensure that crash files are sampled-out for UNKNOWN channel.
*/
@Test
@MediumTest
public void testSampledOutCrashesForUnknownChannel() throws IOException {
testSampleCrashesByChannel(Channel.UNKNOWN, 1, false);
}
/**
* Ensure that crash files are sampled-in for BETA channel.
*/
@Test
@MediumTest
public void testSampledInCrashesForBetaChannel() throws IOException {
// samplingPercentage >= CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes should never be
// sampled-in.
testSampleCrashesByChannel(Channel.BETA, 1, true);
}
/**
* Ensure that crash files are sampled-in for CANARY channel.
*/
@Test
@MediumTest
public void testSampledInCrashesForCanaryChannel() throws IOException {
// samplingPercentage >= CRASH_DUMP_PERCENTAGE_FOR_STABLE so crashes should never be
// sampled-in.
testSampleCrashesByChannel(Channel.CANARY, 1, true);
}
/**
* MinidumpUploaderDelegate sub-class that uses MinidumpUploaderDelegate's implementation of
* CrashReportingPermissionManager.isUsageAndCrashReportingPermittedByUser().
*/
private static class TestCrashSamplingMinidumpUploaderDelegate
extends AwMinidumpUploaderDelegate {
private final boolean mIsSampled;
TestCrashSamplingMinidumpUploaderDelegate(
SamplingDelegate samplingDelegate, boolean isSampled) {
super(samplingDelegate);
mIsSampled = isSampled;
}
@Override
public CrashReportingPermissionManager createCrashReportingPermissionManager() {
final CrashReportingPermissionManager realPermissionManager =
super.createCrashReportingPermissionManager();
return new MockCrashReportingPermissionManager() {
{
// This setup ensures we depend on isClientInMetricsSample().
mIsUserPermitted = true;
mIsNetworkAvailable = true;
mIsEnabledForTests = false;
}
@Override
public boolean isClientInMetricsSample() {
// Ensure that we use the real implementation of isClientInMetricsSample.
boolean isSampled = realPermissionManager.isClientInMetricsSample();
Assert.assertEquals(mIsSampled, isSampled);
return isSampled;
}
};
}
}
private void testSampleCrashesByChannel(int channel, int samplePercentage, boolean isSampled)
throws IOException {
PlatformServiceBridge.injectInstance(new TestPlatformServiceBridge(/* userConsent */ true));
MinidumpUploaderDelegate delegate = new TestCrashSamplingMinidumpUploaderDelegate(
new TestSamplingDelegate(channel, samplePercentage), isSampled);
MinidumpUploadJob minidumpUploadJob = new TestMinidumpUploadJobImpl(delegate);
File firstFile = createMinidumpFileInCrashDir("1_abc.dmp0.try0");
File secondFile = createMinidumpFileInCrashDir("12_abcd.dmp0.try0");
File expectedFirstFile = new File(mTestRule.getCrashDir(),
firstFile.getName().replace(".dmp", isSampled ? ".up" : ".skipped"));
File expectedSecondFile = new File(mTestRule.getCrashDir(),
secondFile.getName().replace(".dmp", isSampled ? ".up" : ".skipped"));
MinidumpUploadTestUtility.uploadMinidumpsSync(
minidumpUploadJob, false /* expectReschedule */);
Assert.assertFalse(firstFile.exists());
Assert.assertTrue(expectedFirstFile.exists());
Assert.assertFalse(secondFile.exists());
Assert.assertTrue(expectedSecondFile.exists());
}
/**
* Ensure that when PlatformServiceBridge returns true we do upload minidumps.
*/
@Test
@MediumTest
public void testPlatformServicesBridgeIsUsedUserConsent() throws IOException {
testPlatformServicesBridgeIsUsed(true);
}
/**
* Ensure that when PlatformServiceBridge returns false we do not upload minidumps.
*/
@Test
@MediumTest
public void testPlatformServicesBridgeIsUsedNoUserConsent() throws IOException {
testPlatformServicesBridgeIsUsed(false);
}
/**
* MinidumpUploaderDelegate sub-class that uses MinidumpUploaderDelegate's implementation of
* CrashReportingPermissionManager.isUsageAndCrashReportingPermittedByUser().
*/
private static class WebViewUserConsentMinidumpUploaderDelegate
extends AwMinidumpUploaderDelegate {
private final boolean mUserConsent;
WebViewUserConsentMinidumpUploaderDelegate(boolean userConsent) {
super(TEST_SAMPLING_DELEGATE);
mUserConsent = userConsent;
}
@Override
public CrashReportingPermissionManager createCrashReportingPermissionManager() {
final CrashReportingPermissionManager realPermissionManager =
super.createCrashReportingPermissionManager();
return new MockCrashReportingPermissionManager() {
{
// This setup ensures we depend on
// isUsageAndCrashReportingPermittedByUser().
mIsInSample = true;
mIsNetworkAvailable = true;
mIsEnabledForTests = false;
}
@Override
public boolean isUsageAndCrashReportingPermittedByUser() {
// Ensure that we use the real implementation of
// isUsageAndCrashReportingPermittedByUser.
boolean userPermitted =
realPermissionManager.isUsageAndCrashReportingPermittedByUser();
Assert.assertEquals(mUserConsent, userPermitted);
return userPermitted;
}
};
}
}
private void testPlatformServicesBridgeIsUsed(final boolean userConsent) throws IOException {
PlatformServiceBridge.injectInstance(new TestPlatformServiceBridge(userConsent));
MinidumpUploaderDelegate delegate =
new WebViewUserConsentMinidumpUploaderDelegate(userConsent);
MinidumpUploadJob minidumpUploadJob = new TestMinidumpUploadJobImpl(delegate);
File firstFile = createMinidumpFileInCrashDir("1_abc.dmp0.try0");
File secondFile = createMinidumpFileInCrashDir("12_abcd.dmp0.try0");
File expectedFirstFile = new File(mTestRule.getCrashDir(),
firstFile.getName().replace(".dmp", userConsent ? ".up" : ".skipped"));
File expectedSecondFile = new File(mTestRule.getCrashDir(),
secondFile.getName().replace(".dmp", userConsent ? ".up" : ".skipped"));
MinidumpUploadTestUtility.uploadMinidumpsSync(
minidumpUploadJob, false /* expectReschedule */);
Assert.assertFalse(firstFile.exists());
Assert.assertTrue(expectedFirstFile.exists());
Assert.assertFalse(secondFile.exists());
Assert.assertTrue(expectedSecondFile.exists());
}
private static String readEntireFile(File file) throws IOException {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
fileInputStream.read(data);
return new String(data);
}
}
/**
* Ensure we can copy and upload several batches of files (i.e. emulate several copying-calls in
* a row without the copying-service being destroyed in between).
*/
@Test
@MediumTest
public void testCopyAndUploadSeveralMinidumpBatches() throws IOException {
final CrashFileManager fileManager =
new CrashFileManager(SystemWideCrashDirectories.getWebViewCrashDir());
// Note that these minidump files are set up directly in the cache dir - not in the WebView
// crash dir. This is to ensure the CrashFileManager doesn't see these minidumps without us
// first copying them.
File firstMinidumpToCopy =
new File(mTestRule.getExistingCacheDir(), "firstToCopy.dmp.try0");
File secondMinidumpToCopy =
new File(mTestRule.getExistingCacheDir(), "secondToCopy.dmp.try0");
CrashTestRule.setUpMinidumpFile(firstMinidumpToCopy, BOUNDARY, "browser");
CrashTestRule.setUpMinidumpFile(secondMinidumpToCopy, BOUNDARY, "renderer");
final String expectedFirstFileContent = readEntireFile(firstMinidumpToCopy);
final String expectedSecondFileContent = readEntireFile(secondMinidumpToCopy);
File[] uploadedFiles = copyAndUploadMinidumpsSync(fileManager,
new File[][] {{firstMinidumpToCopy}, {secondMinidumpToCopy}}, new int[] {0, 0});
// CrashReceiverService will rename the minidumps to some globally unique file name
// meaning that we have to check the contents of the minidump rather than the file
// name.
try {
final String actualFileContent0 = readEntireFile(uploadedFiles[0]);
final String actualFileContent1 = readEntireFile(uploadedFiles[1]);
if (expectedFirstFileContent.equals(actualFileContent0)) {
Assert.assertEquals(expectedSecondFileContent, actualFileContent1);
} else {
Assert.assertEquals(expectedFirstFileContent, actualFileContent1);
Assert.assertEquals(expectedSecondFileContent, actualFileContent0);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Copy and upload {@param minidumps} by one array at a time - i.e. the minidumps in a single
* array in {@param minidumps} will all be copied in the same call into CrashReceiverService.
* @param fileManager the CrashFileManager to use when copying/renaming minidumps.
* @param minidumps an array of arrays of minidumps to copy and upload, by copying one array at
* a time.
* @param uids an array of uids declaring the uids used when calling into CrashReceiverService.
* @return the uploaded files.
*/
private File[] copyAndUploadMinidumpsSync(CrashFileManager fileManager, File[][] minidumps,
int[] uids) throws FileNotFoundException {
CrashReceiverService crashReceiverService = new CrashReceiverService();
Assert.assertEquals(minidumps.length, uids.length);
// Ensure the upload service minidump directory is empty before we start copying files.
File[] initialMinidumps = fileManager.getMinidumpsReadyForUpload(
MinidumpUploadJobImpl.MAX_UPLOAD_TRIES_ALLOWED);
Assert.assertEquals(0, initialMinidumps.length);
// Open file descriptors to the files and then delete the files.
ParcelFileDescriptor[][] fileDescriptors = new ParcelFileDescriptor[minidumps.length][];
int numMinidumps = 0;
for (int n = 0; n < minidumps.length; n++) {
File[] currentMinidumps = minidumps[n];
numMinidumps += currentMinidumps.length;
fileDescriptors[n] = new ParcelFileDescriptor[currentMinidumps.length];
for (int m = 0; m < currentMinidumps.length; m++) {
fileDescriptors[n][m] = ParcelFileDescriptor.open(
currentMinidumps[m], ParcelFileDescriptor.MODE_READ_ONLY);
Assert.assertTrue(currentMinidumps[m].delete());
}
crashReceiverService.performMinidumpCopyingSerially(
uids[n] /* uid */, fileDescriptors[n], null, false /* scheduleUploads */);
}
final CrashReportingPermissionManager permManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
MinidumpUploadJob minidumpUploadJob =
// Use AwMinidumpUploaderDelegate instead of TestMinidumpUploaderDelegate to ensure
// AwMinidumpUploaderDelegate works well together with the minidump-copying methods
// of CrashReceiverService.
new TestMinidumpUploadJobImpl(new AwMinidumpUploaderDelegate(
TEST_SAMPLING_DELEGATE) {
@Override
public CrashReportingPermissionManager createCrashReportingPermissionManager() {
return permManager;
}
});
MinidumpUploadTestUtility.uploadMinidumpsSync(
minidumpUploadJob, false /* expectReschedule */);
// Ensure there are no minidumps left to upload.
File[] nonUploadedMinidumps = fileManager.getMinidumpsReadyForUpload(
MinidumpUploadJobImpl.MAX_UPLOAD_TRIES_ALLOWED);
Assert.assertEquals(0, nonUploadedMinidumps.length);
File[] uploadedFiles = fileManager.getAllUploadedFiles();
Assert.assertEquals(numMinidumps, uploadedFiles.length);
return uploadedFiles;
}
private File createMinidumpFileInCrashDir(String name) throws IOException {
File minidumpFile = new File(mTestRule.getCrashDir(), name);
CrashTestRule.setUpMinidumpFile(minidumpFile, BOUNDARY);
return minidumpFile;
}
}