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