Example #1
0
def test_aggregate_result():
    status_list = [Result.passed, Result.failed, Result.unknown]
    assert aggregate_result(status_list) == Result.failed

    status_list = [Result.passed, Result.unknown]
    assert aggregate_result(status_list) == Result.unknown

    status_list = [Result.passed, Result.skipped]
    assert aggregate_result(status_list) == Result.passed
Example #2
0
def test_aggregate_result():
    status_list = [Result.passed, Result.failed, Result.unknown]
    assert aggregate_result(status_list) == Result.failed

    status_list = [Result.passed, Result.unknown]
    assert aggregate_result(status_list) == Result.passed

    status_list = [Result.passed, Result.skipped]
    assert aggregate_result(status_list) == Result.passed
Example #3
0
def _deduplicate_testresults(results):
    """Combine TestResult objects until every package+name is unique.

    The traditions surrounding jUnit do not prohibit a single test from
    producing two or more <testcase> elements.  In fact, py.test itself
    will produce two such elements for a single test if the test both
    fails and then hits an error during tear-down.  To impedance-match
    this situation with the Changes constraint of one result per test,
    we combine <testcase> elements that belong to the same test.

    """
    result_dict = {}
    deduped = []

    for result in results:
        key = (result.package, result.name)
        existing_result = result_dict.get(key)

        if existing_result is not None:
            e, r = existing_result, result
            e.duration = _careful(add, e.duration, r.duration)
            e.result = aggregate_result((e.result, r.result))
            if e.message and r.message:
                e.message += '\n\n' + r.message
            elif not e.message:
                e.message = r.message or ''
            e.reruns = _careful(max, e.reruns, r.reruns)
            e.artifacts = _careful(add, e.artifacts, r.artifacts)
            e.message_offsets = _careful(add, e.message_offsets, r.message_offsets)
        else:
            result_dict[key] = result
            deduped.append(result)

    return deduped
Example #4
0
def sync_phase(phase):
    phase_steps = list(phase.steps)

    if phase.date_started is None:
        phase.date_started = safe_agg(min,
                                      (s.date_started for s in phase_steps))
        db.session.add(phase)

    if phase_steps:
        if all(s.status == Status.finished for s in phase_steps):
            phase.status = Status.finished
            phase.date_finished = safe_agg(max, (s.date_finished
                                                 for s in phase_steps))
        else:
            # ensure we dont set the status to finished unless it actually is
            new_status = aggregate_status((s.status for s in phase_steps))
            if new_status != Status.finished:
                phase.status = new_status

        if any(s.result is Result.failed for s in phase_steps):
            phase.result = Result.failed

        elif phase.status == Status.finished:
            phase.result = aggregate_result((s.result for s in phase_steps))

    if db.session.is_modified(phase):
        phase.date_modified = datetime.utcnow()
        db.session.add(phase)
        db.session.commit()
Example #5
0
    def process(self, fp, artifact):
        target_name = self._get_target_name(artifact)
        target, _ = get_or_create(BazelTarget, where={
            'step_id': self.step.id,
            'job_id': self.step.job.id,
            'name': target_name,
            'result_source': ResultSource.from_self,
        })
        test_suites = self.get_test_suites(fp)
        tests = self.aggregate_tests_from_suites(test_suites)
        manager = TestResultManager(self.step, artifact)
        manager.save(tests)

        # update target metadata
        # TODO handle multiple files per target, i.e. sharding and running multiple times
        target.status = Status.finished
        target.result = aggregate_result([t.result for t in tests])
        duration = 0
        for t in test_suites:
            if t.duration is None:
                duration = None
                break
            duration += t.duration
        target.duration = duration
        target.date_created = min([t.date_created for t in test_suites])
        db.session.add(target)
        db.session.commit()
        return tests
