From 7045ee6f4191e4855c73da4f69f2c5d5c05fa697 Mon Sep 17 00:00:00 2001 From: swryan Date: Mon, 24 May 2021 17:50:40 -0400 Subject: [PATCH] feat: add `--basedir` and `--submit` options (#287) Enables users to dynamically remove a given base path from the generated config failes and to upload files generated in previous runs of the tool. --- coveralls/api.py | 5 +- coveralls/cli.py | 15 ++++-- coveralls/reporter.py | 11 +++- example/example.json | 1 + tests/api/reporter_test.py | 107 +++++++++++++++++++++++++++++++++++++ tests/cli_test.py | 27 +++++++++- 6 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 example/example.json diff --git a/coveralls/api.py b/coveralls/api.py index 7c707970..cb9db7fd 100644 --- a/coveralls/api.py +++ b/coveralls/api.py @@ -223,7 +223,9 @@ def wear(self, dry_run=False): json_string = self.create_report() if dry_run: return {} + return self.submit_report(json_string) + def submit_report(self, json_string): endpoint = '{}/api/v1/jobs'.format(self._coveralls_host.rstrip('/')) verify = not bool(os.environ.get('COVERALLS_SKIP_SSL_VERIFY')) response = requests.post(endpoint, files={'json_file': json_string}, @@ -368,7 +370,8 @@ def get_coverage(self): workman.load() workman.get_data() - return CoverallReporter(workman, workman.config).coverage + base_dir = self.config.get('base_dir') or '' + return CoverallReporter(workman, workman.config, base_dir).coverage @staticmethod def debug_bad_encoding(data): diff --git a/coveralls/cli.py b/coveralls/cli.py index 3244e2d5..1c90604c 100644 --- a/coveralls/cli.py +++ b/coveralls/cli.py @@ -19,7 +19,9 @@ Global options: --service= Provide an alternative service name to submit. --rcfile= Specify configuration file. [default: .coveragerc] + --basedir= Base directory that is removed from reported paths. --output= Write report to file. Doesn't send anything. + --submit= Upload a previously generated file. --merge= Merge report from file when submitting. --finish Finish parallel jobs. -h --help Display this help. @@ -60,7 +62,8 @@ def main(argv=None): try: coverallz = Coveralls(token_required, config_file=options['--rcfile'], - service_name=options['--service']) + service_name=options['--service'], + base_dir=options.get('--basedir') or '') if options['--merge']: coverallz.merge(options['--merge']) @@ -75,6 +78,11 @@ def main(argv=None): coverallz.save_report(options['--output']) return + if options['--submit']: + with open(options['--submit']) as report_file: + coverallz.submit_report(report_file.read()) + return + if options['--finish']: log.info('Finishing parallel jobs...') coverallz.parallel_finish() @@ -86,8 +94,9 @@ def main(argv=None): log.info('Coverage submitted!') log.debug(result) - log.info(result['message']) - log.info(result['url']) + if result: + log.info(result.get('message')) + log.info(result.get('url')) except KeyboardInterrupt: # pragma: no cover log.info('Aborted') except CoverallsException as e: diff --git a/coveralls/reporter.py b/coveralls/reporter.py index f0f810a0..529a5c76 100644 --- a/coveralls/reporter.py +++ b/coveralls/reporter.py @@ -12,8 +12,13 @@ class CoverallReporter: """Custom coverage.py reporter for coveralls.io.""" - def __init__(self, cov, conf): + def __init__(self, cov, conf, base_dir=''): self.coverage = [] + self.base_dir = base_dir + if self.base_dir: + self.base_dir = self.base_dir.replace(os.path.sep, '/') + if self.base_dir[-1] != '/': + self.base_dir += '/' self.report(cov, conf) def report5(self, cov): @@ -183,9 +188,13 @@ def get_arcs(analysis): def parse_file(self, cu, analysis): """Generate data for single file.""" filename = cu.relative_filename() + # ensure results are properly merged between platforms posix_filename = filename.replace(os.path.sep, '/') + if self.base_dir and posix_filename.startswith(self.base_dir): + posix_filename = posix_filename[len(self.base_dir):] + source = analysis.file_reporter.source() token_lines = analysis.file_reporter.source_token_lines() diff --git a/example/example.json b/example/example.json new file mode 100644 index 00000000..5c0d4708 --- /dev/null +++ b/example/example.json @@ -0,0 +1 @@ +{"source_files": [{"name": "coveralls-python/example/project.py", "source": "def hello():\n print('world')\n\n\nclass Foo:\n \"\"\" Bar \"\"\"\n\n\ndef baz():\n print('this is not tested')\n\ndef branch(cond1, cond2):\n if cond1:\n print('condition tested both ways')\n if cond2:\n print('condition not tested both ways')\n", "coverage": [1, 1, null, null, 1, null, null, null, 1, 0, null, 1, 1, 1, 1, 1]}, {"name": "coveralls-python/example/runtests.py", "source": "from project import branch\nfrom project import hello\n\nif __name__ == '__main__':\n hello()\n branch(False, True)\n branch(True, True)\n", "coverage": [1, 1, null, 1, 1, 1, 1]}], "service_name": "coveralls-python", "config_file": ".coveragerc", "base_dir": null} \ No newline at end of file diff --git a/tests/api/reporter_test.py b/tests/api/reporter_test.py index 8c90e514..31f40be3 100644 --- a/tests/api/reporter_test.py +++ b/tests/api/reporter_test.py @@ -62,6 +62,113 @@ def test_reporter(self): 'name': 'runtests.py', 'coverage': [1, 1, None, 1, 1, 1, 1]}) + def test_reporter_no_base_dir_arg(self): + subprocess.call(['coverage', 'run', '--omit=**/.tox/*', + 'example/runtests.py'], cwd=BASE_DIR) + + # without base_dir arg, file name is prefixed with 'example/' + os.chdir(BASE_DIR) + results = Coveralls(repo_token='xxx').get_coverage() + assert len(results) == 2 + + assert_coverage(results[0], { + 'source': ('def hello():\n' + ' print(\'world\')\n\n\n' + 'class Foo:\n' + ' """ Bar """\n\n\n' + 'def baz():\n' + ' print(\'this is not tested\')\n\n' + 'def branch(cond1, cond2):\n' + ' if cond1:\n' + ' print(\'condition tested both ways\')\n' + ' if cond2:\n' + ' print(\'condition not tested both ways\')\n'), + 'name': 'example/project.py', + 'coverage': [1, 1, None, None, 1, None, None, + None, 1, 0, None, 1, 1, 1, 1, 1]}) + + assert_coverage(results[1], { + 'source': ('from project import branch\n' + 'from project import hello\n\n' + "if __name__ == '__main__':\n" + ' hello()\n' + ' branch(False, True)\n' + ' branch(True, True)\n'), + 'name': 'example/runtests.py', + 'coverage': [1, 1, None, 1, 1, 1, 1]}) + + def test_reporter_with_base_dir_arg(self): + subprocess.call(['coverage', 'run', '--omit=**/.tox/*', + 'example/runtests.py'], cwd=BASE_DIR) + + # without base_dir arg, file name is prefixed with 'example/' + os.chdir(BASE_DIR) + results = Coveralls(repo_token='xxx', + base_dir='example').get_coverage() + assert len(results) == 2 + + assert_coverage(results[0], { + 'source': ('def hello():\n' + ' print(\'world\')\n\n\n' + 'class Foo:\n' + ' """ Bar """\n\n\n' + 'def baz():\n' + ' print(\'this is not tested\')\n\n' + 'def branch(cond1, cond2):\n' + ' if cond1:\n' + ' print(\'condition tested both ways\')\n' + ' if cond2:\n' + ' print(\'condition not tested both ways\')\n'), + 'name': 'project.py', + 'coverage': [1, 1, None, None, 1, None, None, + None, 1, 0, None, 1, 1, 1, 1, 1]}) + + assert_coverage(results[1], { + 'source': ('from project import branch\n' + 'from project import hello\n\n' + "if __name__ == '__main__':\n" + ' hello()\n' + ' branch(False, True)\n' + ' branch(True, True)\n'), + 'name': 'runtests.py', + 'coverage': [1, 1, None, 1, 1, 1, 1]}) + + def test_reporter_with_base_dir_trailing_sep(self): + subprocess.call(['coverage', 'run', '--omit=**/.tox/*', + 'example/runtests.py'], cwd=BASE_DIR) + + # without base_dir arg, file name is prefixed with 'example/' + os.chdir(BASE_DIR) + results = Coveralls(repo_token='xxx', + base_dir='example/').get_coverage() + assert len(results) == 2 + + assert_coverage(results[0], { + 'source': ('def hello():\n' + ' print(\'world\')\n\n\n' + 'class Foo:\n' + ' """ Bar """\n\n\n' + 'def baz():\n' + ' print(\'this is not tested\')\n\n' + 'def branch(cond1, cond2):\n' + ' if cond1:\n' + ' print(\'condition tested both ways\')\n' + ' if cond2:\n' + ' print(\'condition not tested both ways\')\n'), + 'name': 'project.py', + 'coverage': [1, 1, None, None, 1, None, None, + None, 1, 0, None, 1, 1, 1, 1, 1]}) + + assert_coverage(results[1], { + 'source': ('from project import branch\n' + 'from project import hello\n\n' + "if __name__ == '__main__':\n" + ' hello()\n' + ' branch(False, True)\n' + ' branch(True, True)\n'), + 'name': 'runtests.py', + 'coverage': [1, 1, None, 1, 1, 1, 1]}) + def test_reporter_with_branches(self): subprocess.call(['coverage', 'run', '--branch', '--omit=**/.tox/*', 'runtests.py'], cwd=EXAMPLE_DIR) diff --git a/tests/cli_test.py b/tests/cli_test.py index ebafb040..8e85cbce 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -11,6 +11,10 @@ EXC = CoverallsException('bad stuff happened') +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +EXAMPLE_DIR = os.path.join(BASE_DIR, 'example') + + def req_json(request): return json.loads(request.body.decode('utf-8')) @@ -104,7 +108,8 @@ def test_real(mock_wear, mock_log): def test_rcfile(mock_coveralls): coveralls.cli.main(argv=['--rcfile=coveragerc']) mock_coveralls.assert_called_with(True, config_file='coveragerc', - service_name=None) + service_name=None, + base_dir='') @mock.patch.dict(os.environ, {}, clear=True) @@ -112,7 +117,8 @@ def test_rcfile(mock_coveralls): def test_service_name(mock_coveralls): coveralls.cli.main(argv=['--service=travis-pro']) mock_coveralls.assert_called_with(True, config_file='.coveragerc', - service_name='travis-pro') + service_name='travis-pro', + base_dir='') @mock.patch.object(coveralls.cli.log, 'exception') @@ -142,3 +148,20 @@ def test_save_report_to_file_no_token(mock_coveralls): """Check save_report api usage when token is not set.""" coveralls.cli.main(argv=['--output=test.log']) mock_coveralls.assert_called_with('test.log') + + +@mock.patch.object(coveralls.Coveralls, 'submit_report') +@mock.patch.dict(os.environ, {'TRAVIS': 'True'}, clear=True) +def test_submit(mock_submit): + json_file = os.path.join(EXAMPLE_DIR, 'example.json') + json_string = open(json_file).read() + coveralls.cli.main(argv=['--submit=' + json_file]) + mock_submit.assert_called_with(json_string) + + +@mock.patch('coveralls.cli.Coveralls') +def test_base_dir_arg(mock_coveralls): + coveralls.cli.main(argv=['--basedir=foo']) + mock_coveralls.assert_called_with(True, config_file='.coveragerc', + service_name=None, + base_dir='foo')