1# Copyright 2012 the V8 project authors. All rights reserved.
2# Redistribution and use in source and binary forms, with or without
3# modification, are permitted provided that the following conditions are
4# met:
5#
6#     * Redistributions of source code must retain the above copyright
7#       notice, this list of conditions and the following disclaimer.
8#     * Redistributions in binary form must reproduce the above
9#       copyright notice, this list of conditions and the following
10#       disclaimer in the documentation and/or other materials provided
11#       with the distribution.
12#     * Neither the name of Google Inc. nor the names of its
13#       contributors may be used to endorse or promote products derived
14#       from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28
29import fnmatch
30import imp
31import itertools
32import os
33from contextlib import contextmanager
34
35from . import command
36from . import statusfile
37from . import utils
38from ..objects.testcase import TestCase
39from .variants import ALL_VARIANTS, ALL_VARIANT_FLAGS
40
41
42STANDARD_VARIANT = set(["default"])
43
44
45class VariantsGenerator(object):
46  def __init__(self, variants):
47    self._all_variants = [v for v in variants if v in ALL_VARIANTS]
48    self._standard_variant = [v for v in variants if v in STANDARD_VARIANT]
49
50  def gen(self, test):
51    """Generator producing (variant, flags, procid suffix) tuples."""
52    flags_set = self._get_flags_set(test)
53    for n, variant in enumerate(self._get_variants(test)):
54      yield (variant, flags_set[variant][0], n)
55
56  def _get_flags_set(self, test):
57    return ALL_VARIANT_FLAGS
58
59  def _get_variants(self, test):
60    if test.only_standard_variant:
61      return self._standard_variant
62    return self._all_variants
63
64
65class TestCombiner(object):
66  def get_group_key(self, test):
67    """To indicate what tests can be combined with each other we define a group
68    key for each test. Tests with the same group key can be combined. Test
69    without a group key (None) is not combinable with any other test.
70    """
71    raise NotImplementedError()
72
73  def combine(self, name, tests):
74    """Returns test combined from `tests`. Since we identify tests by their
75    suite and name, `name` parameter should be unique within one suite.
76    """
77    return self._combined_test_class()(name, tests)
78
79  def _combined_test_class(self):
80    raise NotImplementedError()
81
82
83class TestLoader(object):
84  """Base class for loading TestSuite tests after applying test suite
85  transformations."""
86
87  def __init__(self, suite, test_class, test_config, test_root):
88    self.suite = suite
89    self.test_class = test_class
90    self.test_config = test_config
91    self.test_root = test_root
92    self.test_count_estimation = len(list(self._list_test_filenames()))
93
94  def _list_test_filenames(self):
95    """Implemented by the subclassed TestLoaders to list filenames.
96
97    Filenames are expected to be sorted and are deterministic."""
98    raise NotImplementedError
99
100  def _should_filter_by_name(self, name):
101    return False
102
103  def _should_filter_by_test(self, test):
104    return False
105
106  def _filename_to_testname(self, filename):
107    """Hook for subclasses to write their own filename transformation
108    logic before the test creation."""
109    return filename
110
111  # TODO: not needed for every TestLoader, extract it into a subclass.
112  def _path_to_name(self, path):
113    if utils.IsWindows():
114      return path.replace(os.path.sep, "/")
115
116    return path
117
118  def _create_test(self, path, suite, **kwargs):
119    """Converts paths into test objects using the given options"""
120    return self.test_class(
121      suite, path, self._path_to_name(path), self.test_config, **kwargs)
122
123  def list_tests(self):
124    """Loads and returns the test objects for a TestSuite"""
125    # TODO: detect duplicate tests.
126    for filename in self._list_test_filenames():
127      if self._should_filter_by_name(filename):
128        continue
129
130      testname = self._filename_to_testname(filename)
131      case = self._create_test(testname, self.suite)
132      if self._should_filter_by_test(case):
133        continue
134
135      yield case
136
137
138class GenericTestLoader(TestLoader):
139  """Generic TestLoader implementing the logic for listing filenames"""
140  @property
141  def excluded_files(self):
142    return set()
143
144  @property
145  def excluded_dirs(self):
146    return set()
147
148  @property
149  def excluded_suffixes(self):
150    return set()
151
152  @property
153  def test_dirs(self):
154    return [self.test_root]
155
156  @property
157  def extensions(self):
158    return []
159
160  def __find_extension(self, filename):
161    for extension in self.extensions:
162      if filename.endswith(extension):
163        return extension
164
165    return False
166
167  def _should_filter_by_name(self, filename):
168    if not self.__find_extension(filename):
169      return True
170
171    for suffix in self.excluded_suffixes:
172      if filename.endswith(suffix):
173        return True
174
175    if os.path.basename(filename) in self.excluded_files:
176      return True
177
178    return False
179
180  def _filename_to_testname(self, filename):
181    extension = self.__find_extension(filename)
182    if not extension:
183      return filename
184
185    return filename[:-len(extension)]
186
187  def _to_relpath(self, abspath, test_root):
188    return os.path.relpath(abspath, test_root)
189
190  def _list_test_filenames(self):
191    for test_dir in sorted(self.test_dirs):
192      test_root = os.path.join(self.test_root, test_dir)
193      for dirname, dirs, files in os.walk(test_root, followlinks=True):
194        dirs.sort()
195        for dir in dirs:
196          if dir in self.excluded_dirs or dir.startswith('.'):
197            dirs.remove(dir)
198
199        files.sort()
200        for filename in files:
201          abspath = os.path.join(dirname, filename)
202
203          yield self._to_relpath(abspath, test_root)
204
205
206class JSTestLoader(GenericTestLoader):
207  @property
208  def extensions(self):
209    return [".js", ".mjs"]
210
211
212class TestGenerator(object):
213  def __init__(self, test_count_estimate, slow_tests, fast_tests):
214    self.test_count_estimate = test_count_estimate
215    self.slow_tests = slow_tests
216    self.fast_tests = fast_tests
217    self._rebuild_iterator()
218
219  def _rebuild_iterator(self):
220    self._iterator = itertools.chain(self.slow_tests, self.fast_tests)
221
222  def __iter__(self):
223    return self
224
225  def __next__(self):
226    return self.next()
227
228  def next(self):
229    return next(self._iterator)
230
231  def merge(self, test_generator):
232    self.test_count_estimate += test_generator.test_count_estimate
233    self.slow_tests = itertools.chain(
234      self.slow_tests, test_generator.slow_tests)
235    self.fast_tests = itertools.chain(
236      self.fast_tests, test_generator.fast_tests)
237    self._rebuild_iterator()
238
239
240@contextmanager
241def _load_testsuite_module(name, root):
242  f = None
243  try:
244    (f, pathname, description) = imp.find_module("testcfg", [root])
245    yield imp.load_module(name + "_testcfg", f, pathname, description)
246  finally:
247    if f:
248      f.close()
249
250class TestSuite(object):
251  @staticmethod
252  def Load(root, test_config, framework_name):
253    name = root.split(os.path.sep)[-1]
254    with _load_testsuite_module(name, root) as module:
255      return module.GetSuite(name, root, test_config, framework_name)
256
257  def __init__(self, name, root, test_config, framework_name):
258    self.name = name  # string
259    self.root = root  # string containing path
260    self.test_config = test_config
261    self.framework_name = framework_name  # name of the test runner impl
262    self.tests = None  # list of TestCase objects
263    self.statusfile = None
264
265    self._test_loader = self._test_loader_class()(
266      self, self._test_class(), self.test_config, self.root)
267
268  def status_file(self):
269    return "%s/%s.status" % (self.root, self.name)
270
271  @property
272  def _test_loader_class(self):
273    raise NotImplementedError
274
275  def ListTests(self):
276    return self._test_loader.list_tests()
277
278  def __initialize_test_count_estimation(self):
279    # Retrieves a single test to initialize the test generator.
280    next(iter(self.ListTests()), None)
281
282  def __calculate_test_count(self):
283    self.__initialize_test_count_estimation()
284    return self._test_loader.test_count_estimation
285
286  def load_tests_from_disk(self, statusfile_variables):
287    self.statusfile = statusfile.StatusFile(
288      self.status_file(), statusfile_variables)
289
290    test_count = self.__calculate_test_count()
291    slow_tests = (test for test in self.ListTests() if test.is_slow)
292    fast_tests = (test for test in self.ListTests() if not test.is_slow)
293    return TestGenerator(test_count, slow_tests, fast_tests)
294
295  def get_variants_gen(self, variants):
296    return self._variants_gen_class()(variants)
297
298  def _variants_gen_class(self):
299    return VariantsGenerator
300
301  def test_combiner_available(self):
302    return bool(self._test_combiner_class())
303
304  def get_test_combiner(self):
305    cls = self._test_combiner_class()
306    if cls:
307      return cls()
308    return None
309
310  def _test_combiner_class(self):
311    """Returns Combiner subclass. None if suite doesn't support combining
312    tests.
313    """
314    return None
315
316  def _test_class(self):
317    raise NotImplementedError
318