xref: /aosp_15_r20/external/autotest/site_utils/gs_offloader_unittest.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/python3
2*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport six.moves.builtins
11*9c5db199SXin Liimport six.moves.queue
12*9c5db199SXin Liimport json
13*9c5db199SXin Liimport logging
14*9c5db199SXin Liimport os
15*9c5db199SXin Liimport shutil
16*9c5db199SXin Liimport signal
17*9c5db199SXin Liimport stat
18*9c5db199SXin Liimport subprocess
19*9c5db199SXin Liimport sys
20*9c5db199SXin Liimport tarfile
21*9c5db199SXin Liimport tempfile
22*9c5db199SXin Liimport time
23*9c5db199SXin Liimport unittest
24*9c5db199SXin Lifrom unittest import mock
25*9c5db199SXin Li
26*9c5db199SXin Liimport common
27*9c5db199SXin Li
28*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
29*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils
30*9c5db199SXin Lifrom autotest_lib.client.common_lib.test_utils.comparators import IsA
31*9c5db199SXin Li
32*9c5db199SXin Li#For unittest without cloud_client.proto compiled.
33*9c5db199SXin Litry:
34*9c5db199SXin Li    from autotest_lib.site_utils import cloud_console_client
35*9c5db199SXin Liexcept ImportError:
36*9c5db199SXin Li    cloud_console_client = None
37*9c5db199SXin Lifrom autotest_lib.site_utils import gs_offloader
38*9c5db199SXin Lifrom autotest_lib.site_utils import job_directories
39*9c5db199SXin Lifrom autotest_lib.site_utils import job_directories_unittest as jd_test
40*9c5db199SXin Lifrom autotest_lib.tko import models
41*9c5db199SXin Lifrom autotest_lib.utils import gslib
42*9c5db199SXin Lifrom autotest_lib.site_utils import pubsub_utils
43*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import timeout_util
44*9c5db199SXin Lifrom six.moves import range
45*9c5db199SXin Li
46*9c5db199SXin Li# Test value to use for `days_old`, if nothing else is required.
47*9c5db199SXin Li_TEST_EXPIRATION_AGE = 7
48*9c5db199SXin Li
49*9c5db199SXin Li
50*9c5db199SXin Lidef _get_options(argv):
51*9c5db199SXin Li    """Helper function to exercise command line parsing.
52*9c5db199SXin Li
53*9c5db199SXin Li    @param argv Value of sys.argv to be parsed.
54*9c5db199SXin Li
55*9c5db199SXin Li    """
56*9c5db199SXin Li    sys.argv = ['bogus.py'] + argv
57*9c5db199SXin Li    return gs_offloader.parse_options()
58*9c5db199SXin Li
59*9c5db199SXin Li
60*9c5db199SXin Lidef is_fifo(path):
61*9c5db199SXin Li    """Determines whether a path is a fifo.
62*9c5db199SXin Li
63*9c5db199SXin Li  @param path: fifo path string.
64*9c5db199SXin Li  """
65*9c5db199SXin Li    return stat.S_ISFIFO(os.lstat(path).st_mode)
66*9c5db199SXin Li
67*9c5db199SXin Li
68*9c5db199SXin Lidef _get_fake_process():
69*9c5db199SXin Li    return FakeProcess()
70*9c5db199SXin Li
71*9c5db199SXin Li
72*9c5db199SXin Liclass FakeProcess(object):
73*9c5db199SXin Li    """Fake process object."""
74*9c5db199SXin Li
75*9c5db199SXin Li    def __init__(self):
76*9c5db199SXin Li        self.returncode = 0
77*9c5db199SXin Li
78*9c5db199SXin Li
79*9c5db199SXin Li    def wait(self):
80*9c5db199SXin Li        return True
81*9c5db199SXin Li
82*9c5db199SXin Li
83*9c5db199SXin Liclass OffloaderOptionsTests(unittest.TestCase):
84*9c5db199SXin Li    """Tests for the `Offloader` constructor.
85*9c5db199SXin Li
86*9c5db199SXin Li    Tests that offloader instance fields are set as expected
87*9c5db199SXin Li    for given command line options.
88*9c5db199SXin Li
89*9c5db199SXin Li    """
90*9c5db199SXin Li
91*9c5db199SXin Li    _REGULAR_ONLY = {job_directories.SwarmingJobDirectory,
92*9c5db199SXin Li                     job_directories.RegularJobDirectory}
93*9c5db199SXin Li    _SPECIAL_ONLY = {job_directories.SwarmingJobDirectory,
94*9c5db199SXin Li                     job_directories.SpecialJobDirectory}
95*9c5db199SXin Li    _BOTH = _REGULAR_ONLY | _SPECIAL_ONLY
96*9c5db199SXin Li
97*9c5db199SXin Li
98*9c5db199SXin Li    def setUp(self):
99*9c5db199SXin Li        super(OffloaderOptionsTests, self).setUp()
100*9c5db199SXin Li        patcher = mock.patch.object(utils, 'get_offload_gsuri')
101*9c5db199SXin Li        self.gsuri_patch = patcher.start()
102*9c5db199SXin Li        self.addCleanup(patcher.stop)
103*9c5db199SXin Li
104*9c5db199SXin Li        gs_offloader.GS_OFFLOADING_ENABLED = True
105*9c5db199SXin Li        gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False
106*9c5db199SXin Li
107*9c5db199SXin Li
108*9c5db199SXin Li    def _mock_get_sub_offloader(self, is_moblab, multiprocessing=False,
109*9c5db199SXin Li                               console_client=None, delete_age=0):
110*9c5db199SXin Li        """Mock the process of getting the offload_dir function."""
111*9c5db199SXin Li        if is_moblab:
112*9c5db199SXin Li            self.expected_gsuri = '%sresults/%s/%s/' % (
113*9c5db199SXin Li                    global_config.global_config.get_config_value(
114*9c5db199SXin Li                            'CROS', 'image_storage_server'),
115*9c5db199SXin Li                    'Fa:ke:ma:c0:12:34', 'rand0m-uu1d')
116*9c5db199SXin Li        else:
117*9c5db199SXin Li            self.expected_gsuri = utils.DEFAULT_OFFLOAD_GSURI
118*9c5db199SXin Li        utils.get_offload_gsuri.return_value = self.expected_gsuri
119*9c5db199SXin Li        sub_offloader = gs_offloader.GSOffloader(self.expected_gsuri,
120*9c5db199SXin Li                                                 multiprocessing, delete_age,
121*9c5db199SXin Li                                                 console_client)
122*9c5db199SXin Li
123*9c5db199SXin Li        GsOffloader_patcher = mock.patch.object(gs_offloader, 'GSOffloader')
124*9c5db199SXin Li        self.GsOffloader_patch = GsOffloader_patcher.start()
125*9c5db199SXin Li        self.addCleanup(GsOffloader_patcher.stop)
126*9c5db199SXin Li
127*9c5db199SXin Li        if cloud_console_client:
128*9c5db199SXin Li            console_patcher = mock.patch.object(
129*9c5db199SXin Li                    cloud_console_client, 'is_cloud_notification_enabled')
130*9c5db199SXin Li            self.ccc_notification_patch = console_patcher.start()
131*9c5db199SXin Li            self.addCleanup(console_patcher.stop)
132*9c5db199SXin Li
133*9c5db199SXin Li        if console_client:
134*9c5db199SXin Li            cloud_console_client.is_cloud_notification_enabled.return_value = True
135*9c5db199SXin Li            gs_offloader.GSOffloader.return_value = sub_offloader
136*9c5db199SXin Li        else:
137*9c5db199SXin Li            if cloud_console_client:
138*9c5db199SXin Li                cloud_console_client.is_cloud_notification_enabled.return_value = False
139*9c5db199SXin Li            gs_offloader.GSOffloader.return_value = sub_offloader
140*9c5db199SXin Li
141*9c5db199SXin Li        return sub_offloader
142*9c5db199SXin Li
143*9c5db199SXin Li    def _verify_sub_offloader(self,
144*9c5db199SXin Li                              is_moblab,
145*9c5db199SXin Li                              multiprocessing=False,
146*9c5db199SXin Li                              console_client=None,
147*9c5db199SXin Li                              delete_age=0):
148*9c5db199SXin Li        if console_client:
149*9c5db199SXin Li            self.GsOffloader_patch.assert_called_with(
150*9c5db199SXin Li                    self.expected_gsuri, multiprocessing, delete_age,
151*9c5db199SXin Li                    IsA(cloud_console_client.PubSubBasedClient))
152*9c5db199SXin Li
153*9c5db199SXin Li        else:
154*9c5db199SXin Li            self.GsOffloader_patch.assert_called_with(self.expected_gsuri,
155*9c5db199SXin Li                                                      multiprocessing,
156*9c5db199SXin Li                                                      delete_age, None)
157*9c5db199SXin Li
158*9c5db199SXin Li    def test_process_no_options(self):
159*9c5db199SXin Li        """Test default offloader options."""
160*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(False)
161*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options([]))
162*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
163*9c5db199SXin Li                         self._REGULAR_ONLY)
164*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
165*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
166*9c5db199SXin Li                         sub_offloader)
167*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
168*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
169*9c5db199SXin Li        self._verify_sub_offloader(False)
170*9c5db199SXin Li
171*9c5db199SXin Li    def test_process_all_option(self):
172*9c5db199SXin Li        """Test offloader handling for the --all option."""
173*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(False)
174*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options(['--all']))
175*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes), self._BOTH)
176*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
177*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
178*9c5db199SXin Li                         sub_offloader)
179*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
180*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
181*9c5db199SXin Li        self._verify_sub_offloader(False)
182*9c5db199SXin Li
183*9c5db199SXin Li
184*9c5db199SXin Li    def test_process_hosts_option(self):
185*9c5db199SXin Li        """Test offloader handling for the --hosts option."""
186*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(False)
187*9c5db199SXin Li        offloader = gs_offloader.Offloader(
188*9c5db199SXin Li                _get_options(['--hosts']))
189*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
190*9c5db199SXin Li                         self._SPECIAL_ONLY)
191*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
192*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
193*9c5db199SXin Li                         sub_offloader)
194*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
195*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
196*9c5db199SXin Li        self._verify_sub_offloader(False)
197*9c5db199SXin Li
198*9c5db199SXin Li
199*9c5db199SXin Li    def test_parallelism_option(self):
200*9c5db199SXin Li        """Test offloader handling for the --parallelism option."""
201*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(False)
202*9c5db199SXin Li        offloader = gs_offloader.Offloader(
203*9c5db199SXin Li                _get_options(['--parallelism', '2']))
204*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
205*9c5db199SXin Li                         self._REGULAR_ONLY)
206*9c5db199SXin Li        self.assertEqual(offloader._processes, 2)
207*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
208*9c5db199SXin Li                         sub_offloader)
209*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
210*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
211*9c5db199SXin Li        self._verify_sub_offloader(False)
212*9c5db199SXin Li
213*9c5db199SXin Li
214*9c5db199SXin Li    def test_delete_only_option(self):
215*9c5db199SXin Li        """Test offloader handling for the --delete_only option."""
216*9c5db199SXin Li        offloader = gs_offloader.Offloader(
217*9c5db199SXin Li                _get_options(['--delete_only']))
218*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
219*9c5db199SXin Li                         self._REGULAR_ONLY)
220*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
221*9c5db199SXin Li        self.assertIsInstance(offloader._gs_offloader,
222*9c5db199SXin Li                              gs_offloader.FakeGSOffloader)
223*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
224*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
225*9c5db199SXin Li
226*9c5db199SXin Li
227*9c5db199SXin Li    def test_days_old_option(self):
228*9c5db199SXin Li        """Test offloader handling for the --days_old option."""
229*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(False, delete_age=7)
230*9c5db199SXin Li        offloader = gs_offloader.Offloader(
231*9c5db199SXin Li                _get_options(['--days_old', '7']))
232*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
233*9c5db199SXin Li                         self._REGULAR_ONLY)
234*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
235*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
236*9c5db199SXin Li                         sub_offloader)
237*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 7)
238*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 7)
239*9c5db199SXin Li        self._verify_sub_offloader(False, delete_age=7)
240*9c5db199SXin Li
241*9c5db199SXin Li
242*9c5db199SXin Li    def test_moblab_gsuri_generation(self):
243*9c5db199SXin Li        """Test offloader construction for Moblab."""
244*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(True)
245*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options([]))
246*9c5db199SXin Li        self.assertEqual(set(offloader._jobdir_classes),
247*9c5db199SXin Li                         self._REGULAR_ONLY)
248*9c5db199SXin Li        self.assertEqual(offloader._processes, 1)
249*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
250*9c5db199SXin Li                         sub_offloader)
251*9c5db199SXin Li        self.assertEqual(offloader._upload_age_limit, 0)
252*9c5db199SXin Li        self.assertEqual(offloader._delete_age_limit, 0)
253*9c5db199SXin Li        self._verify_sub_offloader(True)
254*9c5db199SXin Li
255*9c5db199SXin Li
256*9c5db199SXin Li    def test_globalconfig_offloading_flag(self):
257*9c5db199SXin Li        """Test enabling of --delete_only via global_config."""
258*9c5db199SXin Li        gs_offloader.GS_OFFLOADING_ENABLED = False
259*9c5db199SXin Li        offloader = gs_offloader.Offloader(
260*9c5db199SXin Li                _get_options([]))
261*9c5db199SXin Li        self.assertIsInstance(offloader._gs_offloader,
262*9c5db199SXin Li                             gs_offloader.FakeGSOffloader)
263*9c5db199SXin Li
264*9c5db199SXin Li    def test_offloader_multiprocessing_flag_set(self):
265*9c5db199SXin Li        """Test multiprocessing is set."""
266*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(True, True)
267*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options(['-m']))
268*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
269*9c5db199SXin Li                         sub_offloader)
270*9c5db199SXin Li        self._verify_sub_offloader(True, True)
271*9c5db199SXin Li
272*9c5db199SXin Li    def test_offloader_multiprocessing_flag_not_set_default_false(self):
273*9c5db199SXin Li        """Test multiprocessing is set."""
274*9c5db199SXin Li        gs_offloader.GS_OFFLOADER_MULTIPROCESSING = False
275*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(True, False)
276*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options([]))
277*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
278*9c5db199SXin Li                         sub_offloader)
279*9c5db199SXin Li        self._verify_sub_offloader(True, False)
280*9c5db199SXin Li
281*9c5db199SXin Li    def test_offloader_multiprocessing_flag_not_set_default_true(self):
282*9c5db199SXin Li        """Test multiprocessing is set."""
283*9c5db199SXin Li        gs_offloader.GS_OFFLOADER_MULTIPROCESSING = True
284*9c5db199SXin Li        sub_offloader = self._mock_get_sub_offloader(True, True)
285*9c5db199SXin Li        offloader = gs_offloader.Offloader(_get_options([]))
286*9c5db199SXin Li        self.assertEqual(offloader._gs_offloader,
287*9c5db199SXin Li                         sub_offloader)
288*9c5db199SXin Li        self._verify_sub_offloader(True, True)
289*9c5db199SXin Li
290*9c5db199SXin Li
291*9c5db199SXin Li    def test_offloader_pubsub_enabled(self):
292*9c5db199SXin Li        """Test multiprocessing is set."""
293*9c5db199SXin Li        if not cloud_console_client:
294*9c5db199SXin Li            return
295*9c5db199SXin Li        with mock.patch.object(pubsub_utils, "PubSubClient"):
296*9c5db199SXin Li            sub_offloader = self._mock_get_sub_offloader(
297*9c5db199SXin Li                    True, False, cloud_console_client.PubSubBasedClient())
298*9c5db199SXin Li            offloader = gs_offloader.Offloader(_get_options([]))
299*9c5db199SXin Li            self.assertEqual(offloader._gs_offloader, sub_offloader)
300*9c5db199SXin Li            self._verify_sub_offloader(
301*9c5db199SXin Li                    True, False, cloud_console_client.PubSubBasedClient())
302*9c5db199SXin Li
303*9c5db199SXin Li
304*9c5db199SXin Liclass _MockJobDirectory(job_directories._JobDirectory):
305*9c5db199SXin Li    """Subclass of `_JobDirectory` used as a helper for tests."""
306*9c5db199SXin Li
307*9c5db199SXin Li    GLOB_PATTERN = '[0-9]*-*'
308*9c5db199SXin Li
309*9c5db199SXin Li
310*9c5db199SXin Li    def __init__(self, resultsdir):
311*9c5db199SXin Li        """Create new job in initial state."""
312*9c5db199SXin Li        super(_MockJobDirectory, self).__init__(resultsdir)
313*9c5db199SXin Li        self._timestamp = None
314*9c5db199SXin Li        self.queue_args = [resultsdir, os.path.dirname(resultsdir), self._timestamp]
315*9c5db199SXin Li
316*9c5db199SXin Li
317*9c5db199SXin Li    def get_timestamp_if_finished(self):
318*9c5db199SXin Li        return self._timestamp
319*9c5db199SXin Li
320*9c5db199SXin Li
321*9c5db199SXin Li    def set_finished(self, days_old):
322*9c5db199SXin Li        """Make this job appear to be finished.
323*9c5db199SXin Li
324*9c5db199SXin Li        After calling this function, calls to `enqueue_offload()`
325*9c5db199SXin Li        will find this job as finished, but not expired and ready
326*9c5db199SXin Li        for offload.  Note that when `days_old` is 0,
327*9c5db199SXin Li        `enqueue_offload()` will treat a finished job as eligible
328*9c5db199SXin Li        for offload.
329*9c5db199SXin Li
330*9c5db199SXin Li        @param days_old The value of the `days_old` parameter that
331*9c5db199SXin Li                        will be passed to `enqueue_offload()` for
332*9c5db199SXin Li                        testing.
333*9c5db199SXin Li
334*9c5db199SXin Li        """
335*9c5db199SXin Li        self._timestamp = jd_test.make_timestamp(days_old, False)
336*9c5db199SXin Li        self.queue_args[2] = self._timestamp
337*9c5db199SXin Li
338*9c5db199SXin Li
339*9c5db199SXin Li    def set_expired(self, days_old):
340*9c5db199SXin Li        """Make this job eligible to be offloaded.
341*9c5db199SXin Li
342*9c5db199SXin Li        After calling this function, calls to `offload` will attempt
343*9c5db199SXin Li        to offload this job.
344*9c5db199SXin Li
345*9c5db199SXin Li        @param days_old The value of the `days_old` parameter that
346*9c5db199SXin Li                        will be passed to `enqueue_offload()` for
347*9c5db199SXin Li                        testing.
348*9c5db199SXin Li
349*9c5db199SXin Li        """
350*9c5db199SXin Li        self._timestamp = jd_test.make_timestamp(days_old, True)
351*9c5db199SXin Li        self.queue_args[2] = self._timestamp
352*9c5db199SXin Li
353*9c5db199SXin Li
354*9c5db199SXin Li    def set_incomplete(self):
355*9c5db199SXin Li        """Make this job appear to have failed offload just once."""
356*9c5db199SXin Li        self.offload_count += 1
357*9c5db199SXin Li        self.first_offload_start = time.time()
358*9c5db199SXin Li        if not os.path.isdir(self.dirname):
359*9c5db199SXin Li            os.mkdir(self.dirname)
360*9c5db199SXin Li
361*9c5db199SXin Li
362*9c5db199SXin Li    def set_reportable(self):
363*9c5db199SXin Li        """Make this job be reportable."""
364*9c5db199SXin Li        self.set_incomplete()
365*9c5db199SXin Li        self.offload_count += 1
366*9c5db199SXin Li
367*9c5db199SXin Li
368*9c5db199SXin Li    def set_complete(self):
369*9c5db199SXin Li        """Make this job be completed."""
370*9c5db199SXin Li        self.offload_count += 1
371*9c5db199SXin Li        if os.path.isdir(self.dirname):
372*9c5db199SXin Li            os.rmdir(self.dirname)
373*9c5db199SXin Li
374*9c5db199SXin Li
375*9c5db199SXin Li    def process_gs_instructions(self):
376*9c5db199SXin Li        """Always still offload the job directory."""
377*9c5db199SXin Li        return True
378*9c5db199SXin Li
379*9c5db199SXin Li
380*9c5db199SXin Liclass CommandListTests(unittest.TestCase):
381*9c5db199SXin Li    """Tests for `_get_cmd_list()`."""
382*9c5db199SXin Li
383*9c5db199SXin Li    def _command_list_assertions(self, job, use_rsync=True, multi=False):
384*9c5db199SXin Li        """Call `_get_cmd_list()` and check the return value.
385*9c5db199SXin Li
386*9c5db199SXin Li        Check the following assertions:
387*9c5db199SXin Li          * The command name (argv[0]) is 'gsutil'.
388*9c5db199SXin Li          * '-m' option (argv[1]) is on when the argument, multi, is True.
389*9c5db199SXin Li          * The arguments contain the 'cp' subcommand.
390*9c5db199SXin Li          * The next-to-last argument (the source directory) is the
391*9c5db199SXin Li            job's `queue_args[0]`.
392*9c5db199SXin Li          * The last argument (the destination URL) is the job's
393*9c5db199SXin Li            'queue_args[1]'.
394*9c5db199SXin Li
395*9c5db199SXin Li        @param job A job with properly calculated arguments to
396*9c5db199SXin Li                   `_get_cmd_list()`
397*9c5db199SXin Li        @param use_rsync True when using 'rsync'. False when using 'cp'.
398*9c5db199SXin Li        @param multi True when using '-m' option for gsutil.
399*9c5db199SXin Li
400*9c5db199SXin Li        """
401*9c5db199SXin Li        test_bucket_uri = 'gs://a-test-bucket'
402*9c5db199SXin Li
403*9c5db199SXin Li        gs_offloader.USE_RSYNC_ENABLED = use_rsync
404*9c5db199SXin Li
405*9c5db199SXin Li        gs_path = os.path.join(test_bucket_uri, job.queue_args[1])
406*9c5db199SXin Li
407*9c5db199SXin Li        command = gs_offloader._get_cmd_list(
408*9c5db199SXin Li                multi, job.queue_args[0], gs_path)
409*9c5db199SXin Li
410*9c5db199SXin Li        self.assertEqual(command[0], 'gsutil')
411*9c5db199SXin Li        if multi:
412*9c5db199SXin Li            self.assertEqual(command[1], '-m')
413*9c5db199SXin Li        self.assertEqual(command[-2], job.queue_args[0])
414*9c5db199SXin Li
415*9c5db199SXin Li        if use_rsync:
416*9c5db199SXin Li            self.assertTrue('rsync' in command)
417*9c5db199SXin Li            self.assertEqual(command[-1],
418*9c5db199SXin Li                             os.path.join(test_bucket_uri, job.queue_args[0]))
419*9c5db199SXin Li        else:
420*9c5db199SXin Li            self.assertTrue('cp' in command)
421*9c5db199SXin Li            self.assertEqual(command[-1],
422*9c5db199SXin Li                             os.path.join(test_bucket_uri, job.queue_args[1]))
423*9c5db199SXin Li
424*9c5db199SXin Li        finish_command = gs_offloader._get_finish_cmd_list(gs_path)
425*9c5db199SXin Li        self.assertEqual(finish_command[0], 'gsutil')
426*9c5db199SXin Li        self.assertEqual(finish_command[1], 'cp')
427*9c5db199SXin Li        self.assertEqual(finish_command[2], '/dev/null')
428*9c5db199SXin Li        self.assertEqual(finish_command[3],
429*9c5db199SXin Li                         os.path.join(gs_path, '.finished_offload'))
430*9c5db199SXin Li
431*9c5db199SXin Li
432*9c5db199SXin Li    def test__get_cmd_list_regular(self):
433*9c5db199SXin Li        """Test `_get_cmd_list()` as for a regular job."""
434*9c5db199SXin Li        job = _MockJobDirectory('118-debug')
435*9c5db199SXin Li        self._command_list_assertions(job)
436*9c5db199SXin Li
437*9c5db199SXin Li
438*9c5db199SXin Li    def test__get_cmd_list_special(self):
439*9c5db199SXin Li        """Test `_get_cmd_list()` as for a special job."""
440*9c5db199SXin Li        job = _MockJobDirectory('hosts/host1/118-reset')
441*9c5db199SXin Li        self._command_list_assertions(job)
442*9c5db199SXin Li
443*9c5db199SXin Li
444*9c5db199SXin Li    def test_get_cmd_list_regular_no_rsync(self):
445*9c5db199SXin Li        """Test `_get_cmd_list()` as for a regular job."""
446*9c5db199SXin Li        job = _MockJobDirectory('118-debug')
447*9c5db199SXin Li        self._command_list_assertions(job, use_rsync=False)
448*9c5db199SXin Li
449*9c5db199SXin Li
450*9c5db199SXin Li    def test_get_cmd_list_special_no_rsync(self):
451*9c5db199SXin Li        """Test `_get_cmd_list()` as for a special job."""
452*9c5db199SXin Li        job = _MockJobDirectory('hosts/host1/118-reset')
453*9c5db199SXin Li        self._command_list_assertions(job, use_rsync=False)
454*9c5db199SXin Li
455*9c5db199SXin Li
456*9c5db199SXin Li    def test_get_cmd_list_regular_multi(self):
457*9c5db199SXin Li        """Test `_get_cmd_list()` as for a regular job with True multi."""
458*9c5db199SXin Li        job = _MockJobDirectory('118-debug')
459*9c5db199SXin Li        self._command_list_assertions(job, multi=True)
460*9c5db199SXin Li
461*9c5db199SXin Li
462*9c5db199SXin Li    def test__get_cmd_list_special_multi(self):
463*9c5db199SXin Li        """Test `_get_cmd_list()` as for a special job with True multi."""
464*9c5db199SXin Li        job = _MockJobDirectory('hosts/host1/118-reset')
465*9c5db199SXin Li        self._command_list_assertions(job, multi=True)
466*9c5db199SXin Li
467*9c5db199SXin Li
468*9c5db199SXin Liclass _TempResultsDirTestCase(unittest.TestCase):
469*9c5db199SXin Li    """Mixin class for tests using a temporary results directory."""
470*9c5db199SXin Li
471*9c5db199SXin Li    REGULAR_JOBLIST = [
472*9c5db199SXin Li        '111-fubar', '112-fubar', '113-fubar', '114-snafu']
473*9c5db199SXin Li    HOST_LIST = ['host1', 'host2', 'host3']
474*9c5db199SXin Li    SPECIAL_JOBLIST = [
475*9c5db199SXin Li        'hosts/host1/333-reset', 'hosts/host1/334-reset',
476*9c5db199SXin Li        'hosts/host2/444-reset', 'hosts/host3/555-reset']
477*9c5db199SXin Li
478*9c5db199SXin Li
479*9c5db199SXin Li    def setUp(self):
480*9c5db199SXin Li        super(_TempResultsDirTestCase, self).setUp()
481*9c5db199SXin Li        self._resultsroot = tempfile.mkdtemp()
482*9c5db199SXin Li        self._cwd = os.getcwd()
483*9c5db199SXin Li        os.chdir(self._resultsroot)
484*9c5db199SXin Li
485*9c5db199SXin Li
486*9c5db199SXin Li    def tearDown(self):
487*9c5db199SXin Li        os.chdir(self._cwd)
488*9c5db199SXin Li        shutil.rmtree(self._resultsroot)
489*9c5db199SXin Li        super(_TempResultsDirTestCase, self).tearDown()
490*9c5db199SXin Li
491*9c5db199SXin Li
492*9c5db199SXin Li    def make_job(self, jobdir):
493*9c5db199SXin Li        """Create a job with results in `self._resultsroot`.
494*9c5db199SXin Li
495*9c5db199SXin Li        @param jobdir Name of the subdirectory to be created in
496*9c5db199SXin Li                      `self._resultsroot`.
497*9c5db199SXin Li
498*9c5db199SXin Li        """
499*9c5db199SXin Li        os.makedirs(jobdir)
500*9c5db199SXin Li        return _MockJobDirectory(jobdir)
501*9c5db199SXin Li
502*9c5db199SXin Li
503*9c5db199SXin Li    def make_job_hierarchy(self):
504*9c5db199SXin Li        """Create a sample hierarchy of job directories.
505*9c5db199SXin Li
506*9c5db199SXin Li        `self.REGULAR_JOBLIST` is a list of directories for regular
507*9c5db199SXin Li        jobs to be created; `self.SPECIAL_JOBLIST` is a list of
508*9c5db199SXin Li        directories for special jobs to be created.
509*9c5db199SXin Li
510*9c5db199SXin Li        """
511*9c5db199SXin Li        for d in self.REGULAR_JOBLIST:
512*9c5db199SXin Li            os.mkdir(d)
513*9c5db199SXin Li        hostsdir = 'hosts'
514*9c5db199SXin Li        os.mkdir(hostsdir)
515*9c5db199SXin Li        for host in self.HOST_LIST:
516*9c5db199SXin Li            os.mkdir(os.path.join(hostsdir, host))
517*9c5db199SXin Li        for d in self.SPECIAL_JOBLIST:
518*9c5db199SXin Li            os.mkdir(d)
519*9c5db199SXin Li
520*9c5db199SXin Li
521*9c5db199SXin Liclass _TempResultsDirTestBase(_TempResultsDirTestCase, unittest.TestCase):
522*9c5db199SXin Li    """Base test class for tests using a temporary results directory."""
523*9c5db199SXin Li
524*9c5db199SXin Li
525*9c5db199SXin Liclass FailedOffloadsLogTest(_TempResultsDirTestBase):
526*9c5db199SXin Li    """Test the formatting of failed offloads log file."""
527*9c5db199SXin Li    # Below is partial sample of a failed offload log file.  This text is
528*9c5db199SXin Li    # deliberately hard-coded and then parsed to create the test data; the idea
529*9c5db199SXin Li    # is to make sure the actual text format will be reviewed by a human being.
530*9c5db199SXin Li    #
531*9c5db199SXin Li    # first offload      count  directory
532*9c5db199SXin Li    # --+----1----+----  ----+ ----+----1----+----2----+----3
533*9c5db199SXin Li    _SAMPLE_DIRECTORIES_REPORT = '''\
534*9c5db199SXin Li    =================== ======  ==============================
535*9c5db199SXin Li    2014-03-14 15:09:26      1  118-fubar
536*9c5db199SXin Li    2014-03-14 15:19:23      2  117-fubar
537*9c5db199SXin Li    2014-03-14 15:29:20      6  116-fubar
538*9c5db199SXin Li    2014-03-14 15:39:17     24  115-fubar
539*9c5db199SXin Li    2014-03-14 15:49:14    120  114-fubar
540*9c5db199SXin Li    2014-03-14 15:59:11    720  113-fubar
541*9c5db199SXin Li    2014-03-14 16:09:08   5040  112-fubar
542*9c5db199SXin Li    2014-03-14 16:19:05  40320  111-fubar
543*9c5db199SXin Li    '''
544*9c5db199SXin Li
545*9c5db199SXin Li    def setUp(self):
546*9c5db199SXin Li        super(FailedOffloadsLogTest, self).setUp()
547*9c5db199SXin Li        self._offloader = gs_offloader.Offloader(_get_options([]))
548*9c5db199SXin Li        self._joblist = []
549*9c5db199SXin Li        for line in self._SAMPLE_DIRECTORIES_REPORT.split('\n')[1 : -1]:
550*9c5db199SXin Li            date_, time_, count, dir_ = line.split()
551*9c5db199SXin Li            job = _MockJobDirectory(dir_)
552*9c5db199SXin Li            job.offload_count = int(count)
553*9c5db199SXin Li            timestruct = time.strptime("%s %s" % (date_, time_),
554*9c5db199SXin Li                                       gs_offloader.FAILED_OFFLOADS_TIME_FORMAT)
555*9c5db199SXin Li            job.first_offload_start = time.mktime(timestruct)
556*9c5db199SXin Li            # enter the jobs in reverse order, to make sure we
557*9c5db199SXin Li            # test that the output will be sorted.
558*9c5db199SXin Li            self._joblist.insert(0, job)
559*9c5db199SXin Li
560*9c5db199SXin Li
561*9c5db199SXin Li    def assert_report_well_formatted(self, report_file):
562*9c5db199SXin Li        """Assert that report file is well formatted.
563*9c5db199SXin Li
564*9c5db199SXin Li        @param report_file: Path to report file
565*9c5db199SXin Li        """
566*9c5db199SXin Li        with open(report_file, 'r') as f:
567*9c5db199SXin Li            report_lines = f.read().split()
568*9c5db199SXin Li
569*9c5db199SXin Li        for end_of_header_index in range(len(report_lines)):
570*9c5db199SXin Li            if report_lines[end_of_header_index].startswith('=='):
571*9c5db199SXin Li                break
572*9c5db199SXin Li        self.assertLess(end_of_header_index, len(report_lines),
573*9c5db199SXin Li                        'Failed to find end-of-header marker in the report')
574*9c5db199SXin Li
575*9c5db199SXin Li        relevant_lines = report_lines[end_of_header_index:]
576*9c5db199SXin Li        expected_lines = self._SAMPLE_DIRECTORIES_REPORT.split()
577*9c5db199SXin Li        self.assertListEqual(relevant_lines, expected_lines)
578*9c5db199SXin Li
579*9c5db199SXin Li
580*9c5db199SXin Li    def test_failed_offload_log_format(self):
581*9c5db199SXin Li        """Trigger an e-mail report and check its contents."""
582*9c5db199SXin Li        log_file = os.path.join(self._resultsroot, 'failed_log')
583*9c5db199SXin Li        report = self._offloader._log_failed_jobs_locally(self._joblist,
584*9c5db199SXin Li                                                          log_file=log_file)
585*9c5db199SXin Li        self.assert_report_well_formatted(log_file)
586*9c5db199SXin Li
587*9c5db199SXin Li
588*9c5db199SXin Li    def test_failed_offload_file_overwrite(self):
589*9c5db199SXin Li        """Verify that we can saefly overwrite the log file."""
590*9c5db199SXin Li        log_file = os.path.join(self._resultsroot, 'failed_log')
591*9c5db199SXin Li        with open(log_file, 'w') as f:
592*9c5db199SXin Li            f.write('boohoohoo')
593*9c5db199SXin Li        report = self._offloader._log_failed_jobs_locally(self._joblist,
594*9c5db199SXin Li                                                          log_file=log_file)
595*9c5db199SXin Li        self.assert_report_well_formatted(log_file)
596*9c5db199SXin Li
597*9c5db199SXin Li
598*9c5db199SXin Liclass OffloadDirectoryTests(_TempResultsDirTestBase):
599*9c5db199SXin Li    """Tests for `offload_dir()`."""
600*9c5db199SXin Li
601*9c5db199SXin Li    def setUp(self):
602*9c5db199SXin Li        super(OffloadDirectoryTests, self).setUp()
603*9c5db199SXin Li        # offload_dir() logs messages; silence them.
604*9c5db199SXin Li        self._saved_loglevel = logging.getLogger().getEffectiveLevel()
605*9c5db199SXin Li        logging.getLogger().setLevel(logging.CRITICAL+1)
606*9c5db199SXin Li        self._job = self.make_job(self.REGULAR_JOBLIST[0])
607*9c5db199SXin Li
608*9c5db199SXin Li        cmd_list_patcher = mock.patch.object(gs_offloader, '_get_cmd_list')
609*9c5db199SXin Li        cmd_list_patch = cmd_list_patcher.start()
610*9c5db199SXin Li        self.addCleanup(cmd_list_patcher.stop)
611*9c5db199SXin Li
612*9c5db199SXin Li        alarm = mock.patch('signal.alarm', return_value=0)
613*9c5db199SXin Li        alarm.start()
614*9c5db199SXin Li        self.addCleanup(alarm.stop)
615*9c5db199SXin Li
616*9c5db199SXin Li        cmd_list_patcher = mock.patch.object(models.test, 'parse_job_keyval')
617*9c5db199SXin Li        cmd_list_patch = cmd_list_patcher.start()
618*9c5db199SXin Li        self.addCleanup(cmd_list_patcher.stop)
619*9c5db199SXin Li
620*9c5db199SXin Li        self.should_remove_sarming_req_dir = False
621*9c5db199SXin Li
622*9c5db199SXin Li
623*9c5db199SXin Li    def tearDown(self):
624*9c5db199SXin Li        logging.getLogger().setLevel(self._saved_loglevel)
625*9c5db199SXin Li        super(OffloadDirectoryTests, self).tearDown()
626*9c5db199SXin Li
627*9c5db199SXin Li    def _mock_create_marker_file(self):
628*9c5db199SXin Li        open_patcher = mock.patch.object(six.moves.builtins, 'open')
629*9c5db199SXin Li        open_patch = open_patcher.start()
630*9c5db199SXin Li        self.addCleanup(open_patcher.stop)
631*9c5db199SXin Li
632*9c5db199SXin Li        open.return_value = mock.MagicMock()
633*9c5db199SXin Li
634*9c5db199SXin Li
635*9c5db199SXin Li    def _mock_offload_dir_calls(self, command, queue_args,
636*9c5db199SXin Li                                marker_initially_exists=False):
637*9c5db199SXin Li        """Mock out the calls needed by `offload_dir()`.
638*9c5db199SXin Li
639*9c5db199SXin Li        This covers only the calls made when there is no timeout.
640*9c5db199SXin Li
641*9c5db199SXin Li        @param command Command list to be returned by the mocked
642*9c5db199SXin Li                       call to `_get_cmd_list()`.
643*9c5db199SXin Li
644*9c5db199SXin Li        """
645*9c5db199SXin Li        isfile_patcher = mock.patch.object(os.path, 'isfile')
646*9c5db199SXin Li        isfile_patcher.start()
647*9c5db199SXin Li        self.addCleanup(isfile_patcher.stop)
648*9c5db199SXin Li
649*9c5db199SXin Li        os.path.isfile.return_value = marker_initially_exists
650*9c5db199SXin Li        command.append(queue_args[0])
651*9c5db199SXin Li        gs_offloader._get_cmd_list(
652*9c5db199SXin Li                False, queue_args[0],
653*9c5db199SXin Li                '%s%s' % (utils.DEFAULT_OFFLOAD_GSURI,
654*9c5db199SXin Li                          queue_args[1])).AndReturn(command)
655*9c5db199SXin Li
656*9c5db199SXin Li
657*9c5db199SXin Li    def _run_offload_dir(self, should_succeed, delete_age):
658*9c5db199SXin Li        """Make one call to `offload_dir()`.
659*9c5db199SXin Li
660*9c5db199SXin Li        The caller ensures all mocks are set up already.
661*9c5db199SXin Li
662*9c5db199SXin Li        @param should_succeed True iff the call to `offload_dir()`
663*9c5db199SXin Li                              is expected to succeed and remove the
664*9c5db199SXin Li                              offloaded job directory.
665*9c5db199SXin Li
666*9c5db199SXin Li        """
667*9c5db199SXin Li        gs_offloader.GSOffloader(
668*9c5db199SXin Li                utils.DEFAULT_OFFLOAD_GSURI, False, delete_age).offload(
669*9c5db199SXin Li                        self._job.queue_args[0],
670*9c5db199SXin Li                        self._job.queue_args[1],
671*9c5db199SXin Li                        self._job.queue_args[2])
672*9c5db199SXin Li        self.assertEqual(not should_succeed,
673*9c5db199SXin Li                         os.path.isdir(self._job.queue_args[0]))
674*9c5db199SXin Li        swarming_req_dir = gs_offloader._get_swarming_req_dir(
675*9c5db199SXin Li                self._job.queue_args[0])
676*9c5db199SXin Li        if swarming_req_dir:
677*9c5db199SXin Li            self.assertEqual(not self.should_remove_sarming_req_dir,
678*9c5db199SXin Li                             os.path.exists(swarming_req_dir))
679*9c5db199SXin Li
680*9c5db199SXin Li
681*9c5db199SXin Li    def test_offload_success(self):
682*9c5db199SXin Li        """Test that `offload_dir()` can succeed correctly."""
683*9c5db199SXin Li        self._mock_offload_dir_calls(['test', '-d'],
684*9c5db199SXin Li                                     self._job.queue_args)
685*9c5db199SXin Li        os.path.isfile.return_value = True
686*9c5db199SXin Li        self._mock_create_marker_file()
687*9c5db199SXin Li        self._run_offload_dir(True, 0)
688*9c5db199SXin Li
689*9c5db199SXin Li
690*9c5db199SXin Li    def test_offload_failure(self):
691*9c5db199SXin Li        """Test that `offload_dir()` can fail correctly."""
692*9c5db199SXin Li        self._mock_offload_dir_calls(['test', '!', '-d'],
693*9c5db199SXin Li                                     self._job.queue_args)
694*9c5db199SXin Li        self._run_offload_dir(False, 0)
695*9c5db199SXin Li
696*9c5db199SXin Li
697*9c5db199SXin Li    def test_offload_swarming_req_dir_remove(self):
698*9c5db199SXin Li        """Test that `offload_dir()` can prune the empty swarming task dir."""
699*9c5db199SXin Li        should_remove = os.path.join('results', 'swarming-123abc0')
700*9c5db199SXin Li        self._job = self.make_job(os.path.join(should_remove, '1'))
701*9c5db199SXin Li        self._mock_offload_dir_calls(['test', '-d'],
702*9c5db199SXin Li                                     self._job.queue_args)
703*9c5db199SXin Li
704*9c5db199SXin Li        os.path.isfile.return_value = True
705*9c5db199SXin Li        self.should_remove_sarming_req_dir = True
706*9c5db199SXin Li        self._mock_create_marker_file()
707*9c5db199SXin Li        self._run_offload_dir(True, 0)
708*9c5db199SXin Li
709*9c5db199SXin Li
710*9c5db199SXin Li    def test_offload_swarming_req_dir_exist(self):
711*9c5db199SXin Li        """Test that `offload_dir()` keeps the non-empty swarming task dir."""
712*9c5db199SXin Li        should_not_remove = os.path.join('results', 'swarming-456edf0')
713*9c5db199SXin Li        self._job = self.make_job(os.path.join(should_not_remove, '1'))
714*9c5db199SXin Li        self.make_job(os.path.join(should_not_remove, '2'))
715*9c5db199SXin Li        self._mock_offload_dir_calls(['test', '-d'],
716*9c5db199SXin Li                                     self._job.queue_args)
717*9c5db199SXin Li
718*9c5db199SXin Li        os.path.isfile.return_value = True
719*9c5db199SXin Li        self.should_remove_sarming_req_dir = False
720*9c5db199SXin Li        self._mock_create_marker_file()
721*9c5db199SXin Li        self._run_offload_dir(True, 0)
722*9c5db199SXin Li
723*9c5db199SXin Li
724*9c5db199SXin Li    def test_sanitize_dir(self):
725*9c5db199SXin Li        """Test that folder/file name with invalid character can be corrected.
726*9c5db199SXin Li        """
727*9c5db199SXin Li        results_folder = tempfile.mkdtemp()
728*9c5db199SXin Li        invalid_chars = '_'.join(['[', ']', '*', '?', '#'])
729*9c5db199SXin Li        invalid_files = []
730*9c5db199SXin Li        invalid_folder_name = 'invalid_name_folder_%s' % invalid_chars
731*9c5db199SXin Li        invalid_folder = os.path.join(
732*9c5db199SXin Li                results_folder,
733*9c5db199SXin Li                invalid_folder_name)
734*9c5db199SXin Li        invalid_files.append(os.path.join(
735*9c5db199SXin Li                invalid_folder,
736*9c5db199SXin Li                'invalid_name_file_%s' % invalid_chars))
737*9c5db199SXin Li        good_folder = os.path.join(results_folder, 'valid_name_folder')
738*9c5db199SXin Li        good_file = os.path.join(good_folder, 'valid_name_file')
739*9c5db199SXin Li        for folder in [invalid_folder, good_folder]:
740*9c5db199SXin Li            os.makedirs(folder)
741*9c5db199SXin Li        for f in invalid_files + [good_file]:
742*9c5db199SXin Li            with open(f, 'w'):
743*9c5db199SXin Li                pass
744*9c5db199SXin Li        # check that broken symlinks don't break sanitization
745*9c5db199SXin Li        symlink = os.path.join(invalid_folder, 'broken-link')
746*9c5db199SXin Li        os.symlink(os.path.join(results_folder, 'no-such-file'),
747*9c5db199SXin Li                   symlink)
748*9c5db199SXin Li        fifo1 = os.path.join(results_folder, 'test_fifo1')
749*9c5db199SXin Li        fifo2 = os.path.join(good_folder, 'test_fifo2')
750*9c5db199SXin Li        fifo3 = os.path.join(invalid_folder, 'test_fifo3')
751*9c5db199SXin Li        invalid_fifo4_name = 'test_fifo4_%s' % invalid_chars
752*9c5db199SXin Li        fifo4 = os.path.join(invalid_folder, invalid_fifo4_name)
753*9c5db199SXin Li        os.mkfifo(fifo1)
754*9c5db199SXin Li        os.mkfifo(fifo2)
755*9c5db199SXin Li        os.mkfifo(fifo3)
756*9c5db199SXin Li        os.mkfifo(fifo4)
757*9c5db199SXin Li        gs_offloader.sanitize_dir(results_folder)
758*9c5db199SXin Li        for _, dirs, files in os.walk(results_folder):
759*9c5db199SXin Li            for name in dirs + files:
760*9c5db199SXin Li                self.assertEqual(name, gslib.escape(name))
761*9c5db199SXin Li                for c in name:
762*9c5db199SXin Li                    self.assertFalse(c in ['[', ']', '*', '?', '#'])
763*9c5db199SXin Li        self.assertTrue(os.path.exists(good_file))
764*9c5db199SXin Li
765*9c5db199SXin Li        self.assertTrue(os.path.exists(fifo1))
766*9c5db199SXin Li        self.assertFalse(is_fifo(fifo1))
767*9c5db199SXin Li        self.assertTrue(os.path.exists(fifo2))
768*9c5db199SXin Li        self.assertFalse(is_fifo(fifo2))
769*9c5db199SXin Li        corrected_folder = os.path.join(
770*9c5db199SXin Li                results_folder, gslib.escape(invalid_folder_name))
771*9c5db199SXin Li        corrected_fifo3 = os.path.join(
772*9c5db199SXin Li                corrected_folder,
773*9c5db199SXin Li                'test_fifo3')
774*9c5db199SXin Li        self.assertFalse(os.path.exists(fifo3))
775*9c5db199SXin Li        self.assertTrue(os.path.exists(corrected_fifo3))
776*9c5db199SXin Li        self.assertFalse(is_fifo(corrected_fifo3))
777*9c5db199SXin Li        corrected_fifo4 = os.path.join(
778*9c5db199SXin Li                corrected_folder, gslib.escape(invalid_fifo4_name))
779*9c5db199SXin Li        self.assertFalse(os.path.exists(fifo4))
780*9c5db199SXin Li        self.assertTrue(os.path.exists(corrected_fifo4))
781*9c5db199SXin Li        self.assertFalse(is_fifo(corrected_fifo4))
782*9c5db199SXin Li
783*9c5db199SXin Li        corrected_symlink = os.path.join(
784*9c5db199SXin Li                corrected_folder,
785*9c5db199SXin Li                'broken-link')
786*9c5db199SXin Li        self.assertFalse(os.path.lexists(symlink))
787*9c5db199SXin Li        self.assertTrue(os.path.exists(corrected_symlink))
788*9c5db199SXin Li        self.assertFalse(os.path.islink(corrected_symlink))
789*9c5db199SXin Li        shutil.rmtree(results_folder)
790*9c5db199SXin Li
791*9c5db199SXin Li
792*9c5db199SXin Li    def check_limit_file_count(self, is_test_job=True):
793*9c5db199SXin Li        """Test that folder with too many files can be compressed.
794*9c5db199SXin Li
795*9c5db199SXin Li        @param is_test_job: True to check the method with test job result
796*9c5db199SXin Li                            folder. Set to False for special task folder.
797*9c5db199SXin Li        """
798*9c5db199SXin Li        results_folder = tempfile.mkdtemp()
799*9c5db199SXin Li        host_folder = os.path.join(
800*9c5db199SXin Li                results_folder,
801*9c5db199SXin Li                'lab1-host1' if is_test_job else 'hosts/lab1-host1/1-repair')
802*9c5db199SXin Li        debug_folder = os.path.join(host_folder, 'debug')
803*9c5db199SXin Li        sysinfo_folder = os.path.join(host_folder, 'sysinfo')
804*9c5db199SXin Li        for folder in [debug_folder, sysinfo_folder]:
805*9c5db199SXin Li            os.makedirs(folder)
806*9c5db199SXin Li            for i in range(10):
807*9c5db199SXin Li                with open(os.path.join(folder, str(i)), 'w') as f:
808*9c5db199SXin Li                    f.write('test')
809*9c5db199SXin Li
810*9c5db199SXin Li        gs_offloader._MAX_FILE_COUNT = 100
811*9c5db199SXin Li        gs_offloader.limit_file_count(
812*9c5db199SXin Li                results_folder if is_test_job else host_folder)
813*9c5db199SXin Li        self.assertTrue(os.path.exists(sysinfo_folder))
814*9c5db199SXin Li
815*9c5db199SXin Li        gs_offloader._MAX_FILE_COUNT = 10
816*9c5db199SXin Li        gs_offloader.limit_file_count(
817*9c5db199SXin Li                results_folder if is_test_job else host_folder)
818*9c5db199SXin Li        self.assertFalse(os.path.exists(sysinfo_folder))
819*9c5db199SXin Li        self.assertTrue(os.path.exists(sysinfo_folder + '.tgz'))
820*9c5db199SXin Li        self.assertTrue(os.path.exists(debug_folder))
821*9c5db199SXin Li
822*9c5db199SXin Li        shutil.rmtree(results_folder)
823*9c5db199SXin Li
824*9c5db199SXin Li
825*9c5db199SXin Li    def test_limit_file_count(self):
826*9c5db199SXin Li        """Test that folder with too many files can be compressed.
827*9c5db199SXin Li        """
828*9c5db199SXin Li        self.check_limit_file_count(is_test_job=True)
829*9c5db199SXin Li        self.check_limit_file_count(is_test_job=False)
830*9c5db199SXin Li
831*9c5db199SXin Li
832*9c5db199SXin Li    def test_get_metrics_fields(self):
833*9c5db199SXin Li        """Test method _get_metrics_fields."""
834*9c5db199SXin Li        results_folder, host_folder = self._create_results_folder()
835*9c5db199SXin Li        models.test.parse_job_keyval.return_value = ({
836*9c5db199SXin Li                'build': 'veyron_minnie-cheets-release/R52-8248.0.0',
837*9c5db199SXin Li                'parent_job_id': 'p_id',
838*9c5db199SXin Li                'suite': 'arc-cts'
839*9c5db199SXin Li        })
840*9c5db199SXin Li        try:
841*9c5db199SXin Li            self.assertEqual({'board': 'veyron_minnie-cheets',
842*9c5db199SXin Li                              'milestone': 'R52'},
843*9c5db199SXin Li                             gs_offloader._get_metrics_fields(host_folder))
844*9c5db199SXin Li        finally:
845*9c5db199SXin Li            shutil.rmtree(results_folder)
846*9c5db199SXin Li
847*9c5db199SXin Li
848*9c5db199SXin Li    def _create_results_folder(self):
849*9c5db199SXin Li        results_folder = tempfile.mkdtemp()
850*9c5db199SXin Li        host_folder = os.path.join(results_folder, 'chromeos4-row9-rack11-host22')
851*9c5db199SXin Li
852*9c5db199SXin Li        # Build host keyvals set to parse model info.
853*9c5db199SXin Li        host_info_path = os.path.join(host_folder, 'host_keyvals')
854*9c5db199SXin Li        dir_to_create = '/'
855*9c5db199SXin Li        for tdir in host_info_path.split('/'):
856*9c5db199SXin Li            dir_to_create = os.path.join(dir_to_create, tdir)
857*9c5db199SXin Li            if not os.path.exists(dir_to_create):
858*9c5db199SXin Li                os.mkdir(dir_to_create)
859*9c5db199SXin Li        with open(os.path.join(host_info_path, 'chromeos4-row9-rack11-host22'), 'w') as store_file:
860*9c5db199SXin Li            store_file.write('labels=board%3Acoral,hw_video_acc_vp9,cros,'+
861*9c5db199SXin Li                             'hw_jpeg_acc_dec,bluetooth,model%3Arobo360,'+
862*9c5db199SXin Li                             'accel%3Acros-ec,'+
863*9c5db199SXin Li                             'sku%3Arobo360_IntelR_CeleronR_CPU_N3450_1_10GHz_4Gb')
864*9c5db199SXin Li
865*9c5db199SXin Li        # .autoserv_execute file is needed for the test results package to look
866*9c5db199SXin Li        # legit.
867*9c5db199SXin Li        autoserve_path = os.path.join(host_folder, '.autoserv_execute')
868*9c5db199SXin Li        with open(autoserve_path, 'w') as temp_file:
869*9c5db199SXin Li            temp_file.write(' ')
870*9c5db199SXin Li
871*9c5db199SXin Li        return (results_folder, host_folder)
872*9c5db199SXin Li
873*9c5db199SXin Li
874*9c5db199SXin Liclass OffladerConfigTests(_TempResultsDirTestBase):
875*9c5db199SXin Li    """Tests for the `Offloader` to follow side_effect config."""
876*9c5db199SXin Li
877*9c5db199SXin Li    def setUp(self):
878*9c5db199SXin Li        super(OffladerConfigTests, self).setUp()
879*9c5db199SXin Li        gs_offloader.GS_OFFLOADING_ENABLED = True
880*9c5db199SXin Li        gs_offloader.GS_OFFLOADER_MULTIPROCESSING = True
881*9c5db199SXin Li        self.dest_path = '/results'
882*9c5db199SXin Li
883*9c5db199SXin Li        metrics_fields_patcher = mock.patch.object(gs_offloader,
884*9c5db199SXin Li                                                   '_get_metrics_fields')
885*9c5db199SXin Li        metrics_fields_patcher.start()
886*9c5db199SXin Li        self.addCleanup(metrics_fields_patcher.stop)
887*9c5db199SXin Li
888*9c5db199SXin Li        offloadError_patcher = mock.patch.object(gs_offloader, '_OffloadError')
889*9c5db199SXin Li        offloadError_patcher.start()
890*9c5db199SXin Li        self.addCleanup(offloadError_patcher.stop)
891*9c5db199SXin Li
892*9c5db199SXin Li        offload_metrics_patcher = mock.patch.object(gs_offloader,
893*9c5db199SXin Li                                                    '_emit_offload_metrics')
894*9c5db199SXin Li        offload_metrics_patcher.start()
895*9c5db199SXin Li        self.addCleanup(offload_metrics_patcher.stop)
896*9c5db199SXin Li
897*9c5db199SXin Li        cmd_list_patcher = mock.patch.object(gs_offloader, '_get_cmd_list')
898*9c5db199SXin Li        cmd_list_patcher.start()
899*9c5db199SXin Li        self.addCleanup(cmd_list_patcher.stop)
900*9c5db199SXin Li
901*9c5db199SXin Li        Popen_patcher = mock.patch.object(subprocess, 'Popen')
902*9c5db199SXin Li        Popen_patcher.start()
903*9c5db199SXin Li        self.addCleanup(Popen_patcher.stop)
904*9c5db199SXin Li
905*9c5db199SXin Li        returncode_metric_patcher = mock.patch.object(
906*9c5db199SXin Li                gs_offloader, '_emit_gs_returncode_metric')
907*9c5db199SXin Li        returncode_metric_patcher.start()
908*9c5db199SXin Li        self.addCleanup(returncode_metric_patcher.stop)
909*9c5db199SXin Li
910*9c5db199SXin Li    def _run(self, results_dir, gs_bucket, expect_dest):
911*9c5db199SXin Li        stdout = os.path.join(results_dir, 'std.log')
912*9c5db199SXin Li        stderr = os.path.join(results_dir, 'std.err')
913*9c5db199SXin Li        config = {
914*9c5db199SXin Li            'tko': {
915*9c5db199SXin Li                'proxy_socket': '/file-system/foo-socket',
916*9c5db199SXin Li                'mysql_user': 'foo-user',
917*9c5db199SXin Li                'mysql_password_file': '/file-system/foo-password-file'
918*9c5db199SXin Li            },
919*9c5db199SXin Li            'google_storage': {
920*9c5db199SXin Li                'bucket': gs_bucket,
921*9c5db199SXin Li                'credentials_file': '/foo-creds'
922*9c5db199SXin Li            },
923*9c5db199SXin Li            'this_field_is_ignored': True
924*9c5db199SXin Li        }
925*9c5db199SXin Li        path = os.path.join(results_dir, 'side_effects_config.json')
926*9c5db199SXin Li        with open(path, 'w') as f:
927*9c5db199SXin Li            f.write(json.dumps(config))
928*9c5db199SXin Li        gs_offloader._get_metrics_fields(results_dir)
929*9c5db199SXin Li        gs_offloader._get_cmd_list.return_value = ['test', '-d', expect_dest]
930*9c5db199SXin Li        subprocess.Popen.side_effect = [
931*9c5db199SXin Li                _get_fake_process(), _get_fake_process()
932*9c5db199SXin Li        ]
933*9c5db199SXin Li        gs_offloader._OffloadError(mock.ANY)
934*9c5db199SXin Li        gs_offloader._emit_gs_returncode_metric.return_value = True
935*9c5db199SXin Li        gs_offloader._emit_offload_metrics.return_value = True
936*9c5db199SXin Li        sub_offloader = gs_offloader.GSOffloader(results_dir, True, 0, None)
937*9c5db199SXin Li        sub_offloader._try_offload(results_dir, self.dest_path, stdout, stderr)
938*9c5db199SXin Li        shutil.rmtree(results_dir)
939*9c5db199SXin Li
940*9c5db199SXin Li    def _verify(self, results_dir, gs_bucket, expect_dest):
941*9c5db199SXin Li        gs_offloader._get_cmd_list.assert_called_with(True, mock.ANY,
942*9c5db199SXin Li                                                      expect_dest)
943*9c5db199SXin Li
944*9c5db199SXin Li        stdout = os.path.join(results_dir, 'std.log')
945*9c5db199SXin Li        stderr = os.path.join(results_dir, 'std.err')
946*9c5db199SXin Li
947*9c5db199SXin Li        # x2
948*9c5db199SXin Li        subprocess_call = mock.call(mock.ANY, stdout=stdout, stderr=stderr)
949*9c5db199SXin Li        subprocess.Popen.assert_has_calls([subprocess_call, subprocess_call])
950*9c5db199SXin Li
951*9c5db199SXin Li    def test_skip_gs_prefix(self):
952*9c5db199SXin Li        """Test skip the 'gs://' prefix if already presented."""
953*9c5db199SXin Li        res = tempfile.mkdtemp()
954*9c5db199SXin Li        gs_bucket = 'gs://prod-bucket'
955*9c5db199SXin Li        expect_dest = gs_bucket + self.dest_path
956*9c5db199SXin Li        self._run(res, gs_bucket, expect_dest)
957*9c5db199SXin Li        self._verify(res, gs_bucket, expect_dest)
958*9c5db199SXin Li
959*9c5db199SXin Li
960*9c5db199SXin Liclass JobDirectoryOffloadTests(_TempResultsDirTestBase):
961*9c5db199SXin Li    """Tests for `_JobDirectory.enqueue_offload()`.
962*9c5db199SXin Li
963*9c5db199SXin Li    When testing with a `days_old` parameter of 0, we use
964*9c5db199SXin Li    `set_finished()` instead of `set_expired()`.  This causes the
965*9c5db199SXin Li    job's timestamp to be set in the future.  This is done so as
966*9c5db199SXin Li    to test that when `days_old` is 0, the job is always treated
967*9c5db199SXin Li    as eligible for offload, regardless of the timestamp's value.
968*9c5db199SXin Li
969*9c5db199SXin Li    Testing covers the following assertions:
970*9c5db199SXin Li     A. Each time `enqueue_offload()` is called, a message that
971*9c5db199SXin Li        includes the job's directory name will be logged using
972*9c5db199SXin Li        `logging.debug()`, regardless of whether the job was
973*9c5db199SXin Li        enqueued.  Nothing else is allowed to be logged.
974*9c5db199SXin Li     B. If the job is not eligible to be offloaded,
975*9c5db199SXin Li        `first_offload_start` and `offload_count` are 0.
976*9c5db199SXin Li     C. If the job is not eligible for offload, nothing is
977*9c5db199SXin Li        enqueued in `queue`.
978*9c5db199SXin Li     D. When the job is offloaded, `offload_count` increments
979*9c5db199SXin Li        each time.
980*9c5db199SXin Li     E. When the job is offloaded, the appropriate parameters are
981*9c5db199SXin Li        enqueued exactly once.
982*9c5db199SXin Li     F. The first time a job is offloaded, `first_offload_start` is
983*9c5db199SXin Li        set to the current time.
984*9c5db199SXin Li     G. `first_offload_start` only changes the first time that the
985*9c5db199SXin Li        job is offloaded.
986*9c5db199SXin Li
987*9c5db199SXin Li    The test cases below are designed to exercise all of the
988*9c5db199SXin Li    meaningful state transitions at least once.
989*9c5db199SXin Li
990*9c5db199SXin Li    """
991*9c5db199SXin Li
992*9c5db199SXin Li    def setUp(self):
993*9c5db199SXin Li        super(JobDirectoryOffloadTests, self).setUp()
994*9c5db199SXin Li        self._job = self.make_job(self.REGULAR_JOBLIST[0])
995*9c5db199SXin Li        self._queue = six.moves.queue.Queue()
996*9c5db199SXin Li
997*9c5db199SXin Li
998*9c5db199SXin Li    def _offload_unexpired_job(self, days_old):
999*9c5db199SXin Li        """Make calls to `enqueue_offload()` for an unexpired job.
1000*9c5db199SXin Li
1001*9c5db199SXin Li        This method tests assertions B and C that calling
1002*9c5db199SXin Li        `enqueue_offload()` has no effect.
1003*9c5db199SXin Li
1004*9c5db199SXin Li        """
1005*9c5db199SXin Li        self.assertEqual(self._job.offload_count, 0)
1006*9c5db199SXin Li        self.assertEqual(self._job.first_offload_start, 0)
1007*9c5db199SXin Li        gs_offloader._enqueue_offload(self._job, self._queue, days_old)
1008*9c5db199SXin Li        gs_offloader._enqueue_offload(self._job, self._queue, days_old)
1009*9c5db199SXin Li        self.assertTrue(self._queue.empty())
1010*9c5db199SXin Li        self.assertEqual(self._job.offload_count, 0)
1011*9c5db199SXin Li        self.assertEqual(self._job.first_offload_start, 0)
1012*9c5db199SXin Li
1013*9c5db199SXin Li
1014*9c5db199SXin Li    def _offload_expired_once(self, days_old, count):
1015*9c5db199SXin Li        """Make one call to `enqueue_offload()` for an expired job.
1016*9c5db199SXin Li
1017*9c5db199SXin Li        This method tests assertions D and E regarding side-effects
1018*9c5db199SXin Li        expected when a job is offloaded.
1019*9c5db199SXin Li
1020*9c5db199SXin Li        """
1021*9c5db199SXin Li        gs_offloader._enqueue_offload(self._job, self._queue, days_old)
1022*9c5db199SXin Li        self.assertEqual(self._job.offload_count, count)
1023*9c5db199SXin Li        self.assertFalse(self._queue.empty())
1024*9c5db199SXin Li        v = self._queue.get_nowait()
1025*9c5db199SXin Li        self.assertTrue(self._queue.empty())
1026*9c5db199SXin Li        self.assertEqual(v, self._job.queue_args)
1027*9c5db199SXin Li
1028*9c5db199SXin Li
1029*9c5db199SXin Li    def _offload_expired_job(self, days_old):
1030*9c5db199SXin Li        """Make calls to `enqueue_offload()` for a just-expired job.
1031*9c5db199SXin Li
1032*9c5db199SXin Li        This method directly tests assertions F and G regarding
1033*9c5db199SXin Li        side-effects on `first_offload_start`.
1034*9c5db199SXin Li
1035*9c5db199SXin Li        """
1036*9c5db199SXin Li        t0 = time.time()
1037*9c5db199SXin Li        self._offload_expired_once(days_old, 1)
1038*9c5db199SXin Li        t1 = self._job.first_offload_start
1039*9c5db199SXin Li        self.assertLessEqual(t1, time.time())
1040*9c5db199SXin Li        self.assertGreaterEqual(t1, t0)
1041*9c5db199SXin Li        self._offload_expired_once(days_old, 2)
1042*9c5db199SXin Li        self.assertEqual(self._job.first_offload_start, t1)
1043*9c5db199SXin Li        self._offload_expired_once(days_old, 3)
1044*9c5db199SXin Li        self.assertEqual(self._job.first_offload_start, t1)
1045*9c5db199SXin Li
1046*9c5db199SXin Li
1047*9c5db199SXin Li    def test_case_1_no_expiration(self):
1048*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` of 0.
1049*9c5db199SXin Li
1050*9c5db199SXin Li        This tests that offload works as expected if calls are
1051*9c5db199SXin Li        made both before and after the job becomes expired.
1052*9c5db199SXin Li
1053*9c5db199SXin Li        """
1054*9c5db199SXin Li        self._offload_unexpired_job(0)
1055*9c5db199SXin Li        self._job.set_finished(0)
1056*9c5db199SXin Li        self._offload_expired_job(0)
1057*9c5db199SXin Li
1058*9c5db199SXin Li
1059*9c5db199SXin Li    def test_case_2_no_expiration(self):
1060*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` of 0.
1061*9c5db199SXin Li
1062*9c5db199SXin Li        This tests that offload works as expected if calls are made
1063*9c5db199SXin Li        only after the job becomes expired.
1064*9c5db199SXin Li
1065*9c5db199SXin Li        """
1066*9c5db199SXin Li        self._job.set_finished(0)
1067*9c5db199SXin Li        self._offload_expired_job(0)
1068*9c5db199SXin Li
1069*9c5db199SXin Li
1070*9c5db199SXin Li    def test_case_1_with_expiration(self):
1071*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
1072*9c5db199SXin Li
1073*9c5db199SXin Li        This tests that offload works as expected if calls are made
1074*9c5db199SXin Li        before the job finishes, before the job expires, and after
1075*9c5db199SXin Li        the job expires.
1076*9c5db199SXin Li
1077*9c5db199SXin Li        """
1078*9c5db199SXin Li        self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
1079*9c5db199SXin Li        self._job.set_finished(_TEST_EXPIRATION_AGE)
1080*9c5db199SXin Li        self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
1081*9c5db199SXin Li        self._job.set_expired(_TEST_EXPIRATION_AGE)
1082*9c5db199SXin Li        self._offload_expired_job(_TEST_EXPIRATION_AGE)
1083*9c5db199SXin Li
1084*9c5db199SXin Li
1085*9c5db199SXin Li    def test_case_2_with_expiration(self):
1086*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
1087*9c5db199SXin Li
1088*9c5db199SXin Li        This tests that offload works as expected if calls are made
1089*9c5db199SXin Li        between finishing and expiration, and after the job expires.
1090*9c5db199SXin Li
1091*9c5db199SXin Li        """
1092*9c5db199SXin Li        self._job.set_finished(_TEST_EXPIRATION_AGE)
1093*9c5db199SXin Li        self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
1094*9c5db199SXin Li        self._job.set_expired(_TEST_EXPIRATION_AGE)
1095*9c5db199SXin Li        self._offload_expired_job(_TEST_EXPIRATION_AGE)
1096*9c5db199SXin Li
1097*9c5db199SXin Li
1098*9c5db199SXin Li    def test_case_3_with_expiration(self):
1099*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
1100*9c5db199SXin Li
1101*9c5db199SXin Li        This tests that offload works as expected if calls are made
1102*9c5db199SXin Li        only before finishing and after expiration.
1103*9c5db199SXin Li
1104*9c5db199SXin Li        """
1105*9c5db199SXin Li        self._offload_unexpired_job(_TEST_EXPIRATION_AGE)
1106*9c5db199SXin Li        self._job.set_expired(_TEST_EXPIRATION_AGE)
1107*9c5db199SXin Li        self._offload_expired_job(_TEST_EXPIRATION_AGE)
1108*9c5db199SXin Li
1109*9c5db199SXin Li
1110*9c5db199SXin Li    def test_case_4_with_expiration(self):
1111*9c5db199SXin Li        """Test a series of `enqueue_offload()` calls with `days_old` non-zero.
1112*9c5db199SXin Li
1113*9c5db199SXin Li        This tests that offload works as expected if calls are made
1114*9c5db199SXin Li        only after expiration.
1115*9c5db199SXin Li
1116*9c5db199SXin Li        """
1117*9c5db199SXin Li        self._job.set_expired(_TEST_EXPIRATION_AGE)
1118*9c5db199SXin Li        self._offload_expired_job(_TEST_EXPIRATION_AGE)
1119*9c5db199SXin Li
1120*9c5db199SXin Li
1121*9c5db199SXin Liclass GetJobDirectoriesTests(_TempResultsDirTestBase):
1122*9c5db199SXin Li    """Tests for `_JobDirectory.get_job_directories()`."""
1123*9c5db199SXin Li
1124*9c5db199SXin Li    def setUp(self):
1125*9c5db199SXin Li        super(GetJobDirectoriesTests, self).setUp()
1126*9c5db199SXin Li        self.make_job_hierarchy()
1127*9c5db199SXin Li        os.mkdir('not-a-job')
1128*9c5db199SXin Li        open('not-a-dir', 'w').close()
1129*9c5db199SXin Li
1130*9c5db199SXin Li
1131*9c5db199SXin Li    def _run_get_directories(self, cls, expected_list):
1132*9c5db199SXin Li        """Test `get_job_directories()` for the given class.
1133*9c5db199SXin Li
1134*9c5db199SXin Li        Calls the method, and asserts that the returned list of
1135*9c5db199SXin Li        directories matches the expected return value.
1136*9c5db199SXin Li
1137*9c5db199SXin Li        @param expected_list Expected return value from the call.
1138*9c5db199SXin Li        """
1139*9c5db199SXin Li        dirlist = cls.get_job_directories()
1140*9c5db199SXin Li        self.assertEqual(set(dirlist), set(expected_list))
1141*9c5db199SXin Li
1142*9c5db199SXin Li
1143*9c5db199SXin Li    def test_get_regular_jobs(self):
1144*9c5db199SXin Li        """Test `RegularJobDirectory.get_job_directories()`."""
1145*9c5db199SXin Li        self._run_get_directories(job_directories.RegularJobDirectory,
1146*9c5db199SXin Li                                  self.REGULAR_JOBLIST)
1147*9c5db199SXin Li
1148*9c5db199SXin Li
1149*9c5db199SXin Li    def test_get_special_jobs(self):
1150*9c5db199SXin Li        """Test `SpecialJobDirectory.get_job_directories()`."""
1151*9c5db199SXin Li        self._run_get_directories(job_directories.SpecialJobDirectory,
1152*9c5db199SXin Li                                  self.SPECIAL_JOBLIST)
1153*9c5db199SXin Li
1154*9c5db199SXin Li
1155*9c5db199SXin Liclass AddJobsTests(_TempResultsDirTestBase):
1156*9c5db199SXin Li    """Tests for `Offloader._add_new_jobs()`."""
1157*9c5db199SXin Li
1158*9c5db199SXin Li    MOREJOBS = ['115-fubar', '116-fubar', '117-fubar', '118-snafu']
1159*9c5db199SXin Li
1160*9c5db199SXin Li    def setUp(self):
1161*9c5db199SXin Li        super(AddJobsTests, self).setUp()
1162*9c5db199SXin Li        self._initial_job_names = (
1163*9c5db199SXin Li            set(self.REGULAR_JOBLIST) | set(self.SPECIAL_JOBLIST))
1164*9c5db199SXin Li        self.make_job_hierarchy()
1165*9c5db199SXin Li        self._offloader = gs_offloader.Offloader(_get_options(['-a']))
1166*9c5db199SXin Li
1167*9c5db199SXin Li        logging_patcher = mock.patch.object(logging, 'debug')
1168*9c5db199SXin Li        self.logging_patch = logging_patcher.start()
1169*9c5db199SXin Li        self.addCleanup(logging_patcher.stop)
1170*9c5db199SXin Li
1171*9c5db199SXin Li    def _run_add_new_jobs(self, expected_key_set):
1172*9c5db199SXin Li        """Basic test assertions for `_add_new_jobs()`.
1173*9c5db199SXin Li
1174*9c5db199SXin Li        Asserts the following:
1175*9c5db199SXin Li          * The keys in the offloader's `_open_jobs` dictionary
1176*9c5db199SXin Li            matches the expected set of keys.
1177*9c5db199SXin Li          * For every job in `_open_jobs`, the job has the expected
1178*9c5db199SXin Li            directory name.
1179*9c5db199SXin Li
1180*9c5db199SXin Li        """
1181*9c5db199SXin Li        count = len(expected_key_set) - len(self._offloader._open_jobs)
1182*9c5db199SXin Li        self._offloader._add_new_jobs()
1183*9c5db199SXin Li        self.assertEqual(expected_key_set,
1184*9c5db199SXin Li                         set(self._offloader._open_jobs.keys()))
1185*9c5db199SXin Li        for jobkey, job in self._offloader._open_jobs.items():
1186*9c5db199SXin Li            self.assertEqual(jobkey, job.dirname)
1187*9c5db199SXin Li
1188*9c5db199SXin Li        self.logging_patch.assert_called_with(mock.ANY, count)
1189*9c5db199SXin Li
1190*9c5db199SXin Li    def test_add_jobs_empty(self):
1191*9c5db199SXin Li        """Test adding jobs to an empty dictionary.
1192*9c5db199SXin Li
1193*9c5db199SXin Li        Calls the offloader's `_add_new_jobs()`, then perform
1194*9c5db199SXin Li        the assertions of `self._check_open_jobs()`.
1195*9c5db199SXin Li
1196*9c5db199SXin Li        """
1197*9c5db199SXin Li        self._run_add_new_jobs(self._initial_job_names)
1198*9c5db199SXin Li
1199*9c5db199SXin Li
1200*9c5db199SXin Li    def test_add_jobs_non_empty(self):
1201*9c5db199SXin Li        """Test adding jobs to a non-empty dictionary.
1202*9c5db199SXin Li
1203*9c5db199SXin Li        Calls the offloader's `_add_new_jobs()` twice; once from
1204*9c5db199SXin Li        initial conditions, and then again after adding more
1205*9c5db199SXin Li        directories.  After the second call, perform the assertions
1206*9c5db199SXin Li        of `self._check_open_jobs()`.  Additionally, assert that
1207*9c5db199SXin Li        keys added by the first call still map to their original
1208*9c5db199SXin Li        job object after the second call.
1209*9c5db199SXin Li
1210*9c5db199SXin Li        """
1211*9c5db199SXin Li        self._run_add_new_jobs(self._initial_job_names)
1212*9c5db199SXin Li        jobs_copy = self._offloader._open_jobs.copy()
1213*9c5db199SXin Li        for d in self.MOREJOBS:
1214*9c5db199SXin Li            os.mkdir(d)
1215*9c5db199SXin Li        self._run_add_new_jobs(self._initial_job_names |
1216*9c5db199SXin Li                                 set(self.MOREJOBS))
1217*9c5db199SXin Li        for key in jobs_copy.keys():
1218*9c5db199SXin Li            self.assertIs(jobs_copy[key],
1219*9c5db199SXin Li                          self._offloader._open_jobs[key])
1220*9c5db199SXin Li
1221*9c5db199SXin Li
1222*9c5db199SXin Liclass ReportingTests(_TempResultsDirTestBase):
1223*9c5db199SXin Li    """Tests for `Offloader._report_failed_jobs()`."""
1224*9c5db199SXin Li
1225*9c5db199SXin Li    def setUp(self):
1226*9c5db199SXin Li        super(ReportingTests, self).setUp()
1227*9c5db199SXin Li        self._offloader = gs_offloader.Offloader(_get_options([]))
1228*9c5db199SXin Li
1229*9c5db199SXin Li        failed_jobs_patcher = mock.patch.object(self._offloader,
1230*9c5db199SXin Li                                                '_log_failed_jobs_locally')
1231*9c5db199SXin Li        self.failed_jobs_patch = failed_jobs_patcher.start()
1232*9c5db199SXin Li        self.addCleanup(failed_jobs_patcher.stop)
1233*9c5db199SXin Li
1234*9c5db199SXin Li        logging_patcher = mock.patch.object(logging, 'debug')
1235*9c5db199SXin Li        self.logging_patch = logging_patcher.start()
1236*9c5db199SXin Li        self.addCleanup(logging_patcher.stop)
1237*9c5db199SXin Li
1238*9c5db199SXin Li    def _add_job(self, jobdir):
1239*9c5db199SXin Li        """Add a job to the dictionary of unfinished jobs."""
1240*9c5db199SXin Li        j = self.make_job(jobdir)
1241*9c5db199SXin Li        self._offloader._open_jobs[j.dirname] = j
1242*9c5db199SXin Li        return j
1243*9c5db199SXin Li
1244*9c5db199SXin Li
1245*9c5db199SXin Li    def _expect_log_message(self, new_open_jobs, with_failures, count=None):
1246*9c5db199SXin Li        """Mock expected logging calls.
1247*9c5db199SXin Li
1248*9c5db199SXin Li        `_report_failed_jobs()` logs one message with the number
1249*9c5db199SXin Li        of jobs removed from the open job set and the number of jobs
1250*9c5db199SXin Li        still remaining.  Additionally, if there are reportable
1251*9c5db199SXin Li        jobs, then it logs the number of jobs that haven't yet
1252*9c5db199SXin Li        offloaded.
1253*9c5db199SXin Li
1254*9c5db199SXin Li        This sets up the logging calls using `new_open_jobs` to
1255*9c5db199SXin Li        figure the job counts.  If `with_failures` is true, then
1256*9c5db199SXin Li        the log message is set up assuming that all jobs in
1257*9c5db199SXin Li        `new_open_jobs` have offload failures.
1258*9c5db199SXin Li
1259*9c5db199SXin Li        @param new_open_jobs New job set for calculating counts
1260*9c5db199SXin Li                             in the messages.
1261*9c5db199SXin Li        @param with_failures Whether the log message with a
1262*9c5db199SXin Li                             failure count is expected.
1263*9c5db199SXin Li
1264*9c5db199SXin Li        """
1265*9c5db199SXin Li        if not count:
1266*9c5db199SXin Li            count = len(self._offloader._open_jobs) - len(new_open_jobs)
1267*9c5db199SXin Li        self.logging_patch.assert_called_with(mock.ANY, count,
1268*9c5db199SXin Li                                              len(new_open_jobs))
1269*9c5db199SXin Li        if with_failures:
1270*9c5db199SXin Li            self.logging_patch.assert_called_with(mock.ANY, len(new_open_jobs))
1271*9c5db199SXin Li
1272*9c5db199SXin Li    def _run_update(self, new_open_jobs):
1273*9c5db199SXin Li        """Call `_report_failed_jobs()`.
1274*9c5db199SXin Li
1275*9c5db199SXin Li        Initial conditions are set up by the caller.  This calls
1276*9c5db199SXin Li        `_report_failed_jobs()` once, and then checks these
1277*9c5db199SXin Li        assertions:
1278*9c5db199SXin Li          * The offloader's new `_open_jobs` field contains only
1279*9c5db199SXin Li            the entries in `new_open_jobs`.
1280*9c5db199SXin Li
1281*9c5db199SXin Li        @param new_open_jobs A dictionary representing the expected
1282*9c5db199SXin Li                             new value of the offloader's
1283*9c5db199SXin Li                             `_open_jobs` field.
1284*9c5db199SXin Li        """
1285*9c5db199SXin Li        self._offloader._report_failed_jobs()
1286*9c5db199SXin Li        self._offloader._remove_offloaded_jobs()
1287*9c5db199SXin Li        self.assertEqual(self._offloader._open_jobs, new_open_jobs)
1288*9c5db199SXin Li
1289*9c5db199SXin Li
1290*9c5db199SXin Li    def _expect_failed_jobs(self, failed_jobs):
1291*9c5db199SXin Li        """Mock expected call to log the failed jobs on local disk.
1292*9c5db199SXin Li
1293*9c5db199SXin Li        TODO(crbug.com/686904): The fact that we have to mock an internal
1294*9c5db199SXin Li        function for this test is evidence that we need to pull out the local
1295*9c5db199SXin Li        file formatter in its own object in a future CL.
1296*9c5db199SXin Li
1297*9c5db199SXin Li        @param failed_jobs: The list of jobs being logged as failed.
1298*9c5db199SXin Li        """
1299*9c5db199SXin Li        self._offloader._log_failed_jobs_locally(failed_jobs)
1300*9c5db199SXin Li
1301*9c5db199SXin Li
1302*9c5db199SXin Li    def test_no_jobs(self):
1303*9c5db199SXin Li        """Test `_report_failed_jobs()` with no open jobs.
1304*9c5db199SXin Li
1305*9c5db199SXin Li        Initial conditions are an empty `_open_jobs` list.
1306*9c5db199SXin Li        Expected result is an empty `_open_jobs` list.
1307*9c5db199SXin Li
1308*9c5db199SXin Li        """
1309*9c5db199SXin Li        self._expect_failed_jobs([])
1310*9c5db199SXin Li        self._run_update({})
1311*9c5db199SXin Li        self._expect_log_message({}, False)
1312*9c5db199SXin Li
1313*9c5db199SXin Li
1314*9c5db199SXin Li    def test_all_completed(self):
1315*9c5db199SXin Li        """Test `_report_failed_jobs()` with only complete jobs.
1316*9c5db199SXin Li
1317*9c5db199SXin Li        Initial conditions are an `_open_jobs` list consisting of only completed
1318*9c5db199SXin Li        jobs.
1319*9c5db199SXin Li        Expected result is an empty `_open_jobs` list.
1320*9c5db199SXin Li
1321*9c5db199SXin Li        """
1322*9c5db199SXin Li
1323*9c5db199SXin Li        for d in self.REGULAR_JOBLIST:
1324*9c5db199SXin Li            self._add_job(d).set_complete()
1325*9c5db199SXin Li        count = len(self._offloader._open_jobs)
1326*9c5db199SXin Li        self._expect_failed_jobs([])
1327*9c5db199SXin Li        self._run_update({})
1328*9c5db199SXin Li        self._expect_log_message({}, False, count)
1329*9c5db199SXin Li
1330*9c5db199SXin Li
1331*9c5db199SXin Li    def test_none_finished(self):
1332*9c5db199SXin Li        """Test `_report_failed_jobs()` with only unfinished jobs.
1333*9c5db199SXin Li
1334*9c5db199SXin Li        Initial conditions are an `_open_jobs` list consisting of only
1335*9c5db199SXin Li        unfinished jobs.
1336*9c5db199SXin Li        Expected result is no change to the `_open_jobs` list.
1337*9c5db199SXin Li
1338*9c5db199SXin Li        """
1339*9c5db199SXin Li        for d in self.REGULAR_JOBLIST:
1340*9c5db199SXin Li            self._add_job(d)
1341*9c5db199SXin Li        new_jobs = self._offloader._open_jobs.copy()
1342*9c5db199SXin Li        self._expect_failed_jobs([])
1343*9c5db199SXin Li        self._run_update(new_jobs)
1344*9c5db199SXin Li        self._expect_log_message(new_jobs, False)
1345*9c5db199SXin Li
1346*9c5db199SXin Li
1347*9c5db199SXin Liclass GsOffloaderMockTests(_TempResultsDirTestCase):
1348*9c5db199SXin Li    """Tests using mock instead of mox."""
1349*9c5db199SXin Li
1350*9c5db199SXin Li    def setUp(self):
1351*9c5db199SXin Li        super(GsOffloaderMockTests, self).setUp()
1352*9c5db199SXin Li        alarm = mock.patch('signal.alarm', return_value=0)
1353*9c5db199SXin Li        alarm.start()
1354*9c5db199SXin Li        self.addCleanup(alarm.stop)
1355*9c5db199SXin Li
1356*9c5db199SXin Li        self._saved_loglevel = logging.getLogger().getEffectiveLevel()
1357*9c5db199SXin Li        logging.getLogger().setLevel(logging.CRITICAL + 1)
1358*9c5db199SXin Li
1359*9c5db199SXin Li        self._job = self.make_job(self.REGULAR_JOBLIST[0])
1360*9c5db199SXin Li
1361*9c5db199SXin Li
1362*9c5db199SXin Li    def test_offload_timeout_early(self):
1363*9c5db199SXin Li        """Test that `offload_dir()` times out correctly.
1364*9c5db199SXin Li
1365*9c5db199SXin Li        This test triggers timeout at the earliest possible moment,
1366*9c5db199SXin Li        at the first call to set the timeout alarm.
1367*9c5db199SXin Li
1368*9c5db199SXin Li        """
1369*9c5db199SXin Li        signal.alarm.side_effect = [0, timeout_util.TimeoutError('fubar')]
1370*9c5db199SXin Li        gs_offloader.GSOffloader(
1371*9c5db199SXin Li                utils.DEFAULT_OFFLOAD_GSURI, False, 0).offload(
1372*9c5db199SXin Li                        self._job.queue_args[0],
1373*9c5db199SXin Li                        self._job.queue_args[1],
1374*9c5db199SXin Li                        self._job.queue_args[2])
1375*9c5db199SXin Li        self.assertTrue(os.path.isdir(self._job.queue_args[0]))
1376*9c5db199SXin Li
1377*9c5db199SXin Li
1378*9c5db199SXin Li    # TODO(ayatane): This tests passes when run locally, but it fails
1379*9c5db199SXin Li    # when run on trybot.  I have no idea why, but the assert isdir
1380*9c5db199SXin Li    # fails.
1381*9c5db199SXin Li    #
1382*9c5db199SXin Li    # This test is also kind of redundant since we are using the timeout
1383*9c5db199SXin Li    # from chromite which has its own tests.
1384*9c5db199SXin Li    @unittest.skip('This fails on trybot')
1385*9c5db199SXin Li    def test_offload_timeout_late(self):
1386*9c5db199SXin Li        """Test that `offload_dir()` times out correctly.
1387*9c5db199SXin Li
1388*9c5db199SXin Li        This test triggers timeout at the latest possible moment, at
1389*9c5db199SXin Li        the call to clear the timeout alarm.
1390*9c5db199SXin Li
1391*9c5db199SXin Li        """
1392*9c5db199SXin Li        signal.alarm.side_effect = [0, 0, timeout_util.TimeoutError('fubar')]
1393*9c5db199SXin Li        with mock.patch.object(gs_offloader, '_get_cmd_list',
1394*9c5db199SXin Li                               autospec=True) as get_cmd_list:
1395*9c5db199SXin Li            get_cmd_list.return_value = ['test', '-d', self._job.queue_args[0]]
1396*9c5db199SXin Li            gs_offloader.GSOffloader(
1397*9c5db199SXin Li                    utils.DEFAULT_OFFLOAD_GSURI, False, 0).offload(
1398*9c5db199SXin Li                            self._job.queue_args[0],
1399*9c5db199SXin Li                            self._job.queue_args[1],
1400*9c5db199SXin Li                            self._job.queue_args[2])
1401*9c5db199SXin Li            self.assertTrue(os.path.isdir(self._job.queue_args[0]))
1402*9c5db199SXin Li
1403*9c5db199SXin Li
1404*9c5db199SXin Li
1405*9c5db199SXin Liif __name__ == '__main__':
1406*9c5db199SXin Li    unittest.main()
1407