blob: e4cc2ff6a6c7bbb53ca8cbfa6bfbea73b9deede8 [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 com.android.testutils.host
import com.android.tests.util.ModuleTestUtils
import com.android.tradefed.config.Option
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
import com.android.tradefed.testtype.junit4.DeviceTestRunOptions
import com.android.tradefed.util.AaptParser
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.fail
private data class TestFailure(val description: String, val stacktrace: String)
/**
* Base class for host-driven tests to deflake a test package.
*
* <p>Classes implementing this base class must define a test APK to be run, and default run
* count, timeout and test classes. In manual runs, the run count, timeout and test classes can be
* overridden via command-line parameters, such as:
*
* <pre>
* atest TestName -- \
* --test-arg com.android.tradefed.testtype.HostTest:set-option:deflake_run_count:10 \
* --test-arg com.android.tradefed.testtype.HostTest:set-option:deflake_single_run_timeout:10s \
* --test-arg \
* com.android.tradefed.testtype.HostTest:set-option:deflake_test:one.test.Class \
* --test-arg \
* com.android.tradefed.testtype.HostTest:set-option:deflake_test:another.test.Class
* </pre>
*/
@RunWith(DeviceJUnit4ClassRunner::class)
abstract class DeflakeHostTestBase : BaseHostJUnit4Test() {
/**
* Number of times the device test will be run.
*/
protected abstract val runCount: Int
@Option(name = "deflake_run_count",
description = "How many times to run each test case.",
importance = Option.Importance.ALWAYS)
private var mRunCountOption: Int? = null
/**
* Filename of the APK to run as part of the test.
*
* <p>Typically the java_test_host build rule will have a 'data: [":DeviceTest"]' dependency
* on the build rule for the device tests. In that case the filename will be "DeviceTest.apk".
*/
protected abstract val testApkFilename: String
/**
* Timeout for each run of the test, in milliseconds. The host-driven test will fail if any run
* takes more than the specified timeout.
*/
protected open val singleRunTimeoutMs = 5 * 60_000L
@Option(name = "deflake_single_run_timeout",
description = "Timeout for each single run.",
importance = Option.Importance.ALWAYS,
isTimeVal = true)
private var mSingleRunTimeoutMsOption: Long? = null
/**
* List of classes to run in the test package. If empty, all classes in the package will be run.
*/
protected open val testClasses: List<String> = emptyList()
// TODO: also support single methods, not just whole classes
@Option(name = "deflake_test",
description = "Test class to deflake. Can be repeated. " +
"Default classes configured for the test are run if omitted.",
importance = Option.Importance.ALWAYS)
private var mTestClassesOption: ArrayList<String?> = ArrayList()
@Before
fun setUp() {
// APK will be auto-cleaned
installPackage(testApkFilename)
}
@Test
fun testDeflake() {
val apkFile = ModuleTestUtils(this).getTestFile(testApkFilename)
val pkgName = AaptParser.parse(apkFile)?.packageName
?: fail("Could not parse test package name")
val classes = mTestClassesOption.filterNotNull().ifEmpty { testClasses }
.ifEmpty { listOf(null) } // null class name runs all classes in the package
val runOptions = DeviceTestRunOptions(pkgName)
.setDevice(device)
.setTestTimeoutMs(mSingleRunTimeoutMsOption ?: singleRunTimeoutMs)
.setCheckResults(false)
// Pair is (test identifier, last stacktrace)
val failures = ArrayList<TestFailure>()
val count = mRunCountOption ?: runCount
repeat(count) {
classes.forEach { testClass ->
runDeviceTests(runOptions.setTestClassName(testClass))
failures += getLastRunFailures()
}
}
if (failures.isEmpty()) return
val failuresByTest = failures.groupBy(TestFailure::description)
val failMessage = failuresByTest.toList().fold("") { msg, (testDescription, failures) ->
val stacktraces = formatStacktraces(failures)
msg + "\n$testDescription: ${failures.count()}/$count failures. " +
"Stacktraces:\n$stacktraces"
}
fail("Some tests failed:$failMessage")
}
private fun getLastRunFailures(): List<TestFailure> {
with(lastDeviceRunResults) {
if (isRunFailure) {
return listOf(TestFailure("All tests in run", runFailureMessage))
}
return failedTests.map {
val stackTrace = testResults[it]?.stackTrace
?: fail("Missing stacktrace for failed test $it")
TestFailure(it.toString(), stackTrace)
}
}
}
private fun formatStacktraces(failures: List<TestFailure>): String {
// Calculate list of (stacktrace, frequency) pairs ordered from most to least frequent
val frequencies = failures.groupingBy(TestFailure::stacktrace).eachCount().toList()
.sortedByDescending { it.second }
// Print each stacktrace with its frequency
return frequencies.fold("") { msg, (stacktrace, numFailures) ->
"$msg\n$numFailures failures:\n$stacktrace"
}
}
}