1#!/usr/bin/python
2#pylint: disable-msg=C0111
3
4# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7import collections
8
9import common
10
11from autotest_lib.client.common_lib import host_queue_entry_states
12from autotest_lib.client.common_lib.test_utils import unittest
13from autotest_lib.frontend import setup_django_environment
14from autotest_lib.frontend.afe import frontend_test_utils
15from autotest_lib.frontend.afe import models
16from autotest_lib.frontend.afe import rdb_model_extensions
17from autotest_lib.scheduler import rdb
18from autotest_lib.scheduler import rdb_hosts
19from autotest_lib.scheduler import rdb_lib
20from autotest_lib.scheduler import rdb_requests
21from autotest_lib.scheduler import rdb_testing_utils
22from autotest_lib.server.cros import provision
23
24
25class AssignmentValidator(object):
26    """Utility class to check that priority inversion doesn't happen. """
27
28
29    @staticmethod
30    def check_acls_deps(host, request):
31        """Check if a host and request match by comparing acls and deps.
32
33        @param host: A dictionary representing attributes of the host.
34        @param request: A request, as defined in rdb_requests.
35
36        @return True if the deps/acls of the request match the host.
37        """
38        # Unfortunately the hosts labels are labelnames, not ids.
39        request_deps = set([l.name for l in
40                models.Label.objects.filter(id__in=request.deps)])
41        return (set(host['labels']).intersection(request_deps) == request_deps
42                and set(host['acls']).intersection(request.acls))
43
44
45    @staticmethod
46    def find_matching_host_for_request(hosts, request):
47        """Find a host from the given list of hosts, matching the request.
48
49        @param hosts: A list of dictionaries representing host attributes.
50        @param requetst: The unsatisfied request.
51
52        @return: A host, if a matching host is found from the input list.
53        """
54        if not hosts or not request:
55            return None
56        for host in hosts:
57            if AssignmentValidator.check_acls_deps(host, request):
58                return host
59
60
61    @staticmethod
62    def sort_requests(requests):
63        """Sort the requests by priority.
64
65        @param requests: Unordered requests.
66
67        @return: A list of requests ordered by priority.
68        """
69        return sorted(collections.Counter(requests).items(),
70                key=lambda request: request[0].priority, reverse=True)
71
72
73    @staticmethod
74    def verify_priority(request_queue, result):
75        requests = AssignmentValidator.sort_requests(request_queue)
76        for request, count in requests:
77            hosts = result.get(request)
78            # The request was completely satisfied.
79            if hosts and len(hosts) == count:
80                continue
81            # Go through all hosts given to lower priority requests and
82            # make sure we couldn't have allocated one of them for this
83            # unsatisfied higher priority request.
84            lower_requests = requests[requests.index((request,count))+1:]
85            for lower_request, count in lower_requests:
86                if (lower_request.priority < request.priority and
87                    AssignmentValidator.find_matching_host_for_request(
88                            result.get(lower_request), request)):
89                    raise ValueError('Priority inversion occured between '
90                            'priorities %s and %s' %
91                            (request.priority, lower_request.priority))
92
93
94    @staticmethod
95    def priority_checking_response_handler(request_manager):
96        """Fake response handler wrapper for any request_manager.
97
98        Check that higher priority requests get a response over lower priority
99        requests, by re-validating all the hosts assigned to a lower priority
100        request against the unsatisfied higher priority ones.
101
102        @param request_manager: A request_manager as defined in rdb_lib.
103
104        @raises ValueError: If priority inversion is detected.
105        """
106        # Fist call the rdb to make its decisions, then sort the requests
107        # by priority and make sure unsatisfied requests higher up in the list
108        # could not have been satisfied by hosts assigned to requests lower
109        # down in the list.
110        result = request_manager.api_call(request_manager.request_queue)
111        if not result:
112            raise ValueError('Expected results but got none.')
113        AssignmentValidator.verify_priority(
114                request_manager.request_queue, result)
115        for hosts in result.values():
116            for host in hosts:
117                yield host
118
119
120class BaseRDBTest(rdb_testing_utils.AbstractBaseRDBTester, unittest.TestCase):
121    _config_section = 'AUTOTEST_WEB'
122
123
124    def testAcquireLeasedHostBasic(self):
125        """Test that acquisition of a leased host doesn't happen.
126
127        @raises AssertionError: If the one host that satisfies the request
128            is acquired.
129        """
130        job = self.create_job(deps=set(['a']))
131        host = self.db_helper.create_host('h1', deps=set(['a']))
132        host.leased = 1
133        host.save()
134        queue_entries = self._dispatcher._refresh_pending_queue_entries()
135        hosts = list(rdb_lib.acquire_hosts(queue_entries))
136        self.assertTrue(len(hosts) == 1 and hosts[0] is None)
137
138
139    def testAcquireLeasedHostRace(self):
140        """Test behaviour when hosts are leased just before acquisition.
141
142        If a fraction of the hosts somehow get leased between finding and
143        acquisition, the rdb should just return the remaining hosts for the
144        request to use.
145
146        @raises AssertionError: If both the requests get a host successfully,
147            since one host gets leased before the final attempt to lease both.
148        """
149        j1 = self.create_job(deps=set(['a']))
150        j2 = self.create_job(deps=set(['a']))
151        hosts = [self.db_helper.create_host('h1', deps=set(['a'])),
152                 self.db_helper.create_host('h2', deps=set(['a']))]
153
154        @rdb_hosts.return_rdb_host
155        def local_find_hosts(host_query_manger, deps, acls):
156            """Return a predetermined list of hosts, one of which is leased."""
157            h1 = models.Host.objects.get(hostname='h1')
158            h1.leased = 1
159            h1.save()
160            h2 = models.Host.objects.get(hostname='h2')
161            return [h1, h2]
162
163        self.god.stub_with(rdb.AvailableHostQueryManager, 'find_hosts',
164                           local_find_hosts)
165        queue_entries = self._dispatcher._refresh_pending_queue_entries()
166        hosts = list(rdb_lib.acquire_hosts(queue_entries))
167        self.assertTrue(len(hosts) == 2 and None in hosts)
168        self.check_hosts(iter(hosts))
169
170
171    def testHostReleaseStates(self):
172        """Test that we will only release an unused host if it is in Ready.
173
174        @raises AssertionError: If the host gets released in any other state.
175        """
176        host = self.db_helper.create_host('h1', deps=set(['x']))
177        for state in rdb_model_extensions.AbstractHostModel.Status.names:
178            host.status = state
179            host.leased = 1
180            host.save()
181            self._release_unused_hosts()
182            host = models.Host.objects.get(hostname='h1')
183            self.assertTrue(host.leased == (state != 'Ready'))
184
185
186    def testHostReleseHQE(self):
187        """Test that we will not release a ready host if it's being used.
188
189        @raises AssertionError: If the host is released even though it has
190            been assigned to an active hqe.
191        """
192        # Create a host and lease it out in Ready.
193        host = self.db_helper.create_host('h1', deps=set(['x']))
194        host.status = 'Ready'
195        host.leased = 1
196        host.save()
197
198        # Create a job and give its hqe the leased host.
199        job = self.create_job(deps=set(['x']))
200        self.db_helper.add_host_to_job(host, job.id)
201        hqe = models.HostQueueEntry.objects.get(job_id=job.id)
202
203        # Activate the hqe by setting its state.
204        hqe.status = host_queue_entry_states.ACTIVE_STATUSES[0]
205        hqe.save()
206
207        # Make sure the hqes host isn't released, even if its in ready.
208        self._release_unused_hosts()
209        host = models.Host.objects.get(hostname='h1')
210        self.assertTrue(host.leased == 1)
211
212
213    def testBasicDepsAcls(self):
214        """Test a basic deps/acls request.
215
216        Make sure that a basic request with deps and acls, finds a host from
217        the ready pool that has matching labels and is in a matching aclgroups.
218
219        @raises AssertionError: If the request doesn't find a host, since the
220            we insert a matching host in the ready pool.
221        """
222        deps = set(['a', 'b'])
223        acls = set(['a', 'b'])
224        self.db_helper.create_host('h1', deps=deps, acls=acls)
225        job = self.create_job(user='autotest_system', deps=deps, acls=acls)
226        queue_entries = self._dispatcher._refresh_pending_queue_entries()
227        matching_host  = rdb_lib.acquire_hosts(queue_entries).next()
228        self.check_host_assignment(job.id, matching_host.id)
229        self.assertTrue(matching_host.leased == 1)
230
231
232    def testPreferredDeps(self):
233        """Test that perferred deps is respected.
234
235        If multiple hosts satisfied a job's deps, the one with preferred
236        label will be assigned to the job.
237
238        @raises AssertionError: If a host without a preferred label is
239                                assigned to the job instead of one with
240                                a preferred label.
241        """
242        lumpy_deps = set(['board:lumpy'])
243        stumpy_deps = set(['board:stumpy'])
244        stumpy_deps_with_crosversion = set(
245                ['board:stumpy', 'cros-version:lumpy-release/R41-6323.0.0'])
246
247        acls = set(['a', 'b'])
248        # Hosts lumpy1 and lumpy2 are created as a control group,
249        # which ensures that if no preferred label is used, the host
250        # with a smaller id will be chosen first. We need to make sure
251        # stumpy2 was chosen because it has a cros-version label, but not
252        # because of other randomness.
253        self.db_helper.create_host('lumpy1', deps=lumpy_deps, acls=acls)
254        self.db_helper.create_host('lumpy2', deps=lumpy_deps, acls=acls)
255        self.db_helper.create_host('stumpy1', deps=stumpy_deps, acls=acls)
256        self.db_helper.create_host(
257                    'stumpy2', deps=stumpy_deps_with_crosversion , acls=acls)
258        job_1 = self.create_job(user='autotest_system',
259                              deps=lumpy_deps, acls=acls)
260        job_2 = self.create_job(user='autotest_system',
261                              deps=stumpy_deps_with_crosversion, acls=acls)
262        queue_entries = self._dispatcher._refresh_pending_queue_entries()
263        matching_hosts  = list(rdb_lib.acquire_hosts(queue_entries))
264        assignment = {}
265        import logging
266        for job, host in zip(queue_entries, matching_hosts):
267            self.check_host_assignment(job.id, host.id)
268            assignment[job.id] = host.hostname
269        self.assertEqual(assignment[job_1.id], 'lumpy1')
270        self.assertEqual(assignment[job_2.id], 'stumpy2')
271
272
273    def testBadDeps(self):
274        """Test that we find no hosts when only acls match.
275
276        @raises AssertionError: If the request finds a host, since the only
277            host in the ready pool will not have matching deps.
278        """
279        host_labels = set(['a'])
280        job_deps = set(['b'])
281        acls = set(['a', 'b'])
282        self.db_helper.create_host('h1', deps=host_labels, acls=acls)
283        job = self.create_job(user='autotest_system', deps=job_deps, acls=acls)
284        queue_entries = self._dispatcher._refresh_pending_queue_entries()
285        matching_host  = rdb_lib.acquire_hosts(queue_entries).next()
286        self.assert_(not matching_host)
287
288
289    def testBadAcls(self):
290        """Test that we find no hosts when only deps match.
291
292        @raises AssertionError: If the request finds a host, since the only
293            host in the ready pool will not have matching acls.
294        """
295        deps = set(['a'])
296        host_acls = set(['a'])
297        job_acls = set(['b'])
298        self.db_helper.create_host('h1', deps=deps, acls=host_acls)
299
300        # Create the job as a new user who is only in the 'b' and 'Everyone'
301        # aclgroups. Though there are several hosts in the Everyone group, the
302        # 1 host that has the 'a' dep isn't.
303        job = self.create_job(user='new_user', deps=deps, acls=job_acls)
304        queue_entries = self._dispatcher._refresh_pending_queue_entries()
305        matching_host  = rdb_lib.acquire_hosts(queue_entries).next()
306        self.assert_(not matching_host)
307
308
309    def testBasicPriority(self):
310        """Test that priority inversion doesn't happen.
311
312        Schedule 2 jobs with the same deps, acls and user, but different
313        priorities, and confirm that the higher priority request gets the host.
314        This confirmation happens through the AssignmentValidator.
315
316        @raises AssertionError: If the un important request gets host h1 instead
317            of the important request.
318        """
319        deps = set(['a', 'b'])
320        acls = set(['a', 'b'])
321        self.db_helper.create_host('h1', deps=deps, acls=acls)
322        important_job = self.create_job(user='autotest_system',
323                deps=deps, acls=acls, priority=2)
324        un_important_job = self.create_job(user='autotest_system',
325                deps=deps, acls=acls, priority=0)
326        queue_entries = self._dispatcher._refresh_pending_queue_entries()
327
328        self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
329                AssignmentValidator.priority_checking_response_handler)
330        self.check_hosts(rdb_lib.acquire_hosts(queue_entries))
331
332
333    def testPriorityLevels(self):
334        """Test that priority inversion doesn't happen.
335
336        Increases a job's priority and makes several requests for hosts,
337        checking that priority inversion doesn't happen.
338
339        @raises AssertionError: If the unimportant job gets h1 while it is
340            still unimportant, or doesn't get h1 while after it becomes the
341            most important job.
342        """
343        deps = set(['a', 'b'])
344        acls = set(['a', 'b'])
345        self.db_helper.create_host('h1', deps=deps, acls=acls)
346
347        # Create jobs that will bucket differently and confirm that jobs in an
348        # earlier bucket get a host.
349        first_job = self.create_job(user='autotest_system', deps=deps, acls=acls)
350        important_job = self.create_job(user='autotest_system', deps=deps,
351                acls=acls, priority=2)
352        deps.pop()
353        unimportant_job = self.create_job(user='someother_system', deps=deps,
354                acls=acls, priority=1)
355        queue_entries = self._dispatcher._refresh_pending_queue_entries()
356
357        self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
358                AssignmentValidator.priority_checking_response_handler)
359        self.check_hosts(rdb_lib.acquire_hosts(queue_entries))
360
361        # Elevate the priority of the unimportant job, so we now have
362        # 2 jobs at the same priority.
363        self.db_helper.increment_priority(job_id=unimportant_job.id)
364        queue_entries = self._dispatcher._refresh_pending_queue_entries()
365        self._release_unused_hosts()
366        self.check_hosts(rdb_lib.acquire_hosts(queue_entries))
367
368        # Prioritize the first job, and confirm that it gets the host over the
369        # jobs that got it the last time.
370        self.db_helper.increment_priority(job_id=unimportant_job.id)
371        queue_entries = self._dispatcher._refresh_pending_queue_entries()
372        self._release_unused_hosts()
373        self.check_hosts(rdb_lib.acquire_hosts(queue_entries))
374
375
376    def testFrontendJobScheduling(self):
377        """Test that basic frontend job scheduling.
378
379        @raises AssertionError: If the received and requested host don't match,
380            or the mis-matching host is returned instead.
381        """
382        deps = set(['x', 'y'])
383        acls = set(['a', 'b'])
384
385        # Create 2 frontend jobs and only one matching host.
386        matching_job = self.create_job(acls=acls, deps=deps)
387        matching_host = self.db_helper.create_host('h1', acls=acls, deps=deps)
388        mis_matching_job = self.create_job(acls=acls, deps=deps)
389        mis_matching_host = self.db_helper.create_host(
390                'h2', acls=acls, deps=deps.pop())
391        self.db_helper.add_host_to_job(matching_host, matching_job.id)
392        self.db_helper.add_host_to_job(mis_matching_host, mis_matching_job.id)
393
394        # Check that only the matching host is returned, and that we get 'None'
395        # for the second request.
396        queue_entries = self._dispatcher._refresh_pending_queue_entries()
397        hosts = list(rdb_lib.acquire_hosts(queue_entries))
398        self.assertTrue(len(hosts) == 2 and None in hosts)
399        returned_host = [host for host in hosts if host].pop()
400        self.assertTrue(matching_host.id == returned_host.id)
401
402
403    def testFrontendJobPriority(self):
404        """Test that frontend job scheduling doesn't ignore priorities.
405
406        @raises ValueError: If the priorities of frontend jobs are ignored.
407        """
408        board = 'x'
409        high_priority = self.create_job(priority=2, deps=set([board]))
410        low_priority = self.create_job(priority=1, deps=set([board]))
411        host = self.db_helper.create_host('h1', deps=set([board]))
412        self.db_helper.add_host_to_job(host, low_priority.id)
413        self.db_helper.add_host_to_job(host, high_priority.id)
414
415        queue_entries = self._dispatcher._refresh_pending_queue_entries()
416
417        def local_response_handler(request_manager):
418            """Confirms that a higher priority frontend job gets a host.
419
420            @raises ValueError: If priority inversion happens and the job
421                with priority 1 gets the host instead.
422            """
423            result = request_manager.api_call(request_manager.request_queue)
424            if not result:
425                raise ValueError('Excepted the high priority request to '
426                                 'get a host, but the result is empty.')
427            for request, hosts in result.iteritems():
428                if request.priority == 1:
429                    raise ValueError('Priority of frontend job ignored.')
430                if len(hosts) > 1:
431                    raise ValueError('Multiple hosts returned against one '
432                                     'frontend job scheduling request.')
433                yield hosts[0]
434
435        self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
436                           local_response_handler)
437        self.check_hosts(rdb_lib.acquire_hosts(queue_entries))
438
439
440    def testSuiteOrderedHostAcquisition(self):
441        """Test that older suite jobs acquire hosts first.
442
443        Make sure older suite jobs get hosts first, but not at the expense of
444        higher priority jobs.
445
446        @raises ValueError: If unexpected acquisitions occur, eg:
447            suite_job_2 acquires the last 2 hosts instead of suite_job_1.
448            isolated_important_job doesn't get any hosts.
449            Any job acquires more hosts than necessary.
450        """
451        board = 'x'
452
453        # Create 2 suites such that the later suite has an ordering of deps
454        # that places it ahead of the earlier suite, if parent_job_id is
455        # ignored.
456        suite_without_dep = self.create_suite(num=2, priority=0, board=board)
457
458        suite_with_dep = self.create_suite(num=1, priority=0, board=board)
459        self.db_helper.add_deps_to_job(suite_with_dep[0], dep_names=list('y'))
460
461        # Create an important job that should be ahead of the first suite,
462        # because priority trumps parent_job_id and time of creation.
463        isolated_important_job = self.create_job(priority=3, deps=set([board]))
464
465        # Create 3 hosts, all with the deps to satisfy the last suite.
466        for i in range(0, 3):
467            self.db_helper.create_host('h%s' % i, deps=set([board, 'y']))
468
469        queue_entries = self._dispatcher._refresh_pending_queue_entries()
470
471        def local_response_handler(request_manager):
472            """Reorder requests and check host acquisition.
473
474            @raises ValueError: If unexpected/no acquisitions occur.
475            """
476            if any([request for request in request_manager.request_queue
477                    if request.parent_job_id is None]):
478                raise ValueError('Parent_job_id can never be None.')
479
480            # This will result in the ordering:
481            # [suite_2_1, suite_1_*, suite_1_*, isolated_important_job]
482            # The priority scheduling order should be:
483            # [isolated_important_job, suite_1_*, suite_1_*, suite_2_1]
484            # Since:
485            #   a. the isolated_important_job is the most important.
486            #   b. suite_1 was created before suite_2, regardless of deps
487            disorderly_queue = sorted(request_manager.request_queue,
488                    key=lambda r: -r.parent_job_id)
489            request_manager.request_queue = disorderly_queue
490            result = request_manager.api_call(request_manager.request_queue)
491            if not result:
492                raise ValueError('Expected results but got none.')
493
494            # Verify that the isolated_important_job got a host, and that the
495            # first suite got both remaining free hosts.
496            for request, hosts in result.iteritems():
497                if request.parent_job_id == 0:
498                    if len(hosts) > 1:
499                        raise ValueError('First job acquired more hosts than '
500                                'necessary. Response map: %s' % result)
501                    continue
502                if request.parent_job_id == 1:
503                    if len(hosts) < 2:
504                        raise ValueError('First suite job requests were not '
505                                'satisfied. Response_map: %s' % result)
506                    continue
507                # The second suite job got hosts instead of one of
508                # the others. Eitherway this is a failure.
509                raise ValueError('Unexpected host acquisition '
510                        'Response map: %s' % result)
511            yield None
512
513        self.god.stub_with(rdb_requests.BaseHostRequestManager, 'response',
514                           local_response_handler)
515        list(rdb_lib.acquire_hosts(queue_entries))
516
517
518    def testConfigurations(self):
519        """Test that configurations don't matter.
520        @raises AssertionError: If the request doesn't find a host,
521                 this will happen if configurations are not stripped out.
522        """
523        self.god.stub_with(provision.Cleanup,
524                           '_actions',
525                           {'action': 'fakeTest'})
526        job_labels = set(['action', 'a'])
527        host_deps = set(['a'])
528        db_host = self.db_helper.create_host('h1', deps=host_deps)
529        self.create_job(user='autotest_system', deps=job_labels)
530        queue_entries = self._dispatcher._refresh_pending_queue_entries()
531        matching_host = rdb_lib.acquire_hosts(queue_entries).next()
532        self.assert_(matching_host.id == db_host.id)
533
534
535class RDBMinDutTest(
536        rdb_testing_utils.AbstractBaseRDBTester, unittest.TestCase):
537    """Test AvailableHostRequestHandler"""
538
539    _config_section = 'AUTOTEST_WEB'
540
541
542    def min_dut_test_helper(self, num_hosts, suite_settings):
543        """A helper function to test min_dut logic.
544
545        @param num_hosts: Total number of hosts to create.
546        @param suite_settings: A dictionary specify how suites would be created
547                               and verified.
548                E.g.  {'priority': 10, 'num_jobs': 3,
549                       'min_duts':2, 'expected_aquired': 1}
550                       With this setting, will create a suite that has 3
551                       child jobs, with priority 10 and min_duts 2.
552                       The suite is expected to get 1 dut.
553        """
554        acls = set(['fake_acl'])
555        hosts = []
556        for i in range (0, num_hosts):
557            hosts.append(self.db_helper.create_host(
558                'h%d' % i, deps=set(['board:lumpy']), acls=acls))
559        suites = {}
560        suite_min_duts = {}
561        for setting in suite_settings:
562            s = self.create_suite(num=setting['num_jobs'],
563                                  priority=setting['priority'],
564                                  board='board:lumpy', acls=acls)
565            # Empty list will be used to store acquired hosts.
566            suites[s['parent_job'].id] = (setting, [])
567            suite_min_duts[s['parent_job'].id] = setting['min_duts']
568        queue_entries = self._dispatcher._refresh_pending_queue_entries()
569        matching_hosts = rdb_lib.acquire_hosts(queue_entries, suite_min_duts)
570        for host, queue_entry in zip(matching_hosts, queue_entries):
571            if host:
572                suites[queue_entry.job.parent_job_id][1].append(host)
573
574        for setting, hosts in suites.itervalues():
575            self.assertEqual(len(hosts),setting['expected_aquired'])
576
577
578    def testHighPriorityTakeAll(self):
579        """Min duts not satisfied."""
580        num_hosts = 1
581        suite1 = {'priority':20, 'num_jobs': 3, 'min_duts': 2,
582                  'expected_aquired': 1}
583        suite2 = {'priority':10, 'num_jobs': 7, 'min_duts': 5,
584                  'expected_aquired': 0}
585        self.min_dut_test_helper(num_hosts, [suite1, suite2])
586
587
588    def testHighPriorityMinSatisfied(self):
589        """High priority min duts satisfied."""
590        num_hosts = 4
591        suite1 = {'priority':20, 'num_jobs': 4, 'min_duts': 2,
592                  'expected_aquired': 2}
593        suite2 = {'priority':10, 'num_jobs': 7, 'min_duts': 5,
594                  'expected_aquired': 2}
595        self.min_dut_test_helper(num_hosts, [suite1, suite2])
596
597
598    def testAllPrioritiesMinSatisfied(self):
599        """Min duts satisfied."""
600        num_hosts = 7
601        suite1 = {'priority':20, 'num_jobs': 4, 'min_duts': 2,
602                  'expected_aquired': 2}
603        suite2 = {'priority':10, 'num_jobs': 7, 'min_duts': 5,
604                  'expected_aquired': 5}
605        self.min_dut_test_helper(num_hosts, [suite1, suite2])
606
607
608    def testHighPrioritySatisfied(self):
609        """Min duts satisfied, high priority suite satisfied."""
610        num_hosts = 10
611        suite1 = {'priority':20, 'num_jobs': 4, 'min_duts': 2,
612                  'expected_aquired': 4}
613        suite2 = {'priority':10, 'num_jobs': 7, 'min_duts': 5,
614                  'expected_aquired': 6}
615        self.min_dut_test_helper(num_hosts, [suite1, suite2])
616
617
618    def testEqualPriorityFirstSuiteMinSatisfied(self):
619        """Equal priority, earlier suite got min duts."""
620        num_hosts = 4
621        suite1 = {'priority':20, 'num_jobs': 4, 'min_duts': 2,
622                  'expected_aquired': 2}
623        suite2 = {'priority':20, 'num_jobs': 7, 'min_duts': 5,
624                  'expected_aquired': 2}
625        self.min_dut_test_helper(num_hosts, [suite1, suite2])
626
627
628    def testEqualPriorityAllSuitesMinSatisfied(self):
629        """Equal priority, all suites got min duts."""
630        num_hosts = 7
631        suite1 = {'priority':20, 'num_jobs': 4, 'min_duts': 2,
632                  'expected_aquired': 2}
633        suite2 = {'priority':20, 'num_jobs': 7, 'min_duts': 5,
634                  'expected_aquired': 5}
635        self.min_dut_test_helper(num_hosts, [suite1, suite2])
636
637
638if __name__ == '__main__':
639    unittest.main()
640