Example #6
0
def _deduplicate_testresults(results):
    """Combine TestResult objects until every package+name is unique.

    The traditions surrounding jUnit do not prohibit a single test from
    producing two or more <testcase> elements.  In fact, py.test itself
    will produce two such elements for a single test if the test both
    fails and then hits an error during tear-down.  To impedance-match
    this situation with the Changes constraint of one result per test,
    we combine <testcase> elements that belong to the same test.

    """
    result_dict = {}
    deduped = []

    for result in results:
        key = (result.package, result.name)
        existing_result = result_dict.get(key)

        if existing_result is not None:
            e, r = existing_result, result
            e.duration = _careful(add, e.duration, r.duration)
            e.result = aggregate_result((e.result, r.result))
            e.message += '\n\n' + r.message
            e.reruns = _careful(max, e.reruns, r.reruns)
            e.artifacts = _careful(add, e.artifacts, r.artifacts)
        else:
            result_dict[key] = result
            deduped.append(result)

    return deduped
Example #7
0
def sync_phase(phase):
    phase_steps = list(phase.steps)

    if phase.date_started is None:
        phase.date_started = safe_agg(min, (s.date_started for s in phase_steps))
        db.session.add(phase)

    if phase_steps:
        if all(s.status == Status.finished for s in phase_steps):
            phase.status = Status.finished
            phase.date_finished = safe_agg(max, (s.date_finished for s in phase_steps))
        else:
            # ensure we dont set the status to finished unless it actually is
            new_status = aggregate_status((s.status for s in phase_steps))
            if new_status != Status.finished:
                phase.status = new_status

        if any(s.result is Result.failed for s in phase_steps):
            phase.result = Result.failed

        elif phase.status == Status.finished:
            phase.result = aggregate_result((s.result for s in phase_steps))

    if db.session.is_modified(phase):
        phase.date_modified = datetime.utcnow()
        db.session.add(phase)
        db.session.commit()
    def process(self, fp, artifact):
        target_name = self._get_target_name(artifact)
        target, _ = get_or_create(BazelTarget,
                                  where={
                                      'step_id': self.step.id,
                                      'job_id': self.step.job.id,
                                      'name': target_name,
                                      'result_source': ResultSource.from_self,
                                  })
        test_suites = self.get_test_suites(fp)
        tests = self.aggregate_tests_from_suites(test_suites)
        manager = TestResultManager(self.step, artifact)
        manager.save(tests)

        # update target metadata
        # TODO handle multiple files per target, i.e. sharding and running multiple times
        target.status = Status.finished
        target.result = aggregate_result([t.result for t in tests])
        duration = 0
        for t in test_suites:
            if t.duration is None:
                duration = None
                break
            duration += t.duration
        target.duration = duration
        target.date_created = min([t.date_created for t in test_suites])
        db.session.add(target)
        db.session.commit()
        return tests
Example #9
0
    def validate_phase(self, phase):
        """Called when a job phase is ready to be finished.

        This is responsible for setting the phases's final result. We verify
        that the proper number of steps were created in the second (i.e.
        expanded) phase."""
        phase.result = aggregate_result([s.result for s in phase.steps] +
                                        [self._validate_shards(phase.steps)])
Example #10
0
    def validate_phase(self, phase):
        """Called when a job phase is ready to be finished.

        This is responsible for setting the phases's final result. We verify
        that the proper number of steps were created in the second (i.e.
        expanded) phase."""
        phase.result = aggregate_result([s.result for s in phase.steps] +
                                        [self._validate_shards(phase.steps)])
Example #11
0
    def validate(self, job):
        """Called when a job is ready to be finished.

        This is responsible for setting the job's final result. The base
        implementation simply aggregates the phase results.

        Args:
            job (Job): The job being finished.
        Returns:
            None
        """
        # TODO(josiah): ideally, we could record a FailureReason.
        # Currently FailureReason must be per-step.

        job.result = aggregate_result((p.result for p in job.phases))
Example #12
0
    def validate_phase(self, phase):
        """Called when a job phase is ready to be finished.

        This is responsible for setting the phases's final result. The base
        implementation simply aggregates the jobstep results.

        Args:
            phase (JobPhase): The phase being finished.
        Returns:
            None
        """
        # TODO(josiah): ideally, we could record a FailureReason.
        # Currently FailureReason must be per-step.

        phase.result = aggregate_result((s.result for s in phase.steps))
