Skip to content
This repository was archived by the owner on Nov 12, 2024. It is now read-only.

Commit 6ef2efb

Browse files
authored
Merge pull request #18 from cqse/path_prefix_option
Path prefix option
2 parents 98066d7 + 03dad27 commit 6ef2efb

2 files changed

Lines changed: 110 additions & 29 deletions

File tree

teamscale_precommit_client/precommit_client.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
11
from __future__ import absolute_import
2-
from __future__ import unicode_literals
32
from __future__ import print_function
3+
from __future__ import unicode_literals
44

5+
import argparse
6+
import copy
57
import datetime
6-
import time
78
import os
89
import sys
9-
import argparse
10+
import time
1011

11-
from teamscale_precommit_client.git_utils import get_current_branch, get_current_timestamp
12-
from teamscale_precommit_client.git_utils import get_changed_files_and_content, get_deleted_files
13-
from teamscale_precommit_client.data import PreCommitUploadData
1412
from teamscale_client import TeamscaleClient
13+
1514
from teamscale_precommit_client.client_configuration_utils import get_teamscale_client_configuration
15+
from teamscale_precommit_client.data import PreCommitUploadData
16+
from teamscale_precommit_client.git_utils import get_changed_files_and_content, get_deleted_files
17+
from teamscale_precommit_client.git_utils import get_current_branch, get_current_timestamp
1618
from teamscale_precommit_client.git_utils import get_repo_root_from_file_in_repo
1719

1820
# Filename of the precommit configuration. The client expects this config file at the root of the repository.
1921
PRECOMMIT_CONFIG_FILENAME = '.teamscale-precommit.config'
22+
DEFAULT_PATH_PREFIX = ''
2023

2124

2225
class PrecommitClient:
2326
"""Client for precommit analysis"""
2427
# Number of seconds the client waits until fetching precommit results from the server.
2528
PRECOMMIT_WAITING_TIME_IN_SECONDS = 2
2629

27-
def __init__(self, teamscale_config, repository_path, analyzed_file=None, verify=True,
30+
def __init__(self, teamscale_config, repository_path, path_prefix=DEFAULT_PATH_PREFIX, analyzed_file=None,
31+
verify=True,
2832
omit_links_to_findings=False, exclude_findings_in_changed_code=False, fetch_existing_findings=False,
2933
fetch_all_findings=False, fetch_existing_findings_in_changes=False, fail_on_red_findings=False,
3034
log_to_stderr=False):
3135
"""Constructor"""
3236
self.teamscale_client = TeamscaleClient(teamscale_config.url, teamscale_config.username,
3337
teamscale_config.access_token, teamscale_config.project_id, verify)
3438
self.repository_path = repository_path
39+
# calling os.path.join ensures a tailing '/'
40+
self.path_prefix = os.path.join(path_prefix, '')
3541
self.analyzed_file = analyzed_file
3642
self.omit_links_to_findings = omit_links_to_findings
3743
self.exclude_findings_in_changed_code = exclude_findings_in_changed_code
@@ -58,7 +64,7 @@ def run(self):
5864

5965
if self.changed_files or self.deleted_files:
6066
self._do_precommit_analysis()
61-
self._print_precommit_results_as_error_string() # Always uses precommit branch
67+
self._print_precommit_results_as_error_string() # Always uses precommit branch
6268
elif not self.fetch_all_findings and not self.fetch_existing_findings:
6369
print("No changed files found. Did you forget to `git add` new files?")
6470
exit(0)
@@ -104,11 +110,22 @@ def _upload_precommit_data(self):
104110
self.teamscale_client.branch = self.current_branch
105111

106112
print("Uploading changes on branch '%s' in '%s'..." % (self.current_branch, self.repository_path))
107-
precommit_data = PreCommitUploadData(uniformPathToContentMap=self.changed_files,
108-
deletedUniformPaths=self.deleted_files)
113+
changed_files_with_path_prefix = self._apply_path_prefix_to_changed_files()
114+
deleted_files_with_path_prefix = self._apply_path_prefix_to_deleted_files()
115+
precommit_data = PreCommitUploadData(uniformPathToContentMap=changed_files_with_path_prefix,
116+
deletedUniformPaths=deleted_files_with_path_prefix)
109117
self.teamscale_client.upload_files_for_precommit_analysis(
110118
datetime.datetime.fromtimestamp(self.parent_commit_timestamp), precommit_data)
111119

120+
def _apply_path_prefix_to_changed_files(self):
121+
map_with_prefixes = {}
122+
for key in self.changed_files.keys():
123+
map_with_prefixes[self.path_prefix + key] = self.changed_files[key]
124+
return map_with_prefixes
125+
126+
def _apply_path_prefix_to_deleted_files(self):
127+
return list(map(lambda path: self.path_prefix + path, self.deleted_files))
128+
112129
def _wait_and_get_precommit_result(self):
113130
"""Gets the current precommit results. Waits synchronously until server is ready. """
114131
self.added_findings, self.removed_findings, self.findings_in_changed_code = \
@@ -182,14 +199,16 @@ def _format_findings(self, findings, branch):
182199
if len(findings) == 0:
183200
return ['> No findings.']
184201

185-
sorted_findings = sorted(findings)
202+
findings_without_path_prefix = list(
203+
map(lambda finding: self._copy_finding_without_path_prefix(finding), findings))
204+
sorted_findings = sorted(findings_without_path_prefix)
186205
return [self._format_message(finding) for finding in sorted_findings]
187206

188207
def _format_message(self, finding):
189208
location = os.path.join(self.repository_path, finding.uniformPath)
190209
severity = self._get_finding_severity_message(finding=finding)
191210
link = '%s&t=%s' % (self.teamscale_client.get_finding_url(finding),
192-
self.teamscale_client._get_timestamp_parameter(timestamp=None))
211+
self.teamscale_client._get_timestamp_parameter(timestamp=None))
193212

