예제 #1
0
    def expand(self, max_executors):
        test_stats, avg_test_time = self.get_test_stats()

        group_tests = [[] for _ in range(max_executors)]
        group_weights = [0 for _ in range(max_executors)]
        weights = [0] * max_executors
        weighted_tests = [
            (self.get_test_duration(t, test_stats) or avg_test_time, t)
            for t in self.data['tests']
        ]
        for weight, test in sorted(weighted_tests, reverse=True):
            low_index, _ = min(enumerate(weights), key=itemgetter(1))
            weights[low_index] += 1 + weight
            group_tests[low_index].append(test)
            group_weights[low_index] += 1 + weight

        for test_list, weight in zip(group_tests, group_weights):
            future_command = FutureCommand(
                script=self.data['command'].format(test_names=' '.join(test_list)),
                path=self.data.get('path'),
                env=self.data.get('env'),
                artifacts=self.data.get('artifacts'),
            )
            future_jobstep = FutureJobStep(
                label=self.data.get('label') or future_command.label,
                commands=[future_command],
                data={'weight': weight},
            )
            yield future_jobstep
예제 #2
0
    def test_create_expanded_jobstep(self, get_vcs):
        build = self.create_build(self.create_project())
        job = self.create_job(build)
        jobphase = self.create_jobphase(job, label='foo')
        jobstep = self.create_jobstep(jobphase)

        new_jobphase = self.create_jobphase(job, label='bar')

        vcs = mock.Mock(spec=Vcs)
        vcs.get_buildstep_clone.return_value = 'git clone https://example.com'
        get_vcs.return_value = vcs

        future_jobstep = FutureJobStep(
            label='test',
            commands=[
                FutureCommand('echo 1'),
                FutureCommand('echo "foo"\necho "bar"'),
            ],
        )

        buildstep = self.get_buildstep()
        new_jobstep = buildstep.create_expanded_jobstep(
            jobstep, new_jobphase, future_jobstep)

        db.session.flush()

        assert new_jobstep.data['expanded'] is True

        commands = new_jobstep.commands

        assert len(commands) == 4
        assert commands[0].script == 'git clone https://example.com'
        assert commands[0].cwd == ''
        assert commands[0].type == CommandType.infra_setup
        assert commands[0].order == 0
        assert commands[1].script == 'echo "hello world 2"'
        assert commands[1].cwd == '/usr/test/1'
        assert commands[1].type == CommandType.setup
        assert commands[1].order == 1
        assert commands[2].label == 'echo 1'
        assert commands[2].script == 'echo 1'
        assert commands[2].order == 2
        assert commands[2].cwd == DEFAULT_PATH
        assert commands[3].label == 'echo "foo"'
        assert commands[3].script == 'echo "foo"\necho "bar"'
        assert commands[3].order == 3
        assert commands[3].cwd == DEFAULT_PATH
예제 #3
0
 def get_future_commands(self, params, commands):
     """Create future commands which are later created as comands.
     See models/command.py.
     """
     return map(
         lambda command: FutureCommand(command['script'],
                                       env=self.params_to_env(params)),
         commands)
예제 #4
0
 def expand(self, max_executors):
     for cmd_data in self.data['commands']:
         future_command = FutureCommand(**cmd_data)
         future_jobstep = FutureJobStep(
             label=cmd_data.get('label') or future_command.label,
             commands=[future_command],
         )
         yield future_jobstep
예제 #5
0
 def expand(self, max_executors, **kwargs):
     for cmd_data in self.data['commands']:
         # TODO: group commands with jobsteps so as to respect max_executors
         future_command = FutureCommand(**cmd_data)
         future_jobstep = FutureJobStep(
             label=cmd_data.get('label') or future_command.label,
             commands=[future_command],
         )
         yield future_jobstep
