blob: cc547629dee91046f72b6ee2b99b4bb8688adfbe [file] [log] [blame]
/*
* Copyright (C) 2018 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.tradefed.util.testmapping;
import static com.google.common.base.Preconditions.checkState;
import com.android.tradefed.log.LogUtil.CLog;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/** Stores the test information set in a TEST_MAPPING file. */
public class TestInfo {
private static final String OPTION_INCLUDE_ANNOTATION = "include-annotation";
private static final String OPTION_EXCLUDE_ANNOTATION = "exclude-annotation";
private String mName = null;
private List<TestOption> mOptions = new ArrayList<TestOption>();
// A list of locations with TEST_MAPPING files that containing the test.
private Set<String> mSources = new HashSet<String>();
public TestInfo(String name, String source) {
mName = name;
mSources.add(source);
}
public String getName() {
return mName;
}
public void addOption(TestOption option) {
mOptions.add(option);
Collections.sort(mOptions);
}
public List<TestOption> getOptions() {
return mOptions;
}
public void addSources(Set<String> sources) {
mSources.addAll(sources);
}
public Set<String> getSources() {
return mSources;
}
/**
* Merge with another test.
*
* <p>Update test options so the test has the best possible coverage of both tests.
*
* <p>TODO(b/113616538): Implement a more robust option merging mechanism.
*
* @param test {@link TestInfo} object to be merged with.
*/
public void merge(TestInfo test) {
CLog.d("Merging test %s and %s.", this, test);
// Merge can only happen for tests for the same module.
checkState(
mName.equals(test.getName()),
"Only TestInfo for the same module can be " + "merged.");
List<TestOption> mergedOptions = new ArrayList<>();
// If any test only has exclusive options or no option, only keep the common exclusive
// option in the merged test. For example:
// this.mOptions: include-filter=value1, exclude-annotation=flaky
// test.mOptions: exclude-annotation=flaky, exclude-filter=value2
// merged options: exclude-annotation=flaky
// Note that:
// * The exclude-annotation of flaky is common between the two tests, so it's kept.
// * The include-filter of value1 is dropped as `test` doesn't have any include-filter,
// thus it has larger test coverage and the include-filter is ignored.
// * The exclude-filter of value2 is dropped as it's only for `test`. To achieve maximum
// test coverage for both `this` and `test`, we shall only keep the common exclusive
// filters.
// * In the extreme case that one of the test has no option at all, the merged test will
// also have no option.
if (test.exclusiveOptionsOnly() || this.exclusiveOptionsOnly()) {
Set<TestOption> commonOptions = new HashSet<TestOption>(test.getOptions());
commonOptions.retainAll(new HashSet<TestOption>(mOptions));
mOptions = new ArrayList<TestOption>(commonOptions);
this.addSources(test.getSources());
CLog.d("Options are merged, updated test: %s.", this);
return;
}
// When neither test has no option or with only exclusive options, we try the best to
// merge the test options so the merged test will cover both tests.
// 1. Keep all non-exclusive options, except include-annotation
// 2. Keep common exclusive options
// 3. Keep common include-annotation options
// 4. Keep any exclude-annotation options
// Condition 3 and 4 are added to make sure we have the best test coverage if possible.
// In most cases, one add include-annotation to include only presubmit test, but some other
// test config that doesn't use presubmit annotation doesn't have such option. Therefore,
// uncommon include-annotation option has to be dropped to prevent losing test coverage.
// On the other hand, exclude-annotation is often used to exclude flaky tests. Therefore,
// it's better to keep any exclude-annotation option to prevent flaky tests from being
// included.
// For example:
// this.mOptions: include-filter=value1, exclude-filter=ex-value1, exclude-filter=ex-value2,
// exclude-annotation=flaky, include-annotation=presubmit
// test.mOptions: exclude-filter=ex-value1, include-filter=value3
// merged options: exclude-annotation=flaky, include-filter=value1, include-filter=value3
// Note that:
// * The "exclude-filter=value3" option is kept as it's common in both tests.
// * The "exclude-annotation=flaky" option is kept even though it's only in one test.
// * The "include-annotation=presubmit" option is dropped as it only exists for `this`.
// * The include-filter of value1 and value3 are both kept so the merged test will cover
// both tests.
// * The "exclude-filter=ex-value1" option is kept as it's common in both tests.
// * The "exclude-filter=ex-value2" option is dropped as it's only for `this`. To achieve
// maximum test coverage for both `this` and `test`, we shall only keep the common
// exclusive filters.
// Options from this test:
Set<TestOption> nonExclusiveOptions =
mOptions.stream()
.filter(
option ->
!option.isExclusive()
&& !OPTION_INCLUDE_ANNOTATION.equals(
option.getName()))
.collect(Collectors.toSet());
Set<TestOption> includeAnnotationOptions =
mOptions.stream()
.filter(option -> OPTION_INCLUDE_ANNOTATION.equals(option.getName()))
.collect(Collectors.toSet());
Set<TestOption> exclusiveOptions =
mOptions.stream()
.filter(
option ->
option.isExclusive()
&& !OPTION_EXCLUDE_ANNOTATION.equals(
option.getName()))
.collect(Collectors.toSet());
Set<TestOption> excludeAnnotationOptions =
mOptions.stream()
.filter(option -> OPTION_EXCLUDE_ANNOTATION.equals(option.getName()))
.collect(Collectors.toSet());
// Options from TestInfo to be merged:
Set<TestOption> nonExclusiveOptionsToMerge =
test.getOptions()
.stream()
.filter(
option ->
!option.isExclusive()
&& !OPTION_INCLUDE_ANNOTATION.equals(
option.getName()))
.collect(Collectors.toSet());
Set<TestOption> includeAnnotationOptionsToMerge =
test.getOptions()
.stream()
.filter(option -> OPTION_INCLUDE_ANNOTATION.equals(option.getName()))
.collect(Collectors.toSet());
Set<TestOption> exclusiveOptionsToMerge =
test.getOptions()
.stream()
.filter(
option ->
option.isExclusive()
&& !OPTION_EXCLUDE_ANNOTATION.equals(
option.getName()))
.collect(Collectors.toSet());
Set<TestOption> excludeAnnotationOptionsToMerge =
test.getOptions()
.stream()
.filter(option -> OPTION_EXCLUDE_ANNOTATION.equals(option.getName()))
.collect(Collectors.toSet());
// 1. Keep all non-exclusive options, except include-annotation
nonExclusiveOptions.addAll(nonExclusiveOptionsToMerge);
for (TestOption option : nonExclusiveOptions) {
mergedOptions.add(option);
}
// 2. Keep common exclusive options, except exclude-annotation
exclusiveOptions.retainAll(exclusiveOptionsToMerge);
for (TestOption option : exclusiveOptions) {
mergedOptions.add(option);
}
// 3. Keep common include-annotation options
includeAnnotationOptions.retainAll(includeAnnotationOptionsToMerge);
for (TestOption option : includeAnnotationOptions) {
mergedOptions.add(option);
}
// 4. Keep any exclude-annotation options
excludeAnnotationOptions.addAll(excludeAnnotationOptionsToMerge);
for (TestOption option : excludeAnnotationOptions) {
mergedOptions.add(option);
}
this.mOptions = mergedOptions;
this.addSources(test.getSources());
CLog.d("Options are merged, updated test: %s.", this);
}
/* Check if the TestInfo only has exclusive options.
*
* @return true if the TestInfo only has exclusive options.
*/
private boolean exclusiveOptionsOnly() {
for (TestOption option : mOptions) {
if (option.isInclusive()) {
return false;
}
}
return true;
}
@Override
public String toString() {
if (mOptions.size() == 0) {
return mName;
} else {
return String.format(
"%s: Options: %s",
mName,
Joiner.on(",")
.join(
mOptions.stream()
.map(TestOption::toString)
.collect(Collectors.toList())));
}
}
}