194213
message = finding.message
195214
if not self.omit_links_to_findings:
@@ -201,6 +220,15 @@ def _format_message(self, finding):
201220

202221
return '%s | (%s)' % (message, link)
203222

223+
def _copy_finding_without_path_prefix(self, finding):
224+
finding_without_path_prefix = copy.deepcopy(finding)
225+
finding_without_path_prefix.uniformPath = self._remove_path_prefix(finding_without_path_prefix.uniformPath)
226+
return finding_without_path_prefix
227+
228+
def _remove_path_prefix(self, path):
229+
if path.startswith(self.path_prefix):
230+
return path[len(self.path_prefix):]
231+
return path
204232

205233
@staticmethod
206234
def _get_finding_severity_message(finding):
@@ -243,6 +271,10 @@ def _parse_args():
243271
parser.add_argument('--log-to-stderr', dest='log_to_stderr', action='store_true',
244272
help='When this option is set, any finding will be logged to stderr instead of stdout: '
245273
'(default: False)')
274+
parser.add_argument('--path-prefix', metavar='PATH_PREFIX', type=str,
275+
help='Path prefix on Teamscale as configured with "Prepend repository identifier" or '
276+
'"Path prefix transformation". Please use "/" to separate folders.',
277+
default=DEFAULT_PATH_PREFIX)
246278
return parser.parse_args()
247279

248280