예제 #6
0
    def test_expand_jobstep(self):
        build = self.create_build(self.create_project())
        job = self.create_job(build)
        jobphase = self.create_jobphase(job, label='foo')
        jobstep = self.create_jobstep(jobphase)

        new_jobphase = self.create_jobphase(job, label='bar')

        future_jobstep = FutureJobStep(
            label='test',
            commands=[
                FutureCommand('echo 1'),
                FutureCommand('echo "foo"\necho "bar"'),
            ],
        )

        buildstep = self.get_buildstep()
        new_jobstep = buildstep.expand_jobstep(
            jobstep, new_jobphase, future_jobstep)

        db.session.flush()

        assert new_jobstep.data['generated'] is True

        commands = list(Command.query.filter(
            Command.jobstep_id == new_jobstep.id,
        ).order_by(
            Command.order.asc(),
        ))

        assert len(commands) == 3
        assert commands[0].script == 'echo "hello world 2"'
        assert commands[0].cwd == '/usr/test/1'
        assert commands[0].type == CommandType.setup
        assert commands[0].order == 0
        assert commands[1].label == 'echo 1'
        assert commands[1].script == 'echo 1'
        assert commands[1].order == 1
        assert commands[1].cwd == DEFAULT_PATH
        assert commands[2].label == 'echo "foo"'
        assert commands[2].script == 'echo "foo"\necho "bar"'
        assert commands[2].order == 2
        assert commands[2].cwd == DEFAULT_PATH
예제 #7
0
    def iter_all_commands(self, job):
        source = job.source
        repo = source.repository
        vcs = repo.get_vcs()
        if vcs is not None:
            yield FutureCommand(
                script=vcs.get_buildstep_clone(source, self.path, self.clean),
                env=self.env,
                type=CommandType.infra_setup,
            )

            if source.patch:
                yield FutureCommand(
                    script=vcs.get_buildstep_patch(source, self.path),
                    env=self.env,
                    type=CommandType.infra_setup,
                )

        for command in self.commands:
            yield FutureCommand(**command)
예제 #8
0
    def expand(self, max_executors, test_stats_from=None):
        test_stats, avg_test_time = self.get_test_stats(test_stats_from
                                                        or self.project.slug)

        groups = self.shard_tests(self.data['tests'], max_executors,
                                  test_stats, avg_test_time)

        for weight, test_list in groups:
            future_command = FutureCommand(
                script=self.data['cmd'].format(test_names=' '.join(test_list)),
                path=self.data.get('path'),
                env=self.data.get('env'),
                artifacts=self.data.get('artifacts'),
            )
            future_jobstep = FutureJobStep(
                label=self.data.get('label') or future_command.label,
                commands=[future_command],
                data={
                    'weight': weight,
                    'tests': test_list,
                    'shard_count': len(groups)
                },
            )
            yield future_jobstep
예제 #9
0
    def test_create_replacement_jobstep_expanded(self, get_vcs):
        build = self.create_build(self.create_project())
        job = self.create_job(build)
        jobphase = self.create_jobphase(job, label='foo')
        jobstep = self.create_jobstep(jobphase)

        new_jobphase = self.create_jobphase(job, label='bar')

        vcs = mock.Mock(spec=Vcs)
        vcs.get_buildstep_clone.return_value = 'git clone https://example.com'
        get_vcs.return_value = vcs

        future_jobstep = FutureJobStep(
            label='test',
            commands=[
                FutureCommand('echo 1'),
                FutureCommand('echo "foo"\necho "bar"'),
            ],
            data={'weight': 1, 'forceInfraFailure': True},
        )

        buildstep = self.get_buildstep()
        fail_jobstep = buildstep.create_expanded_jobstep(
            jobstep, new_jobphase, future_jobstep)

        fail_jobstep.result = Result.infra_failed
        fail_jobstep.status = Status.finished
        db.session.add(fail_jobstep)
        db.session.commit()

        new_jobstep = buildstep.create_replacement_jobstep(fail_jobstep)
        # new jobstep should still be part of same job/phase
        assert new_jobstep.job == job
        assert new_jobstep.phase == fail_jobstep.phase
        # make sure .steps actually includes the new jobstep
        assert len(fail_jobstep.phase.steps) == 2
        # make sure replacement id is correctly set
        assert fail_jobstep.replacement_id == new_jobstep.id

        # we want the replacement jobstep to have the same attributes the
        # original jobstep would be expected to after expand_jobstep()
        assert new_jobstep.data['expanded'] is True
        assert new_jobstep.data['weight'] == 1
        # make sure non-whitelisted attributes aren't copied over
        assert 'forceInfraFailure' not in new_jobstep.data

        commands = new_jobstep.commands

        assert len(commands) == 4
        assert commands[0].script == 'git clone https://example.com'
        assert commands[0].cwd == ''
        assert commands[0].type == CommandType.infra_setup
        assert commands[0].order == 0
        assert commands[1].script == 'echo "hello world 2"'
        assert commands[1].cwd == '/usr/test/1'
        assert commands[1].type == CommandType.setup
        assert commands[1].order == 1
        assert commands[2].label == 'echo 1'
        assert commands[2].script == 'echo 1'
        assert commands[2].order == 2
        assert commands[2].cwd == DEFAULT_PATH
        assert commands[3].label == 'echo "foo"'
        assert commands[3].script == 'echo "foo"\necho "bar"'
        assert commands[3].order == 3
        assert commands[3].cwd == DEFAULT_PATH
