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 com.android.tradefed.isolation;
17 
18 import org.junit.runner.Description;
19 import org.junit.runner.manipulation.Filter;
20 
21 import java.lang.annotation.Annotation;
22 import java.util.Collection;
23 import java.util.HashSet;
24 import java.util.Set;
25 
26 /**
27  * Implements JUnit's Filter interface to allow TradeFed style filters to be evaluated during
28  * execution of JUnit tests. Very similar to TradeFed's internal FilterHelper, but freed of the
29  * dependencies that one has.
30  */
31 final class IsolationFilter extends Filter {
32     private Set<String> mIncludeFilters = new HashSet<>();
33     private Set<String> mExcludeFilters = new HashSet<>();
34     private Set<String> mIncludeAnnotations = new HashSet<>();
35     private Set<String> mExcludeAnnotations = new HashSet<>();
36 
IsolationFilter( Collection<String> includeFilters, Collection<String> excludeFilters, Collection<String> includeAnnotations, Collection<String> excludeAnnotations)37     public IsolationFilter(
38             Collection<String> includeFilters,
39             Collection<String> excludeFilters,
40             Collection<String> includeAnnotations,
41             Collection<String> excludeAnnotations) {
42         mIncludeFilters = new HashSet<String>(includeFilters);
43         mExcludeFilters = new HashSet<String>(excludeFilters);
44         mIncludeAnnotations = new HashSet<String>(includeAnnotations);
45         mExcludeAnnotations = new HashSet<String>(excludeAnnotations);
46     }
47 
IsolationFilter(FilterSpec filter)48     public IsolationFilter(FilterSpec filter) {
49         this(
50                 filter.getIncludeFiltersList(),
51                 filter.getExcludeFiltersList(),
52                 filter.getIncludeAnnotationsList(),
53                 filter.getExcludeAnnotationsList());
54     }
55 
IsolationFilter()56     public IsolationFilter() {}
57 
58     @Override
describe()59     public String describe() {
60         return "Tradefed filtering based on name and annotations.";
61     }
62 
63     @Override
shouldRun(Description desc)64     public boolean shouldRun(Description desc) {
65         return this.checkFilters(desc) && this.checkAnnotations(desc);
66     }
67 
checkFilters(Description desc)68     private boolean checkFilters(Description desc) {
69         // If it's a suite, we check if at least one of the children can run, if yes then we run it
70         if (desc.isSuite()) {
71             for (Description child : desc.getChildren()) {
72                 if (checkFilters(child)) {
73                     return true;
74                 }
75             }
76             // If no child can run, then the suite is skipped
77             return false;
78         }
79         String packageName = getPackageName(desc);
80         String className = desc.getClassName();
81         String methodName = String.format("%s#%s", className, desc.getMethodName());
82         return this.checkIncludeFilters(packageName, className, methodName)
83                 && this.checkExcludeFilters(packageName, className, methodName);
84     }
85 
checkAnnotations(Description desc)86     private boolean checkAnnotations(Description desc) {
87         Collection<Annotation> annotations = desc.getAnnotations();
88         return this.checkIncludeAnnotations(annotations)
89                 && this.checkExcludeAnnotations(annotations);
90     }
91 
checkIncludeFilters(String packageName, String className, String methodName)92     private boolean checkIncludeFilters(String packageName, String className, String methodName) {
93         return mIncludeFilters.isEmpty()
94                 || mIncludeFilters.contains(methodName)
95                 || mIncludeFilters.contains(className)
96                 || mIncludeFilters.contains(packageName);
97     }
98 
checkExcludeFilters(String packageName, String className, String methodName)99     private boolean checkExcludeFilters(String packageName, String className, String methodName) {
100         return !(mExcludeFilters.contains(methodName)
101                 || mExcludeFilters.contains(className)
102                 || mExcludeFilters.contains(packageName));
103     }
104 
checkIncludeAnnotations(Collection<Annotation> annotationsList)105     private boolean checkIncludeAnnotations(Collection<Annotation> annotationsList) {
106         if (!mIncludeAnnotations.isEmpty()) {
107             Set<String> neededAnnotation = new HashSet<String>();
108             neededAnnotation.addAll(mIncludeAnnotations);
109             for (Annotation a : annotationsList) {
110                 if (neededAnnotation.contains(a.annotationType().getName())) {
111                     neededAnnotation.remove(a.annotationType().getName());
112                 }
113             }
114             if (!neededAnnotation.isEmpty()) {
115                 // The test needs to have all the include annotation to pass.
116                 return false;
117             }
118         }
119         return true;
120     }
121 
checkExcludeAnnotations(Collection<Annotation> annotationsList)122     private boolean checkExcludeAnnotations(Collection<Annotation> annotationsList) {
123         if (!mExcludeAnnotations.isEmpty()) {
124             for (Annotation a : annotationsList) {
125                 if (mExcludeAnnotations.contains(a.annotationType().getName())) {
126                     // If any of the method annotation match an ExcludeAnnotation, don't run it
127                     return false;
128                 }
129             }
130         }
131         return true;
132     }
133 
getPackageName(Description desc)134     private String getPackageName(Description desc) {
135         // Thankfully, the classname contained in a standard junit Description is derived from
136         // class.getName(), which separates inner static classes with a '$' symbol rather than a
137         // period '.'
138         int loc = desc.getClassName().lastIndexOf('.');
139         if (loc > 0) {
140             return desc.getClassName().substring(0, loc);
141         } else {
142             return "";
143         }
144     }
145 }
146