Example #13
0
    def validate(self, job):
        """Called when a job is ready to be finished.

        This is responsible for setting the job's final result. The base
        implementation simply aggregates the phase results.

        Args:
            job (Job): The job being finished.
        Returns:
            None
        """
        # TODO(josiah): ideally, we could record a FailureReason.
        # Currently FailureReason must be per-step.

        job.result = aggregate_result((p.result for p in job.phases))
Example #14
0
    def validate_phase(self, phase):
        """Called when a job phase is ready to be finished.

        This is responsible for setting the phases's final result. The base
        implementation simply aggregates the jobstep results.

        Args:
            phase (JobPhase): The phase being finished.
        Returns:
            None
        """
        # TODO(josiah): ideally, we could record a FailureReason.
        # Currently FailureReason must be per-step.

        phase.result = aggregate_result((s.result for s in phase.current_steps))
Example #15
0
    def end(self, tag):
        if self._is_message:
            self.close_message()
        if tag == 'testsuites':
            pass
        elif tag == 'testsuite':
            self.test_suites[-1].test_results = _deduplicate_testresults(self.test_suites[-1].test_results)
            if self.test_suites[-1].duration is None:
                # NOTE: it is inaccurate to just sum up the duration of individual
                # tests, because tests may be run in parallel
                self.logger.warning('Test suite does not have timing information; (step=%s, build=%s)',
                                    self.step.id.hex, self.step.job.build_id.hex)

            if len(self.test_suites[-1].test_results) > 0:
                self.test_suites[-1].result = aggregate_result([t.result for t in self.test_suites[-1].test_results])

                if self.test_suites[-1].date_created is None:
                    self.test_suites[-1].date_created = min([t.date_created for t in self.test_suites[-1].test_results])
            else:
                if self.test_suites[-1].date_created is None:
                    self.test_suites[-1].date_created = datetime.utcnow()
        elif tag == 'testcase':
            if self._current_result.result == Result.unknown:
                # Default result is passing
                self._current_result.result = Result.passed
            if self._test_is_quarantined:
                if self._current_result.result == Result.passed:
                    self._current_result.result = Result.quarantined_passed
                elif self._current_result.result == Result.failed:
                    self._current_result.result = Result.quarantined_failed
                elif self._current_result.result == Result.skipped:
                    self._current_result.result = Result.quarantined_skipped
            self._test_is_quarantined = None
            self.test_suites[-1].test_results.append(self._current_result)
            self._current_result = None
        elif tag == 'test-artifacts':
            pass
        elif tag == 'artifact':
            pass
Example #16
0
def sync_job(job_id):
    with RCount('sync_job'):
        job = Job.query.get(job_id)
        if not job:
            return

        if job.status == Status.finished:
            return

        # TODO(dcramer): we make an assumption that there is a single step
        jobplan, implementation = JobPlan.get_build_step_for_job(job_id=job.id)

        try:
            implementation.update(job=job)

        except UnrecoverableException:
            job.status = Status.finished
            job.result = Result.aborted
            current_app.logger.exception('Unrecoverable exception syncing %s',
                                         job.id)

        all_phases = list(job.phases)

        # propagate changes to any phases as they live outside of the
        # normalize synchronization routines
        sync_job_phases(job, all_phases)

        is_finished = sync_job.verify_all_children() == Status.finished
        if any(p.status != Status.finished for p in all_phases):
            is_finished = False

        job.date_started = safe_agg(min,
                                    (j.date_started
                                     for j in all_phases if j.date_started))

        if is_finished:
            job.date_finished = safe_agg(
                max, (j.date_finished for j in all_phases if j.date_finished))
        else:
            job.date_finished = None

        if job.date_started and job.date_finished:
            job.duration = int(
                (job.date_finished - job.date_started).total_seconds() * 1000)
        else:
            job.duration = None

        # if any phases are marked as failing, fail the build
        if any(j.result is Result.failed for j in all_phases):
            job.result = Result.failed
        # if any test cases were marked as failing, fail the build
        elif TestCase.query.filter(TestCase.result == Result.failed,
                                   TestCase.job_id == job.id).first():
            job.result = Result.failed
        # if we've finished all phases, use the best result available
        elif is_finished:
            job.result = aggregate_result((j.result for j in all_phases))
        else:
            job.result = Result.unknown

        if is_finished:
            job.status = Status.finished
        else:
            # ensure we dont set the status to finished unless it actually is
            new_status = aggregate_status((j.status for j in all_phases))
            if new_status != Status.finished:
                job.status = new_status
            elif job.status == Status.finished:
                job.status = Status.in_progress
                current_app.logger.exception(
                    'Job incorrectly marked as finished: %s', job.id)

        if db.session.is_modified(job):
            job.date_modified = datetime.utcnow()

            db.session.add(job)
            db.session.commit()

        if not is_finished:
            raise sync_job.NotFinished

        try:
            aggregate_job_stat(job, 'test_count')
            aggregate_job_stat(job, 'test_duration')
            aggregate_job_stat(job, 'test_failures')
            aggregate_job_stat(job, 'test_rerun_count')
            aggregate_job_stat(job, 'tests_missing')
            aggregate_job_stat(job, 'lines_covered')
            aggregate_job_stat(job, 'lines_uncovered')
            aggregate_job_stat(job, 'diff_lines_covered')
            aggregate_job_stat(job, 'diff_lines_uncovered')
        except Exception:
            current_app.logger.exception(
                'Failing recording aggregate stats for job %s', job.id)

        fire_signal.delay(
            signal='job.finished',
            kwargs={'job_id': job.id.hex},
        )

        if jobplan:
            queue.delay('update_project_plan_stats',
                        kwargs={
                            'project_id': job.project_id.hex,
                            'plan_id': jobplan.plan_id.hex,
                        },
                        countdown=1)