예제 #10
0
    def test_simple_expander(self, mock_get_expander,
                             mock_get_build_step_for_job):
        project = self.create_project()
        build = self.create_build(project)
        job = self.create_job(build)
        jobphase = self.create_jobphase(job)
        jobstep = self.create_jobstep(jobphase, data={
            'max_executors': 10,
        })
        plan = self.create_plan(project, label='test')
        self.create_step(plan)
        jobplan = self.create_job_plan(job, plan)
        command = self.create_command(jobstep,
                                      type=CommandType.collect_tests,
                                      status=Status.in_progress)

        def dummy_expand_jobstep(jobstep, new_jobphase, future_jobstep):
            return future_jobstep.as_jobstep(new_jobphase)

        dummy_expander = Mock(spec=Expander)
        dummy_expander.expand.return_value = [
            FutureJobStep(
                label='test',
                commands=[
                    FutureCommand(script='echo 1', ),
                    FutureCommand(script='echo "foo"\necho "bar"', )
                ],
            )
        ]
        mock_get_expander.return_value.return_value = dummy_expander
        mock_buildstep = Mock(spec=BuildStep)
        mock_buildstep.expand_jobstep.side_effect = dummy_expand_jobstep

        mock_get_build_step_for_job.return_value = jobplan, mock_buildstep

        path = '/api/0/commands/{0}/'.format(command.id.hex)

        # missing output
        resp = self.client.post(path, data={
            'status': 'finished',
        })
        assert resp.status_code == 400, resp.data

        mock_get_expander.reset_mock()

        # valid params
        resp = self.client.post(path,
                                data={
                                    'status': 'finished',
                                    'output': '{"foo": "bar"}',
                                })
        assert resp.status_code == 200, resp.data

        mock_get_expander.assert_called_once_with(command.type)
        mock_get_expander.return_value.assert_called_once_with(
            project=project,
            data={'foo': 'bar'},
        )
        dummy_expander.validate.assert_called_once_with()
        dummy_expander.expand.assert_called_once_with(max_executors=10)

        new_jobstep = JobStep.query.filter(
            JobStep.job_id == job.id,
            JobStep.id != jobstep.id,
        ).first()
        assert new_jobstep.label == 'test'
