← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/buildqueue-estimation-extract into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/buildqueue-estimation-extract into lp:launchpad.

Commit message:
Extract job start time estimation from the main BuildQueue module.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/buildqueue-estimation-extract/+merge/193378

A majority of buildqueue.py and test_buildqueue.py consists of job start time estimation. Let's extract that to its own module to make things clearer.
-- 
https://code.launchpad.net/~wgrant/launchpad/buildqueue-estimation-extract/+merge/193378
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/buildqueue-estimation-extract into lp:launchpad.
=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
--- lib/lp/buildmaster/model/buildqueue.py	2013-09-02 08:11:58 +0000
+++ lib/lp/buildmaster/model/buildqueue.py	2013-10-31 06:45:03 +0000
@@ -9,11 +9,7 @@
     'specific_job_classes',
     ]
 
-from collections import defaultdict
-from datetime import (
-    datetime,
-    timedelta,
-    )
+from datetime import datetime
 from itertools import groupby
 from operator import attrgetter
 
@@ -37,11 +33,7 @@
 from lp.services.database.bulk import load_related
 from lp.services.database.constants import DEFAULT
 from lp.services.database.enumcol import EnumCol
-from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import (
-    SQLBase,
-    sqlvalues,
-    )
+from lp.services.database.sqlbase import SQLBase
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
 from lp.services.propertycache import (
@@ -50,12 +42,6 @@
     )
 
 
-def normalize_virtualization(virtualized):
-    """Jobs with NULL virtualization settings should be treated the
-       same way as virtualized jobs."""
-    return virtualized is None or virtualized
-
-
 def specific_job_classes():
     """Job classes that may run on the build farm."""
     job_classes = dict()
@@ -72,31 +58,6 @@
     return job_classes
 
 
-def get_builder_data():
-    """How many working builders are there, how are they configured?"""
-    builder_data = """
-        SELECT processor, virtualized, COUNT(id) FROM builder
-        WHERE builderok = TRUE AND manual = FALSE
-        GROUP BY processor, virtualized;
-    """
-    results = IStore(BuildQueue).execute(builder_data).get_all()
-    builders_in_total = virtualized_total = 0
-
-    builder_stats = defaultdict(int)
-    for processor, virtualized, count in results:
-        builders_in_total += count
-        if virtualized:
-            virtualized_total += count
-        builder_stats[(processor, virtualized)] = count
-
-    builder_stats[(None, True)] = virtualized_total
-    # Jobs with a NULL virtualized flag should be treated the same as
-    # jobs where virtualized=TRUE.
-    builder_stats[(None, None)] = virtualized_total
-    builder_stats[(None, False)] = builders_in_total - virtualized_total
-    return builder_stats
-
-
 class BuildQueue(SQLBase):
     implements(IBuildQueue)
     _table = "BuildQueue"
@@ -219,295 +180,15 @@
         self.specific_job.jobCancel()
         self.destroySelf()
 
-    def _getFreeBuildersCount(self, processor, virtualized):
-        """How many builders capable of running jobs for the given processor
-        and virtualization combination are idle/free at present?"""
-        query = """
-            SELECT COUNT(id) FROM builder
-            WHERE
-                builderok = TRUE AND manual = FALSE
-                AND id NOT IN (
-                    SELECT builder FROM BuildQueue WHERE builder IS NOT NULL)
-                AND virtualized = %s
-            """ % sqlvalues(normalize_virtualization(virtualized))
-        if processor is not None:
-            query += """
-                AND processor = %s
-            """ % sqlvalues(processor)
-        result_set = IStore(BuildQueue).execute(query)
-        free_builders = result_set.get_one()[0]
-        return free_builders
-
-    def _estimateTimeToNextBuilder(self):
-        """Estimate time until next builder becomes available.
-
-        For the purpose of estimating the dispatch time of the job of interest
-        (JOI) we need to know how long it will take until the job at the head
-        of JOI's queue is dispatched.
-
-        There are two cases to consider here: the head job is
-
-            - processor dependent: only builders with the matching
-              processor/virtualization combination should be considered.
-            - *not* processor dependent: all builders with the matching
-              virtualization setting should be considered.
-
-        :return: The estimated number of seconds untils a builder capable of
-            running the head job becomes available.
-        """
-        head_job_platform = self._getHeadJobPlatform()
-
-        # Return a zero delay if we still have free builders available for the
-        # given platform/virtualization combination.
-        free_builders = self._getFreeBuildersCount(*head_job_platform)
-        if free_builders > 0:
-            return 0
-
-        head_job_processor, head_job_virtualized = head_job_platform
-
-        now = self._now()
-        delay_query = """
-            SELECT MIN(
-              CASE WHEN
-                EXTRACT(EPOCH FROM
-                  (BuildQueue.estimated_duration -
-                   (((%s AT TIME ZONE 'UTC') - Job.date_started))))  >= 0
-              THEN
-                EXTRACT(EPOCH FROM
-                  (BuildQueue.estimated_duration -
-                   (((%s AT TIME ZONE 'UTC') - Job.date_started))))
-              ELSE
-                -- Assume that jobs that have overdrawn their estimated
-                -- duration time budget will complete within 2 minutes.
-                -- This is a wild guess but has worked well so far.
-                --
-                -- Please note that this is entirely innocuous i.e. if our
-                -- guess is off nothing bad will happen but our estimate will
-                -- not be as good as it could be.
-                120
-              END)
-            FROM
-                BuildQueue, Job, Builder
-            WHERE
-                BuildQueue.job = Job.id
-                AND BuildQueue.builder = Builder.id
-                AND Builder.manual = False
-                AND Builder.builderok = True
-                AND Job.status = %s
-                AND Builder.virtualized = %s
-            """ % sqlvalues(
-                now, now, JobStatus.RUNNING,
-                normalize_virtualization(head_job_virtualized))
-
-        if head_job_processor is not None:
-            # Only look at builders with specific processor types.
-            delay_query += """
-                AND Builder.processor = %s
-                """ % sqlvalues(head_job_processor)
-
-        result_set = IStore(BuildQueue).execute(delay_query)
-        head_job_delay = result_set.get_one()[0]
-        return (0 if head_job_delay is None else int(head_job_delay))
-
-    def _getPendingJobsClauses(self):
-        """WHERE clauses for pending job queries, used for dipatch time
-        estimation."""
-        virtualized = normalize_virtualization(self.virtualized)
-        clauses = """
-            BuildQueue.job = Job.id
-            AND Job.status = %s
-            AND (
-                -- The score must be either above my score or the
-                -- job must be older than me in cases where the
-                -- score is equal.
-                BuildQueue.lastscore > %s OR
-                (BuildQueue.lastscore = %s AND Job.id < %s))
-            -- The virtualized values either match or the job
-            -- does not care about virtualization and the job
-            -- of interest (JOI) is to be run on a virtual builder
-            -- (we want to prevent the execution of untrusted code
-            -- on native builders).
-            AND COALESCE(buildqueue.virtualized, TRUE) = %s
-            """ % sqlvalues(
-                JobStatus.WAITING, self.lastscore, self.lastscore, self.job,
-                virtualized)
-        processor_clause = """
-            AND (
-                -- The processor values either match or the candidate
-                -- job is processor-independent.
-                buildqueue.processor = %s OR
-                buildqueue.processor IS NULL)
-            """ % sqlvalues(self.processor)
-        # We don't care about processors if the estimation is for a
-        # processor-independent job.
-        if self.processor is not None:
-            clauses += processor_clause
-        return clauses
-
-    def _getHeadJobPlatform(self):
-        """Find the processor and virtualization setting for the head job.
-
-        Among the jobs that compete with the job of interest (JOI) for
-        builders and are queued ahead of it the head job is the one in pole
-        position i.e. the one to be dispatched to a builder next.
-
-        :return: A (processor, virtualized) tuple which is the head job's
-        platform or None if the JOI is the head job.
-        """
-        my_platform = (
-            getattr(self.processor, 'id', None),
-            normalize_virtualization(self.virtualized))
-        query = """
-            SELECT
-                processor,
-                virtualized
-            FROM
-                BuildQueue, Job
-            WHERE
-            """
-        query += self._getPendingJobsClauses()
-        query += """
-            ORDER BY lastscore DESC, job LIMIT 1
-            """
-        result = IStore(BuildQueue).execute(query).get_one()
-        return (my_platform if result is None else result)
-
-    def _estimateJobDelay(self, builder_stats):
-        """Sum of estimated durations for *pending* jobs ahead in queue.
-
-        For the purpose of estimating the dispatch time of the job of
-        interest (JOI) we need to know the delay caused by all the pending
-        jobs that are ahead of the JOI in the queue and that compete with it
-        for builders.
-
-        :param builder_stats: A dictionary with builder counts where the
-            key is a (processor, virtualized) combination (aka "platform") and
-            the value is the number of builders that can take on jobs
-            requiring that combination.
-        :return: An integer value holding the sum of delays (in seconds)
-            caused by the jobs that are ahead of and competing with the JOI.
-        """
-        def jobs_compete_for_builders(a, b):
-            """True if the two jobs compete for builders."""
-            a_processor, a_virtualized = a
-            b_processor, b_virtualized = b
-            if a_processor is None or b_processor is None:
-                # If either of the jobs is platform-independent then the two
-                # jobs compete for the same builders if the virtualization
-                # settings match.
-                if a_virtualized == b_virtualized:
-                    return True
-            else:
-                # Neither job is platform-independent, match processor and
-                # virtualization settings.
-                return a == b
-
-        my_platform = (
-            getattr(self.processor, 'id', None),
-            normalize_virtualization(self.virtualized))
-        query = """
-            SELECT
-                BuildQueue.processor,
-                BuildQueue.virtualized,
-                COUNT(BuildQueue.job),
-                CAST(EXTRACT(
-                    EPOCH FROM
-                        SUM(BuildQueue.estimated_duration)) AS INTEGER)
-            FROM
-                BuildQueue, Job
-            WHERE
-            """
-        query += self._getPendingJobsClauses()
-        query += """
-            GROUP BY BuildQueue.processor, BuildQueue.virtualized
-            """
-
-        delays_by_platform = IStore(BuildQueue).execute(query).get_all()
-
-        # This will be used to capture per-platform delay totals.
-        delays = defaultdict(int)
-        # This will be used to capture per-platform job counts.
-        job_counts = defaultdict(int)
-
-        # Divide the estimated duration of the jobs as follows:
-        #   - if a job is tied to a processor TP then divide the estimated
-        #     duration of that job by the number of builders that target TP
-        #     since only these can build the job.
-        #   - if the job is processor-independent then divide its estimated
-        #     duration by the total number of builders with the same
-        #     virtualization setting because any one of them may run it.
-        for processor, virtualized, job_count, delay in delays_by_platform:
-            virtualized = normalize_virtualization(virtualized)
-            platform = (processor, virtualized)
-            builder_count = builder_stats.get(platform, 0)
-            if builder_count == 0:
-                # There is no builder that can run this job, ignore it
-                # for the purpose of dispatch time estimation.
-                continue
-
-            if jobs_compete_for_builders(my_platform, platform):
-                # The jobs that target the platform at hand compete with
-                # the JOI for builders, add their delays.
-                delays[platform] += delay
-                job_counts[platform] += job_count
-
-        sum_of_delays = 0
-        # Now devide the delays based on a jobs/builders comparison.
-        for platform, duration in delays.iteritems():
-            jobs = job_counts[platform]
-            builders = builder_stats[platform]
-            # If there are less jobs than builders that can take them on,
-            # the delays should be averaged/divided by the number of jobs.
-            denominator = (jobs if jobs < builders else builders)
-            if denominator > 1:
-                duration = int(duration / float(denominator))
-
-            sum_of_delays += duration
-
-        return sum_of_delays
-
-    def getEstimatedJobStartTime(self):
-        """See `IBuildQueue`.
-
-        The estimated dispatch time for the build farm job at hand is
-        calculated from the following ingredients:
-            * the start time for the head job (job at the
-              head of the respective build queue)
-            * the estimated build durations of all jobs that
-              precede the job of interest (JOI) in the build queue
-              (divided by the number of machines in the respective
-              build pool)
-        """
-        # This method may only be invoked for pending jobs.
-        if self.job.status != JobStatus.WAITING:
-            raise AssertionError(
-                "The start time is only estimated for pending jobs.")
-
-        builder_stats = get_builder_data()
-        platform = (getattr(self.processor, 'id', None), self.virtualized)
-        if builder_stats[platform] == 0:
-            # No builders that can run the job at hand
-            #   -> no dispatch time estimation available.
-            return None
-
-        # Get the sum of the estimated run times for *pending* jobs that are
-        # ahead of us in the queue.
-        sum_of_delays = self._estimateJobDelay(builder_stats)
-
-        # Get the minimum time duration until the next builder becomes
-        # available.
-        min_wait_time = self._estimateTimeToNextBuilder()
-
-        # A job will not get dispatched in less than 5 seconds no matter what.
-        start_time = max(5, min_wait_time + sum_of_delays)
-        result = self._now() + timedelta(seconds=start_time)
-
-        return result
+    def getEstimatedJobStartTime(self, now=None):
+        """See `IBuildQueue`."""
+        from lp.buildmaster.queuedepth import estimate_job_start_time
+        return estimate_job_start_time(self, now)
 
     @staticmethod
     def _now():
         """Return current time (UTC).  Overridable for test purposes."""
-        return datetime.now(pytz.UTC)
+        return datetime.now(pytz.utc)
 
 
 class BuildQueueSet(object):

