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
23from atest import atest_utils
24from atest import 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 else ''
66    if not self.options:
67      return self.name + host_info
68    options = ''
69    for option in self.options:
70      options += '%s: %s, ' % option
71
72    return '%s (%s)%s' % (self.name, options.strip(', '), host_info)
73
74  def __hash__(self):
75    """Get the hash of TestDetail based on the details"""
76    return hash(str(self))
77
78  def __eq__(self, other):
79    return str(self) == str(other)
80
81
82class Import:
83  """Store test mapping import details."""
84
85  def __init__(self, test_mapping_file, details):
86    """Import constructor
87
88    Parse import details from a dictionary, e.g.,
89    {
90        "path": "..\folder1"
91    }
92    in which, project is the name of the project, by default it's the
93    current project of the containing TEST_MAPPING file.
94
95    Args:
96        test_mapping_file: Path to the TEST_MAPPING file that contains the
97          import.
98        details: A dictionary of details about importing another TEST_MAPPING
99          file.
100    """
101    self.test_mapping_file = test_mapping_file
102    self.path = details['path']
103
104  def __str__(self):
105    """String value of the Import object."""
106    return 'Source: %s, path: %s' % (self.test_mapping_file, self.path)
107
108  def get_path(self):
109    """Get the path to TEST_MAPPING import directory."""
110    path = os.path.realpath(
111        os.path.join(os.path.dirname(self.test_mapping_file), self.path)
112    )
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
126  file_patterns of TEST_MAPPING files.
127
128  Args:
129      test_mapping_file: Path to a TEST_MAPPING file.
130      test_detail: A TestDetail object.
131
132  Returns:
133      True if the test's file_patterns setting is not set or contains a
134      pattern matches any of the modified files.
135  """
136  # Only check if the altered files are located in the same or sub directory
137  # of the TEST_MAPPING file. Extract the relative path of the modified files
138  # which match file patterns.
139  file_patterns = test_detail.get('file_patterns', [])
140  if not file_patterns:
141    return True
142  test_mapping_dir = os.path.dirname(test_mapping_file)
143  modified_files = atest_utils.get_modified_files(test_mapping_dir)
144  if not modified_files:
145    return False
146  modified_files_in_source_dir = [
147      os.path.relpath(filepath, test_mapping_dir)
148      for filepath in fnmatch.filter(
149          modified_files, os.path.join(test_mapping_dir, '*')
150      )
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