1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6# TODO(borenet): This module was copied from build.git and heavily modified to
7# remove dependencies on other modules in build.git.  It belongs in a different
8# repo. Remove this once it has been moved.
9
10
11from recipe_engine import recipe_api
12
13
14class SwarmingClientApi(recipe_api.RecipeApi):
15  """Code that both isolate and swarming recipe modules depend on.
16
17  Both swarming and isolate scripts live in a single repository called
18  'swarming client'. This module include common functionality like finding
19  existing swarming client checkout, fetching a new one, getting version of
20  a swarming script, etc.
21  """
22
23  def __init__(self, **kwargs):
24    super(SwarmingClientApi, self).__init__(**kwargs)
25    self._client_path = None
26    self._script_version = {}
27
28  def checkout(self, revision=None, curl_trace_file=None, can_fail_build=True):
29    """Returns a step to checkout swarming client into a separate directory.
30
31    Ordinarily swarming client is checked out via Chromium DEPS into
32    src/tools/swarming_client. This step configures recipe module to use
33    a separate checkout.
34
35    If |revision| is None, this requires the build property
36    'parent_got_swarming_client_revision' to be present, and raises an exception
37    otherwise. Fail-fast behavior is used because if machines silently fell back
38    to checking out the entire workspace, that would cause dramatic increases
39    in cycle time if a misconfiguration were made and it were no longer possible
40    for the bot to check out swarming_client separately.
41    """
42    # If the following line throws an exception, it either means the
43    # bot is misconfigured, or, if you're testing locally, that you
44    # need to pass in some recent legal revision for this property.
45    if revision is None:
46      revision = self.m.properties['parent_got_swarming_client_revision']
47    self._client_path = self.m.path['start_dir'].join('swarming.client')
48    self.m.git.checkout(
49        url='https://chromium.googlesource.com/external/swarming.client.git',
50        ref=revision,
51        dir_path=self._client_path,
52        step_suffix='swarming_client',
53        curl_trace_file=curl_trace_file,
54        can_fail_build=can_fail_build)
55
56  @property
57  def path(self):
58    """Returns path to a swarming client checkout.
59
60    It's subdirectory of Chromium src/ checkout or a separate directory if
61    'checkout_swarming_client' step was used.
62    """
63    if self._client_path:
64      return self._client_path
65    # Default is swarming client path in chromium src/ checkout.
66    # TODO(vadimsh): This line assumes the recipe is working with
67    # Chromium checkout.
68    return self.m.path['checkout'].join('tools', 'swarming_client')
69
70  def query_script_version(self, script, step_test_data=None):
71    """Yields a step to query a swarming script for its version.
72
73    Version tuple is later accessible via 'get_script_version' method. If
74    |step_test_data| is given, it is a tuple with version to use in expectation
75    tests by default.
76
77    Does nothing if script's version is already known.
78    """
79    # Convert |step_test_data| from tuple of ints back to a version string.
80    if step_test_data:
81      assert isinstance(step_test_data, tuple)
82      assert all(isinstance(x, int) for x in step_test_data)
83      as_text = '.'.join(map(str, step_test_data))
84      step_test_data_cb = lambda: self.m.raw_io.test_api.stream_output(as_text)
85    else:
86      step_test_data_cb = None
87
88    if script not in self._script_version:
89      try:
90        self.m.python(
91          name='%s --version' % script,
92          script=self.path.join(script),
93          args=['--version'],
94          stdout=self.m.raw_io.output_text(),
95          step_test_data=step_test_data_cb)
96      finally:
97        step_result = self.m.step.active_result
98        version = step_result.stdout.strip()
99        step_result.presentation.step_text = version
100        self._script_version[script] = tuple(map(int, version.split('.')))
101
102      return step_result
103
104  def get_script_version(self, script):
105    """Returns a version of some swarming script as a tuple (Major, Minor, Rev).
106
107    It should have been queried by 'query_script_version' step before. Raises
108    AssertionError if it wasn't.
109    """
110    assert script in self._script_version, script
111    return self._script_version[script]
112
113  def ensure_script_version(self, script, min_version, step_test_data=None):
114    """Yields steps to ensure a script version is not older than |min_version|.
115
116    Will abort recipe execution if it is.
117    """
118    step_result = self.query_script_version(
119        script, step_test_data=step_test_data or min_version)
120    version = self.get_script_version(script)
121    if version < min_version:
122      expecting = '.'.join(map(str, min_version))
123      got = '.'.join(map(str, version))
124      abort_reason = 'Expecting at least v%s, got v%s' % (expecting, got)
125
126      # TODO(martiniss) remove once recipe 1.5 migration done
127      step_result = self.m.python.inline(
128          '%s is too old' % script,
129          'import sys; sys.exit(1)',
130          add_python_log=False)
131      # TODO(martiniss) get rid of this bare string.
132      step_result.presentation.status = self.m.step.FAILURE
133      step_result.presentation.step_text = abort_reason
134
135      raise self.m.step.StepFailure(abort_reason)
136