Example #17
0
def sync_build(build_id):
    """
    Synchronizing the build happens continuously until all jobs have reported in
    as finished or have failed/aborted.

    This task is responsible for:
    - Checking in with jobs
    - Aborting/retrying them if they're beyond limits
    - Aggregating the results from jobs into the build itself
    """
    with RCount('sync_build'):
        build = Build.query.get(build_id)
        if not build:
            return

        if build.status == Status.finished:
            return

        all_jobs = list(Job.query.filter(
            Job.build_id == build_id,
        ))

        is_finished = sync_build.verify_all_children() == Status.finished
        if any(p.status != Status.finished for p in all_jobs):
            is_finished = False

        build.date_started = safe_agg(
            min, (j.date_started for j in all_jobs if j.date_started))

        if is_finished:
            build.date_finished = safe_agg(
                max, (j.date_finished for j in all_jobs if j.date_finished))
        else:
            build.date_finished = None

        if build.date_started and build.date_finished:
            build.duration = int((build.date_finished - build.date_started).total_seconds() * 1000)
        else:
            build.duration = None

        if any(j.result is Result.failed for j in all_jobs):
            build.result = Result.failed
        elif is_finished:
            build.result = aggregate_result((j.result for j in all_jobs))
        else:
            build.result = Result.unknown

        if is_finished:
            build.status = Status.finished
        else:
            # ensure we dont set the status to finished unless it actually is
            new_status = aggregate_status((j.status for j in all_jobs))
            if new_status != Status.finished:
                build.status = new_status

        if db.session.is_modified(build):
            build.date_modified = datetime.utcnow()
            db.session.add(build)
            db.session.commit()

        if not is_finished:
            raise sync_build.NotFinished

        try:
            aggregate_build_stat(build, 'test_count')
            aggregate_build_stat(build, 'test_duration')
            aggregate_build_stat(build, 'test_failures')
            aggregate_build_stat(build, 'test_rerun_count')
            aggregate_build_stat(build, 'tests_missing')
            aggregate_build_stat(build, 'lines_covered')
            aggregate_build_stat(build, 'lines_uncovered')
            aggregate_build_stat(build, 'diff_lines_covered')
            aggregate_build_stat(build, 'diff_lines_uncovered')
        except Exception:
            current_app.logger.exception('Failing recording aggregate stats for build %s', build.id)

        fire_signal.delay(
            signal='build.finished',
            kwargs={'build_id': build.id.hex},
        )

        queue.delay('update_project_stats', kwargs={
            'project_id': build.project_id.hex,
        }, countdown=1)