=== added file 'lib/lp/buildmaster/queuedepth.py'
--- lib/lp/buildmaster/queuedepth.py	1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/queuedepth.py	2013-10-31 06:45:03 +0000
@@ -0,0 +1,342 @@
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+    'estimate_job_start_time',
+    ]
+
+from collections import defaultdict
+from datetime import (
+    datetime,
+    timedelta,
+    )
+
+from pytz import utc
+
+from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import sqlvalues
+from lp.services.job.interfaces.job import JobStatus
+
+
+def get_builder_data():
+    """How many working builders are there, how are they configured?"""
+    builder_data = """
+        SELECT processor, virtualized, COUNT(id) FROM builder
+        WHERE builderok = TRUE AND manual = FALSE
+        GROUP BY processor, virtualized;
+    """
+    results = IStore(BuildQueue).execute(builder_data).get_all()
+    builders_in_total = virtualized_total = 0
+
+    builder_stats = defaultdict(int)
+    for processor, virtualized, count in results:
+        builders_in_total += count
+        if virtualized:
+            virtualized_total += count
+        builder_stats[(processor, virtualized)] = count
+
+    builder_stats[(None, True)] = virtualized_total
+    # Jobs with a NULL virtualized flag should be treated the same as
+    # jobs where virtualized=TRUE.
+    builder_stats[(None, None)] = virtualized_total
+    builder_stats[(None, False)] = builders_in_total - virtualized_total
+    return builder_stats
+
+
+def normalize_virtualization(virtualized):
+    """Jobs with NULL virtualization settings should be treated the
+       same way as virtualized jobs."""
+    return virtualized is None or virtualized
+
+
+def get_free_builders_count(processor, virtualized):
+    """How many builders capable of running jobs for the given processor
+    and virtualization combination are idle/free at present?"""
+    query = """
+        SELECT COUNT(id) FROM builder
+        WHERE
+            builderok = TRUE AND manual = FALSE
+            AND id NOT IN (
+                SELECT builder FROM BuildQueue WHERE builder IS NOT NULL)
+            AND virtualized = %s
+        """ % sqlvalues(normalize_virtualization(virtualized))
+    if processor is not None:
+        query += """
+            AND processor = %s
+        """ % sqlvalues(processor)
+    result_set = IStore(BuildQueue).execute(query)
+    free_builders = result_set.get_one()[0]
+    return free_builders
+
+
+def get_head_job_platform(bq):
+    """Find the processor and virtualization setting for the head job.
+
+    Among the jobs that compete with the job of interest (JOI) for
+    builders and are queued ahead of it the head job is the one in pole
+    position i.e. the one to be dispatched to a builder next.
+
+    :return: A (processor, virtualized) tuple which is the head job's
+    platform or None if the JOI is the head job.
+    """
+    my_platform = (
+        getattr(bq.processor, 'id', None),
+        normalize_virtualization(bq.virtualized))
+    query = """
+        SELECT
+            processor,
+            virtualized
+        FROM
+            BuildQueue, Job
+        WHERE
+        """
+    query += get_pending_jobs_clauses(bq)
+    query += """
+        ORDER BY lastscore DESC, job LIMIT 1
+        """
+    result = IStore(BuildQueue).execute(query).get_one()
+    return (my_platform if result is None else result)
+
+
+def estimate_time_to_next_builder(bq, now=None):
+    """Estimate time until next builder becomes available.
+
+    For the purpose of estimating the dispatch time of the job of interest
+    (JOI) we need to know how long it will take until the job at the head
+    of JOI's queue is dispatched.
+
+    There are two cases to consider here: the head job is
+
+        - processor dependent: only builders with the matching
+            processor/virtualization combination should be considered.
+        - *not* processor dependent: all builders with the matching
+            virtualization setting should be considered.
+
+    :return: The estimated number of seconds untils a builder capable of
+        running the head job becomes available.
+    """
+    head_job_platform = get_head_job_platform(bq)
+
+    # Return a zero delay if we still have free builders available for the
+    # given platform/virtualization combination.
+    free_builders = get_free_builders_count(*head_job_platform)
+    if free_builders > 0:
+        return 0
+
+    head_job_processor, head_job_virtualized = head_job_platform
+
+    now = now or datetime.datetime.now(utc)
+    delay_query = """
+        SELECT MIN(
+            CASE WHEN
+            EXTRACT(EPOCH FROM
+                (BuildQueue.estimated_duration -
+                (((%s AT TIME ZONE 'UTC') - Job.date_started))))  >= 0
+            THEN
+            EXTRACT(EPOCH FROM
+                (BuildQueue.estimated_duration -
+                (((%s AT TIME ZONE 'UTC') - Job.date_started))))
+            ELSE
+            -- Assume that jobs that have overdrawn their estimated
+            -- duration time budget will complete within 2 minutes.
+            -- This is a wild guess but has worked well so far.
+            --
+            -- Please note that this is entirely innocuous i.e. if our
+            -- guess is off nothing bad will happen but our estimate will
+            -- not be as good as it could be.
+            120
+            END)
+        FROM
+            BuildQueue, Job, Builder
+        WHERE
+            BuildQueue.job = Job.id
+            AND BuildQueue.builder = Builder.id
+            AND Builder.manual = False
+            AND Builder.builderok = True
+            AND Job.status = %s
+            AND Builder.virtualized = %s
+        """ % sqlvalues(
+            now, now, JobStatus.RUNNING,
+            normalize_virtualization(head_job_virtualized))
+
+    if head_job_processor is not None:
+        # Only look at builders with specific processor types.
+        delay_query += """
+            AND Builder.processor = %s
+            """ % sqlvalues(head_job_processor)
+
+    result_set = IStore(BuildQueue).execute(delay_query)
+    head_job_delay = result_set.get_one()[0]
+    return (0 if head_job_delay is None else int(head_job_delay))
+
+
+def get_pending_jobs_clauses(bq):
+    """WHERE clauses for pending job queries, used for dipatch time
+    estimation."""
+    virtualized = normalize_virtualization(bq.virtualized)
+    clauses = """
+        BuildQueue.job = Job.id
+        AND Job.status = %s
+        AND (
+            -- The score must be either above my score or the
+            -- job must be older than me in cases where the
+            -- score is equal.
+            BuildQueue.lastscore > %s OR
+            (BuildQueue.lastscore = %s AND Job.id < %s))
+        -- The virtualized values either match or the job
+        -- does not care about virtualization and the job
+        -- of interest (JOI) is to be run on a virtual builder
+        -- (we want to prevent the execution of untrusted code
+        -- on native builders).
+        AND COALESCE(buildqueue.virtualized, TRUE) = %s
+        """ % sqlvalues(
+            JobStatus.WAITING, bq.lastscore, bq.lastscore, bq.job,
+            virtualized)
+    processor_clause = """
+        AND (
+            -- The processor values either match or the candidate
+            -- job is processor-independent.
+            buildqueue.processor = %s OR
+            buildqueue.processor IS NULL)
+        """ % sqlvalues(bq.processor)
+    # We don't care about processors if the estimation is for a
+    # processor-independent job.
+    if bq.processor is not None:
+        clauses += processor_clause
+    return clauses
+
+
+def estimate_job_delay(bq, builder_stats):
+    """Sum of estimated durations for *pending* jobs ahead in queue.
+
+    For the purpose of estimating the dispatch time of the job of
+    interest (JOI) we need to know the delay caused by all the pending
+    jobs that are ahead of the JOI in the queue and that compete with it
+    for builders.
+
+    :param builder_stats: A dictionary with builder counts where the
+        key is a (processor, virtualized) combination (aka "platform") and
+        the value is the number of builders that can take on jobs
+        requiring that combination.
+    :return: An integer value holding the sum of delays (in seconds)
+        caused by the jobs that are ahead of and competing with the JOI.
+    """
+    def jobs_compete_for_builders(a, b):
+        """True if the two jobs compete for builders."""
+        a_processor, a_virtualized = a
+        b_processor, b_virtualized = b
+        if a_processor is None or b_processor is None:
+            # If either of the jobs is platform-independent then the two
+            # jobs compete for the same builders if the virtualization
+            # settings match.
+            if a_virtualized == b_virtualized:
+                return True
+        else:
+            # Neither job is platform-independent, match processor and
+            # virtualization settings.
+            return a == b
+
+    my_platform = (
+        getattr(bq.processor, 'id', None),
+        normalize_virtualization(bq.virtualized))
+    query = """
+        SELECT
+            BuildQueue.processor,
+            BuildQueue.virtualized,
+            COUNT(BuildQueue.job),
+            CAST(EXTRACT(
+                EPOCH FROM
+                    SUM(BuildQueue.estimated_duration)) AS INTEGER)
+        FROM
+            BuildQueue, Job
+        WHERE
+        """
+    query += get_pending_jobs_clauses(bq)
+    query += """
+        GROUP BY BuildQueue.processor, BuildQueue.virtualized
+        """
+
+    delays_by_platform = IStore(BuildQueue).execute(query).get_all()
+
+    # This will be used to capture per-platform delay totals.
+    delays = defaultdict(int)
+    # This will be used to capture per-platform job counts.
+    job_counts = defaultdict(int)
+
+    # Divide the estimated duration of the jobs as follows:
+    #   - if a job is tied to a processor TP then divide the estimated
+    #     duration of that job by the number of builders that target TP
+    #     since only these can build the job.
+    #   - if the job is processor-independent then divide its estimated
+    #     duration by the total number of builders with the same
+    #     virtualization setting because any one of them may run it.
+    for processor, virtualized, job_count, delay in delays_by_platform:
+        virtualized = normalize_virtualization(virtualized)
+        platform = (processor, virtualized)
+        builder_count = builder_stats.get(platform, 0)
+        if builder_count == 0:
+            # There is no builder that can run this job, ignore it
+            # for the purpose of dispatch time estimation.
+            continue
+
+        if jobs_compete_for_builders(my_platform, platform):
+            # The jobs that target the platform at hand compete with
+            # the JOI for builders, add their delays.
+            delays[platform] += delay
+            job_counts[platform] += job_count
+
+    sum_of_delays = 0
+    # Now devide the delays based on a jobs/builders comparison.
+    for platform, duration in delays.iteritems():
+        jobs = job_counts[platform]
+        builders = builder_stats[platform]
+        # If there are less jobs than builders that can take them on,
+        # the delays should be averaged/divided by the number of jobs.
+        denominator = (jobs if jobs < builders else builders)
+        if denominator > 1:
+            duration = int(duration / float(denominator))
+
+        sum_of_delays += duration
+
+    return sum_of_delays
+
+
+def estimate_job_start_time(bq, now=None):
+    """Estimate the start time of the given `IBuildQueue`.
+
+    The estimated dispatch time for the build farm job at hand is
+    calculated from the following ingredients:
+        * the start time for the head job (job at the
+            head of the respective build queue)
+        * the estimated build durations of all jobs that
+            precede the job of interest (JOI) in the build queue
+            (divided by the number of machines in the respective
+            build pool)
+    """
+    # This method may only be invoked for pending jobs.
+    if bq.job.status != JobStatus.WAITING:
+        raise AssertionError(
+            "The start time is only estimated for pending jobs.")
+
+    builder_stats = get_builder_data()
+    platform = (getattr(bq.processor, 'id', None), bq.virtualized)
+    if builder_stats[platform] == 0:
+        # No builders that can run the job at hand
+        #   -> no dispatch time estimation available.
+        return None
+
+    # Get the sum of the estimated run times for *pending* jobs that are
+    # ahead of us in the queue.
+    sum_of_delays = estimate_job_delay(bq, builder_stats)
+
+    # Get the minimum time duration until the next builder becomes
+    # available.
+    min_wait_time = estimate_time_to_next_builder(bq, now=now)
+
+    # A job will not get dispatched in less than 5 seconds no matter what.
+    start_time = max(5, min_wait_time + sum_of_delays)
+    result = (now or datetime.now(utc)) + timedelta(seconds=start_time)
+    return result

=== modified file 'lib/lp/buildmaster/tests/test_buildqueue.py'
--- lib/lp/buildmaster/tests/test_buildqueue.py	2013-09-13 06:20:49 +0000
+++ lib/lp/buildmaster/tests/test_buildqueue.py	2013-10-31 06:45:03 +0000
@@ -2,40 +2,27 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 """Test BuildQueue features."""
 
-from datetime import (
-    datetime,
-    timedelta,
-    )
+from datetime import timedelta
 
-from pytz import utc
 from storm.sqlobject import SQLObjectNotFound
 from storm.store import Store
 from zope import component
-from zope.component import (
-    getGlobalSiteManager,
-    getUtility,
-    )
-from zope.security.proxy import removeSecurityProxy
+from zope.component import getGlobalSiteManager
 
 from lp.buildmaster.enums import (
     BuildFarmJobType,
     BuildStatus,
     )
-from lp.buildmaster.interfaces.builder import IBuilderSet
 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
 from lp.buildmaster.model.builder import specific_job_classes
 from lp.buildmaster.model.buildfarmjob import BuildFarmJobMixin
-from lp.buildmaster.model.buildqueue import (
-    BuildQueue,
-    get_builder_data,
-    )
+from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.services.database.interfaces import IStore
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
     )
 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
-from lp.soyuz.interfaces.processor import IProcessorSet
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import TestCaseWithFactory
 from lp.testing.fakemethod import FakeMethod
@@ -66,31 +53,6 @@
     return (None, None)
 
 
-def nth_builder(test, bq, n):
-    """Find nth builder that can execute the given build."""
-
-    def builder_key(job):
-        """Access key for builders capable of running the given job."""
-        return (getattr(job.processor, 'id', None), job.virtualized)
-
-    builder = None
-    builders = test.builders.get(builder_key(bq), [])
-    try:
-        for builder in builders[n - 1:]:
-            if builder.builderok:
-                break
-    except IndexError:
-        pass
-    return builder
-
-
-def assign_to_builder(test, job_name, builder_number, processor='386'):
-    """Simulate assigning a build to a builder."""
-    build, bq = find_job(test, job_name, processor)
-    builder = nth_builder(test, bq, builder_number)
-    bq.markAsBuilding(builder)
-
-
 def print_build_setup(builds):
     """Show the build set-up for a particular test."""
 
@@ -114,389 +76,6 @@
             queue_entry.lastscore)
 
 
