Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F8063522
D239.1759837605.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
D239.1759837605.diff
View Options
diff --git a/Containerfile.worker b/Containerfile.worker
--- a/Containerfile.worker
+++ b/Containerfile.worker
@@ -4,7 +4,7 @@
RUN apk add --no-cache podman podman-compose python3 \
py3-virtualenv dumb-init bash shadow git openssh rsync php php-curl \
&& virtualenv --python=python3 /buildbot_venv \
- && /buildbot_venv/bin/pip3 install 'twisted[tls]' \
+ && /buildbot_venv/bin/pip3 install 'twisted[tls]' pycobertura \
&& mkdir /buildbot \
&& useradd -ms /bin/bash buildbot \
&& mkdir -pv /tools \
diff --git a/lilybuild/lilybuild/coverage.py b/lilybuild/lilybuild/coverage.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/coverage.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+
+import json
+import sys
+import os
+import shutil
+from pycobertura import Cobertura
+from pycobertura.filesystem import DirectoryFileSystem
+
+def is_covered(covered):
+ # old version returns True for covered and False for uncovered
+ # new version is 'hit' 'partial' and 'miss'
+ # https://github.com/aconrad/pycobertura/commit/866da18254c3eaf645719b11158daccf73acfe18
+ return covered == 'hit' or covered == 'partial' or covered is True
+
+def convert_lines(cobertura, fs, filename):
+ statuses = cobertura.line_statuses(filename)
+ with fs.open(filename) as f:
+ lines = ['N' for _ in f]
+ for (line_num, covered) in statuses:
+ lines[line_num - 1] = 'C' if is_covered(covered) else 'U'
+ return ''.join(lines)
+
+def get_file_full_path(base_dir, file_name):
+ res_abs = os.path.realpath(base_dir, strict=True)
+ file_abs = os.path.realpath(os.path.join(base_dir, file_name), strict=True)
+ if not file_abs.startswith(res_abs + os.sep):
+ raise RuntimeError(f'File is not in the base dir: {file_name}')
+ return file_abs
+
+class SafeDirectoryFileSystem(DirectoryFileSystem):
+ def real_filename(self, filename):
+ return get_file_full_path(self.source_dir, filename)
+
+def main(kwargs):
+ source_dir = kwargs['source_dir']
+ result_dir = kwargs['result_dir']
+ untrusted_coverage_file = kwargs['untrusted_coverage_file']
+ output_dir = kwargs['output_dir']
+
+ coverage_file = get_file_full_path(result_dir, untrusted_coverage_file)
+
+ fs = SafeDirectoryFileSystem(source_dir)
+ cobertura = Cobertura(coverage_file, fs)
+ files = cobertura.files()
+
+ result = {}
+
+ for f in files:
+ try:
+ result[f] = convert_lines(cobertura, fs, f)
+ except Exception as e:
+ print(f'Error while processing coverage for file "{f}". The file does not exist in the source repository, or cannot be read.', file=sys.stderr)
+
+ shutil.copyfile(coverage_file, os.path.join(output_dir, 'coverage-cobertura.xml'))
+ with open(os.path.join(output_dir, 'coverage-phorge.json'), 'w') as f:
+ print(json.dumps(result), file=f)
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ a = json.loads(sys.argv[1])
+ else:
+ a = json.loads(sys.stdin.read())
+ try:
+ main(a)
+ except:
+ print('Cannot read coverage file.')
diff --git a/lilybuild/lilybuild/tests/coverage_res/README b/lilybuild/lilybuild/tests/coverage_res/README
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/README
@@ -0,0 +1 @@
+The test resources under this directory is taken and/or adapted from the pycobertura repository: https://github.com/aconrad/pycobertura/tree/master/tests/dummy.source1 . The original license of that repository is MIT.
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-notfound.xml b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-notfound.xml
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-notfound.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" ?>
+<!DOCTYPE coverage
+ SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
+<coverage branch-rate="0" line-rate="0.4167" timestamp="1420591320487" version="3.7.1">
+ <!-- Generated by coverage.py: http://nedbatchelder.com/code/coverage -->
+ <packages>
+ <package branch-rate="0" complexity="0" line-rate="0.4167" name="">
+ <classes>
+ <class branch-rate="0" complexity="0" filename="dummy/__init__.py" line-rate="0" name="dummy/__init__">
+ <methods/>
+ <lines/>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy.py" line-rate="0.6" name="dummy/dummy">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ <line hits="1" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy2.py" line-rate="1" name="dummy/dummy2">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy5.py" line-rate="0" name="dummy/dummy5">
+ <methods/>
+ <lines>
+ <line hits="0" number="1"/>
+ <line hits="0" number="2"/>
+ <line hits="0" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ </classes>
+ </package>
+ </packages>
+</coverage>
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-outofbounds.xml b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-outofbounds.xml
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage-outofbounds.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" ?>
+<!DOCTYPE coverage
+ SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
+<coverage branch-rate="0" line-rate="0.4167" timestamp="1420591320487" version="3.7.1">
+ <!-- Generated by coverage.py: http://nedbatchelder.com/code/coverage -->
+ <packages>
+ <package branch-rate="0" complexity="0" line-rate="0.4167" name="">
+ <classes>
+ <class branch-rate="0" complexity="0" filename="dummy/__init__.py" line-rate="0" name="dummy/__init__">
+ <methods/>
+ <lines/>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy.py" line-rate="0.6" name="dummy/dummy">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ <line hits="1" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="../../pages_test.py" line-rate="1" name="pages_test">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="/etc/passwd" line-rate="0" name="etc/passwd">
+ <methods/>
+ <lines>
+ <line hits="0" number="1"/>
+ <line hits="0" number="2"/>
+ <line hits="0" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ </classes>
+ </package>
+ </packages>
+</coverage>
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage.xml b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage.xml
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.result1/coverage.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" ?>
+<!DOCTYPE coverage
+ SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
+<coverage branch-rate="0" line-rate="0.4167" timestamp="1420591320487" version="3.7.1">
+ <!-- Generated by coverage.py: http://nedbatchelder.com/code/coverage -->
+ <packages>
+ <package branch-rate="0" complexity="0" line-rate="0.4167" name="">
+ <classes>
+ <class branch-rate="0" complexity="0" filename="dummy/__init__.py" line-rate="0" name="dummy/__init__">
+ <methods/>
+ <lines/>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy.py" line-rate="0.6" name="dummy/dummy">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ <line hits="1" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy2.py" line-rate="1" name="dummy/dummy2">
+ <methods/>
+ <lines>
+ <line hits="1" number="1"/>
+ <line hits="1" number="2"/>
+ </lines>
+ </class>
+ <class branch-rate="0" complexity="0" filename="dummy/dummy4.py" line-rate="0" name="dummy/dummy4">
+ <methods/>
+ <lines>
+ <line hits="0" number="1"/>
+ <line hits="0" number="2"/>
+ <line hits="0" number="4"/>
+ <line hits="0" number="5"/>
+ <line hits="0" number="6"/>
+ </lines>
+ </class>
+ </classes>
+ </package>
+ </packages>
+</coverage>
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.result2/coverage.xml b/lilybuild/lilybuild/tests/coverage_res/dummy.result2/coverage.xml
new file mode 120000
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.result2/coverage.xml
@@ -0,0 +1 @@
+../dummy.result1/coverage.xml
\ No newline at end of file
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/__init__.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/__init__.py
new file mode 100644
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy.py
@@ -0,0 +1,6 @@
+def foo():
+ pass
+
+def bar():
+ a = 'a'
+ b = 'b'
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy2.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy2.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy2.py
@@ -0,0 +1,2 @@
+def baz():
+ pass
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy4.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy4.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/dummy/dummy4.py
@@ -0,0 +1,6 @@
+def barbaz():
+ pass
+
+def foobarbaz():
+ a = 1 + 3
+ pass
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/setup.cfg b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/setup.cfg
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/setup.cfg
@@ -0,0 +1,30 @@
+[pytest]
+norecursedirs = build docs/_build *.egg .tox *.venv tests/dummy
+addopts =
+ # Shows a line for every test
+ # You probably want to turn this off if you use pytest-sugar.
+ # Or you can keep it and run `py.test -q`.
+ --verbose
+
+ # Shorter tracebacks are sometimes easier to read
+ --tb=short
+
+ # Turn on --capture to have brief, less noisy output.
+ # You will only see output if the test fails.
+ # Use --capture no (same as -s) if you want to see it all or have problems
+ # debugging.
+ # --capture=fd
+ # --capture=no
+
+ # Show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed.
+ -rfEsxX
+
+ # Output test results to junit.xml for Jenkins to consume
+ --junitxml=junit.xml
+
+ # Measure code coverage
+ --cov=dummy --cov-report=xml --cov-report=term-missing
+
+ # Previous versions included the following, but it's a bad idea because it
+ # hard-codes the value and makes it hard to change from the command-line
+ test_dummy.py
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source1/test_dummy.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/test_dummy.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source1/test_dummy.py
@@ -0,0 +1,8 @@
+def test_foo():
+ from dummy.dummy import foo
+ foo()
+
+
+def test_baz():
+ from dummy.dummy2 import baz
+ baz()
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/__init__.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/__init__.py
new file mode 100644
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy.py
@@ -0,0 +1,6 @@
+def foo():
+ pass
+
+def bar():
+ a = 'a'
+ b = 'b'
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy2.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy2.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy2.py
@@ -0,0 +1,2 @@
+def baz():
+ pass
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy4.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy4.py
new file mode 120000
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/dummy/dummy4.py
@@ -0,0 +1 @@
+../../dummy.source1/dummy/dummy4.py
\ No newline at end of file
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/setup.cfg b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/setup.cfg
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/setup.cfg
@@ -0,0 +1,30 @@
+[pytest]
+norecursedirs = build docs/_build *.egg .tox *.venv tests/dummy
+addopts =
+ # Shows a line for every test
+ # You probably want to turn this off if you use pytest-sugar.
+ # Or you can keep it and run `py.test -q`.
+ --verbose
+
+ # Shorter tracebacks are sometimes easier to read
+ --tb=short
+
+ # Turn on --capture to have brief, less noisy output.
+ # You will only see output if the test fails.
+ # Use --capture no (same as -s) if you want to see it all or have problems
+ # debugging.
+ # --capture=fd
+ # --capture=no
+
+ # Show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed.
+ -rfEsxX
+
+ # Output test results to junit.xml for Jenkins to consume
+ --junitxml=junit.xml
+
+ # Measure code coverage
+ --cov=dummy --cov-report=xml --cov-report=term-missing
+
+ # Previous versions included the following, but it's a bad idea because it
+ # hard-codes the value and makes it hard to change from the command-line
+ test_dummy.py
diff --git a/lilybuild/lilybuild/tests/coverage_res/dummy.source2/test_dummy.py b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/test_dummy.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_res/dummy.source2/test_dummy.py
@@ -0,0 +1,8 @@
+def test_foo():
+ from dummy.dummy import foo
+ foo()
+
+
+def test_baz():
+ from dummy.dummy2 import baz
+ baz()
diff --git a/lilybuild/lilybuild/tests/coverage_test_worker.py b/lilybuild/lilybuild/tests/coverage_test_worker.py
new file mode 100644
--- /dev/null
+++ b/lilybuild/lilybuild/tests/coverage_test_worker.py
@@ -0,0 +1,116 @@
+
+import unittest
+import tempfile
+import os
+import json
+import lilybuild.coverage as lc
+
+self_dir = os.path.dirname(__file__)
+res_dir = os.path.join(self_dir, 'coverage_res')
+
+def r(name):
+ return os.path.join(res_dir, name)
+
+class CoverageTest(unittest.TestCase):
+ def test_simple(self):
+ with tempfile.TemporaryDirectory() as dir_name:
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.result1'),
+ 'untrusted_coverage_file': 'coverage.xml',
+ 'output_dir': dir_name
+ })
+
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-cobertura.xml')))
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-phorge.json')))
+ with open(os.path.join(dir_name, 'coverage-phorge.json')) as f:
+ c = json.loads(f.read())
+ self.assertEqual(c, {
+ 'dummy/__init__.py': '',
+ 'dummy/dummy.py': 'CCNCUU',
+ 'dummy/dummy2.py': 'CC',
+ 'dummy/dummy4.py': 'UUNUUU',
+ })
+
+ def test_source_outofbounds(self):
+ with tempfile.TemporaryDirectory() as dir_name:
+ lc.main({
+ 'source_dir': r('dummy.source2'),
+ 'result_dir': r('dummy.result1'),
+ 'untrusted_coverage_file': 'coverage.xml',
+ 'output_dir': dir_name
+ })
+
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-cobertura.xml')))
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-phorge.json')))
+ with open(os.path.join(dir_name, 'coverage-phorge.json')) as f:
+ c = json.loads(f.read())
+ self.assertEqual(c, {
+ 'dummy/__init__.py': '',
+ 'dummy/dummy.py': 'CCNCUU',
+ 'dummy/dummy2.py': 'CC',
+ })
+
+ def test_notfound(self):
+ with tempfile.TemporaryDirectory() as dir_name:
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.result1'),
+ 'untrusted_coverage_file': 'coverage-notfound.xml',
+ 'output_dir': dir_name
+ })
+
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-cobertura.xml')))
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-phorge.json')))
+ with open(os.path.join(dir_name, 'coverage-phorge.json')) as f:
+ c = json.loads(f.read())
+ self.assertEqual(c, {
+ 'dummy/__init__.py': '',
+ 'dummy/dummy.py': 'CCNCUU',
+ 'dummy/dummy2.py': 'CC',
+ })
+
+ def test_outofbounds(self):
+ with tempfile.TemporaryDirectory() as dir_name:
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.result1'),
+ 'untrusted_coverage_file': 'coverage-outofbounds.xml',
+ 'output_dir': dir_name
+ })
+
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-cobertura.xml')))
+ self.assertTrue(os.path.exists(os.path.join(dir_name, 'coverage-phorge.json')))
+ with open(os.path.join(dir_name, 'coverage-phorge.json')) as f:
+ c = json.loads(f.read())
+ self.assertEqual(c, {
+ 'dummy/__init__.py': '',
+ 'dummy/dummy.py': 'CCNCUU',
+ })
+
+ def test_coverage_file_not_found(self):
+ with tempfile.TemporaryDirectory() as dir_name:
+ with self.assertRaises(RuntimeError):
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.source1'),
+ 'untrusted_coverage_file': '../dummy.result1/coverage.xml',
+ 'output_dir': dir_name
+ })
+
+ with self.assertRaises(RuntimeError):
+ # A symlink to out-of-bounds file
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.result2'),
+ 'untrusted_coverage_file': 'coverage.xml',
+ 'output_dir': dir_name
+ })
+
+ with self.assertRaises(FileNotFoundError):
+ lc.main({
+ 'source_dir': r('dummy.source1'),
+ 'result_dir': r('dummy.result1'),
+ 'untrusted_coverage_file': 'nosuchfile.xml',
+ 'output_dir': dir_name
+ })
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Oct 7, 4:46 AM (19 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
499733
Default Alt Text
D239.1759837605.diff (19 KB)
Attached To
Mode
D239: Add coverage converter
Attached
Detach File
Event Timeline
Log In to Comment