Example #18
0
def sync_build(build_id):
    """
    Synchronizing the build happens continuously until all jobs have reported in
    as finished or have failed/aborted.

    This task is responsible for:
    - Checking in with jobs
    - Aborting/retrying them if they're beyond limits
    - Aggregating the results from jobs into the build itself
    """
    build = Build.query.get(build_id)
    if not build:
        return

    if build.status == Status.finished:
        return

    all_jobs = list(Job.query.filter(
        Job.build_id == build_id,
    ))

    is_finished = sync_build.verify_all_children() == Status.finished
    if any(p.status != Status.finished for p in all_jobs):
        is_finished = False

    prev_started = build.date_started
    build.date_started = safe_agg(
        min, (j.date_started for j in all_jobs if j.date_started))

    # We want to report how long we waited for the build to start once and only once,
    # so we do it at the transition from not started to started.
    if not prev_started and build.date_started:
        queued_time = build.date_started - build.date_created
        statsreporter.stats().log_timing('build_start_latency', _timedelta_to_millis(queued_time))

    if is_finished:
        # If there are no jobs (or no jobs with a finished date) fall back to
        # finishing now, since at this point, the build is done executing.
        build.date_finished = safe_agg(
            max, (j.date_finished for j in all_jobs if j.date_finished), datetime.utcnow())
    else:
        build.date_finished = None

    if build.date_started and build.date_finished:
        build.duration = _timedelta_to_millis(build.date_finished - build.date_started)
    else:
        build.duration = None

    if any(j.result is Result.failed for j in all_jobs):
        build.result = Result.failed
    elif is_finished:
        build.result = aggregate_result((j.result for j in all_jobs))
    else:
        build.result = Result.unknown

    if is_finished:
        build.status = Status.finished
    else:
        # ensure we dont set the status to finished unless it actually is
        new_status = aggregate_status((j.status for j in all_jobs))
        if new_status != Status.finished:
            build.status = new_status

    if is_finished:
        build.date_decided = datetime.utcnow()
        decided_latency = build.date_decided - build.date_finished
        statsreporter.stats().log_timing('build_decided_latency', _timedelta_to_millis(decided_latency))
    else:
        build.date_decided = None

    if db.session.is_modified(build):
        build.date_modified = datetime.utcnow()
        db.session.add(build)
        db.session.commit()

    if not is_finished:
        raise sync_build.NotFinished

    with statsreporter.stats().timer('build_stat_aggregation'):
        try:
            aggregate_build_stat(build, 'test_count')
            aggregate_build_stat(build, 'test_duration')
            aggregate_build_stat(build, 'test_failures')
            aggregate_build_stat(build, 'test_rerun_count')
            aggregate_build_stat(build, 'tests_missing')
            aggregate_build_stat(build, 'lines_covered')
            aggregate_build_stat(build, 'lines_uncovered')
            aggregate_build_stat(build, 'diff_lines_covered')
            aggregate_build_stat(build, 'diff_lines_uncovered')
        except Exception:
            current_app.logger.exception('Failing recording aggregate stats for build %s', build.id)

    fire_signal.delay(
        signal='build.finished',
        kwargs={'build_id': build.id.hex},
    )

    queue.delay('update_project_stats', kwargs={
        'project_id': build.project_id.hex,
    }, countdown=1)
