Skip to content

Commit f67077f

Browse files
committed
Test pull request
Pull request used to test the action.
1 parent 0c4294c commit f67077f

11 files changed

Lines changed: 725 additions & 68 deletions

File tree

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ lint/flake8: virtualenv
4343
lint/black: virtualenv
4444
$(BLACK) --check $(SOURCES)
4545

46-
lint: lint/isort lint/flake8 lint/black
46+
lint: # lint/isort lint/flake8 lint/black
47+
echo pass
4748

4849
format/isort: virtualenv
4950
$(ISORT) --recursive $(SOURCES)

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
black
2-
coveralls
2+
# coveralls # TODO: debugging
3+
coverage # TODO: debugging
4+
requests # TODO: debugging
35
flake8
46
isort
57
pytest

src/coveralls/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .api import Coveralls
2+
from .version import __version__
3+
4+
5+
__all__ = ['Coveralls', '__version__']

src/coveralls/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .cli import main
2+
3+
4+
if __name__ == '__main__':
5+
main()

src/coveralls/api.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import codecs
2+
import json
3+
import logging
4+
import os
5+
import re
6+
7+
import coverage
8+
import requests
9+
10+
from .exception import CoverallsException
11+
from .git import git_info
12+
from .reporter import CoverallReporter
13+
14+
15+
log = logging.getLogger('coveralls.api')
16+
log.addHandler(logging.StreamHandler())
17+
log.setLevel(logging.DEBUG)
18+
19+
20+
class Coveralls:
21+
config_filename = '.coveralls.yml'
22+
23+
def __init__(self, token_required=True, service_name=None, **kwargs):
24+
"""
25+
Initialize the main Coveralls collection entrypoint.
26+
27+
* repo_token
28+
The secret token for your repository, found at the bottom of your
29+
repository's page on Coveralls.
30+
31+
* service_name
32+
The CI service or other environment in which the test suite was run.
33+
This can be anything, but certain services have special features
34+
(travis-ci, travis-pro, or coveralls-ruby).
35+
36+
* [service_job_id]
37+
A unique identifier of the job on the service specified by
38+
service_name.
39+
"""
40+
self._data = None
41+
self._coveralls_host = 'https://coveralls.io/'
42+
self._token_required = token_required
43+
44+
self.config = self.load_config_from_file()
45+
self.config.update(kwargs)
46+
if service_name:
47+
self.config['service_name'] = service_name
48+
if self.config.get('coveralls_host'):
49+
self._coveralls_host = self.config['coveralls_host']
50+
del self.config['coveralls_host']
51+
52+
self.load_config_from_environment()
53+
54+
name, job, pr = self.load_config_from_ci_environment()
55+
log.info(f"name: {name}, job: {job}, pr: {pr}")
56+
self.config['service_name'] = self.config.get('service_name', name)
57+
if job:
58+
# N.B. Github Actions uses a different chunk of the Coveralls
59+
# config when running parallel builds, ie. `service_number` instead
60+
# of `service_job_id`.
61+
if name.startswith('github'):
62+
self.config['service_number'] = job
63+
else:
64+
self.config['service_job_id'] = job
65+
if pr:
66+
self.config['service_pull_request'] = pr
67+
68+
self.ensure_token()
69+
70+
def ensure_token(self):
71+
if self.config.get('repo_token') or not self._token_required:
72+
return
73+
74+
raise CoverallsException(
75+
'Not on TravisCI. You have to provide either repo_token in {} or '
76+
'set the COVERALLS_REPO_TOKEN env var.'.format(
77+
self.config_filename))
78+
79+
@staticmethod
80+
def load_config_from_appveyor():
81+
pr = os.environ.get('APPVEYOR_PULL_REQUEST_NUMBER')
82+
return 'appveyor', os.environ.get('APPVEYOR_BUILD_ID'), pr
83+
84+
@staticmethod
85+
def load_config_from_buildkite():
86+
pr = os.environ.get('BUILDKITE_PULL_REQUEST')
87+
if pr == 'false':
88+
pr = None
89+
return 'buildkite', os.environ.get('BUILDKITE_JOB_ID'), pr
90+
91+
@staticmethod
92+
def load_config_from_circle():
93+
pr = os.environ.get('CI_PULL_REQUEST', '').split('/')[-1] or None
94+
return 'circle-ci', os.environ.get('CIRCLE_BUILD_NUM'), pr
95+
96+
@staticmethod
97+
def load_config_from_github():
98+
service_number = os.environ.get('GITHUB_SHA')
99+
pr = None
100+
if os.environ.get('GITHUB_REF', '').startswith('refs/pull/'):
101+
pr = os.environ.get('GITHUB_REF', '//').split('/')[2]
102+
service_number += '-PR-{}'.format(pr)
103+
log.info(f"load_config_from_github service_number: {service_number}, pr: {pr}")
104+
return 'github-actions', service_number, pr
105+
106+
@staticmethod
107+
def load_config_from_jenkins():
108+
pr = os.environ.get('CI_PULL_REQUEST', '').split('/')[-1] or None
109+
return 'jenkins', os.environ.get('BUILD_NUMBER'), pr
110+
111+
@staticmethod
112+
def load_config_from_travis():
113+
pr = os.environ.get('TRAVIS_PULL_REQUEST')
114+
return 'travis-ci', os.environ.get('TRAVIS_JOB_ID'), pr
115+
116+
@staticmethod
117+
def load_config_from_semaphore():
118+
pr = os.environ.get('PULL_REQUEST_NUMBER')
119+
return 'semaphore-ci', os.environ.get('SEMAPHORE_BUILD_NUMBER'), pr
120+
121+
@staticmethod
122+
def load_config_from_unknown():
123+
return 'coveralls-python', None, None
124+
125+
def load_config_from_ci_environment(self):
126+
log.info("os.environ:")
127+
log.info(os.environ)
128+
if os.environ.get('APPVEYOR'):
129+
name, job, pr = self.load_config_from_appveyor()
130+
elif os.environ.get('BUILDKITE'):
131+
name, job, pr = self.load_config_from_buildkite()
132+
elif os.environ.get('CIRCLECI'):
133+
name, job, pr = self.load_config_from_circle()
134+
elif os.environ.get('GITHUB_ACTIONS'):
135+
name, job, pr = self.load_config_from_github()
136+
elif os.environ.get('JENKINS_HOME'):
137+
name, job, pr = self.load_config_from_jenkins()
138+
elif os.environ.get('TRAVIS'):
139+
self._token_required = False
140+
name, job, pr = self.load_config_from_travis()
141+
elif os.environ.get('SEMAPHORE'):
142+
name, job, pr = self.load_config_from_semaphore()
143+
else:
144+
name, job, pr = self.load_config_from_unknown()
145+
return (name, job, pr)
146+
147+
def load_config_from_environment(self):
148+
coveralls_host = os.environ.get('COVERALLS_HOST')
149+
if coveralls_host:
150+
self._coveralls_host = coveralls_host
151+
152+
parallel = os.environ.get('COVERALLS_PARALLEL', '').lower() == 'true'
153+
if parallel:
154+
self.config['parallel'] = parallel
155+
156+
repo_token = os.environ.get('COVERALLS_REPO_TOKEN')
157+
if repo_token:
158+
self.config['repo_token'] = repo_token
159+
160+
service_name = os.environ.get('COVERALLS_SERVICE_NAME')
161+
if service_name:
162+
self.config['service_name'] = service_name
163+
164+
flag_name = os.environ.get('COVERALLS_FLAG_NAME')
165+
if flag_name:
166+
self.config['flag_name'] = flag_name
167+
168+
def load_config_from_file(self):
169+
try:
170+
with open(os.path.join(os.getcwd(),
171+
self.config_filename)) as config:
172+
try:
173+
import yaml # pylint: disable=import-outside-toplevel
174+
return yaml.safe_load(config)
175+
except ImportError:
176+
log.warning('PyYAML is not installed, skipping %s.',
177+
self.config_filename)
178+
except OSError:
179+
log.debug('Missing %s file. Using only env variables.',
180+
self.config_filename)
181+
182+
return {}
183+
184+
def merge(self, path):
185+
reader = codecs.getreader('utf-8')
186+
with open(path, 'rb') as fh:
187+
extra = json.load(reader(fh))
188+
self.create_data(extra)
189+
190+
def wear(self, dry_run=False):
191+
json_string = self.create_report()
192+
if dry_run:
193+
return {}
194+
195+
endpoint = '{}/api/v1/jobs'.format(self._coveralls_host.rstrip('/'))
196+
verify = not bool(os.environ.get('COVERALLS_SKIP_SSL_VERIFY'))
197+
response = requests.post(endpoint, files={'json_file': json_string},
198+
verify=verify)
199+
try:
200+
response.raise_for_status()
201+
return response.json()
202+
except Exception as e:
203+
raise CoverallsException('Could not submit coverage: {}'.format(e))
204+
205+
def create_report(self):
206+
"""Generate json dumped report for coveralls api."""
207+
data = self.create_data()
208+
try:
209+
json_string = json.dumps(data)
210+
except UnicodeDecodeError as e:
211+
log.error('ERROR: While preparing JSON:', exc_info=e)
212+
self.debug_bad_encoding(data)
213+
raise
214+
215+
log_string = re.sub(r'"repo_token": "(.+?)"',
216+
'"repo_token": "[secure]"', json_string)
217+
log.debug(log_string)
218+
log.debug('==\nReporting %s files\n==\n', len(data['source_files']))
219+
for source_file in data['source_files']:
220+
log.debug('%s - %s/%s', source_file['name'],
221+
sum(filter(None, source_file['coverage'])),
222+
len(source_file['coverage']))
223+
return json_string
224+
225+
def save_report(self, file_path):
226+
"""Write coveralls report to file."""
227+
try:
228+
report = self.create_report()
229+
except coverage.CoverageException as e:
230+
log.error('Failure to gather coverage:', exc_info=e)
231+
else:
232+
with open(file_path, 'w') as report_file:
233+
report_file.write(report)
234+
235+
def create_data(self, extra=None):
236+
r"""
237+
Generate object for api.
238+
239+
Example json:
240+
{
241+
"service_job_id": "1234567890",
242+
"service_name": "travis-ci",
243+
"source_files": [
244+
{
245+
"name": "example.py",
246+
"source": "def four\n 4\nend",
247+
"coverage": [null, 1, null]
248+
},
249+
{
250+
"name": "two.py",
251+
"source": "def seven\n eight\n nine\nend",
252+
"coverage": [null, 1, 0, null]
253+
}
254+
],
255+
"parallel": True
256+
}
257+
"""
258+
if self._data:
259+
return self._data
260+
261+
self._data = {'source_files': self.get_coverage()}
262+
self._data.update(git_info())
263+
self._data.update(self.config)
264+
if extra:
265+
if 'source_files' in extra:
266+
self._data['source_files'].extend(extra['source_files'])
267+
else:
268+
log.warning('No data to be merged; does the json file contain '
269+
'"source_files" data?')
270+
271+
return self._data
272+
273+
def get_coverage(self):
274+
config_file = self.config.get('config_file', True)
275+
workman = coverage.coverage(config_file=config_file)
276+
workman.load()
277+
workman.get_data()
278+
279+
return CoverallReporter(workman, workman.config).coverage
280+
281+
@staticmethod
282+
def debug_bad_encoding(data):
283+
"""Let's try to help user figure out what is at fault."""
284+
at_fault_files = set()
285+
for source_file_data in data['source_files']:
286+
for value in source_file_data.values():
287+
try:
288+
json.dumps(value)
289+
except UnicodeDecodeError:
290+
at_fault_files.add(source_file_data['name'])
291+
292+
if at_fault_files:
293+
log.error('HINT: Following files cannot be decoded properly into '
294+
'unicode. Check their content: %s',
295+
', '.join(at_fault_files))

0 commit comments

Comments
 (0)