-def check_mintime_to_builder(test, bq, min_time):
-    """Test the estimated time until a builder becomes available."""
-    # Monkey-patch BuildQueueSet._now() so it returns a constant time stamp
-    # that's not too far in the future. This avoids spurious test failures.
-    monkey_patch_the_now_property(bq)
-    delay = removeSecurityProxy(bq)._estimateTimeToNextBuilder()
-    test.assertTrue(
-        delay <= min_time,
-        "Wrong min time to next available builder (%s > %s)"
-        % (delay, min_time))
-
-
-def set_remaining_time_for_running_job(bq, remainder):
-    """Set remaining running time for job."""
-    offset = bq.estimated_duration.seconds - remainder
-    removeSecurityProxy(bq.job).date_started = (
-        datetime.now(utc) - timedelta(seconds=offset))
-
-
-def check_delay_for_job(test, the_job, delay):
-    # Obtain the builder statistics pertaining to this job.
-    builder_data = get_builder_data()
-    estimated_delay = removeSecurityProxy(the_job)._estimateJobDelay(
-        builder_data)
-    test.assertEqual(delay, estimated_delay)
-
-
-def total_builders():
-    """How many available builders do we have in total?"""
-    builder_data = get_builder_data()
-    return builder_data[(None, False)] + builder_data[(None, True)]
-
-
-def builders_for_job(job):
-    """How many available builders can run the given job?"""
-    builder_data = get_builder_data()
-    return builder_data[(getattr(job.processor, 'id', None), job.virtualized)]
-
-
-def monkey_patch_the_now_property(buildqueue):
-    """Patch BuildQueue._now() so it returns a constant time stamp.
-
-    This avoids spurious test failures.
-    """
-    # Use the date/time the job started if available.
-    naked_buildqueue = removeSecurityProxy(buildqueue)
-    if buildqueue.job.date_started:
-        time_stamp = buildqueue.job.date_started
-    else:
-        time_stamp = naked_buildqueue._now()
-
-    naked_buildqueue._now = FakeMethod(result=time_stamp)
-    return time_stamp
-
-
-def check_estimate(test, job, delay_in_seconds):
-    """Does the dispatch time estimate match the expectation?"""
-    # Monkey-patch BuildQueueSet._now() so it returns a constant time stamp.
-    # This avoids spurious test failures.
-    time_stamp = monkey_patch_the_now_property(job)
-    estimate = job.getEstimatedJobStartTime()
-    if delay_in_seconds is None:
-        test.assertEquals(
-            delay_in_seconds, estimate,
-            "An estimate should not be possible at present but one was "
-            "returned (%s) nevertheless." % estimate)
-    else:
-        estimate -= time_stamp
-        test.assertTrue(
-            estimate.seconds <= delay_in_seconds,
-            "The estimated delay deviates from the expected one (%s > %s)" %
-            (estimate.seconds, delay_in_seconds))
-
-
-def disable_builders(test, processor_name, virtualized):
-    """Disable bulders with the given processor and virtualization setting."""
-    if processor_name is not None:
-        processor = getUtility(IProcessorSet).getByName(processor_name)
-    for builder in test.builders[(processor.id, virtualized)]:
-        builder.builderok = False
-
-
-class TestBuildQueueBase(TestCaseWithFactory):
-    """Setup the test publisher and some builders."""
-
-    layer = LaunchpadZopelessLayer
-
-    def setUp(self):
-        super(TestBuildQueueBase, self).setUp()
-        self.publisher = SoyuzTestPublisher()
-        self.publisher.prepareBreezyAutotest()
-
-        # First make nine 'i386' builders.
-        self.i1 = self.factory.makeBuilder(name='i386-v-1')
-        self.i2 = self.factory.makeBuilder(name='i386-v-2')
-        self.i3 = self.factory.makeBuilder(name='i386-v-3')
-        self.i4 = self.factory.makeBuilder(name='i386-v-4')
-        self.i5 = self.factory.makeBuilder(name='i386-v-5')
-        self.i6 = self.factory.makeBuilder(name='i386-n-6', virtualized=False)
-        self.i7 = self.factory.makeBuilder(name='i386-n-7', virtualized=False)
-        self.i8 = self.factory.makeBuilder(name='i386-n-8', virtualized=False)
-        self.i9 = self.factory.makeBuilder(name='i386-n-9', virtualized=False)
-
-        # Next make seven 'hppa' builders.
-        self.hppa_proc = getUtility(IProcessorSet).getByName('hppa')
-        self.h1 = self.factory.makeBuilder(
-            name='hppa-v-1', processor=self.hppa_proc)
-        self.h2 = self.factory.makeBuilder(
-            name='hppa-v-2', processor=self.hppa_proc)
-        self.h3 = self.factory.makeBuilder(
-            name='hppa-v-3', processor=self.hppa_proc)
-        self.h4 = self.factory.makeBuilder(
-            name='hppa-v-4', processor=self.hppa_proc)
-        self.h5 = self.factory.makeBuilder(
-            name='hppa-n-5', processor=self.hppa_proc, virtualized=False)
-        self.h6 = self.factory.makeBuilder(
-            name='hppa-n-6', processor=self.hppa_proc, virtualized=False)
-        self.h7 = self.factory.makeBuilder(
-            name='hppa-n-7', processor=self.hppa_proc, virtualized=False)
-
-        # Finally make five 'amd64' builders.
-        self.amd_proc = getUtility(IProcessorSet).getByName('amd64')
-        self.a1 = self.factory.makeBuilder(
-            name='amd64-v-1', processor=self.amd_proc)
-        self.a2 = self.factory.makeBuilder(
-            name='amd64-v-2', processor=self.amd_proc)
-        self.a3 = self.factory.makeBuilder(
-            name='amd64-v-3', processor=self.amd_proc)
-        self.a4 = self.factory.makeBuilder(
-            name='amd64-n-4', processor=self.amd_proc, virtualized=False)
-        self.a5 = self.factory.makeBuilder(
-            name='amd64-n-5', processor=self.amd_proc, virtualized=False)
-
-        self.builders = dict()
-        self.x86_proc = getUtility(IProcessorSet).getByName('386')
-        # x86 native
-        self.builders[(self.x86_proc.id, False)] = [
-            self.i6, self.i7, self.i8, self.i9]
-        # x86 virtual
-        self.builders[(self.x86_proc.id, True)] = [
-            self.i1, self.i2, self.i3, self.i4, self.i5]
-
-        # amd64 native
-        self.builders[(self.amd_proc.id, False)] = [self.a4, self.a5]
-        # amd64 virtual
-        self.builders[(self.amd_proc.id, True)] = [self.a1, self.a2, self.a3]
-
-        # hppa native
-        self.builders[(self.hppa_proc.id, False)] = [
-            self.h5,
-            self.h6,
-            self.h7,
-            ]
-        # hppa virtual
-        self.builders[(self.hppa_proc.id, True)] = [
-            self.h1, self.h2, self.h3, self.h4]
-
-        # Ensure all builders are operational.
-        for builders in self.builders.values():
-            for builder in builders:
-                builder.builderok = True
-                builder.manual = False
-
-        # Native builders irrespective of processor.
-        self.builders[(None, False)] = []
-        self.builders[(None, False)].extend(
-            self.builders[(self.x86_proc.id, False)])
-        self.builders[(None, False)].extend(
-            self.builders[(self.amd_proc.id, False)])
-        self.builders[(None, False)].extend(
-            self.builders[(self.hppa_proc.id, False)])
-
-        # Virtual builders irrespective of processor.
-        self.builders[(None, True)] = []
-        self.builders[(None, True)].extend(
-            self.builders[(self.x86_proc.id, True)])
-        self.builders[(None, True)].extend(
-            self.builders[(self.amd_proc.id, True)])
-        self.builders[(None, True)].extend(
-            self.builders[(self.hppa_proc.id, True)])
-
-        # Disable the sample data builders.
-        getUtility(IBuilderSet)['bob'].builderok = False
-        getUtility(IBuilderSet)['frog'].builderok = False
-
-
-class SingleArchBuildsBase(TestBuildQueueBase):
-    """Set up a test environment with builds that target a single
-    processor."""
-
-    def setUp(self):
-        """Set up some native x86 builds for the test archive."""
-        super(SingleArchBuildsBase, self).setUp()
-        # The builds will be set up as follows:
-        #
-        #      gedit, p:  386, v:False e:0:01:00 *** s: 1001
-        #    firefox, p:  386, v:False e:0:02:00 *** s: 1002
-        #        apg, p:  386, v:False e:0:03:00 *** s: 1003
-        #        vim, p:  386, v:False e:0:04:00 *** s: 1004
-        #        gcc, p:  386, v:False e:0:05:00 *** s: 1005
-        #      bison, p:  386, v:False e:0:06:00 *** s: 1006
-        #       flex, p:  386, v:False e:0:07:00 *** s: 1007
-        #   postgres, p:  386, v:False e:0:08:00 *** s: 1008
-        #
-        # p=processor, v=virtualized, e=estimated_duration, s=score
-
-        # First mark all builds in the sample data as already built.
-        sample_data = IStore(BinaryPackageBuild).find(BinaryPackageBuild)
-        for build in sample_data:
-            build.buildstate = BuildStatus.FULLYBUILT
-        IStore(BinaryPackageBuild).flush()
-
-        # We test builds that target a primary archive.
-        self.non_ppa = self.factory.makeArchive(
-            name="primary", purpose=ArchivePurpose.PRIMARY)
-        self.non_ppa.require_virtualized = False
-
-        self.builds = []
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="firefox",
-                status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="apg", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="vim", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="gcc", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="bison", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="flex", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="postgres",
-                status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa).createMissingBuilds())
-        # Set up the builds for test.
-        score = 1000
-        duration = 0
-        for build in self.builds:
-            score += 1
-            duration += 60
-            bq = build.buildqueue_record
-            bq.lastscore = score
-            bq.estimated_duration = timedelta(seconds=duration)
-
-
-class TestBuilderData(SingleArchBuildsBase):
-    """Test the retrieval of builder related data. The latter is required
-    for job dispatch time estimations irrespective of job processor
-    architecture and virtualization setting."""
-
-    def test_builder_data(self):
-        # Make sure the builder numbers are correct. The builder data will
-        # be the same for all of our builds.
-        bq = self.builds[0].buildqueue_record
-        self.assertEqual(
-            21, total_builders(),
-            "The total number of builders is wrong.")
-        self.assertEqual(
-            4, builders_for_job(bq),
-            "[1] The total number of builders that can build the job in "
-            "question is wrong.")
-        builder_stats = get_builder_data()
-        self.assertEqual(
-            4, builder_stats[(self.x86_proc.id, False)],
-            "The number of native x86 builders is wrong")
-        self.assertEqual(
-            5, builder_stats[(self.x86_proc.id, True)],
-            "The number of virtual x86 builders is wrong")
-        self.assertEqual(
-            2, builder_stats[(self.amd_proc.id, False)],
-            "The number of native amd64 builders is wrong")
-        self.assertEqual(
-            3, builder_stats[(self.amd_proc.id, True)],
-            "The number of virtual amd64 builders is wrong")
-        self.assertEqual(
-            3, builder_stats[(self.hppa_proc.id, False)],
-            "The number of native hppa builders is wrong")
-        self.assertEqual(
-            4, builder_stats[(self.hppa_proc.id, True)],
-            "The number of virtual hppa builders is wrong")
-        self.assertEqual(
-            9, builder_stats[(None, False)],
-            "The number of *virtual* builders across all processors is wrong")
-        self.assertEqual(
-            12, builder_stats[(None, True)],
-            "The number of *native* builders across all processors is wrong")
-        # Disable the native x86 builders.
-        for builder in self.builders[(self.x86_proc.id, False)]:
-            builder.builderok = False
-        # Since all native x86 builders were disabled there are none left
-        # to build the job.
-        self.assertEqual(
-            0, builders_for_job(bq),
-            "[2] The total number of builders that can build the job in "
-            "question is wrong.")
-        # Re-enable one of them.
-        for builder in self.builders[(self.x86_proc.id, False)]:
-            builder.builderok = True
-            break
-        # Now there should be one builder available to build the job.
-        self.assertEqual(
-            1, builders_for_job(bq),
-            "[3] The total number of builders that can build the job in "
-            "question is wrong.")
-        # Disable the *virtual* x86 builders -- should not make any
-        # difference.
-        for builder in self.builders[(self.x86_proc.id, True)]:
-            builder.builderok = False
-        # There should still be one builder available to build the job.
-        self.assertEqual(
-            1, builders_for_job(bq),
-            "[4] The total number of builders that can build the job in "
-            "question is wrong.")
-
-    def test_free_builder_counts(self):
-        # Make sure the builder numbers are correct. The builder data will
-        # be the same for all of our builds.
-        build = self.builds[0]
-        # The build in question is an x86/native one.
-        self.assertEqual(self.x86_proc.id, build.processor.id)
-        self.assertEqual(False, build.is_virtualized)
-
-        # To test this non-interface method, we need to remove the
-        # security proxy.
-        bq = removeSecurityProxy(build.buildqueue_record)
-        builder_stats = get_builder_data()
-        # We have 4 x86 native builders.
-        self.assertEqual(
-            4, builder_stats[(self.x86_proc.id, False)],
-            "The number of native x86 builders is wrong")
-        # Initially all 4 builders are free.
-        free_count = bq._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(4, free_count)
-        # Once we assign a build to one of them we should see the free
-        # builders count drop by one.
-        assign_to_builder(self, 'postgres', 1)
-        free_count = bq._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(3, free_count)
-        # When we assign another build to one of them we should see the free
-        # builders count drop by one again.
-        assign_to_builder(self, 'gcc', 2)
-        free_count = bq._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(2, free_count)
-        # Let's use up another builder.
-        assign_to_builder(self, 'apg', 3)
-        free_count = bq._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(1, free_count)
-        # And now for the last one.
-        assign_to_builder(self, 'flex', 4)
-        free_count = bq._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(0, free_count)
-        # If we reset the 'flex' build the builder that was assigned to it
-        # will be free again.
-        build, bq = find_job(self, 'flex')
-        bq.reset()
-        free_count = removeSecurityProxy(bq)._getFreeBuildersCount(
-            build.processor, build.is_virtualized)
-        self.assertEqual(1, free_count)
-
-
 class TestBuildCancellation(TestCaseWithFactory):
     """Test cases for cancelling builds."""
 
@@ -536,303 +115,6 @@
         self.assertCancelled(build, bq)
 
 