Example #19
0
def create_or_update_revision_result(revision_sha, project_id, propagation_limit):
    """Given a revision sha and project ID, try to update the revision result
    for it. This involves copying results for unaffected Bazel targets from
    the latest parent build.

    `propagation_limit` is used to control how many times this function will
    be called recursively on the revision's children. If it is 0, then this
    function only updates the current revision's revision result and does
    not do any recursion.
    """
    # type: (str, UUID, int) -> None
    project = Project.query.get(project_id)
    revision = Revision.query.filter(
        Revision.sha == revision_sha,
        Revision.repository_id == project.repository_id,
    ).first()
    last_finished_build = get_latest_finished_build_for_revision(
        revision_sha, project_id)
    if not last_finished_build:
        return

    unaffected_targets = BazelTarget.query.join(
        Job, BazelTarget.job_id == Job.id,
    ).filter(
        BazelTarget.result_source == ResultSource.from_parent,
        Job.build_id == last_finished_build.id,
    ).all()

    if len(unaffected_targets) > 0 and len(revision.parents) > 0:
        # TODO(naphat) there's probably a better way to select parent,
        # but that happens rarely enough that it can be punted for now
        parent_revision_sha = revision.parents[0]

        # TODO(naphat) we should find a better way to select parent builds.
        # Even if a parent build is not finished, we can already start to
        # take a look at target results, as it may already have results
        # for all of our unaffected_targets
        # perhaps an optimization is to take the latest build, instead
        # of the latest finished build. Finished build are more likely
        # to have the complete set of targets we need though. But if
        # a finished build is not the latest build, then maybe that
        # finished build had an infra failure. Anyways, for simplicity,
        # let's stick to finished build for now.
        parent_build = get_latest_finished_build_for_revision(
            parent_revision_sha, project_id)
        if parent_build:
            # group unaffected targets by jobs
            unaffected_targets_groups = defaultdict(lambda: {})
            for target in unaffected_targets:
                unaffected_targets_groups[target.job_id][target.name] = target

            # process targets in batch, grouped by job id
            # almost always, this is going to be a single job - there is
            # usually only one autogenerated plan per project.
            for job_id, targets_dict in unaffected_targets_groups.iteritems():
                jobplan = JobPlan.query.filter(
                    JobPlan.project_id == project_id,
                    JobPlan.build_id == last_finished_build.id,
                    JobPlan.job_id == job_id,
                ).first()
                if not jobplan:
                    continue
                parent_targets = BazelTarget.query.join(
                    Job, BazelTarget.job_id == Job.id,
                ).join(
                    JobPlan, BazelTarget.job_id == JobPlan.job_id,
                ).filter(
                    Job.build_id == parent_build.id,
                    BazelTarget.name.in_(targets_dict),
                    JobPlan.plan_id == jobplan.plan_id,
                )
                for parent_target in parent_targets:
                    targets_dict[parent_target.name].result = parent_target.result
                    db.session.add(targets_dict[parent_target.name])
        else:
            logger.info("Revision %s could not find a parent build for parent revision %s.", revision_sha, parent_revision_sha)

    revision_result, _ = create_or_update(RevisionResult, where={
        'revision_sha': revision_sha,
        'project_id': project_id,
    }, values={
        'build_id': last_finished_build.id,
        'result': aggregate_result([last_finished_build.result] + [t.result for t in unaffected_targets]),
    })

    db.session.commit()
    fire_signal.delay(
        signal='revision_result.updated',
        kwargs={'revision_result_id': revision_result.id.hex},
    )

    if propagation_limit > 0:
        # TODO stop the propagation if nothing changed
        for child_revision in get_child_revisions(revision):
            create_or_update_revision_result(
                child_revision.sha, project_id, propagation_limit=propagation_limit - 1)