예제 #11
0
    def test_create_expanded_jobstep(self, get_vcs):
        build = self.create_build(self.create_project())
        job = self.create_job(build)
        jobphase = self.create_jobphase(job, label='foo')
        jobstep = self.create_jobstep(jobphase)

        new_jobphase = self.create_jobphase(job, label='bar')

        vcs = mock.Mock(spec=Vcs)
        vcs.get_buildstep_clone.return_value = 'git clone https://example.com'
        get_vcs.return_value = vcs

        future_jobstep = FutureJobStep(
            label='test',
            commands=[
                FutureCommand('echo 1'),
                FutureCommand('echo "foo"\necho "bar"', path='subdir'),
            ],
        )

        buildstep = self.get_buildstep(cluster='foo')
        new_jobstep = buildstep.create_expanded_jobstep(
            jobstep, new_jobphase, future_jobstep)

        db.session.flush()

        assert new_jobstep.data['expanded'] is True
        assert new_jobstep.cluster == 'foo'

        commands = new_jobstep.commands

        assert len(commands) == 4
        assert commands[0].script == 'git clone https://example.com'
        assert commands[0].cwd == ''
        assert commands[0].type == CommandType.infra_setup
        assert commands[0].artifacts == []
        assert commands[0].env == DEFAULT_ENV
        assert commands[0].order == 0
        assert commands[1].script == 'echo "hello world 2"'
        assert commands[1].cwd == '/usr/test/1'
        assert commands[1].type == CommandType.setup
        assert tuple(commands[1].artifacts) == ('artifact1.txt',
                                                'artifact2.txt')
        assert commands[1].env['PATH'] == '/usr/test/1'
        for k, v in DEFAULT_ENV.items():
            if k != 'PATH':
                assert commands[1].env[k] == v
        assert commands[1].order == 1
        assert commands[2].label == 'echo 1'
        assert commands[2].script == 'echo 1'
        assert commands[2].order == 2
        assert commands[2].cwd == DEFAULT_PATH
        assert commands[2].type == CommandType.default
        assert tuple(commands[2].artifacts) == tuple(DEFAULT_ARTIFACTS)
        assert commands[2].env == DEFAULT_ENV
        assert commands[3].label == 'echo "foo"'
        assert commands[3].script == 'echo "foo"\necho "bar"'
        assert commands[3].order == 3
        assert commands[3].cwd == './source/subdir'
        assert commands[3].type == CommandType.default
        assert tuple(commands[3].artifacts) == tuple(DEFAULT_ARTIFACTS)
        assert commands[3].env == DEFAULT_ENV
예제 #12
0
    def __init__(self,
                 commands=None,
                 path=DEFAULT_PATH,
                 env=None,
                 artifacts=DEFAULT_ARTIFACTS,
                 release=DEFAULT_RELEASE,
                 max_executors=10,
                 cpus=4,
                 memory=8 * 1024,
                 clean=True,
                 debug_config=None,
                 test_stats_from=None,
                 cluster=None,
                 **kwargs):
        """
        Constructor for DefaultBuildStep.

        Args:
            cpus: How many cpus to limit the container to (not applicable for basic)
            memory: How much memory to limit the container to (not applicable for basic)
            clean: controls if the repository should be cleaned before
                tests are run.
                Defaults to true, because False may be unsafe; it may be
                useful to set to False if snapshots are in use and they
                intentionally leave useful incremental build products in the
                repository.
            debug_config: A dictionary of debug config options. These are passed through
                to changes-client. There is also an infra_failures option, which takes a
                dictionary used to force infrastructure failures in builds. The keys of
                this dictionary refer to the phase (for DefaultBuildSteps, only possible
                value is 'primary'), and the values are the probabilities with which
                a JobStep in that phase will fail.
                An example: "debug_config": {"infra_failures": {"primary": 0.5}}
                This will then cause an infra failure in the primary JobStep with
                probability 0.5.
            test_stats_from: project to get test statistics from, or
                None (the default) to use this project.  Useful if the
                project runs a different subset of tests each time, so
                test timing stats from the parent are not reliable.
            cluster: a cluster to associate jobs of this BuildStep with.
                Jobsteps will then only be run on slaves of the given cluster.
        """
        if commands is None:
            raise ValueError("Missing required config: need commands")

        if env is None:
            env = DEFAULT_ENV.copy()

        self.artifacts = artifacts
        self.env = env
        self.path = path
        self.release = release
        self.max_executors = max_executors
        self.resources = {
            'cpus': cpus,
            'mem': memory,
        }
        self.clean = clean
        self.debug_config = debug_config or {}
        self.test_stats_from = test_stats_from
        self.cluster = cluster
        future_commands = []
        for command in commands:
            command_copy = command.copy()
            if 'type' in command_copy:
                command_copy['type'] = CommandType[command_copy['type']]
            future_command = FutureCommand(**command_copy)
            self._set_command_defaults(future_command)
            future_commands.append(future_command)
        self.commands = future_commands

        super(DefaultBuildStep, self).__init__(**kwargs)