1# Copyright 2018, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Classes for test mapping related objects.""" 16 17 18import copy 19import fnmatch 20import os 21import re 22 23import atest_utils 24import constants 25 26TEST_MAPPING = 'TEST_MAPPING' 27 28 29class TestDetail: 30 """Stores the test details set in a TEST_MAPPING file.""" 31 32 def __init__(self, details): 33 """TestDetail constructor 34 35 Parse test detail from a dictionary, e.g., 36 { 37 "name": "SettingsUnitTests", 38 "host": true, 39 "options": [ 40 { 41 "instrumentation-arg": 42 "annotation=android.platform.test.annotations.Presubmit" 43 }, 44 "file_patterns": ["(/|^)Window[^/]*\\.java", 45 "(/|^)Activity[^/]*\\.java"] 46 } 47 48 Args: 49 details: A dictionary of test detail. 50 """ 51 self.name = details['name'] 52 self.options = [] 53 # True if the test should run on host and require no device. 54 self.host = details.get('host', False) 55 assert isinstance(self.host, bool), 'host can only have boolean value.' 56 options = details.get('options', []) 57 for option in options: 58 assert len(option) == 1, 'Each option can only have one key.' 59 self.options.append(copy.deepcopy(option).popitem()) 60 self.options.sort(key=lambda o: o[0]) 61 self.file_patterns = details.get('file_patterns', []) 62 63 def __str__(self): 64 """String value of the TestDetail object.""" 65 host_info = (', runs on host without device required.' if self.host 66 else '') 67 if not self.options: 68 return self.name + host_info 69 options = '' 70 for option in self.options: 71 options += '%s: %s, ' % option 72 73 return '%s (%s)%s' % (self.name, options.strip(', '), host_info) 74 75 def __hash__(self): 76 """Get the hash of TestDetail based on the details""" 77 return hash(str(self)) 78 79 def __eq__(self, other): 80 return str(self) == str(other) 81 82 83class Import: 84 """Store test mapping import details.""" 85 86 def __init__(self, test_mapping_file, details): 87 """Import constructor 88 89 Parse import details from a dictionary, e.g., 90 { 91 "path": "..\folder1" 92 } 93 in which, project is the name of the project, by default it's the 94 current project of the containing TEST_MAPPING file. 95 96 Args: 97 test_mapping_file: Path to the TEST_MAPPING file that contains the 98 import. 99 details: A dictionary of details about importing another 100 TEST_MAPPING file. 101 """ 102 self.test_mapping_file = test_mapping_file 103 self.path = details['path'] 104 105 def __str__(self): 106 """String value of the Import object.""" 107 return 'Source: %s, path: %s' % (self.test_mapping_file, self.path) 108 109 def get_path(self): 110 """Get the path to TEST_MAPPING import directory.""" 111 path = os.path.realpath(os.path.join( 112 os.path.dirname(self.test_mapping_file), self.path)) 113 if os.path.exists(path): 114 return path 115 root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, os.sep) 116 path = os.path.realpath(os.path.join(root_dir, self.path)) 117 if os.path.exists(path): 118 return path 119 # The import path can't be located. 120 return None 121 122 123def is_match_file_patterns(test_mapping_file, test_detail): 124 """Check if the changed file names match the regex pattern defined in 125 file_patterns of TEST_MAPPING files. 126 127 Args: 128 test_mapping_file: Path to a TEST_MAPPING file. 129 test_detail: A TestDetail object. 130 131 Returns: 132 True if the test's file_patterns setting is not set or contains a 133 pattern matches any of the modified files. 134 """ 135 # Only check if the altered files are located in the same or sub directory 136 # of the TEST_MAPPING file. Extract the relative path of the modified files 137 # which match file patterns. 138 file_patterns = test_detail.get('file_patterns', []) 139 if not file_patterns: 140 return True 141 test_mapping_dir = os.path.dirname(test_mapping_file) 142 modified_files = atest_utils.get_modified_files(test_mapping_dir) 143 if not modified_files: 144 return False 145 modified_files_in_source_dir = [ 146 os.path.relpath(filepath, test_mapping_dir) 147 for filepath in fnmatch.filter(modified_files, 148 os.path.join(test_mapping_dir, '*')) 149 ] 150 for modified_file in modified_files_in_source_dir: 151 # Force to run the test if it's in a TEST_MAPPING file included in the 152 # changesets. 153 if modified_file == constants.TEST_MAPPING: 154 return True 155 for pattern in file_patterns: 156 if re.search(pattern, modified_file): 157 return True 158 return False 159