summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMasayuki Igawa <masayuki@igawa.me>2017-02-07 18:22:20 +0900
committerMasayuki Igawa <masayuki@igawa.me>2017-03-08 18:39:32 +0900
commit856a9739dab34e8a9b0172a907497e201daeedb3 (patch)
tree95900b988bac9199641b6a682fe7aaf67ef34eff
parentff3bbb9766311e67479be4c6a95b8d5f9fa3e247 (diff)
Add files table
This commit adds 'files' table. This files table has coverage_id column as a foreign key for the coverages table. And this commit also adds a release note for it. Change-Id: I8998a79a1ba79bbdab1cd79810cc85bcbccbe7d8
Notes
Notes (review): Code-Review+2: Masayuki Igawa <masayuki@igawa.me> Workflow+1: Masayuki Igawa <masayuki@igawa.me> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Wed, 08 Mar 2017 09:52:47 +0000 Reviewed-on: https://review.openstack.org/430146 Project: openstack/coverage2sql Branch: refs/heads/master
-rw-r--r--coverage2sql/db/api.py33
-rw-r--r--coverage2sql/db/models.py14
-rw-r--r--coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py2
-rw-r--r--coverage2sql/migrations/versions/79dead6f7c26_add_files_table.py54
-rw-r--r--coverage2sql/read_coverage.py25
-rw-r--r--coverage2sql/shell.py9
-rw-r--r--coverage2sql/tests/db/test_api.py11
-rw-r--r--coverage2sql/tests/test_shell.py4
-rw-r--r--releasenotes/notes/add-files-table-e60ac2e98b5f543b.yaml4
-rw-r--r--releasenotes/source/index.rst9
-rw-r--r--requirements.txt2
-rw-r--r--test-requirements.txt1
12 files changed, 155 insertions, 13 deletions
diff --git a/coverage2sql/db/api.py b/coverage2sql/db/api.py
index 001127e..a425e0a 100644
--- a/coverage2sql/db/api.py
+++ b/coverage2sql/db/api.py
@@ -70,8 +70,8 @@ def get_session(autocommit=True, expire_on_commit=False):
70 return session 70 return session
71 71
72 72
73def create_coverage(project_name, coverage_rate=0.0, report_time=None, 73def create_coverage(project_name, coverage_rate=0.0, rates=[],
74 test_type='py27', session=None): 74 report_time=None, test_type='py27', session=None):
75 """Create a new coverage record in the database. 75 """Create a new coverage record in the database.
76 76
77 This method is used to add a new coverage in the database. 77 This method is used to add a new coverage in the database.
@@ -98,12 +98,41 @@ def create_coverage(project_name, coverage_rate=0.0, report_time=None,
98 report_time_microsecond = None 98 report_time_microsecond = None
99 coverage.report_time = report_time 99 coverage.report_time = report_time
100 coverage.report_time_microsecond = report_time_microsecond 100 coverage.report_time_microsecond = report_time_microsecond
101
101 session = session or get_session() 102 session = session or get_session()
102 with session.begin(): 103 with session.begin():
103 session.add(coverage) 104 session.add(coverage)
105
104 return coverage 106 return coverage
105 107
106 108
109def add_file_rates(coverage_id, rates=[], session=None):
110 """Add rates a specific coverage.
111
112 This method is used to add rate records in the database.
113 It tracks the coverage history by individual files.
114
115 :param int coverage_id: coverage_id
116 :param list rates: rates dict list which has
117 e.g. [{'filename': 'foo', 'line-rate': '0.1'}, ...]
118 :param session: optional session object if one isn't provided a new session
119 will be acquired for the duration of this operation
120 :return: The list of created files objects
121 :rtype: coverage2sql.models.File
122 """
123 session = session or get_session()
124 files = []
125 with session.begin():
126 for r in rates:
127 f = models.File()
128 f.coverage_id = coverage_id
129 f.filename = r['filename']
130 f.line_rate = r['line-rate']
131 session.add(f)
132 files.append(f)
133 return files
134
135
107def get_coverage(project_name=None, test_type=None, session=None): 136def get_coverage(project_name=None, test_type=None, session=None):
108 """Get new coverage records in the database. 137 """Get new coverage records in the database.
109 138
diff --git a/coverage2sql/db/models.py b/coverage2sql/db/models.py
index 5533085..c33a598 100644
--- a/coverage2sql/db/models.py
+++ b/coverage2sql/db/models.py
@@ -54,3 +54,17 @@ class Coverage(BASE, CoverageBase):
54 test_type = sa.Column(sa.String(256), nullable=False, default='py27') 54 test_type = sa.Column(sa.String(256), nullable=False, default='py27')
55 report_time = sa.Column(sa.DateTime(), default=datetime.datetime.utcnow()) 55 report_time = sa.Column(sa.DateTime(), default=datetime.datetime.utcnow())
56 report_time_microsecond = sa.Column(sa.Integer(), default=0) 56 report_time_microsecond = sa.Column(sa.Integer(), default=0)
57
58
59class File(BASE, CoverageBase):
60 __tablename__ = 'files'
61 __table_args__ = (sa.Index('ix_file_coverage_id', 'coverage_id'),
62 sa.Index('ix_filename', 'filename'))
63 id = sa.Column(sa.BigInteger, primary_key=True)
64 coverage_id = sa.Column(sa.BigInteger, nullable=False)
65 filename = sa.Column(sa.String(256), nullable=False)
66 line_rate = sa.Column(sa.Float())
67 coverage = sa.orm.relationship(Coverage,
68 backref=sa.orm.backref('file_coverage'),
69 foreign_keys=coverage_id,
70 primaryjoin=coverage_id == Coverage.id)
diff --git a/coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py b/coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py
index dd7349b..c4ebc94 100644
--- a/coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py
+++ b/coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py
@@ -52,4 +52,4 @@ def upgrade():
52 52
53 53
54def downgrade(): 54def downgrade():
55 raise NotImplementedError() 55 op.drop_table('classes')
diff --git a/coverage2sql/migrations/versions/79dead6f7c26_add_files_table.py b/coverage2sql/migrations/versions/79dead6f7c26_add_files_table.py
new file mode 100644
index 0000000..0bf39fd
--- /dev/null
+++ b/coverage2sql/migrations/versions/79dead6f7c26_add_files_table.py
@@ -0,0 +1,54 @@
1# Copyright (c) 2017 Hewlett Packard Enterprise Development LP
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Add files table
17
18Revision ID: 79dead6f7c26
19Revises: cb0e61ce633e
20Create Date: 2017-02-07 17:57:28.777311
21
22"""
23
24# revision identifiers, used by Alembic.
25revision = '79dead6f7c26'
26down_revision = 'cb0e61ce633e'
27branch_labels = None
28depends_on = None
29
30from alembic import context
31from alembic import op
32import sqlalchemy as sa
33
34
35def upgrade():
36 migration_context = context.get_context()
37 if migration_context.dialect.name == 'sqlite':
38 id_type = sa.Integer
39 else:
40 id_type = sa.BigInteger
41
42 op.create_table('files',
43 sa.Column('id', id_type, autoincrement=True,
44 primary_key=True),
45 sa.Column('coverage_id', id_type, nullable=False),
46 sa.Column('filename', sa.String(256), nullable=False),
47 sa.Column('line_rate', sa.Float()),
48 mysql_engine='InnoDB')
49 op.create_index('ix_class_coverage_id', 'files', ['coverage_id'])
50 op.create_index('ix_filename', 'files', ['filename'])
51
52
53def downgrade():
54 op.drop_table('files')
diff --git a/coverage2sql/read_coverage.py b/coverage2sql/read_coverage.py
index 7aa1979..8a9aa53 100644
--- a/coverage2sql/read_coverage.py
+++ b/coverage2sql/read_coverage.py
@@ -12,7 +12,10 @@
12# License for the specific language governing permissions and limitations 12# License for the specific language governing permissions and limitations
13# under the License. 13# under the License.
14 14
15import tempfile
16
15import coverage 17import coverage
18import xmltodict
16 19
17 20
18class DevNull(object): 21class DevNull(object):
@@ -26,10 +29,30 @@ class ReadCoverage(object):
26 def __init__(self, coverage_file=None): 29 def __init__(self, coverage_file=None):
27 self.cov = coverage.Coverage(data_file=coverage_file) 30 self.cov = coverage.Coverage(data_file=coverage_file)
28 self.cov.load() 31 self.cov.load()
29 self.cov_pct = self.cov.report(file=DevNull()) 32 xmlfile = tempfile.NamedTemporaryFile(suffix='.xml')
33 self.cov_pct = self.cov.xml_report(outfile=xmlfile.name)
34 self.covdoc = xmltodict.parse(xmlfile.read())
35 xmlfile.close()
30 36
31 def get_data(self): 37 def get_data(self):
32 return self.cov.get_data() 38 return self.cov.get_data()
33 39
34 def get_coverage_rate(self): 40 def get_coverage_rate(self):
35 return self.cov_pct / 100 41 return self.cov_pct / 100
42
43 def get_rates_by_files(self):
44 rates = []
45 for p in self.covdoc['coverage']['packages']['package']:
46 try:
47 # FIXME(masayukig): Try to access the first element. This is
48 # ugly..
49 p['classes']['class'][0]
50 for c in p['classes']['class']:
51 rates.append({'filename': c['@filename'],
52 'line-rate': c['@line-rate']})
53 except KeyError:
54 # NOTE(masayukig): This has only one class
55 c = p['classes']['class']
56 rates.append({'filename': c['@filename'],
57 'line-rate': c['@line-rate']})
58 return rates
diff --git a/coverage2sql/shell.py b/coverage2sql/shell.py
index 0bb6925..15d12c4 100644
--- a/coverage2sql/shell.py
+++ b/coverage2sql/shell.py
@@ -62,9 +62,11 @@ def parse_args(argv, default_config_files=None):
62 default_config_files=default_config_files) 62 default_config_files=default_config_files)
63 63
64 64
65def process_results(project_name=".", coverage_rate=0.0): 65def process_results(project_name=".", coverage_rate=0.0, rates=[]):
66 session = api.get_session() 66 session = api.get_session()
67 api.create_coverage(project_name, coverage_rate, test_type=CONF.test_type) 67 cov = api.create_coverage(project_name, coverage_rate, rates,
68 test_type=CONF.test_type, session=session)
69 api.add_file_rates(cov.id, rates, session)
68 session.close() 70 session.close()
69 71
70 72
@@ -76,9 +78,10 @@ def main():
76 if CONF.coverage_file: 78 if CONF.coverage_file:
77 cov = coverage.ReadCoverage(CONF.coverage_file) 79 cov = coverage.ReadCoverage(CONF.coverage_file)
78 coverage_rate = cov.get_coverage_rate() 80 coverage_rate = cov.get_coverage_rate()
81 rates = cov.get_rates_by_files()
79 else: 82 else:
80 raise NotImplementedError() 83 raise NotImplementedError()
81 process_results(project_name, coverage_rate) 84 process_results(project_name, coverage_rate, rates)
82 85
83 86
84if __name__ == "__main__": 87if __name__ == "__main__":
diff --git a/coverage2sql/tests/db/test_api.py b/coverage2sql/tests/db/test_api.py
index 90e0349..c8692da 100644
--- a/coverage2sql/tests/db/test_api.py
+++ b/coverage2sql/tests/db/test_api.py
@@ -65,3 +65,14 @@ class TestDatabaseAPI(base.TestCase):
65 self.assertTrue(covs is not None) 65 self.assertTrue(covs is not None)
66 self.assertEqual(len(covs), 1) 66 self.assertEqual(len(covs), 1)
67 self.assertEqual(covs[0].project_name, 'foo1_project') 67 self.assertEqual(covs[0].project_name, 'foo1_project')
68
69 def test_add_file_rates(self):
70 rates = []
71 rates.append({'filename': 'foo/bar0', 'line-rate': '0'})
72 rates.append({'filename': 'foo/bar1', 'line-rate': '1'})
73 rates.append({'filename': 'foo/bar2', 'line-rate': '0.92'})
74 files = api.add_file_rates(1, rates)
75 self.assertEqual(3, len(files))
76 for r, f in zip(rates, files):
77 self.assertEqual(r['filename'], f.filename)
78 self.assertEqual(r['line-rate'], f.line_rate)
diff --git a/coverage2sql/tests/test_shell.py b/coverage2sql/tests/test_shell.py
index 1c3cc02..5e54676 100644
--- a/coverage2sql/tests/test_shell.py
+++ b/coverage2sql/tests/test_shell.py
@@ -44,6 +44,10 @@ class TestMain(base.TestCase):
44 'get_coverage_rate') 44 'get_coverage_rate')
45 fake_read_coverage.get_coverage_rate.return_value = ( 45 fake_read_coverage.get_coverage_rate.return_value = (
46 fake_get_coverage_rate) 46 fake_get_coverage_rate)
47 fake_read_coverage.get_rates_by_files = mock.MagicMock(
48 'get_rates_by_files')
49 fake_read_coverage.get_rates_by_files.return_value = (
50 {'filename': 'foo/bar.py', 'line-rate': '0.99'})
47 read_coverage_mock.return_value = fake_read_coverage 51 read_coverage_mock.return_value = fake_read_coverage
48 shell.main() 52 shell.main()
49 read_coverage_mock.assert_called_with(mock.ANY) 53 read_coverage_mock.assert_called_with(mock.ANY)
diff --git a/releasenotes/notes/add-files-table-e60ac2e98b5f543b.yaml b/releasenotes/notes/add-files-table-e60ac2e98b5f543b.yaml
new file mode 100644
index 0000000..ff8b7d5
--- /dev/null
+++ b/releasenotes/notes/add-files-table-e60ac2e98b5f543b.yaml
@@ -0,0 +1,4 @@
1---
2features:
3 - |
4 Add 'Files' table to store individual coverage data by file.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 78e8ca8..ecc034f 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -1,14 +1,13 @@
1Welcome to Coverage2sql Release Notes documentation! 1Welcome to Coverage2sql Release Notes documentation!
2=================================================== 2====================================================
3 3
4Contents 4Contents
5======== 5========
6 6
7.. toctree:: 7 .. toctree::
8 :maxdepth: 2 8 :maxdepth: 1
9
10 unreleased
11 9
10 unreleased
12 11
13Indices and tables 12Indices and tables
14================== 13==================
diff --git a/requirements.txt b/requirements.txt
index f0a3376..6ea5d57 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,5 @@ pbr>=1.6
6SQLAlchemy>=0.8.2 6SQLAlchemy>=0.8.2
7alembic>=0.4.1 7alembic>=0.4.1
8oslo.config>=1.4.0.0a3 8oslo.config>=1.4.0.0a3
9coverage>=4.3
10xmltodict>=0.10.1 # MIT
diff --git a/test-requirements.txt b/test-requirements.txt
index 3e23420..113c6eb 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,6 @@
4 4
5hacking<0.11,>=0.10.0 5hacking<0.11,>=0.10.0
6 6
7coverage>=3.6
8docutils>=0.11 # OSI-Approved Open Source, Public Domain 7docutils>=0.11 # OSI-Approved Open Source, Public Domain
9fixtures>=0.3.14 8fixtures>=0.3.14
10python-subunit>=0.0.18 9python-subunit>=0.0.18