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