-class TestMinTimeToNextBuilder(SingleArchBuildsBase):
-    """Test estimated time-to-builder with builds targetting a single
-    processor."""
-
-    def test_min_time_to_next_builder(self):
-        """When is the next builder capable of running the job at the head of
-        the queue becoming available?"""
-        # Test the estimation of the minimum time until a builder becomes
-        # available.
-
-        # The builds will be set up as follows:
-        #
-        #      gedit, p:  386, v:False e:0:01:00 *** s: 1001
-        #    firefox, p:  386, v:False e:0:02:00 *** s: 1002
-        #        apg, p:  386, v:False e:0:03:00 *** s: 1003
-        #        vim, p:  386, v:False e:0:04:00 *** s: 1004
-        #        gcc, p:  386, v:False e:0:05:00 *** s: 1005
-        #      bison, p:  386, v:False e:0:06:00 *** s: 1006
-        #       flex, p:  386, v:False e:0:07:00 *** s: 1007
-        #   postgres, p:  386, v:False e:0:08:00 *** s: 1008
-        #
-        # p=processor, v=virtualized, e=estimated_duration, s=score
-
-        # This will be the job of interest.
-        apg_build, apg_job = find_job(self, 'apg')
-        # One of four builders for the 'apg' build is immediately available.
-        check_mintime_to_builder(self, apg_job, 0)
-
-        # Assign the postgres job to a builder.
-        assign_to_builder(self, 'postgres', 1)
-        # Now one builder is gone. But there should still be a builder
-        # immediately available.
-        check_mintime_to_builder(self, apg_job, 0)
-
-        assign_to_builder(self, 'flex', 2)
-        check_mintime_to_builder(self, apg_job, 0)
-
-        assign_to_builder(self, 'bison', 3)
-        check_mintime_to_builder(self, apg_job, 0)
-
-        assign_to_builder(self, 'gcc', 4)
-        # Now that no builder is immediately available, the shortest
-        # remaing build time (based on the estimated duration) is returned:
-        #   300 seconds
-        # This is equivalent to the 'gcc' job's estimated duration.
-        check_mintime_to_builder(self, apg_job, 300)
-
-        # Now we pretend that the 'postgres' started 6 minutes ago. Its
-        # remaining execution time should be 2 minutes = 120 seconds and
-        # it now becomes the job whose builder becomes available next.
-        build, bq = find_job(self, 'postgres')
-        set_remaining_time_for_running_job(bq, 120)
-        check_mintime_to_builder(self, apg_job, 120)
-
-        # What happens when jobs overdraw the estimated duration? Let's
-        # pretend the 'flex' job started 8 minutes ago.
-        build, bq = find_job(self, 'flex')
-        set_remaining_time_for_running_job(bq, -60)
-        # In such a case we assume that the job will complete within 2
-        # minutes, this is a guess that has worked well so far.
-        check_mintime_to_builder(self, apg_job, 120)
-
-        # If there's a job that will complete within a shorter time then
-        # we expect to be given that time frame.
-        build, bq = find_job(self, 'postgres')
-        set_remaining_time_for_running_job(bq, 30)
-        check_mintime_to_builder(self, apg_job, 30)
-
-        # Disable the native x86 builders.
-        for builder in self.builders[(self.x86_proc.id, False)]:
-            builder.builderok = False
-
-        # No builders capable of running the job at hand are available now.
-        self.assertEquals(0, builders_for_job(apg_job))
-        # The "minimum time to builder" estimation logic is not aware of this
-        # though.
-        check_mintime_to_builder(self, apg_job, 0)
-
-        # The following job can only run on a native builder.
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=111, sourcename=u'xxr-gftp', score=1055,
-            virtualized=False)
-        self.builds.append(job.specific_job.build)
-
-        # Disable all native builders.
-        for builder in self.builders[(None, False)]:
-            builder.builderok = False
-
-        # All native builders are disabled now.  No builders capable of
-        # running the job at hand are available.
-        self.assertEquals(0, builders_for_job(job))
-        # The "minimum time to builder" estimation logic is not aware of the
-        # fact that no builders capable of running the job are available.
-        check_mintime_to_builder(self, job, 0)
-
-
-class MultiArchBuildsBase(TestBuildQueueBase):
-    """Set up a test environment with builds and multiple processors."""
-
-    def setUp(self):
-        """Set up some native x86 builds for the test archive."""
-        super(MultiArchBuildsBase, self).setUp()
-        # The builds will be set up as follows:
-        #
-        #      gedit, p: hppa, v:False e:0:01:00 *** s: 1001
-        #      gedit, p:  386, v:False e:0:02:00 *** s: 1002
-        #    firefox, p: hppa, v:False e:0:03:00 *** s: 1003
-        #    firefox, p:  386, v:False e:0:04:00 *** s: 1004
-        #        apg, p: hppa, v:False e:0:05:00 *** s: 1005
-        #        apg, p:  386, v:False e:0:06:00 *** s: 1006
-        #        vim, p: hppa, v:False e:0:07:00 *** s: 1007
-        #        vim, p:  386, v:False e:0:08:00 *** s: 1008
-        #        gcc, p: hppa, v:False e:0:09:00 *** s: 1009
-        #        gcc, p:  386, v:False e:0:10:00 *** s: 1010
-        #      bison, p: hppa, v:False e:0:11:00 *** s: 1011
-        #      bison, p:  386, v:False e:0:12:00 *** s: 1012
-        #       flex, p: hppa, v:False e:0:13:00 *** s: 1013
-        #       flex, p:  386, v:False e:0:14:00 *** s: 1014
-        #   postgres, p: hppa, v:False e:0:15:00 *** s: 1015
-        #   postgres, p:  386, v:False e:0:16:00 *** s: 1016
-        #
-        # p=processor, v=virtualized, e=estimated_duration, s=score
-
-        # First mark all builds in the sample data as already built.
-        sample_data = IStore(BinaryPackageBuild).find(BinaryPackageBuild)
-        for build in sample_data:
-            build.buildstate = BuildStatus.FULLYBUILT
-        IStore(BinaryPackageBuild).flush()
-
-        # We test builds that target a primary archive.
-        self.non_ppa = self.factory.makeArchive(
-            name="primary", purpose=ArchivePurpose.PRIMARY)
-        self.non_ppa.require_virtualized = False
-
-        self.builds = []
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="firefox",
-                status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="apg", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="vim", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="gcc", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="bison", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="flex", status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        self.builds.extend(
-            self.publisher.getPubSource(
-                sourcename="postgres",
-                status=PackagePublishingStatus.PUBLISHED,
-                archive=self.non_ppa,
-                architecturehintlist='any').createMissingBuilds())
-        # Set up the builds for test.
-        score = 1000
-        duration = 0
-        for build in self.builds:
-            score += getattr(self, 'score_increment', 1)
-            score += 1
-            duration += 60
-            bq = build.buildqueue_record
-            bq.lastscore = score
-            bq.estimated_duration = timedelta(seconds=duration)
-
-
-class TestMinTimeToNextBuilderMulti(MultiArchBuildsBase):
-    """Test estimated time-to-builder with builds and multiple processors."""
-
-    def disabled_test_min_time_to_next_builder(self):
-        """When is the next builder capable of running the job at the head of
-        the queue becoming available?"""
-        # XXX AaronBentley 2010-03-19 bug=541914: Fails spuriously
-        # One of four builders for the 'apg' build is immediately available.
-        apg_build, apg_job = find_job(self, 'apg', 'hppa')
-        check_mintime_to_builder(self, apg_job, 0)
-
-        # Assign the postgres job to a builder.
-        assign_to_builder(self, 'postgres', 1, 'hppa')
-        # Now one builder is gone. But there should still be a builder
-        # immediately available.
-        check_mintime_to_builder(self, apg_job, 0)
-
-        assign_to_builder(self, 'flex', 2, 'hppa')
-        check_mintime_to_builder(self, apg_job, 0)
-
-        assign_to_builder(self, 'bison', 3, 'hppa')
-        # Now that no builder is immediately available, the shortest
-        # remaing build time (based on the estimated duration) is returned:
-        #   660 seconds
-        # This is equivalent to the 'bison' job's estimated duration.
-        check_mintime_to_builder(self, apg_job, 660)
-
-        # Now we pretend that the 'postgres' started 13 minutes ago. Its
-        # remaining execution time should be 2 minutes = 120 seconds and
-        # it now becomes the job whose builder becomes available next.
-        build, bq = find_job(self, 'postgres', 'hppa')
-        set_remaining_time_for_running_job(bq, 120)
-        check_mintime_to_builder(self, apg_job, 120)
-
-        # What happens when jobs overdraw the estimated duration? Let's
-        # pretend the 'flex' job started 14 minutes ago.
-        build, bq = find_job(self, 'flex', 'hppa')
-        set_remaining_time_for_running_job(bq, -60)
-        # In such a case we assume that the job will complete within 2
-        # minutes, this is a guess that has worked well so far.
-        check_mintime_to_builder(self, apg_job, 120)
-
-        # If there's a job that will complete within a shorter time then
-        # we expect to be given that time frame.
-        build, bq = find_job(self, 'postgres', 'hppa')
-        set_remaining_time_for_running_job(bq, 30)
-        check_mintime_to_builder(self, apg_job, 30)
-
-        # Disable the native hppa builders.
-        for builder in self.builders[(self.hppa_proc.id, False)]:
-            builder.builderok = False
-
-        # No builders capable of running the job at hand are available now.
-        self.assertEquals(0, builders_for_job(apg_job))
-        check_mintime_to_builder(self, apg_job, 0)
-
-        # Let's add a processor-independent job to the mix.
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=False, estimated_duration=22,
-            sourcename='my-recipe-digikam', score=9999)
-        # There are still builders available for the processor-independent
-        # job.
-        self.assertEquals(6, builders_for_job(job))
-        # Even free ones.
-        self.assertTrue(
-            bq._getFreeBuildersCount(job.processor, job.virtualized) > 0,
-            "Builders are immediately available for processor-independent "
-            "jobs.")
-        check_mintime_to_builder(self, job, 0)
-
-        # Let's disable all builders.
-        for builders in self.builders.itervalues():
-            for builder in builders:
-                builder.builderok = False
-
-        # There are no builders capable of running even the processor
-        # independent jobs now.
-        self.assertEquals(0, builders_for_job(job))
-        check_mintime_to_builder(self, job, 0)
-
-        # Re-enable the native hppa builders.
-        for builder in self.builders[(self.hppa_proc.id, False)]:
-            builder.builderok = True
-
-        # The builder that's becoming available next is the one that's
-        # running the 'postgres' build.
-        check_mintime_to_builder(self, apg_job, 30)
-
-        # Make sure we'll find an x86 builder as well.
-        builder = self.builders[(self.x86_proc.id, False)][0]
-        builder.builderok = True
-
-        # Now this builder is the one that becomes available next (29 minutes
-        # remaining build time).
-        assign_to_builder(self, 'gcc', 1, '386')
-        build, bq = find_job(self, 'gcc', '386')
-        set_remaining_time_for_running_job(bq, 29)
-
-        check_mintime_to_builder(self, apg_job, 29)
-
-        # Make a second, idle x86 builder available.
-        builder = self.builders[(self.x86_proc.id, False)][1]
-        builder.builderok = True
-
-        # That builder should be available immediately since it's idle.
-        check_mintime_to_builder(self, apg_job, 0)
-
-
 class TestBuildQueueDuration(TestCaseWithFactory):
     layer = ZopelessDatabaseLayer
 
@@ -983,386 +265,6 @@
             "The 'virtualized' property deviates.")
 
 
