Add indexes to ModuleBuild and ComponentBuild

Tests are updated accordingly as well.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>
This commit is contained in:
Chenxiong Qi
2020-01-20 12:09:17 +08:00
committed by mprahl
parent 83b9e56f46
commit 81b36b1d6e
21 changed files with 226 additions and 51 deletions

View File

@@ -16,6 +16,8 @@ import sqlalchemy
from sqlalchemy import func, and_
from sqlalchemy.orm import lazyload
from sqlalchemy.orm import validates, load_only
from sqlalchemy.schema import Index
from module_build_service import db, log, get_url_for, conf
from module_build_service.common.errors import UnprocessableEntity
@@ -121,17 +123,17 @@ module_builds_to_arches = db.Table(
class ModuleBuild(MBSBase):
__tablename__ = "module_builds"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
name = db.Column(db.String, nullable=False, index=True)
stream = db.Column(db.String, nullable=False)
version = db.Column(db.String, nullable=False)
build_context = db.Column(db.String)
build_context_no_bms = db.Column(db.String)
runtime_context = db.Column(db.String)
context = db.Column(db.String, nullable=False, server_default=DEFAULT_MODULE_CONTEXT)
state = db.Column(db.Integer, nullable=False)
state = db.Column(db.Integer, nullable=False, index=True)
state_reason = db.Column(db.String)
modulemd = db.Column(db.String, nullable=False)
koji_tag = db.Column(db.String) # This gets set after 'wait'
koji_tag = db.Column(db.String, index=True) # This gets set after 'wait'
# Koji tag to which tag the Content Generator Koji build.
cg_build_koji_tag = db.Column(db.String) # This gets set after wait
scmurl = db.Column(db.String)
@@ -172,6 +174,13 @@ class ModuleBuild(MBSBase):
backref="buildrequire_for",
)
__table_args__ = (
Index(
"idx_module_builds_name_stream_version_context",
"name", "stream", "version", "context", unique=True
),
)
rebuild_strategies = {
"all": "All components will be rebuilt",
"changed-and-after": (
@@ -1079,7 +1088,7 @@ class ComponentBuild(MBSBase):
# iteration this *component* is currently in. This relates to the owning
# module's batch. This one defaults to None, which means that this
# component is not currently part of a batch.
batch = db.Column(db.Integer, default=0)
batch = db.Column(db.Integer, default=0, index=True)
module_id = db.Column(db.Integer, db.ForeignKey("module_builds.id"), nullable=False)
module_build = db.relationship("ModuleBuild", backref="component_builds", lazy=False)
@@ -1090,6 +1099,11 @@ class ComponentBuild(MBSBase):
# get_build_weights function
weight = db.Column(db.Float, default=0)
__table_args__ = (
Index("idx_component_builds_build_id_task_id", "module_id", "task_id", unique=True),
Index("idx_component_builds_build_id_nvr", "module_id", "nvr", unique=True),
)
@classmethod
def from_component_event(cls, db_session, task_id, module_id=None):
_filter = db_session.query(cls).filter

View File

@@ -0,0 +1,60 @@
"""Add indexes to models
Revision ID: 440a8a3c0d96
Revises: 4d1e2e13e514
Create Date: 2020-02-04 17:15:37.150756
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "440a8a3c0d96"
down_revision = "4d1e2e13e514"
def upgrade():
op.create_index(
"idx_component_builds_build_id_nvr",
"component_builds",
["module_id", "nvr"],
unique=True,
)
op.create_index(
"idx_component_builds_build_id_task_id",
"component_builds",
["module_id", "task_id"],
unique=True,
)
op.create_index(
op.f("ix_component_builds_batch"), "component_builds", ["batch"], unique=False
)
op.create_index(
"idx_module_builds_name_stream_version_context",
"module_builds",
["name", "stream", "version", "context"],
unique=True,
)
op.create_index(
op.f("ix_module_builds_name"), "module_builds", ["name"], unique=False
)
op.create_index(
op.f("ix_module_builds_state"), "module_builds", ["state"], unique=False
)
op.create_index(
op.f("ix_module_builds_koji_tag"), "module_builds", ["koji_tag"], unique=False
)
def downgrade():
op.drop_index(op.f("ix_module_builds_koji_tag"), table_name="module_builds")
op.drop_index(op.f("ix_module_builds_state"), table_name="module_builds")
op.drop_index(op.f("ix_module_builds_name"), table_name="module_builds")
op.drop_index(
"idx_module_builds_name_stream_version_context", table_name="module_builds"
)
op.drop_index(op.f("ix_component_builds_batch"), table_name="component_builds")
op.drop_index(
"idx_component_builds_build_id_task_id", table_name="component_builds"
)
op.drop_index("idx_component_builds_build_id_nvr", table_name="component_builds")

View File

@@ -4,8 +4,10 @@ from __future__ import absolute_import
from datetime import datetime, timedelta
import functools
import hashlib
import itertools
import os
import re
import six
import time
from traceback import extract_stack
@@ -165,6 +167,7 @@ def _populate_data(data_size=10, contexts=False, scratch=False):
# like "Object '<ModuleBuild at 0x7f4ccc805c50>' is already attached to
# session '275' (this is '276')" when add new module build object to passed
# session.
task_id_counter = itertools.count(1)
arch = db_session.query(module_build_service.common.models.ModuleArch).get(1)
num_contexts = 2 if contexts else 1
for index in range(data_size):
@@ -214,7 +217,7 @@ def _populate_data(data_size=10, contexts=False, scratch=False):
scmurl="git://pkgs.domain.local/rpms/nginx?"
"#ga95886c8a443b36a9ce31abda1f9bed22f2f8c3",
format="rpms",
task_id=12312345 + index,
task_id=six.next(task_id_counter),
state=koji.BUILD_STATES["COMPLETE"],
nvr="nginx-1.10.1-2.{0}".format(build_one_component_release),
batch=1,
@@ -226,7 +229,7 @@ def _populate_data(data_size=10, contexts=False, scratch=False):
scmurl="/tmp/module_build_service-build-macrosWZUPeK/SRPMS/"
"module-build-macros-0.1-1.module_nginx_1_2.src.rpm",
format="rpms",
task_id=12312321 + index,
task_id=six.next(task_id_counter),
state=koji.BUILD_STATES["COMPLETE"],
nvr="module-build-macros-01-1.{0}".format(build_one_component_release),
batch=2,

View File

@@ -8,7 +8,7 @@ data:
dependency of mksh., ref: master, repository: 'https://src.fedoraproject.org/rpms/ed'}
dependencies:
buildrequires: {testmodule: master}
requires: {platform: f28}
requires: {platform: f30}
description: This module demonstrates how to write simple modulemd files And can
be used for testing the build and release pipeline.
license:
@@ -28,7 +28,7 @@ data:
testmodule: {ref: 147dca4ca65aa9a1ac51f71b7e687f9178ffa5df, stream: master,
version: '20170616125652', context: '321'}
requires:
platform: {ref: virtual, stream: f28, version: '3', context: '00000000'}
platform: {ref: virtual, stream: f30, version: '3', context: '00000000'}
commit: 722fd739fd6cf66faf29f6fb95dd64f60ba3e39a
rpms:
ed: {ref: 01bf8330812fea798671925cc537f2f29b0bd216}

View File

@@ -18,7 +18,7 @@ data:
dependencies:
buildrequires: {}
requires: {}
description: Fedora 28 traditional base
description: Fedora 30 traditional base
license:
module: [MIT]
name: platform
@@ -35,6 +35,7 @@ data:
stream: f28
summary: A test module in all its beautiful beauty
version: 3
context: 00000000
xmd:
mbs:
buildrequires:

View File

@@ -0,0 +1,26 @@
---
document: modulemd
version: 1
data:
description: Fedora 30 traditional base
name: platform
license:
module: [MIT]
profiles:
default:
rpms: [mksh]
buildroot:
rpms: foo
srpm-buildroot:
rpms: bar
stream: f30
summary: A test module in all its beautiful beauty
version: 3
context: 00000000
xmd:
mbs:
buildrequires: {}
commit: virtual
requires: {}
mse: true
koji_tag: module-f30-build

View File

@@ -7,8 +7,8 @@ data:
ed: {cache: 'http://pkgs.fedoraproject.org/repo/pkgs/ed', rationale: A build
dependency of mksh., ref: master, repository: 'https://src.fedoraproject.org/rpms/ed'}
dependencies:
buildrequires: {platform: f28}
requires: {platform: f28}
buildrequires: {platform: f30}
requires: {platform: f30}
description: This module demonstrates how to write simple modulemd files And can
be used for testing the build and release pipeline.
license:
@@ -25,7 +25,7 @@ data:
xmd:
mbs:
buildrequires:
platform: {ref: virtual, stream: f28, version: '3', context: '00000000'}
platform: {ref: virtual, stream: f30, version: '3', context: '00000000'}
commit: 722fd739fd6cf66faf29f6fb95dd64f60ba3e39a
rpms:
ed: {ref: 01bf8330812fea798671925cc537f2f29b0bd216}

View File

@@ -7,8 +7,8 @@ data:
ed: {cache: 'http://pkgs.fedoraproject.org/repo/pkgs/ed', rationale: A build
dependency of mksh., ref: master, repository: 'https://src.fedoraproject.org/rpms/ed'}
dependencies:
buildrequires: {platform: f28}
requires: {platform: f28}
buildrequires: {platform: f30}
requires: {platform: f30}
description: This module demonstrates how to write simple modulemd files And can
be used for testing the build and release pipeline.
license:
@@ -25,7 +25,7 @@ data:
xmd:
mbs:
buildrequires:
platform: {ref: virtual, stream: f28, version: '3', context: '00000000'}
platform: {ref: virtual, stream: f30, version: '3', context: '00000000'}
commit: 722fd739fd6cf66faf29f6fb95dd64f60ba3e39a
rpms:
ed: {ref: 01bf8330812fea798671925cc537f2f29b0bd216}

View File

@@ -14,8 +14,8 @@ data:
mksh: {cache: 'http://pkgs.fedoraproject.org/repo/pkgs/mksh', rationale: A build
dependency of mksh., ref: master, repository: 'https://src.fedoraproject.org/rpms/mksh'}
dependencies:
buildrequires: {platform: f28, testmodule: master}
requires: {platform: f28}
buildrequires: {platform: f30, testmodule: master}
requires: {platform: f30}
description: This module demonstrates how to write simple modulemd files And can
be used for testing the build and release pipeline.
license:
@@ -31,7 +31,7 @@ data:
xmd:
mbs:
buildrequires:
platform: {ref: virtual, stream: f28, version: '3'}
platform: {ref: virtual, stream: f30, version: '3'}
testmodule: {stream: master, version: '20170816080815'}
commit: null
rpms:

View File

@@ -0,0 +1,37 @@
document: modulemd
version: 1
data:
summary: A test module in all its beautiful beauty
description: >-
This module demonstrates how to write simple modulemd files And
can be used for testing the build and release pipeline.
license:
module: [ MIT ]
dependencies:
buildrequires:
platform: f30
requires:
platform: f30
references:
community: https://docs.pagure.org/modularity/
documentation: https://fedoraproject.org/wiki/Fedora_Packaging_Guidelines_for_Modules
profiles:
default:
rpms:
- tangerine
api:
rpms:
- perl-Tangerine
- tangerine
components:
rpms:
perl-List-Compare:
rationale: A dependency of tangerine.
ref: master
perl-Tangerine:
rationale: Provides API for this module and is a dependency of tangerine.
ref: master
tangerine:
rationale: Provides API for this module.
buildorder: 10
ref: master

View File

@@ -1909,11 +1909,11 @@ class TestLocalBuild(BaseTestBuild):
"""
Tests local module build dependency.
"""
load_local_builds(["platform"])
load_local_builds(["platform:f30"])
FakeSCM(
mocked_scm,
"testmodule",
"testmodule.yaml",
"testmodule-buildrequires-f30.yaml",
"620ec77321b2ea7b0d67d82992dda3e1d67055b4",
)

View File

@@ -140,6 +140,7 @@ class TestKojiBuilder:
dest_tagged = [{"nvr": "foo-1.0-1.module+e0095747", "task_id": 12345, "build_id": 91}]
builder.koji_session.listTagged.side_effect = [build_tagged, dest_tagged]
module_build = module_build_service.common.models.ModuleBuild.get_by_id(db_session, 4)
module_build.component_builds.sort(key=lambda item: item.id)
component_build = module_build.component_builds[0]
component_build.task_id = None
component_build.state = None
@@ -195,6 +196,7 @@ class TestKojiBuilder:
build_info = {"nvr": "foo-1.0-1.{0}".format(dist_tag), "task_id": 12345, "build_id": 91}
builder.koji_session.getBuild.return_value = build_info
module_build = module_build_service.common.models.ModuleBuild.get_by_id(db_session, 4)
module_build.component_builds.sort(key=lambda item: item.id)
component_build = module_build.component_builds[0]
component_build.task_id = None
component_build.nvr = None
@@ -245,6 +247,7 @@ class TestKojiBuilder:
"build_id": 91}
builder.koji_session.getBuild.return_value = build_info
module_build = module_build_service.common.models.ModuleBuild.get_by_id(db_session, 4)
module_build.component_builds.sort(key=lambda item: item.id)
component_build = module_build.component_builds[1]
component_build.task_id = None
component_build.nvr = None

View File

@@ -367,15 +367,15 @@ class TestLocalBuilds:
models.ModuleBuild.local_modules(db_session)
def test_load_local_builds_platform(self, conf_system, conf_resultsdir):
load_local_builds("platform")
load_local_builds("platform:f30")
local_modules = models.ModuleBuild.local_modules(db_session)
assert len(local_modules) == 1
assert local_modules[0].koji_tag.endswith("/module-platform-f28-3/results")
assert local_modules[0].koji_tag.endswith("/module-platform-f30-3/results")
def test_load_local_builds_platform_f28(self, conf_system, conf_resultsdir):
load_local_builds("platform:f28")
load_local_builds("platform:f30")
local_modules = models.ModuleBuild.local_modules(db_session)
assert len(local_modules) == 1
assert local_modules[0].koji_tag.endswith("/module-platform-f28-3/results")
assert local_modules[0].koji_tag.endswith("/module-platform-f30-3/results")

View File

@@ -71,6 +71,7 @@ class TestModels:
mmd = load_mmd(read_staged_data("formatted_testmodule"))
for i in range(3):
build = module_build_from_modulemd(mmd_to_str(mmd))
build.context = "f6e2aec" + str(i)
build.build_context = "f6e2aeec7576196241b9afa0b6b22acf2b6873d" + str(i)
build.runtime_context = "bbc84c7b817ab3dd54916c0bcd6c6bdf512f7f9c" + str(i)
db_session.add(build)
@@ -80,7 +81,7 @@ class TestModels:
sibling_ids = build_one.siblings(db_session)
db_session.commit()
assert sibling_ids == [3, 4]
assert sorted(sibling_ids) == [3, 4]
@pytest.mark.parametrize(
"stream,right_pad,expected",
@@ -127,7 +128,7 @@ class TestModelsGetStreamsContexts:
for build in builds
]
db_session.commit()
assert builds == ["nginx:1:3:d5a6c0fa", "nginx:1:3:795e97c1"]
assert sorted(builds) == ["nginx:1:3:795e97c1", "nginx:1:3:d5a6c0fa"]
def test_get_last_builds_in_stream_version_lte(self):
init_data_contexts(1, multiple_stream_versions=True)

View File

@@ -12,7 +12,7 @@ from module_build_service import app, conf
from module_build_service.common import models
import module_build_service.common.monitor
from module_build_service.scheduler.db_session import db_session
from tests import init_data, make_module_in_db
from tests import clean_database, init_data, make_module_in_db
num_of_metrics = 18
@@ -52,6 +52,7 @@ def test_standalone_metrics_server():
@mock.patch("module_build_service.common.monitor.builder_failed_counter.labels")
@mock.patch("module_build_service.common.monitor.builder_success_counter.inc")
def test_monitor_state_changing_success(succ_cnt, failed_cnt):
clean_database(add_platform_module=False, add_default_arches=False)
b = make_module_in_db(
"pkg:0.1:1:c1",
[
@@ -72,6 +73,7 @@ def test_monitor_state_changing_success(succ_cnt, failed_cnt):
@mock.patch("module_build_service.common.monitor.builder_failed_counter.labels")
@mock.patch("module_build_service.common.monitor.builder_success_counter.inc")
def test_monitor_state_changing_failure(succ_cnt, failed_cnt):
clean_database(add_platform_module=False, add_default_arches=False)
failure_type = "user"
b = make_module_in_db(
"pkg:0.1:1:c1",

View File

@@ -51,7 +51,12 @@ class TestMBSManage:
def test_retire_build(self, prompt_bool, overrides, identifier, changed_count):
prompt_bool.return_value = True
module_builds = db_session.query(ModuleBuild).filter_by(state=BUILD_STATES["ready"]).all()
module_builds = (
db_session.query(ModuleBuild)
.filter_by(state=BUILD_STATES["ready"])
.order_by(ModuleBuild.id.desc())
.all()
)
# Verify our assumption of the amount of ModuleBuilds in database
assert len(module_builds) == 3
@@ -68,7 +73,10 @@ class TestMBSManage:
retire(identifier)
retired_module_builds = (
db_session.query(ModuleBuild).filter_by(state=BUILD_STATES["garbage"]).all()
db_session.query(ModuleBuild)
.filter_by(state=BUILD_STATES["garbage"])
.order_by(ModuleBuild.id.desc())
.all()
)
assert len(retired_module_builds) == changed_count
@@ -96,7 +104,7 @@ class TestMBSManage:
assert len(module_builds) == 3
for x, build in enumerate(module_builds):
build.name = "spam"
build.name = "spam" + str(x) if x > 0 else "spam"
build.stream = "eggs"
db_session.commit()
@@ -106,7 +114,7 @@ class TestMBSManage:
db_session.query(ModuleBuild).filter_by(state=BUILD_STATES["garbage"]).all()
)
expected_changed_count = 3 if confirm_expected else 0
expected_changed_count = 1 if confirm_expected else 0
assert len(retired_module_builds) == expected_changed_count

View File

@@ -144,7 +144,7 @@ class TestDBModule:
"""
Tests that it returns the requires of the buildrequires recursively
"""
load_local_builds(["platform", "parent", "child", "testmodule"])
load_local_builds(["platform:f30", "parent", "child", "testmodule"])
build = models.ModuleBuild.local_modules(db_session, "child", "master")
resolver = mbs_resolver.GenericResolver.create(db_session, conf, backend="db")
@@ -186,6 +186,7 @@ class TestDBModule:
mmd = load_mmd(tests.read_staged_data("formatted_testmodule"))
for i in range(3):
build = tests.module_build_from_modulemd(mmd_to_str(mmd))
build.context = "f6e2ae" + str(i)
build.build_context = "f6e2aeec7576196241b9afa0b6b22acf2b6873d" + str(i)
build.runtime_context = "bbc84c7b817ab3dd54916c0bcd6c6bdf512f7f9c" + str(i)
build.state = models.BUILD_STATES["ready"]
@@ -267,7 +268,15 @@ class TestDBModule:
"""
Test that profiles get resolved recursively on local builds
"""
load_local_builds(["platform"])
# This test requires a platform module loaded from local rather than
# the one added to database.
platform = db_session.query(models.ModuleBuild).filter(
models.ModuleBuild.name == "platform"
).one()
db_session.delete(platform)
db_session.commit()
load_local_builds(["platform:f28"])
mmd = models.ModuleBuild.get_by_id(db_session, 2).mmd()
resolver = mbs_resolver.GenericResolver.create(db_session, conf, backend="mbs")
result = resolver.resolve_profiles(mmd, ("buildroot", "srpm-buildroot"))

View File

@@ -331,8 +331,8 @@ class TestMBSModule:
def test_resolve_profiles_local_module(
self, local_builds, conf_system, formatted_testmodule_mmd
):
tests.clean_database()
load_local_builds(["platform"])
tests.clean_database(add_platform_module=False)
load_local_builds(["platform:f28"])
resolver = mbs_resolver.GenericResolver.create(db_session, conf, backend="mbs")
result = resolver.resolve_profiles(

View File

@@ -431,12 +431,15 @@ class TestPoller:
# Make sure module_build_two stayed the same
assert module_build_two.state == models.BUILD_STATES["failed"]
# Make sure the builds were untagged
builder.untag_artifacts.assert_called_once_with([
"perl-Tangerine-0.23-1.module+0+d027b723",
"perl-List-Compare-0.53-5.module+0+d027b723",
"tangerine-0.22-3.module+0+d027b723",
builder.untag_artifacts.assert_called_once()
args, _ = builder.untag_artifacts.call_args
expected = [
"module-build-macros-0.1-1.module+0+d027b723",
])
"perl-List-Compare-0.53-5.module+0+d027b723",
"perl-Tangerine-0.23-1.module+0+d027b723",
"tangerine-0.22-3.module+0+d027b723",
]
assert expected == sorted(args[0])
def test_cleanup_stale_failed_builds_no_components(self, create_builder, dbg):
""" Test that a module build without any components built gets to the garbage state when

View File

@@ -341,9 +341,11 @@ class TestUtilsModuleReuse:
xmd["mbs"]["buildrequires"]["platform"]["stream"] = "f29"
mmd.set_xmd(xmd)
latest_module.modulemd = mmd_to_str(mmd)
latest_module.build_context = models.ModuleBuild.contexts_from_mmd(
contexts = models.ModuleBuild.contexts_from_mmd(
latest_module.modulemd
).build_context
)
latest_module.build_context = contexts.build_context
latest_module.context = contexts.context
latest_module.buildrequires = [platform_f29]
# Set the `id` to None, so new one is generated by SQLAlchemy.
@@ -362,8 +364,12 @@ class TestUtilsModuleReuse:
if allow_ocbm:
assert reusable_module.id == latest_module.id
else:
# There are two testmodules in ready state, the first one with
# lower id is what we want.
first_module = db_session.query(models.ModuleBuild).filter_by(
name="testmodule", state=models.BUILD_STATES["ready"]).first()
name="testmodule", state=models.BUILD_STATES["ready"]
).order_by(models.ModuleBuild.id).first()
assert reusable_module.id == first_module.id
@pytest.mark.parametrize("allow_ocbm", (True, False))

View File

@@ -143,13 +143,13 @@ class TestViews:
assert data["tasks"] == {
"rpms": {
"module-build-macros": {
"task_id": 12312321,
"task_id": 2,
"state": 1,
"state_reason": None,
"nvr": "module-build-macros-01-1.module+2+b8661ee4",
},
"nginx": {
"task_id": 12312345,
"task_id": 1,
"state": 1,
"state_reason": None,
"nvr": "nginx-1.10.1-2.module+2+b8661ee4",
@@ -186,7 +186,7 @@ class TestViews:
rv = self.client.get("/module-build-service/1/module-builds/2?verbose=true")
data = json.loads(rv.data)
assert data["base_module_buildrequires"] == []
assert data["component_builds"] == [1, 2]
assert sorted(data["component_builds"]) == [1, 2]
assert data["context"] == "00000000"
# There is no xmd information on this module, so these values should be None
assert data["build_context"] is None
@@ -214,13 +214,13 @@ class TestViews:
assert data["tasks"] == {
"rpms": {
"module-build-macros": {
"task_id": 12312321,
"task_id": 2,
"state": 1,
"state_reason": None,
"nvr": "module-build-macros-01-1.module+2+b8661ee4",
},
"nginx": {
"task_id": 12312345,
"task_id": 1,
"state": 1,
"state_reason": None,
"nvr": "nginx-1.10.1-2.module+2+b8661ee4",
@@ -359,6 +359,8 @@ class TestViews:
},
]
for module_build in items:
module_build["component_builds"].sort()
assert items == expected
def test_query_builds_with_context(self):
@@ -528,7 +530,7 @@ class TestViews:
assert data["state"] == 1
assert data["state_name"] == "COMPLETE"
assert data["state_reason"] is None
assert data["task_id"] == 12312345
assert data["task_id"] == 1
def test_query_component_build_short(self):
rv = self.client.get("/module-build-service/1/component-builds/1?short=True")
@@ -541,7 +543,7 @@ class TestViews:
assert data["state"] == 1
assert data["state_name"] == "COMPLETE"
assert data["state_reason"] is None
assert data["task_id"] == 12312345
assert data["task_id"] == 1
def test_query_component_build_verbose(self):
rv = self.client.get("/module-build-service/1/component-builds/3?verbose=true")
@@ -622,7 +624,7 @@ class TestViews:
assert data["meta"]["total"] == 1
def test_query_component_builds_filter_task_id(self):
rv = self.client.get("/module-build-service/1/component-builds/?task_id=12312346")
rv = self.client.get("/module-build-service/1/component-builds/?task_id=1")
data = json.loads(rv.data)
assert data["meta"]["total"] == 1