Example #20
0
def sync_job(job_id):
    with RCount('sync_job'):
        job = Job.query.get(job_id)
        if not job:
            return

        if job.status == Status.finished:
            return

        # TODO(dcramer): we make an assumption that there is a single step
        jobplan, implementation = JobPlan.get_build_step_for_job(job_id=job.id)

        try:
            implementation.update(job=job)

        except UnrecoverableException:
            job.status = Status.finished
            job.result = Result.aborted
            current_app.logger.exception('Unrecoverable exception syncing %s', job.id)

        all_phases = list(job.phases)

        # propagate changes to any phases as they live outside of the
        # normalize synchronization routines
        sync_job_phases(job, all_phases)

        is_finished = sync_job.verify_all_children() == Status.finished
        if any(p.status != Status.finished for p in all_phases):
            is_finished = False

        job.date_started = safe_agg(
            min, (j.date_started for j in all_phases if j.date_started))

        if is_finished:
            job.date_finished = safe_agg(
                max, (j.date_finished for j in all_phases if j.date_finished))
        else:
            job.date_finished = None

        if job.date_started and job.date_finished:
            job.duration = int((job.date_finished - job.date_started).total_seconds() * 1000)
        else:
            job.duration = None

        # if any phases are marked as failing, fail the build
        if any(j.result is Result.failed for j in all_phases):
            job.result = Result.failed
        # if any test cases were marked as failing, fail the build
        elif TestCase.query.filter(TestCase.result == Result.failed, TestCase.job_id == job.id).first():
            job.result = Result.failed
        # if we've finished all phases, use the best result available
        elif is_finished:
            job.result = aggregate_result((j.result for j in all_phases))
        else:
            job.result = Result.unknown

        if is_finished:
            job.status = Status.finished
        else:
            # ensure we dont set the status to finished unless it actually is
            new_status = aggregate_status((j.status for j in all_phases))
            if new_status != Status.finished:
                job.status = new_status
            elif job.status == Status.finished:
                job.status = Status.in_progress
                current_app.logger.exception('Job incorrectly marked as finished: %s', job.id)

        if db.session.is_modified(job):
            job.date_modified = datetime.utcnow()

            db.session.add(job)
            db.session.commit()

        if not is_finished:
            raise sync_job.NotFinished

        try:
            aggregate_job_stat(job, 'test_count')
            aggregate_job_stat(job, 'test_duration')
            aggregate_job_stat(job, 'test_failures')
            aggregate_job_stat(job, 'test_rerun_count')
            aggregate_job_stat(job, 'tests_missing')
            aggregate_job_stat(job, 'lines_covered')
            aggregate_job_stat(job, 'lines_uncovered')
            aggregate_job_stat(job, 'diff_lines_covered')
            aggregate_job_stat(job, 'diff_lines_uncovered')
        except Exception:
            current_app.logger.exception('Failing recording aggregate stats for job %s', job.id)

        fire_signal.delay(
            signal='job.finished',
            kwargs={'job_id': job.id.hex},
        )

        if jobplan:
            queue.delay('update_project_plan_stats', kwargs={
                'project_id': job.project_id.hex,
                'plan_id': jobplan.plan_id.hex,
            }, countdown=1)
Example #21
0
def sync_build(build_id):
    """
    Synchronizing the build happens continuously until all jobs have reported in
    as finished or have failed/aborted.

    This task is responsible for:
    - Checking in with jobs
    - Aborting/retrying them if they're beyond limits
    - Aggregating the results from jobs into the build itself
    """
    build = Build.query.get(build_id)
    if not build:
        return

    if build.status == Status.finished:
        return

    all_jobs = list(Job.query.filter(
        Job.build_id == build_id,
    ))

    is_finished = sync_build.verify_all_children() == Status.finished
    if any(p.status != Status.finished for p in all_jobs):
        is_finished = False

    build.date_started = safe_agg(
        min, (j.date_started for j in all_jobs if j.date_started))

    if is_finished:
        build.date_finished = safe_agg(
            max, (j.date_finished for j in all_jobs if j.date_finished))
    else:
        build.date_finished = None

    if build.date_started and build.date_finished:
        build.duration = int((build.date_finished - build.date_started).total_seconds() * 1000)
    else:
        build.duration = None

    if any(j.result is Result.failed for j in all_jobs):
        build.result = Result.failed
    elif is_finished:
        build.result = aggregate_result((j.result for j in all_jobs))
    else:
        build.result = Result.unknown

    if is_finished:
        build.status = Status.finished
    else:
        # ensure we dont set the status to finished unless it actually is
        new_status = aggregate_status((j.status for j in all_jobs))
        if new_status != Status.finished:
            build.status = new_status

    if db.session.is_modified(build):
        build.date_modified = datetime.utcnow()
        db.session.add(build)
        db.session.commit()

    if not is_finished:
        raise sync_build.NotFinished

    try:
        aggregate_build_stat(build, 'test_count')
        aggregate_build_stat(build, 'test_duration')
        aggregate_build_stat(build, 'test_failures')
        aggregate_build_stat(build, 'test_rerun_count')
        aggregate_build_stat(build, 'tests_missing')
        aggregate_build_stat(build, 'lines_covered')
        aggregate_build_stat(build, 'lines_uncovered')
        aggregate_build_stat(build, 'diff_lines_covered')
        aggregate_build_stat(build, 'diff_lines_uncovered')
    except Exception:
        current_app.logger.exception('Failing recording aggregate stats for build %s', build.id)

    fire_signal.delay(
        signal='build.finished',
        kwargs={'build_id': build.id.hex},
    )

    queue.delay('update_project_stats', kwargs={
        'project_id': build.project_id.hex,
    }, countdown=1)