@@ -261,7 +293,8 @@ def _configure_precommit_client(parsed_args):
261293
repo_path = get_repo_root_from_file_in_repo(os.path.normpath(path_to_file_in_repo))
262294
config_file = os.path.join(repo_path, PRECOMMIT_CONFIG_FILENAME)
263295
config = get_teamscale_client_configuration(config_file)
264-
return PrecommitClient(config, repository_path=repo_path, analyzed_file=path_to_file_in_repo,
296+
return PrecommitClient(config, repository_path=repo_path, path_prefix=parsed_args.path_prefix,
297+
analyzed_file=path_to_file_in_repo,
265298
verify=parsed_args.verify, omit_links_to_findings=parsed_args.omit_links_to_findings,
266299
exclude_findings_in_changed_code=parsed_args.exclude_findings_in_changed_code,
267300
fetch_existing_findings=parsed_args.fetch_existing_findings,

tests/precommit_test.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import responses
21
import re
32
import sys
3+
from io import StringIO
4+
5+
import responses
46

57
# The mock package is only available from Python 3.3 onwards. Thank you, Python.
68
if sys.version_info >= (3, 3):
@@ -11,6 +13,7 @@
1113
from unittest import TestCase
1214
from teamscale_client.teamscale_client_config import TeamscaleClientConfig
1315
from teamscale_precommit_client import PrecommitClient
16+
from teamscale_precommit_client.precommit_client import DEFAULT_PATH_PREFIX
1417
from teamscale_client.utils import to_json
1518

1619
URL = 'http://localhost:8080'
@@ -19,7 +22,9 @@
1922
USERNAME = 'johndoe'
2023
ACCESS_TOKEN = 'secret'
2124
REPO_PATH = 'path/to/repo/'
22-
ANALYZED_FILE = REPO_PATH + 'file.ext'
25+
ANALYZED_FILE_NAME = 'file.ext'
26+
ANALYZED_FILE_PATH = REPO_PATH + ANALYZED_FILE_NAME
27+
DELETED_FILE_NAME = 'deletedFile.ext'
2328
CURRENT_BRANCH = 'my_feature_branch'
2429

2530

@@ -131,22 +136,56 @@ def test_get_added_and_existing_findings_in_changes_for_changes(self):
131136
self.assert_findings_ids(self.precommit_client.findings_in_changed_code, [])
132137
self.assert_findings_ids(self.precommit_client.existing_findings, [4, 5, 6, 7])
133138

139+
@responses.activate
140+
def test_adding_path_prefix(self):
141+
path_prefix = 'prefix'
142+
self.precommit_client = self._get_precommit_client(self._get_changed_file(),
143+
[DELETED_FILE_NAME], path_prefix=path_prefix)
144+
self.mock_precommit_findings_churn(added_findings=[1], path_prefix=path_prefix + '/')
145+
self.precommit_client.run()
146+
147+
changed_file_path_with_prefix = path_prefix + '/' + ANALYZED_FILE_NAME
148+
deleted_file_path_with_prefix = path_prefix + '/' + DELETED_FILE_NAME
149+
precommit_request = next(call.request for call in responses.calls if call.request.method == 'PUT')
150+
151+
# Check if the path prefix was applied and sent correctly in the request's body
152+
self.assertIn(changed_file_path_with_prefix, precommit_request.body)
153+
self.assertIn(deleted_file_path_with_prefix, precommit_request.body)
154+
155+
@responses.activate
156+
def test_stripping_path_prefix(self):
157+
path_prefix = 'prefix'
158+
self.precommit_client = self._get_precommit_client(self._get_changed_file(),
159+
[DELETED_FILE_NAME], path_prefix=path_prefix)
160+
self.mock_precommit_findings_churn(added_findings=[1], path_prefix=path_prefix + '/')
161+
162+
captured_output = StringIO()
163+
sys.stdout = captured_output
164+
165+
self.precommit_client.run()
166+
167+
# Check if the path prefix was applied and sent correctly in the request's body
168+
self.assertNotIn(path_prefix, captured_output.getvalue())
169+
134170
@staticmethod
135-
def mock_precommit_findings_churn(added_findings=None, findings_in_changed_code=None, removed_findings=None):
171+
def mock_precommit_findings_churn(added_findings=None, findings_in_changed_code=None, removed_findings=None,
172+
path_prefix=DEFAULT_PATH_PREFIX):
136173
"""Mocks returning the findings churn for the given added, removed, and findings in changed code.
137174
Findings can be provided as list of integers each of which represents a finding instance."""
138-
precommit_response = to_json(PrecommitClientTest._get_findings_churn(added_findings, findings_in_changed_code,
139-
removed_findings))
175+
precommit_response = to_json(
176+
PrecommitClientTest._get_findings_churn(path_prefix, added_findings, findings_in_changed_code,
177+
removed_findings))
140178
responses.add(responses.PUT, PrecommitClientTest.get_project_service_mock('pre-commit'), body=SUCCESS,
141179
status=200)
142180
responses.add(responses.GET, PrecommitClientTest.get_project_service_mock('pre-commit'),
143181
body=precommit_response, status=200, content_type="application/json", )
144182

145183
@staticmethod
146-
def mock_existing_findings(branch, existing_findings=None):
184+
def mock_existing_findings(branch, existing_findings=None, path_prefix=DEFAULT_PATH_PREFIX):
147185
"""Mocks returning the given existing findings for the provided branch.
148186
Findings can be provided as list of integers each of which represents a finding instance."""
149-
existing_findings_from_current_branch = to_json(PrecommitClientTest._get_findings_as_dicts(existing_findings))
187+
existing_findings_from_current_branch = to_json(
188+
PrecommitClientTest._get_findings_as_dicts(existing_findings, path_prefix))
150189
responses.add(responses.GET, PrecommitClientTest.get_project_service_mock('findings', branch),
151190
body=existing_findings_from_current_branch, status=200,
152191
content_type="application/json", )
@@ -162,13 +201,15 @@ def get_project_service_mock(service_id, branch=''):
162201
return re.compile(r'%s/p/%s/%s/.*%s.*' % (URL, PROJECT, service_id, branch))
163202

164203
@staticmethod
165-
def _get_precommit_client(changed_files, deleted_files, fetch_existing_findings=False,
204+
def _get_precommit_client(changed_files, deleted_files, path_prefix=DEFAULT_PATH_PREFIX,
205+
fetch_existing_findings=False,
166206
fetch_existing_findings_in_changes=False):
167207
"""Gets a precommit client some of whose methods are mocked out for testing."""
168208
responses.add(responses.GET, PrecommitClientTest.get_global_service_mock('service-api-info'), status=200,
169209
content_type="application/json", body='{"apiVersion": 6}')
170210
precommit_client = PrecommitClient(PrecommitClientTest._get_precommit_client_config(),
171-
repository_path=REPO_PATH, analyzed_file=ANALYZED_FILE, verify=False,
211+
repository_path=REPO_PATH, path_prefix=path_prefix,
212+
analyzed_file=ANALYZED_FILE_PATH, verify=False,
172213
omit_links_to_findings=True, fetch_existing_findings=fetch_existing_findings,
173214
fetch_existing_findings_in_changes=fetch_existing_findings_in_changes)
174215
precommit_client._calculate_modifications = Mock()
@@ -195,35 +236,42 @@ def _get_no_changed_files():
195236
@staticmethod
196237
def _get_changed_file():
197238
"""Helper that returns a single changed path and content."""
198-
return {ANALYZED_FILE: 'def foo():\n pass'}
239+
return {ANALYZED_FILE_NAME: 'def foo():\n pass'}
199240

200241
@staticmethod
201242
def _get_no_deleted_files():
202243
"""Helper that provides an empty list of deleted files."""
203244
return []
204245

205246
@staticmethod
206-
def _get_findings_churn(added_findings=None, findings_in_changed_code=None, removed_findings=None):
247+
def _get_findings_churn(path_prefix, added_findings=None, findings_in_changed_code=None, removed_findings=None):
207248
"""Returns the precommit findings churn as dict for the provided findings number list.
208249
Findings can be provided as list of integers each of which represents a finding instance."""
209-
return {"addedFindings": PrecommitClientTest._get_findings_as_dicts(added_findings),
210-
"findingsInChangedCode": PrecommitClientTest._get_findings_as_dicts(findings_in_changed_code),
211-
'removedFindings': PrecommitClientTest._get_findings_as_dicts(removed_findings)}
250+
return {"addedFindings": PrecommitClientTest._get_findings_as_dicts(added_findings, path_prefix),
251+
"findingsInChangedCode": PrecommitClientTest._get_findings_as_dicts(findings_in_changed_code,
252+
path_prefix),
253+
'removedFindings': PrecommitClientTest._get_findings_as_dicts(removed_findings, path_prefix)}
212254

213255
@staticmethod
214-
def _get_findings_as_dicts(findings_number_list):
256+
def _get_findings_as_dicts(findings_number_list, path_prefix):
215257
"""Transforms a list of integers each of which represents a finding instance into a list of dicts."""
216258
if not findings_number_list:
217259
return []
218260
findings_dicts = []
219261
for finding_number in findings_number_list:
220262
findings_dict = {'id': str(finding_number), 'typeId': 'id%i' % finding_number,
221263
'message': 'message%i' % finding_number, 'assessment': 'RED',
222-
'location': {'uniformPath': ANALYZED_FILE, 'rawStartLine': finding_number}}
264+
'location': {'uniformPath': path_prefix + ANALYZED_FILE_NAME,
265+
'rawStartLine': finding_number}}
223266
findings_dicts.append(findings_dict)
224267
return findings_dicts
225268

226269
def assert_findings_ids(self, actual_findings, expected_ids):
227270
"""Asserts that the given findings have the specified ids."""
228271
actual_findings_ids = [int(actual_finding.finding_id) for actual_finding in actual_findings]
229272
self.assertListEqual(actual_findings_ids, expected_ids)
273+
274+
def assert_findings_paths(self, actual_findings, expected_paths):
275+
"""Asserts that the given findings have the specified paths."""
276+
actual_findings_paths = [actual_finding.uniformPath for actual_finding in actual_findings]
277+
self.assertListEqual(actual_findings_paths, expected_paths)

0 commit comments

Comments
 (0)