def get(self, build: Build): """ Return a build. """ with nplusone.ignore("eager_load"): build.source = Source.query.options(joinedload("revision"), joinedload("patch")).get( build.source_id) build.stats = list(ItemStat.query.filter(ItemStat.item_id == build.id)) return self.respond_with_schema(build_schema, build)
def get(self, build: Build): """ Return a build. """ with nplusone.ignore("eager_load"): build.revision = Revision.query.filter( Revision.sha == build.revision_sha, Revision.repository_id == build.repository_id, ).first() build.stats = list(ItemStat.query.filter(ItemStat.item_id == build.id)) return self.respond_with_schema(build_schema, build)
def post(self, repo: Repository): """ Create a new build. """ schema = BuildCreateSchema(strict=True, context={"repository": repo}) result = self.schema_from_request(schema, partial=True) if result.errors: return self.respond(result.errors, 403) data = result.data # TODO(dcramer): only if we create a source via a patch will we need the author # author_data = data.pop('author') # if author_data.get('email'): # author = Author.query.filter( # Author.repository_id == repo.id, Author.email == author_data['email'] # ).first() # else: # author = None # if not author: # author = Author(repository_id=repo.id, **author_data) # db.session.add(author) # db.session.flush() # TODO(dcramer): need to handle patch case yet source = (Source.query.options( joinedload("author"), joinedload("revision")).filter( Source.revision_sha == data.pop("ref"), Source.repository_id == repo.id).first()) build = Build(repository=repo, **data) # TODO(dcramer): we should convert source in the schema build.source = source # build.source_id = source.id build.author = source.author if not source.patch_id: if not build.label: build.label = source.revision.message.split("\n")[0] if not build.label: return self.error("missing build label") db.session.add(build) try: db.session.commit() except IntegrityError: db.session.rollback() return self.respond(status=422) result = build_schema.dump(build) assert not result.errors, "this should never happen" publish("builds", "build.create", result.data) return self.respond(result.data, 200)
def post(self, repo: Repository): """ Create a new build. """ result = self.schema_from_request(build_create_schema, partial=True) if result.errors: return self.respond(result.errors, 403) data = result.data ref = data.pop('ref', None) if ref is None: return self.error('missing ref') try: revision = identify_revision(repo, ref) except UnknownRevision: current_app.logger.warn('invalid ref received', exc_info=True) return self.error('unable to find a revision matching ref') # TODO(dcramer): only if we create a source via a patch will we need the author # author_data = data.pop('author') # if author_data.get('email'): # author = Author.query.filter( # Author.repository_id == repo.id, Author.email == author_data['email'] # ).first() # else: # author = None # if not author: # author = Author(repository_id=repo.id, **author_data) # db.session.add(author) # db.session.flush() # TODO(dcramer): need to handle patch case yet source = Source.query.options(joinedload('author'), ).filter( Source.revision_sha == revision.sha, Source.repository_id == repo.id, ).first() build = Build(repository=repo, **data) # TODO(dcramer): we should convert source in the schema build.source = source # build.source_id = source.id build.author = source.author if not source.patch_id: if not build.label: build.label = source.revision.message.split('\n')[0] db.session.add(build) db.session.commit() return self.respond_with_schema(build_schema, build)
def merge_builds(target: Build, build: Build) -> Build: # explicitly unset the default id as target should begin as an empty instance target.id = None # Store the original build so we can retrieve its ID or number later, or # show a list of all builds in the UI target.original.append(build) # These properties should theoretically always be the same within a build # group, so merging is not necessary. We assign here so the initial build # gets populated. target.source = build.source target.label = build.source.revision.message # Merge properties, if they already exist. In the first run, everything # will be empty, since every group is initialized with an empty build. # Afterwards, we always use the more extreme value (e.g. earlier start # date or worse result). target.stats = target.stats + build.stats if target.stats else build.stats target.status = ( Status(max(target.status.value, build.status.value)) if target.status else build.status ) target.result = ( Result(max(target.result.value, build.result.value)) if target.result else build.result ) target.date_started = ( min(target.date_started, build.date_started) if target.date_started and build.date_started else target.date_started or build.date_started ) target.date_finished = ( max(target.date_finished, build.date_finished) if target.date_finished and build.date_finished else target.date_finished or build.date_finished ) target.provider = ( "%s, %s" % (target.provider, build.provider) if target.provider else build.provider ) # NOTE: The build number is not merged, as it would not convey any meaning # in the context of a build group. In that fashion, build numbers should # not be used in the UI, until we create a better interface. # NOTE: We do not merge data here, as it is not really used in the UI # If there is an actual use for data, then it should be merged or appended return target
def get(self, build: Build): """ Return a build. """ build.source = Source.query.options(joinedload('revision'), joinedload('patch')).get( build.source_id) return self.respond_with_schema(build_schema, build)
def merge_build_group(build_group): if len(build_group) == 1: build = build_group[0] build.original = [build] return build providers = groupby(build_group, lambda build: build.provider) latest_builds = [ max(build, key=lambda build: build.number) for _, build in providers ] if len(latest_builds) == 1: build = latest_builds[0] build.original = [build] return build build = Build() build.original = [] return reduce(merge_builds, latest_builds, build)
def post(self, repo: Repository): """ Create a new build. """ result = self.schema_from_request(build_create_schema, partial=True) if result.errors: return self.respond(result.errors, 403) data = result.data revision_sha = data.pop('revision_sha') source = Source.query.filter( Source.revision_sha == revision_sha, Source.repository_id == repo.id, ).first() if not source: return self.error('invalid source') # TODO(dcramer): only if we create a source via a patch will we need the author # author_data = data.pop('author') # if author_data.get('email'): # author = Author.query.filter( # Author.repository_id == repo.id, Author.email == author_data['email'] # ).first() # else: # author = None # if not author: # author = Author(repository_id=repo.id, **author_data) # db.session.add(author) # db.session.flush() build = Build(repository=repo, **data) # TODO(dcramer): we should convert source in the schema build.source = source build.source_id = source.id if not source.patch_id: if not build.label: build.label = source.revision.message.split('\n')[0] assert build.source_id db.session.add(build) db.session.commit() return self.respond_with_schema(build_schema, build)
def merge_build_group( build_group: Tuple[Any, List[Build]], required_hook_ids: List[str] = None ) -> Build: # XXX(dcramer): required_hook_ids is still dirty here, but its our simplest way # to get it into place grouped_builds = groupby( build_group, lambda build: (str(build.hook_id), build.provider) ) latest_builds = [ max(build, key=lambda build: build.number) for _, build in grouped_builds ] build = Build() build.original = [] if set(required_hook_ids or ()).difference( set(str(b.hook_id) for b in build_group) ): build.result = Result.failed return reduce(merge_builds, latest_builds, build)
def setUp(self): zeus = Project.objects.create( name='zeus', url='https://github.com/lukaszb/zeus', repo_url='git://github.com/lukaszb/zeus.git', ) self.buildset = Buildset.objects.create( project=zeus, number=1, ) self.build1 = Build.objects.create( buildset=self.buildset, number=1, ) self.build1_cmd1 = Command.objects.create( number=1, build=self.build1, title='Step 1 -- Configuration', cmd=['./configure'], ) output = Output.objects.create(output='Configured') self.build1_cmd1.command_output = output delta = datetime.timedelta(seconds=2) self.build1_cmd1.started_at = self.build1_cmd1.created_at + delta self.build1_cmd1.finished_at = self.build1_cmd1.started_at + delta self.build1_cmd1.returncode = 0 self.build1_cmd1.status = Status.PASSED self.build1_cmd1.save() self.build1_cmd2 = Command.objects.create( number=2, build=self.build1, title='Step 2 -- Build', cmd=['make', 'all'], ) output = Output.objects.create(output='Build in progress ...') self.build1_cmd2.command_output = output self.build1_cmd2.started_at = self.build1_cmd2.created_at self.build1_cmd2.status = Status.RUNNING self.build1_cmd2.save() dt = datetime.datetime(2013, 7, 2, 22, 8) self.build2 = Build( buildset=self.buildset, number=2, created_at=dt, finished_at=(dt + datetime.timedelta(seconds=3)), ) self.build2.save() cache.clear()
def build_instance(self, data, **kwargs): revision = self.context.get("resolved_revision") build = Build( repository=self.context.get("repository"), revision_sha=revision.sha if revision else None, **data ) if build.data is None: build.data = {} build.data["required_hook_ids"] = Hook.get_required_hook_ids( build.repository.id ) if revision: if not build.label: build.label = revision.message.split("\n")[0] if not build.authors and revision.authors: build.authors = revision.authors return build
class TestBuildApi(BaseApiTestCase): maxDiff = None def setUp(self): zeus = Project.objects.create( name='zeus', url='https://github.com/lukaszb/zeus', repo_url='git://github.com/lukaszb/zeus.git', ) self.buildset = Buildset.objects.create( project=zeus, number=1, ) self.build1 = Build.objects.create( buildset=self.buildset, number=1, ) self.build1_cmd1 = Command.objects.create( number=1, build=self.build1, title='Step 1 -- Configuration', cmd=['./configure'], ) output = Output.objects.create(output='Configured') self.build1_cmd1.command_output = output delta = datetime.timedelta(seconds=2) self.build1_cmd1.started_at = self.build1_cmd1.created_at + delta self.build1_cmd1.finished_at = self.build1_cmd1.started_at + delta self.build1_cmd1.returncode = 0 self.build1_cmd1.status = Status.PASSED self.build1_cmd1.save() self.build1_cmd2 = Command.objects.create( number=2, build=self.build1, title='Step 2 -- Build', cmd=['make', 'all'], ) output = Output.objects.create(output='Build in progress ...') self.build1_cmd2.command_output = output self.build1_cmd2.started_at = self.build1_cmd2.created_at self.build1_cmd2.status = Status.RUNNING self.build1_cmd2.save() dt = datetime.datetime(2013, 7, 2, 22, 8) self.build2 = Build( buildset=self.buildset, number=2, created_at=dt, finished_at=(dt + datetime.timedelta(seconds=3)), ) self.build2.save() cache.clear() def test_build_detail(self): url_params = {'name': 'zeus', 'buildset_no': 1, 'build_no': 1} url = reverse('zeus_api_build_detail', kwargs=url_params) response = self.client.get(url) expected = { 'uri': self.make_api_build_detail_url('zeus', 1, 1), 'url': self.build1.get_absolute_url(), 'number': 1, 'created_at': self.build1.created_at, 'finished_at': self.build1.finished_at, 'status': 'running', 'commands': [ { 'number': 1, 'title': 'Step 1 -- Configuration', 'cmd': './configure', 'output': 'Configured', 'started_at': self.build1_cmd1.started_at, 'finished_at': self.build1_cmd1.finished_at, 'status': 'passed', 'returncode': 0, }, { 'number': 2, 'title': 'Step 2 -- Build', 'cmd': 'make all', 'output': 'Build in progress ...', 'started_at': self.build1_cmd2.started_at, 'finished_at': None, 'status': 'running', 'returncode': None, }, ], } self.assertDictEqual(response.data, expected) url_params = {'name': 'zeus', 'buildset_no': 1, 'build_no': 2} url = reverse('zeus_api_build_detail', kwargs=url_params) response = self.client.get(url) expected = { 'uri': self.make_api_build_detail_url('zeus', 1, 2), 'url': self.build2.get_absolute_url(), 'number': 2, 'created_at': self.build2.created_at, 'finished_at': self.build2.finished_at, 'status': 'pending', 'commands': [], } self.assertDictEqual(response.data, expected) def test_build_restart_fails_if_build_is_still_running(self): url_params = {'name': 'zeus', 'buildset_no': 1, 'build_no': 1} url = reverse('zeus_api_build_detail', kwargs=url_params) response = self.client.put(url) self.assertEqual(response.status_code, 409) def test_build_restart(self): url_params = {'name': 'zeus', 'buildset_no': 1, 'build_no': 1} url = reverse('zeus_api_build_detail', kwargs=url_params) self.build1.commands.update(status=Status.FAILED) response = self.client.put(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['status'], Status.PENDING) self.assertIsNone(response.data['finished_at']) self.assertEqual(self.build1.commands.count(), 0) def test_build_status_is_changing_correctly(self): self.assertEqual(self.build1.status, Status.RUNNING) self.build1_cmd1.status = Status.FAILED self.build1_cmd1.save() self.assertEqual(self.build1.status, Status.FAILED) self.build1.commands.update(status=Status.PASSED) self.assertEqual(self.build1.status, Status.PASSED) self.build1.commands.all().delete() self.assertEqual(self.build1.status, Status.PENDING) # Create a command and make sure status us RUNNING once again Command.objects.create( number=1, build=self.build1, title='Step 1 -- Configuration', cmd=['./configure'], status=Status.RUNNING, ) self.assertEqual(self.build1.status, Status.RUNNING)