Browse Source

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
changes/46/430146/4
Masayuki Igawa 2 years ago
parent
commit
856a9739da
No account linked to committer's email address

+ 31
- 2
coverage2sql/db/api.py View File

@@ -70,8 +70,8 @@ def get_session(autocommit=True, expire_on_commit=False):
70 70
     return session
71 71
 
72 72
 
73
-def create_coverage(project_name, coverage_rate=0.0, report_time=None,
74
-                    test_type='py27', session=None):
73
+def create_coverage(project_name, coverage_rate=0.0, rates=[],
74
+                    report_time=None, test_type='py27', session=None):
75 75
     """Create a new coverage record in the database.
76 76
 
77 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 98
         report_time_microsecond = None
99 99
     coverage.report_time = report_time
100 100
     coverage.report_time_microsecond = report_time_microsecond
101
+
101 102
     session = session or get_session()
102 103
     with session.begin():
103 104
         session.add(coverage)
105
+
104 106
     return coverage
105 107
 
106 108
 
109
+def 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
+
107 136
 def get_coverage(project_name=None, test_type=None, session=None):
108 137
     """Get new coverage records in the database.
109 138
 

+ 14
- 0
coverage2sql/db/models.py View File

@@ -54,3 +54,17 @@ class Coverage(BASE, CoverageBase):
54 54
     test_type = sa.Column(sa.String(256), nullable=False, default='py27')
55 55
     report_time = sa.Column(sa.DateTime(), default=datetime.datetime.utcnow())
56 56
     report_time_microsecond = sa.Column(sa.Integer(), default=0)
57
+
58
+
59
+class 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)

+ 1
- 1
coverage2sql/migrations/versions/52dfb338f74e_add_coverages_table.py View File

@@ -52,4 +52,4 @@ def upgrade():
52 52
 
53 53
 
54 54
 def downgrade():
55
-    raise NotImplementedError()
55
+    op.drop_table('classes')

+ 54
- 0
coverage2sql/migrations/versions/79dead6f7c26_add_files_table.py View File

@@ -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
+
18
+Revision ID: 79dead6f7c26
19
+Revises: cb0e61ce633e
20
+Create Date: 2017-02-07 17:57:28.777311
21
+
22
+"""
23
+
24
+# revision identifiers, used by Alembic.
25
+revision = '79dead6f7c26'
26
+down_revision = 'cb0e61ce633e'
27
+branch_labels = None
28
+depends_on = None
29
+
30
+from alembic import context
31
+from alembic import op
32
+import sqlalchemy as sa
33
+
34
+
35
+def 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
+
53
+def downgrade():
54
+    op.drop_table('files')

+ 24
- 1
coverage2sql/read_coverage.py View File

@@ -12,7 +12,10 @@
12 12
 # License for the specific language governing permissions and limitations
13 13
 # under the License.
14 14
 
15
+import tempfile
16
+
15 17
 import coverage
18
+import xmltodict
16 19
 
17 20
 
18 21
 class DevNull(object):
@@ -26,10 +29,30 @@ class ReadCoverage(object):
26 29
     def __init__(self, coverage_file=None):
27 30
         self.cov = coverage.Coverage(data_file=coverage_file)
28 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 37
     def get_data(self):
32 38
         return self.cov.get_data()
33 39
 
34 40
     def get_coverage_rate(self):
35 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

+ 6
- 3
coverage2sql/shell.py View File

@@ -62,9 +62,11 @@ def parse_args(argv, default_config_files=None):
62 62
          default_config_files=default_config_files)
63 63
 
64 64
 
65
-def process_results(project_name=".", coverage_rate=0.0):
65
+def process_results(project_name=".", coverage_rate=0.0, rates=[]):
66 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 70
     session.close()
69 71
 
70 72
 
@@ -76,9 +78,10 @@ def main():
76 78
     if CONF.coverage_file:
77 79
         cov = coverage.ReadCoverage(CONF.coverage_file)
78 80
         coverage_rate = cov.get_coverage_rate()
81
+        rates = cov.get_rates_by_files()
79 82
     else:
80 83
         raise NotImplementedError()
81
-    process_results(project_name, coverage_rate)
84
+    process_results(project_name, coverage_rate, rates)
82 85
 
83 86
 
84 87
 if __name__ == "__main__":

+ 11
- 0
coverage2sql/tests/db/test_api.py View File

@@ -65,3 +65,14 @@ class TestDatabaseAPI(base.TestCase):
65 65
         self.assertTrue(covs is not None)
66 66
         self.assertEqual(len(covs), 1)
67 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)

+ 4
- 0
coverage2sql/tests/test_shell.py View File

@@ -44,6 +44,10 @@ class TestMain(base.TestCase):
44 44
             'get_coverage_rate')
45 45
         fake_read_coverage.get_coverage_rate.return_value = (
46 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 51
         read_coverage_mock.return_value = fake_read_coverage
48 52
         shell.main()
49 53
         read_coverage_mock.assert_called_with(mock.ANY)

+ 4
- 0
releasenotes/notes/add-files-table-e60ac2e98b5f543b.yaml View File

@@ -0,0 +1,4 @@
1
+---
2
+features:
3
+  - |
4
+    Add 'Files' table to store individual coverage data by file.

+ 4
- 5
releasenotes/source/index.rst View File

@@ -1,14 +1,13 @@
1 1
 Welcome to Coverage2sql Release Notes documentation!
2
-===================================================
2
+====================================================
3 3
 
4 4
 Contents
5 5
 ========
6 6
 
7
-.. toctree::
8
-   :maxdepth: 2
9
-
10
-   unreleased
7
+ .. toctree::
8
+    :maxdepth: 1
11 9
 
10
+    unreleased
12 11
 
13 12
 Indices and tables
14 13
 ==================

+ 2
- 0
requirements.txt View File

@@ -6,3 +6,5 @@ pbr>=1.6
6 6
 SQLAlchemy>=0.8.2
7 7
 alembic>=0.4.1
8 8
 oslo.config>=1.4.0.0a3
9
+coverage>=4.3
10
+xmltodict>=0.10.1  # MIT

+ 0
- 1
test-requirements.txt View File

@@ -4,7 +4,6 @@
4 4
 
5 5
 hacking<0.11,>=0.10.0
6 6
 
7
-coverage>=3.6
8 7
 docutils>=0.11  # OSI-Approved Open Source, Public Domain
9 8
 fixtures>=0.3.14
10 9
 python-subunit>=0.0.18

Loading…
Cancel
Save