-class TestMultiArchJobDelayEstimation(MultiArchBuildsBase):
-    """Test estimated job delays with various processors."""
-    score_increment = 2
-
-    def setUp(self):
-        """Add 2 'build source package from recipe' builds to the mix.
-
-        The two platform-independent jobs will have a score of 1025 and 1053
-        respectively.
-        In case of jobs with equal scores the one with the lesser 'job' value
-        (i.e. the older one wins).
-
-            3,              gedit, p: hppa, v:False e:0:01:00 *** s: 1003
-            4,              gedit, p:  386, v:False e:0:02:00 *** s: 1006
-            5,            firefox, p: hppa, v:False e:0:03:00 *** s: 1009
-            6,            firefox, p:  386, v:False e:0:04:00 *** s: 1012
-            7,                apg, p: hppa, v:False e:0:05:00 *** s: 1015
-            9,                vim, p: hppa, v:False e:0:07:00 *** s: 1021
-           10,                vim, p:  386, v:False e:0:08:00 *** s: 1024
-            8,                apg, p:  386, v:False e:0:06:00 *** s: 1024
-      -->  19,     xx-recipe-bash, p: None, v:False e:0:00:22 *** s: 1025
-           11,                gcc, p: hppa, v:False e:0:09:00 *** s: 1027
-           12,                gcc, p:  386, v:False e:0:10:00 *** s: 1030
-           13,              bison, p: hppa, v:False e:0:11:00 *** s: 1033
-           14,              bison, p:  386, v:False e:0:12:00 *** s: 1036
-           15,               flex, p: hppa, v:False e:0:13:00 *** s: 1039
-           16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
-           17,           postgres, p: hppa, v:False e:0:15:00 *** s: 1045
-           18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
-      -->  20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
-
-         p=processor, v=virtualized, e=estimated_duration, s=score
-        """
-        super(TestMultiArchJobDelayEstimation, self).setUp()
-
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=False, estimated_duration=22,
-            sourcename=u'xx-recipe-bash', score=1025)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=False, estimated_duration=222,
-            sourcename=u'xx-recipe-zsh', score=1053)
-        self.builds.append(job.specific_job.build)
-
-        # Assign the same score to the '386' vim and apg build jobs.
-        _apg_build, apg_job = find_job(self, 'apg', '386')
-        apg_job.lastscore = 1024
-
-    def disabled_test_job_delay_for_binary_builds(self):
-        # One of four builders for the 'flex' build is immediately available.
-        flex_build, flex_job = find_job(self, 'flex', 'hppa')
-        check_mintime_to_builder(self, flex_job, 0)
-
-        # The delay will be 900 (= 15*60) + 222 seconds
-        check_delay_for_job(self, flex_job, 1122)
-
-        # Assign the postgres job to a builder.
-        assign_to_builder(self, 'postgres', 1, 'hppa')
-        # The 'postgres' job is not pending any more.  Now only the 222
-        # seconds (the estimated duration of the platform-independent job)
-        # should be returned.
-        check_delay_for_job(self, flex_job, 222)
-
-        # How about some estimates for x86 builds?
-        _bison_build, bison_job = find_job(self, 'bison', '386')
-        check_mintime_to_builder(self, bison_job, 0)
-        # The delay will be 900 (= (14+16)*60/2) + 222 seconds.
-        check_delay_for_job(self, bison_job, 1122)
-
-        # The 2 tests that follow exercise the estimation in conjunction with
-        # longer pending job queues. Please note that the sum of estimates for
-        # the '386' jobs is divided by 4 which is the number of native '386'
-        # builders.
-
-        # Also, this tests that jobs with equal score but a lower 'job' value
-        # (i.e. older jobs) are queued ahead of the job of interest (JOI).
-        _vim_build, vim_job = find_job(self, 'vim', '386')
-        check_mintime_to_builder(self, vim_job, 0)
-        # The delay will be 870 (= (6+10+12+14+16)*60/4) + 122 (= (222+22)/2)
-        # seconds.
-        check_delay_for_job(self, vim_job, 992)
-
-        _gedit_build, gedit_job = find_job(self, 'gedit', '386')
-        check_mintime_to_builder(self, gedit_job, 0)
-        # The delay will be
-        #   1080 (= (4+6+8+10+12+14+16)*60/4) + 122 (= (222+22)/2)
-        # seconds.
-        check_delay_for_job(self, gedit_job, 1172)
-
-    def disabled_test_job_delay_for_recipe_builds(self):
-        # One of the 9 builders for the 'bash' build is immediately available.
-        bash_build, bash_job = find_job(self, 'xx-recipe-bash', None)
-        check_mintime_to_builder(self, bash_job, 0)
-
-        # The delay will be 960 + 780 + 222 = 1962, where
-        #   hppa job delays: 960 = (9+11+13+15)*60/3
-        #    386 job delays: 780 = (10+12+14+16)*60/4
-        check_delay_for_job(self, bash_job, 1962)
-
-        # One of the 9 builders for the 'zsh' build is immediately available.
-        zsh_build, zsh_job = find_job(self, 'xx-recipe-zsh', None)
-        check_mintime_to_builder(self, zsh_job, 0)
-
-        # The delay will be 0 since this is the head job.
-        check_delay_for_job(self, zsh_job, 0)
-
-        # Assign the zsh job to a builder.
-        self.assertEquals((None, False), bash_job._getHeadJobPlatform())
-        assign_to_builder(self, 'xx-recipe-zsh', 1, None)
-        self.assertEquals((1, False), bash_job._getHeadJobPlatform())
-
-        # Now that the highest-scored job is out of the way, the estimation
-        # for the 'bash' recipe build is 222 seconds shorter.
-
-        # The delay will be 960 + 780 = 1740, where
-        #   hppa job delays: 960 = (9+11+13+15)*60/3
-        #    386 job delays: 780 = (10+12+14+16)*60/4
-        check_delay_for_job(self, bash_job, 1740)
-
-        _postgres_build, postgres_job = find_job(self, 'postgres', '386')
-        # The delay will be 0 since this is the head job now.
-        check_delay_for_job(self, postgres_job, 0)
-        # Also, the platform of the postgres job is returned since it *is*
-        # the head job now.
-        pg_platform = (postgres_job.processor.id, postgres_job.virtualized)
-        self.assertEquals(pg_platform, postgres_job._getHeadJobPlatform())
-
-    def test_job_delay_for_unspecified_virtualization(self):
-        # Make sure that jobs with a NULL 'virtualized' flag get the same
-        # treatment as the ones with virtualized=TRUE.
-        # First toggle the 'virtualized' flag for all hppa jobs.
-        for build in self.builds:
-            bq = build.buildqueue_record
-            if bq.processor == self.hppa_proc:
-                removeSecurityProxy(bq).virtualized = True
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=True, estimated_duration=332,
-            sourcename=u'xxr-openssh-client', score=1050)
-        self.builds.append(job.specific_job.build)
-        # print_build_setup(self.builds)
-        #   ...
-        #   15,               flex, p: hppa, v: True e:0:13:00 *** s: 1039
-        #   16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
-        #   17,           postgres, p: hppa, v: True e:0:15:00 *** s: 1045
-        #   18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
-        #   21, xxr-openssh-client, p: None, v: True e:0:05:32 *** s: 1050
-        #   20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
-
-        flex_build, flex_job = find_job(self, 'flex', 'hppa')
-        # The head job platform is the one of job #21 (xxr-openssh-client).
-        self.assertEquals(
-            (None, True), removeSecurityProxy(flex_job)._getHeadJobPlatform())
-        # The delay will be 900 (= 15*60) + 332 seconds
-        check_delay_for_job(self, flex_job, 1232)
-
-        # Now add a job with a NULL 'virtualized' flag. It should be treated
-        # like jobs with virtualized=TRUE.
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=111, sourcename=u'xxr-gwibber', score=1051,
-            virtualized=None)
-        self.builds.append(job.specific_job.build)
-        # print_build_setup(self.builds)
-        self.assertEqual(None, job.virtualized)
-        #   ...
-        #   15,               flex, p: hppa, v: True e:0:13:00 *** s: 1039
-        #   16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
-        #   17,           postgres, p: hppa, v: True e:0:15:00 *** s: 1045
-        #   18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
-        #   21, xxr-openssh-client, p: None, v: True e:0:05:32 *** s: 1050
-        #   22,        xxr-gwibber, p: None, v: None e:0:01:51 *** s: 1051
-        #   20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
-
-        # The newly added 'xxr-gwibber' job is the new head job now.
-        self.assertEquals(
-            (None, None), removeSecurityProxy(flex_job)._getHeadJobPlatform())
-        # The newly added 'xxr-gwibber' job now weighs in as well and the
-        # delay is 900 (= 15*60) + (332+111)/2 seconds
-        check_delay_for_job(self, flex_job, 1121)
-
-        # The '386' flex job does not care about the 'xxr-gwibber' and
-        # 'xxr-openssh-client' jobs since the 'virtualized' values do not
-        # match.
-        flex_build, flex_job = find_job(self, 'flex', '386')
-        self.assertEquals(
-            (None, False),
-            removeSecurityProxy(flex_job)._getHeadJobPlatform())
-        # delay is 960 (= 16*60) + 222 seconds
-        check_delay_for_job(self, flex_job, 1182)
-
-
-class TestJobDispatchTimeEstimation(MultiArchBuildsBase):
-    """Test estimated job delays with various processors."""
-    score_increment = 2
-
-    def setUp(self):
-        """Add more processor-independent jobs to the mix, make the '386' jobs
-        virtual.
-
-            3,              gedit, p: hppa, v:False e:0:01:00 *** s: 1003
-            4,              gedit, p:  386, v: True e:0:02:00 *** s: 1006
-            5,            firefox, p: hppa, v:False e:0:03:00 *** s: 1009
-            6,            firefox, p:  386, v: True e:0:04:00 *** s: 1012
-            7,                apg, p: hppa, v:False e:0:05:00 *** s: 1015
-            9,                vim, p: hppa, v:False e:0:07:00 *** s: 1021
-           10,                vim, p:  386, v: True e:0:08:00 *** s: 1024
-            8,                apg, p:  386, v: True e:0:06:00 *** s: 1024
-           19,       xxr-aptitude, p: None, v:False e:0:05:32 *** s: 1025
-           11,                gcc, p: hppa, v:False e:0:09:00 *** s: 1027
-           12,                gcc, p:  386, v: True e:0:10:00 *** s: 1030
-           13,              bison, p: hppa, v:False e:0:11:00 *** s: 1033
-           14,              bison, p:  386, v: True e:0:12:00 *** s: 1036
-           15,               flex, p: hppa, v:False e:0:13:00 *** s: 1039
-           16,               flex, p:  386, v: True e:0:14:00 *** s: 1042
-           23,      xxr-apt-build, p: None, v: True e:0:12:56 *** s: 1043
-           22,       xxr-cron-apt, p: None, v: True e:0:11:05 *** s: 1043
-           26,           xxr-cupt, p: None, v: None e:0:18:30 *** s: 1044
-           25,            xxr-apt, p: None, v: None e:0:16:38 *** s: 1044
-           24,       xxr-debdelta, p: None, v: None e:0:14:47 *** s: 1044
-           17,           postgres, p: hppa, v:False e:0:15:00 *** s: 1045
-           18,           postgres, p:  386, v: True e:0:16:00 *** s: 1048
-           21,         xxr-daptup, p: None, v: None e:0:09:14 *** s: 1051
-           20,       xxr-auto-apt, p: None, v:False e:0:07:23 *** s: 1053
-
-         p=processor, v=virtualized, e=estimated_duration, s=score
-        """
-        super(TestJobDispatchTimeEstimation, self).setUp()
-
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=False, estimated_duration=332,
-            sourcename=u'xxr-aptitude', score=1025)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            virtualized=False, estimated_duration=443,
-            sourcename=u'xxr-auto-apt', score=1053)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=554, sourcename=u'xxr-daptup', score=1051,
-            virtualized=None)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=665, sourcename=u'xxr-cron-apt', score=1043)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=776, sourcename=u'xxr-apt-build', score=1043)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=887, sourcename=u'xxr-debdelta', score=1044,
-            virtualized=None)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=998, sourcename=u'xxr-apt', score=1044,
-            virtualized=None)
-        self.builds.append(job.specific_job.build)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            estimated_duration=1110, sourcename=u'xxr-cupt', score=1044,
-            virtualized=None)
-        self.builds.append(job.specific_job.build)
-
-        # Assign the same score to the '386' vim and apg build jobs.
-        _apg_build, apg_job = find_job(self, 'apg', '386')
-        apg_job.lastscore = 1024
-
-        # Also, toggle the 'virtualized' flag for all '386' jobs.
-        for build in self.builds:
-            bq = build.buildqueue_record
-            if bq.processor == self.x86_proc:
-                removeSecurityProxy(bq).virtualized = True
-
-    def test_pending_jobs_only(self):
-        # Let's see the assertion fail for a job that's not pending any more.
-        assign_to_builder(self, 'gedit', 1, 'hppa')
-        gedit_build, gedit_job = find_job(self, 'gedit', 'hppa')
-        self.assertRaises(AssertionError, gedit_job.getEstimatedJobStartTime)
-
-    def test_estimation_binary_virtual(self):
-        gcc_build, gcc_job = find_job(self, 'gcc', '386')
-        # The delay of 1671 seconds is calculated as follows:
-        #                     386 jobs: (12+14+16)*60/3           = 840
-        #   processor-independent jobs:
-        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
-        check_estimate(self, gcc_job, 1671)
-        self.assertEquals(5, builders_for_job(gcc_job))
-
-    def test_proc_indep_virtual_true(self):
-        xxr_build, xxr_job = find_job(self, 'xxr-apt-build', None)
-        # The delay of 1802 seconds is calculated as follows:
-        #                     386 jobs: 16*60                    = 960
-        #   processor-independent jobs:
-        #       (11:05 + 18:30 + 16:38 + 14:47 + 9:14)/5         = 842
-        check_estimate(self, xxr_job, 1802)
-
-    def test_estimation_binary_virtual_long_queue(self):
-        gedit_build, gedit_job = find_job(self, 'gedit', '386')
-        # The delay of 1671 seconds is calculated as follows:
-        #                     386 jobs:
-        #       (4+6+8+10+12+14+16)*60/5                          = 840
-        #   processor-independent jobs:
-        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
-        check_estimate(self, gedit_job, 1671)
-
-    def test_proc_indep_virtual_null_headjob(self):
-        xxr_build, xxr_job = find_job(self, 'xxr-daptup', None)
-        # This job is at the head of the queue for virtualized builders and
-        # will get dispatched within the next 5 seconds.
-        check_estimate(self, xxr_job, 5)
-
-    def test_proc_indep_virtual_false(self):
-        xxr_build, xxr_job = find_job(self, 'xxr-aptitude', None)
-        # The delay of 1403 seconds is calculated as follows:
-        #                    hppa jobs: (9+11+13+15)*60/3        = 960
-        #   processor-independent jobs: 7:23                     = 443
-        check_estimate(self, xxr_job, 1403)
-
-    def test_proc_indep_virtual_false_headjob(self):
-        xxr_build, xxr_job = find_job(self, 'xxr-auto-apt', None)
-        # This job is at the head of the queue for native builders and
-        # will get dispatched within the next 5 seconds.
-        check_estimate(self, xxr_job, 5)
-
-    def test_estimation_binary_virtual_same_score(self):
-        vim_build, vim_job = find_job(self, 'vim', '386')
-        # The apg job is ahead of the vim job.
-        # The delay of 1527 seconds is calculated as follows:
-        #                     386 jobs: (6+10+12+14+16)*60/5      = 696
-        #   processor-independent jobs:
-        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
-        check_estimate(self, vim_job, 1527)
-
-    def test_no_builder_no_estimate(self):
-        # No dispatch estimate is provided in the absence of builders that
-        # can run the job of interest (JOI).
-        disable_builders(self, '386', True)
-        vim_build, vim_job = find_job(self, 'vim', '386')
-        check_estimate(self, vim_job, None)
-
-    def disabled_test_estimates_with_small_builder_pool(self):
-        # Test that a reduced builder pool results in longer dispatch time
-        # estimates.
-        vim_build, vim_job = find_job(self, 'vim', '386')
-        disable_builders(self, '386', True)
-        # Re-enable one builder.
-        builder = self.builders[(self.x86_proc.id, True)][0]
-        builder.builderok = True
-        # Dispatch the firefox job to it.
-        assign_to_builder(self, 'firefox', 1, '386')
-        # Dispatch the head job, making postgres/386 the new head job and
-        # resulting in a 240 seconds head job dispatch delay.
-        assign_to_builder(self, 'xxr-daptup', 1, None)
-        check_mintime_to_builder(self, vim_job, 240)
-        # Re-enable another builder.
-        builder = self.builders[(self.x86_proc.id, True)][1]
-        builder.builderok = True
-        # Assign a job to it.
-        assign_to_builder(self, 'gedit', 2, '386')
-        check_mintime_to_builder(self, vim_job, 120)
-
-        xxr_build, xxr_job = find_job(self, 'xxr-apt', None)
-        # The delay of 2627+120 seconds is calculated as follows:
-        #                     386 jobs : (6+10+12+14+16)*60/2     = 1740
-        #   processor-independent jobs :
-        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47)/5         =  887
-        # waiting time for next builder:                          =  120
-        self.assertEquals(2, builders_for_job(vim_job))
-        self.assertEquals(9, builders_for_job(xxr_job))
-        check_estimate(self, vim_job, 2747)
-
-    def test_estimation_binary_virtual_headjob(self):
-        # The head job only waits for the next builder to become available.
-        disable_builders(self, '386', True)
-        # Re-enable one builder.
-        builder = self.builders[(self.x86_proc.id, True)][0]
-        builder.builderok = True
-        # Assign a job to it.
-        assign_to_builder(self, 'gedit', 1, '386')
-        # Dispatch the head job, making postgres/386 the new head job.
-        assign_to_builder(self, 'xxr-daptup', 1, None)
-        postgres_build, postgres_job = find_job(self, 'postgres', '386')
-        check_estimate(self, postgres_job, 120)
-
-
 class TestBuildQueueManual(TestCaseWithFactory):
     layer = ZopelessDatabaseLayer
 