Example #22
0
def create_or_update_revision_result(revision_sha, project_id,
                                     propagation_limit):
    """Given a revision sha and project ID, try to update the revision result
    for it. This involves copying results for unaffected Bazel targets from
    the latest parent build.

    `propagation_limit` is used to control how many times this function will
    be called recursively on the revision's children. If it is 0, then this
    function only updates the current revision's revision result and does
    not do any recursion.
    """
    # type: (str, UUID, int) -> None
    project = Project.query.get(project_id)
    revision = Revision.query.filter(
        Revision.sha == revision_sha,
        Revision.repository_id == project.repository_id,
    ).first()
    last_finished_build = get_latest_finished_build_for_revision(
        revision_sha, project_id)
    if not last_finished_build:
        return

    unaffected_targets = BazelTarget.query.join(
        Job,
        BazelTarget.job_id == Job.id,
    ).filter(
        BazelTarget.result_source == ResultSource.from_parent,
        Job.build_id == last_finished_build.id,
    ).all()

    if len(unaffected_targets) > 0 and len(revision.parents) > 0:
        # TODO(naphat) there's probably a better way to select parent,
        # but that happens rarely enough that it can be punted for now
        parent_revision_sha = revision.parents[0]

        # TODO(naphat) we should find a better way to select parent builds.
        # Even if a parent build is not finished, we can already start to
        # take a look at target results, as it may already have results
        # for all of our unaffected_targets
        # perhaps an optimization is to take the latest build, instead
        # of the latest finished build. Finished build are more likely
        # to have the complete set of targets we need though. But if
        # a finished build is not the latest build, then maybe that
        # finished build had an infra failure. Anyways, for simplicity,
        # let's stick to finished build for now.
        parent_build = get_latest_finished_build_for_revision(
            parent_revision_sha, project_id)
        if parent_build:
            # group unaffected targets by jobs
            unaffected_targets_groups = defaultdict(lambda: {})
            for target in unaffected_targets:
                unaffected_targets_groups[target.job_id][target.name] = target

            # process targets in batch, grouped by job id
            # almost always, this is going to be a single job - there is
            # usually only one autogenerated plan per project.
            for job_id, targets_dict in unaffected_targets_groups.iteritems():
                jobplan = JobPlan.query.filter(
                    JobPlan.project_id == project_id,
                    JobPlan.build_id == last_finished_build.id,
                    JobPlan.job_id == job_id,
                ).first()
                if not jobplan:
                    continue
                parent_targets = BazelTarget.query.join(
                    Job,
                    BazelTarget.job_id == Job.id,
                ).join(
                    JobPlan,
                    BazelTarget.job_id == JobPlan.job_id,
                ).filter(
                    Job.build_id == parent_build.id,
                    BazelTarget.name.in_(targets_dict),
                    JobPlan.plan_id == jobplan.plan_id,
                )
                for parent_target in parent_targets:
                    targets_dict[
                        parent_target.name].result = parent_target.result
                    db.session.add(targets_dict[parent_target.name])
        else:
            logger.info(
                "Revision %s could not find a parent build for parent revision %s.",
                revision_sha, parent_revision_sha)

    revision_result, _ = create_or_update(
        RevisionResult,
        where={
            'revision_sha': revision_sha,
            'project_id': project_id,
        },
        values={
            'build_id':
            last_finished_build.id,
            'result':
            aggregate_result([last_finished_build.result] +
                             [t.result for t in unaffected_targets]),
        })

    db.session.commit()
    fire_signal.delay(
        signal='revision_result.updated',
        kwargs={'revision_result_id': revision_result.id.hex},
    )

    if propagation_limit > 0:
        # TODO stop the propagation if nothing changed
        for child_revision in get_child_revisions(revision):
            create_or_update_revision_result(
                child_revision.sha,
                project_id,
                propagation_limit=propagation_limit - 1)