=== added file 'lib/lp/buildmaster/tests/test_queuedepth.py'
--- lib/lp/buildmaster/tests/test_queuedepth.py	1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/tests/test_queuedepth.py	2013-10-31 06:45:03 +0000
@@ -0,0 +1,1098 @@
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Test BuildQueue start time estimation."""
+
+from datetime import (
+    datetime,
+    timedelta,
+    )
+
+from pytz import utc
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.builder import IBuilderSet
+from lp.buildmaster.queuedepth import (
+    estimate_job_delay,
+    estimate_time_to_next_builder,
+    get_builder_data,
+    get_free_builders_count,
+    get_head_job_platform,
+    )
+from lp.buildmaster.tests.test_buildqueue import find_job
+from lp.services.database.interfaces import IStore
+from lp.soyuz.enums import (
+    ArchivePurpose,
+    PackagePublishingStatus,
+    )
+from lp.soyuz.interfaces.processor import IProcessorSet
+from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+def check_mintime_to_builder(test, bq, min_time):
+    """Test the estimated time until a builder becomes available."""
+    time_stamp = bq.job.date_started or datetime.now(utc)
+    delay = estimate_time_to_next_builder(
+        removeSecurityProxy(bq), now=time_stamp)
+    test.assertTrue(
+        delay <= min_time,
+        "Wrong min time to next available builder (%s > %s)"
+        % (delay, min_time))
+
+
+def set_remaining_time_for_running_job(bq, remainder):
+    """Set remaining running time for job."""
+    offset = bq.estimated_duration.seconds - remainder
+    removeSecurityProxy(bq.job).date_started = (
+        datetime.now(utc) - timedelta(seconds=offset))
+
+
+def check_delay_for_job(test, the_job, delay):
+    # Obtain the builder statistics pertaining to this job.
+    builder_data = get_builder_data()
+    estimated_delay = estimate_job_delay(
+        removeSecurityProxy(the_job), builder_data)
+    test.assertEqual(delay, estimated_delay)
+
+
+def total_builders():
+    """How many available builders do we have in total?"""
+    builder_data = get_builder_data()
+    return builder_data[(None, False)] + builder_data[(None, True)]
+
+
+def builders_for_job(job):
+    """How many available builders can run the given job?"""
+    builder_data = get_builder_data()
+    return builder_data[(getattr(job.processor, 'id', None), job.virtualized)]
+
+
+def check_estimate(test, job, delay_in_seconds):
+    time_stamp = job.job.date_started or datetime.now(utc)
+    estimate = job.getEstimatedJobStartTime(now=time_stamp)
+    if delay_in_seconds is None:
+        test.assertEquals(
+            delay_in_seconds, estimate,
+            "An estimate should not be possible at present but one was "
+            "returned (%s) nevertheless." % estimate)
+    else:
+        estimate -= time_stamp
+        test.assertTrue(
+            estimate.seconds <= delay_in_seconds,
+            "The estimated delay deviates from the expected one (%s > %s)" %
+            (estimate.seconds, delay_in_seconds))
+
+
+def disable_builders(test, processor_name, virtualized):
+    """Disable bulders with the given processor and virtualization setting."""
+    if processor_name is not None:
+        processor = getUtility(IProcessorSet).getByName(processor_name)
+    for builder in test.builders[(processor.id, virtualized)]:
+        builder.builderok = False
+
+
+def nth_builder(test, bq, n):
+    """Find nth builder that can execute the given build."""
+
+    def builder_key(job):
+        """Access key for builders capable of running the given job."""
+        return (getattr(job.processor, 'id', None), job.virtualized)
+
+    builder = None
+    builders = test.builders.get(builder_key(bq), [])
+    try:
+        for builder in builders[n - 1:]:
+            if builder.builderok:
+                break
+    except IndexError:
+        pass
+    return builder
+
+
+def assign_to_builder(test, job_name, builder_number, processor='386'):
+    """Simulate assigning a build to a builder."""
+    build, bq = find_job(test, job_name, processor)
+    builder = nth_builder(test, bq, builder_number)
+    bq.markAsBuilding(builder)
+
+
+class TestBuildQueueBase(TestCaseWithFactory):
+    """Setup the test publisher and some builders."""
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestBuildQueueBase, self).setUp()
+        self.publisher = SoyuzTestPublisher()
+        self.publisher.prepareBreezyAutotest()
+
+        # First make nine 'i386' builders.
+        self.i1 = self.factory.makeBuilder(name='i386-v-1')
+        self.i2 = self.factory.makeBuilder(name='i386-v-2')
+        self.i3 = self.factory.makeBuilder(name='i386-v-3')
+        self.i4 = self.factory.makeBuilder(name='i386-v-4')
+        self.i5 = self.factory.makeBuilder(name='i386-v-5')
+        self.i6 = self.factory.makeBuilder(name='i386-n-6', virtualized=False)
+        self.i7 = self.factory.makeBuilder(name='i386-n-7', virtualized=False)
+        self.i8 = self.factory.makeBuilder(name='i386-n-8', virtualized=False)
+        self.i9 = self.factory.makeBuilder(name='i386-n-9', virtualized=False)
+
+        # Next make seven 'hppa' builders.
+        self.hppa_proc = getUtility(IProcessorSet).getByName('hppa')
+        self.h1 = self.factory.makeBuilder(
+            name='hppa-v-1', processor=self.hppa_proc)
+        self.h2 = self.factory.makeBuilder(
+            name='hppa-v-2', processor=self.hppa_proc)
+        self.h3 = self.factory.makeBuilder(
+            name='hppa-v-3', processor=self.hppa_proc)
+        self.h4 = self.factory.makeBuilder(
+            name='hppa-v-4', processor=self.hppa_proc)
+        self.h5 = self.factory.makeBuilder(
+            name='hppa-n-5', processor=self.hppa_proc, virtualized=False)
+        self.h6 = self.factory.makeBuilder(
+            name='hppa-n-6', processor=self.hppa_proc, virtualized=False)
+        self.h7 = self.factory.makeBuilder(
+            name='hppa-n-7', processor=self.hppa_proc, virtualized=False)
+
+        # Finally make five 'amd64' builders.
+        self.amd_proc = getUtility(IProcessorSet).getByName('amd64')
+        self.a1 = self.factory.makeBuilder(
+            name='amd64-v-1', processor=self.amd_proc)
+        self.a2 = self.factory.makeBuilder(
+            name='amd64-v-2', processor=self.amd_proc)
+        self.a3 = self.factory.makeBuilder(
+            name='amd64-v-3', processor=self.amd_proc)
+        self.a4 = self.factory.makeBuilder(
+            name='amd64-n-4', processor=self.amd_proc, virtualized=False)
+        self.a5 = self.factory.makeBuilder(
+            name='amd64-n-5', processor=self.amd_proc, virtualized=False)
+
+        self.builders = dict()
+        self.x86_proc = getUtility(IProcessorSet).getByName('386')
+        # x86 native
+        self.builders[(self.x86_proc.id, False)] = [
+            self.i6, self.i7, self.i8, self.i9]
+        # x86 virtual
+        self.builders[(self.x86_proc.id, True)] = [
+            self.i1, self.i2, self.i3, self.i4, self.i5]
+
+        # amd64 native
+        self.builders[(self.amd_proc.id, False)] = [self.a4, self.a5]
+        # amd64 virtual
+        self.builders[(self.amd_proc.id, True)] = [self.a1, self.a2, self.a3]
+
+        # hppa native
+        self.builders[(self.hppa_proc.id, False)] = [
+            self.h5,
+            self.h6,
+            self.h7,
+            ]
+        # hppa virtual
+        self.builders[(self.hppa_proc.id, True)] = [
+            self.h1, self.h2, self.h3, self.h4]
+
+        # Ensure all builders are operational.
+        for builders in self.builders.values():
+            for builder in builders:
+                builder.builderok = True
+                builder.manual = False
+
+        # Native builders irrespective of processor.
+        self.builders[(None, False)] = []
+        self.builders[(None, False)].extend(
+            self.builders[(self.x86_proc.id, False)])
+        self.builders[(None, False)].extend(
+            self.builders[(self.amd_proc.id, False)])
+        self.builders[(None, False)].extend(
+            self.builders[(self.hppa_proc.id, False)])
+
+        # Virtual builders irrespective of processor.
+        self.builders[(None, True)] = []
+        self.builders[(None, True)].extend(
+            self.builders[(self.x86_proc.id, True)])
+        self.builders[(None, True)].extend(
+            self.builders[(self.amd_proc.id, True)])
+        self.builders[(None, True)].extend(
+            self.builders[(self.hppa_proc.id, True)])
+
+        # Disable the sample data builders.
+        getUtility(IBuilderSet)['bob'].builderok = False
+        getUtility(IBuilderSet)['frog'].builderok = False
+
+
+class SingleArchBuildsBase(TestBuildQueueBase):
+    """Set up a test environment with builds that target a single
+    processor."""
+
+    def setUp(self):
+        """Set up some native x86 builds for the test archive."""
+        super(SingleArchBuildsBase, self).setUp()
+        # The builds will be set up as follows:
+        #
+        #      gedit, p:  386, v:False e:0:01:00 *** s: 1001
+        #    firefox, p:  386, v:False e:0:02:00 *** s: 1002
+        #        apg, p:  386, v:False e:0:03:00 *** s: 1003
+        #        vim, p:  386, v:False e:0:04:00 *** s: 1004
+        #        gcc, p:  386, v:False e:0:05:00 *** s: 1005
+        #      bison, p:  386, v:False e:0:06:00 *** s: 1006
+        #       flex, p:  386, v:False e:0:07:00 *** s: 1007
+        #   postgres, p:  386, v:False e:0:08:00 *** s: 1008
+        #
+        # p=processor, v=virtualized, e=estimated_duration, s=score
+
+        # First mark all builds in the sample data as already built.
+        sample_data = IStore(BinaryPackageBuild).find(BinaryPackageBuild)
+        for build in sample_data:
+            build.buildstate = BuildStatus.FULLYBUILT
+        IStore(BinaryPackageBuild).flush()
+
+        # We test builds that target a primary archive.
+        self.non_ppa = self.factory.makeArchive(
+            name="primary", purpose=ArchivePurpose.PRIMARY)
+        self.non_ppa.require_virtualized = False
+
+        self.builds = []
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="firefox",
+                status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="apg", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="vim", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="gcc", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="bison", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="flex", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="postgres",
+                status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa).createMissingBuilds())
+        # Set up the builds for test.
+        score = 1000
+        duration = 0
+        for build in self.builds:
+            score += 1
+            duration += 60
+            bq = build.buildqueue_record
+            bq.lastscore = score
+            bq.estimated_duration = timedelta(seconds=duration)
+
+
+class TestBuilderData(SingleArchBuildsBase):
+    """Test the retrieval of builder related data. The latter is required
+    for job dispatch time estimations irrespective of job processor
+    architecture and virtualization setting."""
+
+    def test_builder_data(self):
+        # Make sure the builder numbers are correct. The builder data will
+        # be the same for all of our builds.
+        bq = self.builds[0].buildqueue_record
+        self.assertEqual(
+            21, total_builders(),
+            "The total number of builders is wrong.")
+        self.assertEqual(
+            4, builders_for_job(bq),
+            "[1] The total number of builders that can build the job in "
+            "question is wrong.")
+        builder_stats = get_builder_data()
+        self.assertEqual(
+            4, builder_stats[(self.x86_proc.id, False)],
+            "The number of native x86 builders is wrong")
+        self.assertEqual(
+            5, builder_stats[(self.x86_proc.id, True)],
+            "The number of virtual x86 builders is wrong")
+        self.assertEqual(
+            2, builder_stats[(self.amd_proc.id, False)],
+            "The number of native amd64 builders is wrong")
+        self.assertEqual(
+            3, builder_stats[(self.amd_proc.id, True)],
+            "The number of virtual amd64 builders is wrong")
+        self.assertEqual(
+            3, builder_stats[(self.hppa_proc.id, False)],
+            "The number of native hppa builders is wrong")
+        self.assertEqual(
+            4, builder_stats[(self.hppa_proc.id, True)],
+            "The number of virtual hppa builders is wrong")
+        self.assertEqual(
+            9, builder_stats[(None, False)],
+            "The number of *virtual* builders across all processors is wrong")
+        self.assertEqual(
+            12, builder_stats[(None, True)],
+            "The number of *native* builders across all processors is wrong")
+        # Disable the native x86 builders.
+        for builder in self.builders[(self.x86_proc.id, False)]:
+            builder.builderok = False
+        # Since all native x86 builders were disabled there are none left
+        # to build the job.
+        self.assertEqual(
+            0, builders_for_job(bq),
+            "[2] The total number of builders that can build the job in "
+            "question is wrong.")
+        # Re-enable one of them.
+        for builder in self.builders[(self.x86_proc.id, False)]:
+            builder.builderok = True
+            break
+        # Now there should be one builder available to build the job.
+        self.assertEqual(
+            1, builders_for_job(bq),
+            "[3] The total number of builders that can build the job in "
+            "question is wrong.")
+        # Disable the *virtual* x86 builders -- should not make any
+        # difference.
+        for builder in self.builders[(self.x86_proc.id, True)]:
+            builder.builderok = False
+        # There should still be one builder available to build the job.
+        self.assertEqual(
+            1, builders_for_job(bq),
+            "[4] The total number of builders that can build the job in "
+            "question is wrong.")
+
+    def test_free_builder_counts(self):
+        # Make sure the builder numbers are correct. The builder data will
+        # be the same for all of our builds.
+        build = self.builds[0]
+        # The build in question is an x86/native one.
+        self.assertEqual(self.x86_proc.id, build.processor.id)
+        self.assertEqual(False, build.is_virtualized)
+
+        # To test this non-interface method, we need to remove the
+        # security proxy.
+        bq = removeSecurityProxy(build.buildqueue_record)
+        builder_stats = get_builder_data()
+        # We have 4 x86 native builders.
+        self.assertEqual(
+            4, builder_stats[(self.x86_proc.id, False)],
+            "The number of native x86 builders is wrong")
+        # Initially all 4 builders are free.
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(4, free_count)
+        # Once we assign a build to one of them we should see the free
+        # builders count drop by one.
+        assign_to_builder(self, 'postgres', 1)
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(3, free_count)
+        # When we assign another build to one of them we should see the free
+        # builders count drop by one again.
+        assign_to_builder(self, 'gcc', 2)
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(2, free_count)
+        # Let's use up another builder.
+        assign_to_builder(self, 'apg', 3)
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(1, free_count)
+        # And now for the last one.
+        assign_to_builder(self, 'flex', 4)
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(0, free_count)
+        # If we reset the 'flex' build the builder that was assigned to it
+        # will be free again.
+        build, bq = find_job(self, 'flex')
+        bq.reset()
+        free_count = get_free_builders_count(
+            build.processor, build.is_virtualized)
+        self.assertEqual(1, free_count)
+
+
+class TestMinTimeToNextBuilder(SingleArchBuildsBase):
+    """Test estimated time-to-builder with builds targetting a single
+    processor."""
+
+    def test_min_time_to_next_builder(self):
+        """When is the next builder capable of running the job at the head of
+        the queue becoming available?"""
+        # Test the estimation of the minimum time until a builder becomes
+        # available.
+
+        # The builds will be set up as follows:
+        #
+        #      gedit, p:  386, v:False e:0:01:00 *** s: 1001
+        #    firefox, p:  386, v:False e:0:02:00 *** s: 1002
+        #        apg, p:  386, v:False e:0:03:00 *** s: 1003
+        #        vim, p:  386, v:False e:0:04:00 *** s: 1004
+        #        gcc, p:  386, v:False e:0:05:00 *** s: 1005
+        #      bison, p:  386, v:False e:0:06:00 *** s: 1006
+        #       flex, p:  386, v:False e:0:07:00 *** s: 1007
+        #   postgres, p:  386, v:False e:0:08:00 *** s: 1008
+        #
+        # p=processor, v=virtualized, e=estimated_duration, s=score
+
+        # This will be the job of interest.
+        apg_build, apg_job = find_job(self, 'apg')
+        # One of four builders for the 'apg' build is immediately available.
+        check_mintime_to_builder(self, apg_job, 0)
+
+        # Assign the postgres job to a builder.
+        assign_to_builder(self, 'postgres', 1)
+        # Now one builder is gone. But there should still be a builder
+        # immediately available.
+        check_mintime_to_builder(self, apg_job, 0)
+
+        assign_to_builder(self, 'flex', 2)
+        check_mintime_to_builder(self, apg_job, 0)
+
+        assign_to_builder(self, 'bison', 3)
+        check_mintime_to_builder(self, apg_job, 0)
+
+        assign_to_builder(self, 'gcc', 4)
+        # Now that no builder is immediately available, the shortest
+        # remaing build time (based on the estimated duration) is returned:
+        #   300 seconds
+        # This is equivalent to the 'gcc' job's estimated duration.
+        check_mintime_to_builder(self, apg_job, 300)
+
+        # Now we pretend that the 'postgres' started 6 minutes ago. Its
+        # remaining execution time should be 2 minutes = 120 seconds and
+        # it now becomes the job whose builder becomes available next.
+        build, bq = find_job(self, 'postgres')
+        set_remaining_time_for_running_job(bq, 120)
+        check_mintime_to_builder(self, apg_job, 120)
+
+        # What happens when jobs overdraw the estimated duration? Let's
+        # pretend the 'flex' job started 8 minutes ago.
+        build, bq = find_job(self, 'flex')
+        set_remaining_time_for_running_job(bq, -60)
+        # In such a case we assume that the job will complete within 2
+        # minutes, this is a guess that has worked well so far.
+        check_mintime_to_builder(self, apg_job, 120)
+
+        # If there's a job that will complete within a shorter time then
+        # we expect to be given that time frame.
+        build, bq = find_job(self, 'postgres')
+        set_remaining_time_for_running_job(bq, 30)
+        check_mintime_to_builder(self, apg_job, 30)
+
+        # Disable the native x86 builders.
+        for builder in self.builders[(self.x86_proc.id, False)]:
+            builder.builderok = False
+
+        # No builders capable of running the job at hand are available now.
+        self.assertEquals(0, builders_for_job(apg_job))
+        # The "minimum time to builder" estimation logic is not aware of this
+        # though.
+        check_mintime_to_builder(self, apg_job, 0)
+
+        # The following job can only run on a native builder.
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=111, sourcename=u'xxr-gftp', score=1055,
+            virtualized=False)
+        self.builds.append(job.specific_job.build)
+
+        # Disable all native builders.
+        for builder in self.builders[(None, False)]:
+            builder.builderok = False
+
+        # All native builders are disabled now.  No builders capable of
+        # running the job at hand are available.
+        self.assertEquals(0, builders_for_job(job))
+        # The "minimum time to builder" estimation logic is not aware of the
+        # fact that no builders capable of running the job are available.
+        check_mintime_to_builder(self, job, 0)
+
+
+class MultiArchBuildsBase(TestBuildQueueBase):
+    """Set up a test environment with builds and multiple processors."""
+
+    def setUp(self):
+        """Set up some native x86 builds for the test archive."""
+        super(MultiArchBuildsBase, self).setUp()
+        # The builds will be set up as follows:
+        #
+        #      gedit, p: hppa, v:False e:0:01:00 *** s: 1001
+        #      gedit, p:  386, v:False e:0:02:00 *** s: 1002
+        #    firefox, p: hppa, v:False e:0:03:00 *** s: 1003
+        #    firefox, p:  386, v:False e:0:04:00 *** s: 1004
+        #        apg, p: hppa, v:False e:0:05:00 *** s: 1005
+        #        apg, p:  386, v:False e:0:06:00 *** s: 1006
+        #        vim, p: hppa, v:False e:0:07:00 *** s: 1007
+        #        vim, p:  386, v:False e:0:08:00 *** s: 1008
+        #        gcc, p: hppa, v:False e:0:09:00 *** s: 1009
+        #        gcc, p:  386, v:False e:0:10:00 *** s: 1010
+        #      bison, p: hppa, v:False e:0:11:00 *** s: 1011
+        #      bison, p:  386, v:False e:0:12:00 *** s: 1012
+        #       flex, p: hppa, v:False e:0:13:00 *** s: 1013
+        #       flex, p:  386, v:False e:0:14:00 *** s: 1014
+        #   postgres, p: hppa, v:False e:0:15:00 *** s: 1015
+        #   postgres, p:  386, v:False e:0:16:00 *** s: 1016
+        #
+        # p=processor, v=virtualized, e=estimated_duration, s=score
+
+        # First mark all builds in the sample data as already built.
+        sample_data = IStore(BinaryPackageBuild).find(BinaryPackageBuild)
+        for build in sample_data:
+            build.buildstate = BuildStatus.FULLYBUILT
+        IStore(BinaryPackageBuild).flush()
+
+        # We test builds that target a primary archive.
+        self.non_ppa = self.factory.makeArchive(
+            name="primary", purpose=ArchivePurpose.PRIMARY)
+        self.non_ppa.require_virtualized = False
+
+        self.builds = []
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="firefox",
+                status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="apg", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="vim", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="gcc", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="bison", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="flex", status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        self.builds.extend(
+            self.publisher.getPubSource(
+                sourcename="postgres",
+                status=PackagePublishingStatus.PUBLISHED,
+                archive=self.non_ppa,
+                architecturehintlist='any').createMissingBuilds())
+        # Set up the builds for test.
+        score = 1000
+        duration = 0
+        for build in self.builds:
+            score += getattr(self, 'score_increment', 1)
+            score += 1
+            duration += 60
+            bq = build.buildqueue_record
+            bq.lastscore = score
+            bq.estimated_duration = timedelta(seconds=duration)
+
+
+class TestMinTimeToNextBuilderMulti(MultiArchBuildsBase):
+    """Test estimated time-to-builder with builds and multiple processors."""
+
+    def disabled_test_min_time_to_next_builder(self):
+        """When is the next builder capable of running the job at the head of
+        the queue becoming available?"""
+        # XXX AaronBentley 2010-03-19 bug=541914: Fails spuriously
+        # One of four builders for the 'apg' build is immediately available.
+        apg_build, apg_job = find_job(self, 'apg', 'hppa')
+        check_mintime_to_builder(self, apg_job, 0)
+
+        # Assign the postgres job to a builder.
+        assign_to_builder(self, 'postgres', 1, 'hppa')
+        # Now one builder is gone. But there should still be a builder
+        # immediately available.
+        check_mintime_to_builder(self, apg_job, 0)
+
+        assign_to_builder(self, 'flex', 2, 'hppa')
+        check_mintime_to_builder(self, apg_job, 0)
+
+        assign_to_builder(self, 'bison', 3, 'hppa')
+        # Now that no builder is immediately available, the shortest
+        # remaing build time (based on the estimated duration) is returned:
+        #   660 seconds
+        # This is equivalent to the 'bison' job's estimated duration.
+        check_mintime_to_builder(self, apg_job, 660)
+
+        # Now we pretend that the 'postgres' started 13 minutes ago. Its
+        # remaining execution time should be 2 minutes = 120 seconds and
+        # it now becomes the job whose builder becomes available next.
+        build, bq = find_job(self, 'postgres', 'hppa')
+        set_remaining_time_for_running_job(bq, 120)
+        check_mintime_to_builder(self, apg_job, 120)
+
+        # What happens when jobs overdraw the estimated duration? Let's
+        # pretend the 'flex' job started 14 minutes ago.
+        build, bq = find_job(self, 'flex', 'hppa')
+        set_remaining_time_for_running_job(bq, -60)
+        # In such a case we assume that the job will complete within 2
+        # minutes, this is a guess that has worked well so far.
+        check_mintime_to_builder(self, apg_job, 120)
+
+        # If there's a job that will complete within a shorter time then
+        # we expect to be given that time frame.
+        build, bq = find_job(self, 'postgres', 'hppa')
+        set_remaining_time_for_running_job(bq, 30)
+        check_mintime_to_builder(self, apg_job, 30)
+
+        # Disable the native hppa builders.
+        for builder in self.builders[(self.hppa_proc.id, False)]:
+            builder.builderok = False
+
+        # No builders capable of running the job at hand are available now.
+        self.assertEquals(0, builders_for_job(apg_job))
+        check_mintime_to_builder(self, apg_job, 0)
+
+        # Let's add a processor-independent job to the mix.
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=False, estimated_duration=22,
+            sourcename='my-recipe-digikam', score=9999)
+        # There are still builders available for the processor-independent
+        # job.
+        self.assertEquals(6, builders_for_job(job))
+        # Even free ones.
+        self.assertTrue(
+            bq._getFreeBuildersCount(job.processor, job.virtualized) > 0,
+            "Builders are immediately available for processor-independent "
+            "jobs.")
+        check_mintime_to_builder(self, job, 0)
+
+        # Let's disable all builders.
+        for builders in self.builders.itervalues():
+            for builder in builders:
+                builder.builderok = False
+
+        # There are no builders capable of running even the processor
+        # independent jobs now.
+        self.assertEquals(0, builders_for_job(job))
+        check_mintime_to_builder(self, job, 0)
+
+        # Re-enable the native hppa builders.
+        for builder in self.builders[(self.hppa_proc.id, False)]:
+            builder.builderok = True
+
+        # The builder that's becoming available next is the one that's
+        # running the 'postgres' build.
+        check_mintime_to_builder(self, apg_job, 30)
+
+        # Make sure we'll find an x86 builder as well.
+        builder = self.builders[(self.x86_proc.id, False)][0]
+        builder.builderok = True
+
+        # Now this builder is the one that becomes available next (29 minutes
+        # remaining build time).
+        assign_to_builder(self, 'gcc', 1, '386')
+        build, bq = find_job(self, 'gcc', '386')
+        set_remaining_time_for_running_job(bq, 29)
+
+        check_mintime_to_builder(self, apg_job, 29)
+
+        # Make a second, idle x86 builder available.
+        builder = self.builders[(self.x86_proc.id, False)][1]
+        builder.builderok = True
+
+        # That builder should be available immediately since it's idle.
+        check_mintime_to_builder(self, apg_job, 0)
+
+
+class TestMultiArchJobDelayEstimation(MultiArchBuildsBase):
+    """Test estimated job delays with various processors."""
+    score_increment = 2
+
+    def setUp(self):
+        """Add 2 'build source package from recipe' builds to the mix.
+
+        The two platform-independent jobs will have a score of 1025 and 1053
+        respectively.
+        In case of jobs with equal scores the one with the lesser 'job' value
+        (i.e. the older one wins).
+
+            3,              gedit, p: hppa, v:False e:0:01:00 *** s: 1003
+            4,              gedit, p:  386, v:False e:0:02:00 *** s: 1006
+            5,            firefox, p: hppa, v:False e:0:03:00 *** s: 1009
+            6,            firefox, p:  386, v:False e:0:04:00 *** s: 1012
+            7,                apg, p: hppa, v:False e:0:05:00 *** s: 1015
+            9,                vim, p: hppa, v:False e:0:07:00 *** s: 1021
+           10,                vim, p:  386, v:False e:0:08:00 *** s: 1024
+            8,                apg, p:  386, v:False e:0:06:00 *** s: 1024
+      -->  19,     xx-recipe-bash, p: None, v:False e:0:00:22 *** s: 1025
+           11,                gcc, p: hppa, v:False e:0:09:00 *** s: 1027
+           12,                gcc, p:  386, v:False e:0:10:00 *** s: 1030
+           13,              bison, p: hppa, v:False e:0:11:00 *** s: 1033
+           14,              bison, p:  386, v:False e:0:12:00 *** s: 1036
+           15,               flex, p: hppa, v:False e:0:13:00 *** s: 1039
+           16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
+           17,           postgres, p: hppa, v:False e:0:15:00 *** s: 1045
+           18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
+      -->  20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
+
+         p=processor, v=virtualized, e=estimated_duration, s=score
+        """
+        super(TestMultiArchJobDelayEstimation, self).setUp()
+
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=False, estimated_duration=22,
+            sourcename=u'xx-recipe-bash', score=1025)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=False, estimated_duration=222,
+            sourcename=u'xx-recipe-zsh', score=1053)
+        self.builds.append(job.specific_job.build)
+
+        # Assign the same score to the '386' vim and apg build jobs.
+        _apg_build, apg_job = find_job(self, 'apg', '386')
+        apg_job.lastscore = 1024
+
+    def disabled_test_job_delay_for_binary_builds(self):
+        # One of four builders for the 'flex' build is immediately available.
+        flex_build, flex_job = find_job(self, 'flex', 'hppa')
+        check_mintime_to_builder(self, flex_job, 0)
+
+        # The delay will be 900 (= 15*60) + 222 seconds
+        check_delay_for_job(self, flex_job, 1122)
+
+        # Assign the postgres job to a builder.
+        assign_to_builder(self, 'postgres', 1, 'hppa')
+        # The 'postgres' job is not pending any more.  Now only the 222
+        # seconds (the estimated duration of the platform-independent job)
+        # should be returned.
+        check_delay_for_job(self, flex_job, 222)
+
+        # How about some estimates for x86 builds?
+        _bison_build, bison_job = find_job(self, 'bison', '386')
+        check_mintime_to_builder(self, bison_job, 0)
+        # The delay will be 900 (= (14+16)*60/2) + 222 seconds.
+        check_delay_for_job(self, bison_job, 1122)
+
+        # The 2 tests that follow exercise the estimation in conjunction with
+        # longer pending job queues. Please note that the sum of estimates for
+        # the '386' jobs is divided by 4 which is the number of native '386'
+        # builders.
+
+        # Also, this tests that jobs with equal score but a lower 'job' value
+        # (i.e. older jobs) are queued ahead of the job of interest (JOI).
+        _vim_build, vim_job = find_job(self, 'vim', '386')
+        check_mintime_to_builder(self, vim_job, 0)
+        # The delay will be 870 (= (6+10+12+14+16)*60/4) + 122 (= (222+22)/2)
+        # seconds.
+        check_delay_for_job(self, vim_job, 992)
+
+        _gedit_build, gedit_job = find_job(self, 'gedit', '386')
+        check_mintime_to_builder(self, gedit_job, 0)
+        # The delay will be
+        #   1080 (= (4+6+8+10+12+14+16)*60/4) + 122 (= (222+22)/2)
+        # seconds.
+        check_delay_for_job(self, gedit_job, 1172)
+
+    def disabled_test_job_delay_for_recipe_builds(self):
+        # One of the 9 builders for the 'bash' build is immediately available.
+        bash_build, bash_job = find_job(self, 'xx-recipe-bash', None)
+        check_mintime_to_builder(self, bash_job, 0)
+
+        # The delay will be 960 + 780 + 222 = 1962, where
+        #   hppa job delays: 960 = (9+11+13+15)*60/3
+        #    386 job delays: 780 = (10+12+14+16)*60/4
+        check_delay_for_job(self, bash_job, 1962)
+
+        # One of the 9 builders for the 'zsh' build is immediately available.
+        zsh_build, zsh_job = find_job(self, 'xx-recipe-zsh', None)
+        check_mintime_to_builder(self, zsh_job, 0)
+
+        # The delay will be 0 since this is the head job.
+        check_delay_for_job(self, zsh_job, 0)
+
+        # Assign the zsh job to a builder.
+        self.assertEquals((None, False), bash_job._getHeadJobPlatform())
+        assign_to_builder(self, 'xx-recipe-zsh', 1, None)
+        self.assertEquals((1, False), bash_job._getHeadJobPlatform())
+
+        # Now that the highest-scored job is out of the way, the estimation
+        # for the 'bash' recipe build is 222 seconds shorter.
+
+        # The delay will be 960 + 780 = 1740, where
+        #   hppa job delays: 960 = (9+11+13+15)*60/3
+        #    386 job delays: 780 = (10+12+14+16)*60/4
+        check_delay_for_job(self, bash_job, 1740)
+
+        _postgres_build, postgres_job = find_job(self, 'postgres', '386')
+        # The delay will be 0 since this is the head job now.
+        check_delay_for_job(self, postgres_job, 0)
+        # Also, the platform of the postgres job is returned since it *is*
+        # the head job now.
+        pg_platform = (postgres_job.processor.id, postgres_job.virtualized)
+        self.assertEquals(pg_platform, postgres_job._getHeadJobPlatform())
+
+    def test_job_delay_for_unspecified_virtualization(self):
+        # Make sure that jobs with a NULL 'virtualized' flag get the same
+        # treatment as the ones with virtualized=TRUE.
+        # First toggle the 'virtualized' flag for all hppa jobs.
+        for build in self.builds:
+            bq = build.buildqueue_record
+            if bq.processor == self.hppa_proc:
+                removeSecurityProxy(bq).virtualized = True
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=True, estimated_duration=332,
+            sourcename=u'xxr-openssh-client', score=1050)
+        self.builds.append(job.specific_job.build)
+        # print_build_setup(self.builds)
+        #   ...
+        #   15,               flex, p: hppa, v: True e:0:13:00 *** s: 1039
+        #   16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
+        #   17,           postgres, p: hppa, v: True e:0:15:00 *** s: 1045
+        #   18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
+        #   21, xxr-openssh-client, p: None, v: True e:0:05:32 *** s: 1050
+        #   20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
+
+        flex_build, flex_job = find_job(self, 'flex', 'hppa')
+        # The head job platform is the one of job #21 (xxr-openssh-client).
+        self.assertEquals(
+            (None, True), get_head_job_platform(removeSecurityProxy(flex_job)))
+        # The delay will be 900 (= 15*60) + 332 seconds
+        check_delay_for_job(self, flex_job, 1232)
+
+        # Now add a job with a NULL 'virtualized' flag. It should be treated
+        # like jobs with virtualized=TRUE.
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=111, sourcename=u'xxr-gwibber', score=1051,
+            virtualized=None)
+        self.builds.append(job.specific_job.build)
+        # print_build_setup(self.builds)
+        self.assertEqual(None, job.virtualized)
+        #   ...
+        #   15,               flex, p: hppa, v: True e:0:13:00 *** s: 1039
+        #   16,               flex, p:  386, v:False e:0:14:00 *** s: 1042
+        #   17,           postgres, p: hppa, v: True e:0:15:00 *** s: 1045
+        #   18,           postgres, p:  386, v:False e:0:16:00 *** s: 1048
+        #   21, xxr-openssh-client, p: None, v: True e:0:05:32 *** s: 1050
+        #   22,        xxr-gwibber, p: None, v: None e:0:01:51 *** s: 1051
+        #   20,      xx-recipe-zsh, p: None, v:False e:0:03:42 *** s: 1053
+
+        # The newly added 'xxr-gwibber' job is the new head job now.
+        self.assertEquals(
+            (None, None), get_head_job_platform(removeSecurityProxy(flex_job)))
+        # The newly added 'xxr-gwibber' job now weighs in as well and the
+        # delay is 900 (= 15*60) + (332+111)/2 seconds
+        check_delay_for_job(self, flex_job, 1121)
+
+        # The '386' flex job does not care about the 'xxr-gwibber' and
+        # 'xxr-openssh-client' jobs since the 'virtualized' values do not
+        # match.
+        flex_build, flex_job = find_job(self, 'flex', '386')
+        self.assertEquals(
+            (None, False),
+            get_head_job_platform(removeSecurityProxy(flex_job)))
+        # delay is 960 (= 16*60) + 222 seconds
+        check_delay_for_job(self, flex_job, 1182)
+
+
+class TestJobDispatchTimeEstimation(MultiArchBuildsBase):
+    """Test estimated job delays with various processors."""
+    score_increment = 2
+
+    def setUp(self):
+        """Add more processor-independent jobs to the mix, make the '386' jobs
+        virtual.
+
+            3,              gedit, p: hppa, v:False e:0:01:00 *** s: 1003
+            4,              gedit, p:  386, v: True e:0:02:00 *** s: 1006
+            5,            firefox, p: hppa, v:False e:0:03:00 *** s: 1009
+            6,            firefox, p:  386, v: True e:0:04:00 *** s: 1012
+            7,                apg, p: hppa, v:False e:0:05:00 *** s: 1015
+            9,                vim, p: hppa, v:False e:0:07:00 *** s: 1021
+           10,                vim, p:  386, v: True e:0:08:00 *** s: 1024
+            8,                apg, p:  386, v: True e:0:06:00 *** s: 1024
+           19,       xxr-aptitude, p: None, v:False e:0:05:32 *** s: 1025
+           11,                gcc, p: hppa, v:False e:0:09:00 *** s: 1027
+           12,                gcc, p:  386, v: True e:0:10:00 *** s: 1030
+           13,              bison, p: hppa, v:False e:0:11:00 *** s: 1033
+           14,              bison, p:  386, v: True e:0:12:00 *** s: 1036
+           15,               flex, p: hppa, v:False e:0:13:00 *** s: 1039
+           16,               flex, p:  386, v: True e:0:14:00 *** s: 1042
+           23,      xxr-apt-build, p: None, v: True e:0:12:56 *** s: 1043
+           22,       xxr-cron-apt, p: None, v: True e:0:11:05 *** s: 1043
+           26,           xxr-cupt, p: None, v: None e:0:18:30 *** s: 1044
+           25,            xxr-apt, p: None, v: None e:0:16:38 *** s: 1044
+           24,       xxr-debdelta, p: None, v: None e:0:14:47 *** s: 1044
+           17,           postgres, p: hppa, v:False e:0:15:00 *** s: 1045
+           18,           postgres, p:  386, v: True e:0:16:00 *** s: 1048
+           21,         xxr-daptup, p: None, v: None e:0:09:14 *** s: 1051
+           20,       xxr-auto-apt, p: None, v:False e:0:07:23 *** s: 1053
+
+         p=processor, v=virtualized, e=estimated_duration, s=score
+        """
+        super(TestJobDispatchTimeEstimation, self).setUp()
+
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=False, estimated_duration=332,
+            sourcename=u'xxr-aptitude', score=1025)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            virtualized=False, estimated_duration=443,
+            sourcename=u'xxr-auto-apt', score=1053)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=554, sourcename=u'xxr-daptup', score=1051,
+            virtualized=None)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=665, sourcename=u'xxr-cron-apt', score=1043)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=776, sourcename=u'xxr-apt-build', score=1043)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=887, sourcename=u'xxr-debdelta', score=1044,
+            virtualized=None)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=998, sourcename=u'xxr-apt', score=1044,
+            virtualized=None)
+        self.builds.append(job.specific_job.build)
+        job = self.factory.makeSourcePackageRecipeBuildJob(
+            estimated_duration=1110, sourcename=u'xxr-cupt', score=1044,
+            virtualized=None)
+        self.builds.append(job.specific_job.build)
+
+        # Assign the same score to the '386' vim and apg build jobs.
+        _apg_build, apg_job = find_job(self, 'apg', '386')
+        apg_job.lastscore = 1024
+
+        # Also, toggle the 'virtualized' flag for all '386' jobs.
+        for build in self.builds:
+            bq = build.buildqueue_record
+            if bq.processor == self.x86_proc:
+                removeSecurityProxy(bq).virtualized = True
+
+    def test_pending_jobs_only(self):
+        # Let's see the assertion fail for a job that's not pending any more.
+        assign_to_builder(self, 'gedit', 1, 'hppa')
+        gedit_build, gedit_job = find_job(self, 'gedit', 'hppa')
+        self.assertRaises(AssertionError, gedit_job.getEstimatedJobStartTime)
+
+    def test_estimation_binary_virtual(self):
+        gcc_build, gcc_job = find_job(self, 'gcc', '386')
+        # The delay of 1671 seconds is calculated as follows:
+        #                     386 jobs: (12+14+16)*60/3           = 840
+        #   processor-independent jobs:
+        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
+        check_estimate(self, gcc_job, 1671)
+        self.assertEquals(5, builders_for_job(gcc_job))
+
+    def test_proc_indep_virtual_true(self):
+        xxr_build, xxr_job = find_job(self, 'xxr-apt-build', None)
+        # The delay of 1802 seconds is calculated as follows:
+        #                     386 jobs: 16*60                    = 960
+        #   processor-independent jobs:
+        #       (11:05 + 18:30 + 16:38 + 14:47 + 9:14)/5         = 842
+        check_estimate(self, xxr_job, 1802)
+
+    def test_estimation_binary_virtual_long_queue(self):
+        gedit_build, gedit_job = find_job(self, 'gedit', '386')
+        # The delay of 1671 seconds is calculated as follows:
+        #                     386 jobs:
+        #       (4+6+8+10+12+14+16)*60/5                          = 840
+        #   processor-independent jobs:
+        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
+        check_estimate(self, gedit_job, 1671)
+
+    def test_proc_indep_virtual_null_headjob(self):
+        xxr_build, xxr_job = find_job(self, 'xxr-daptup', None)
+        # This job is at the head of the queue for virtualized builders and
+        # will get dispatched within the next 5 seconds.
+        check_estimate(self, xxr_job, 5)
+
+    def test_proc_indep_virtual_false(self):
+        xxr_build, xxr_job = find_job(self, 'xxr-aptitude', None)
+        # The delay of 1403 seconds is calculated as follows:
+        #                    hppa jobs: (9+11+13+15)*60/3        = 960
+        #   processor-independent jobs: 7:23                     = 443
+        check_estimate(self, xxr_job, 1403)
+
+    def test_proc_indep_virtual_false_headjob(self):
+        xxr_build, xxr_job = find_job(self, 'xxr-auto-apt', None)
+        # This job is at the head of the queue for native builders and
+        # will get dispatched within the next 5 seconds.
+        check_estimate(self, xxr_job, 5)
+
+    def test_estimation_binary_virtual_same_score(self):
+        vim_build, vim_job = find_job(self, 'vim', '386')
+        # The apg job is ahead of the vim job.
+        # The delay of 1527 seconds is calculated as follows:
+        #                     386 jobs: (6+10+12+14+16)*60/5      = 696
+        #   processor-independent jobs:
+        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47 + 9:14)/6  = 831
+        check_estimate(self, vim_job, 1527)
+
+    def test_no_builder_no_estimate(self):
+        # No dispatch estimate is provided in the absence of builders that
+        # can run the job of interest (JOI).
+        disable_builders(self, '386', True)
+        vim_build, vim_job = find_job(self, 'vim', '386')
+        check_estimate(self, vim_job, None)
+
+    def disabled_test_estimates_with_small_builder_pool(self):
+        # Test that a reduced builder pool results in longer dispatch time
+        # estimates.
+        vim_build, vim_job = find_job(self, 'vim', '386')
+        disable_builders(self, '386', True)
+        # Re-enable one builder.
+        builder = self.builders[(self.x86_proc.id, True)][0]
+        builder.builderok = True
+        # Dispatch the firefox job to it.
+        assign_to_builder(self, 'firefox', 1, '386')
+        # Dispatch the head job, making postgres/386 the new head job and
+        # resulting in a 240 seconds head job dispatch delay.
+        assign_to_builder(self, 'xxr-daptup', 1, None)
+        check_mintime_to_builder(self, vim_job, 240)
+        # Re-enable another builder.
+        builder = self.builders[(self.x86_proc.id, True)][1]
+        builder.builderok = True
+        # Assign a job to it.
+        assign_to_builder(self, 'gedit', 2, '386')
+        check_mintime_to_builder(self, vim_job, 120)
+
+        xxr_build, xxr_job = find_job(self, 'xxr-apt', None)
+        # The delay of 2627+120 seconds is calculated as follows:
+        #                     386 jobs : (6+10+12+14+16)*60/2     = 1740
+        #   processor-independent jobs :
+        #       (12:56 + 11:05 + 18:30 + 16:38 + 14:47)/5         =  887
+        # waiting time for next builder:                          =  120
+        self.assertEquals(2, builders_for_job(vim_job))
+        self.assertEquals(9, builders_for_job(xxr_job))
+        check_estimate(self, vim_job, 2747)
+
+    def test_estimation_binary_virtual_headjob(self):
+        # The head job only waits for the next builder to become available.
+        disable_builders(self, '386', True)
+        # Re-enable one builder.
+        builder = self.builders[(self.x86_proc.id, True)][0]
+        builder.builderok = True
+        # Assign a job to it.
+        assign_to_builder(self, 'gedit', 1, '386')
+        # Dispatch the head job, making postgres/386 the new head job.
+        assign_to_builder(self, 'xxr-daptup', 1, None)
+        postgres_build, postgres_job = find_job(self, 'postgres', '386')
+        check_estimate(self, postgres_job, 120)


Follow ups