pytestomatio 2.8.2.dev36__tar.gz → 2.8.2.dev38__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytestomatio-2.8.2.dev36/pytestomatio.egg-info → pytestomatio-2.8.2.dev38}/PKG-INFO +1 -2
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pyproject.toml +3 -5
- pytestomatio-2.8.2.dev38/pytestomatio/connect/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/pytestomatio/connect/connector.py +190 -0
- pytestomatio-2.8.2.dev38/pytestomatio/connect/s3_connector.py +69 -0
- pytestomatio-2.8.2.dev38/pytestomatio/decor/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/pytestomatio/decor/decorator_updater.py +15 -0
- pytestomatio-2.8.2.dev38/pytestomatio/decor/default.py +85 -0
- pytestomatio-2.8.2.dev38/pytestomatio/decor/pep8.py +79 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio/main.py +6 -5
- pytestomatio-2.8.2.dev38/pytestomatio/testing/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testing/code_collector.py +15 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testing/testItem.py +144 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testomatio/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testomatio/filter_plugin.py +52 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testRunConfig.py +80 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testomat_item.py +18 -0
- pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testomatio.py +29 -0
- pytestomatio-2.8.2.dev38/pytestomatio/utils/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/pytestomatio/utils/helper.py +98 -0
- pytestomatio-2.8.2.dev38/pytestomatio/utils/parser_setup.py +77 -0
- pytestomatio-2.8.2.dev38/pytestomatio/utils/validations.py +23 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38/pytestomatio.egg-info}/PKG-INFO +1 -2
- pytestomatio-2.8.2.dev38/pytestomatio.egg-info/SOURCES.txt +42 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/requires.txt +0 -1
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/top_level.txt +1 -0
- pytestomatio-2.8.2.dev38/tests/sub/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/tests/sub/sub_mob/__init__.py +0 -0
- pytestomatio-2.8.2.dev38/tests/sub/sub_mob/sub_sub_class_test.py +21 -0
- pytestomatio-2.8.2.dev38/tests/sub/sub_mob/sub_sub_test.py +30 -0
- pytestomatio-2.8.2.dev38/tests/sub/test_class_sub.py +20 -0
- pytestomatio-2.8.2.dev38/tests/sub/test_sub.py +30 -0
- pytestomatio-2.8.2.dev36/pytestomatio.egg-info/SOURCES.txt +0 -17
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/LICENSE +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/README.md +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/dependency_links.txt +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/entry_points.txt +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/setup.cfg +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_class_root.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_cli_param_test_id.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_cli_params.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_decorators.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_root.py +0 -0
- {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_xdist.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytestomatio
|
|
3
|
-
Version: 2.8.2.
|
|
3
|
+
Version: 2.8.2.dev38
|
|
4
4
|
Summary: Pytest plugin to sync test with testomat.io
|
|
5
5
|
Author: Oleksii Ostapov, TikoQA
|
|
6
6
|
Project-URL: Testomat.io, https://testomat.io/
|
|
@@ -19,7 +19,6 @@ Requires-Dist: boto3>=1.28.28
|
|
|
19
19
|
Requires-Dist: libcst==1.1.0
|
|
20
20
|
Requires-Dist: commitizen>=3.18.1
|
|
21
21
|
Requires-Dist: autopep8>=2.1.0
|
|
22
|
-
Requires-Dist: pytest-xdist>=3.6.1
|
|
23
22
|
|
|
24
23
|
[](https://github.com/support-ukraine/support-ukraine)
|
|
25
24
|
|
|
@@ -3,7 +3,6 @@ requires = ["setuptools>=65.5.1", "wheel"]
|
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[tool.setuptools.packages.find]
|
|
6
|
-
include = ["pytestomatio"]
|
|
7
6
|
exclude = [".github", "tests", "build", "dist", ".venv", "pytestomatio.egg-info", ".env", ".gitignore", "CHANGELOG.md"]
|
|
8
7
|
|
|
9
8
|
[tool.commitizen]
|
|
@@ -11,10 +10,10 @@ name = "cz_conventional_commits"
|
|
|
11
10
|
tag_format = "$version"
|
|
12
11
|
version_scheme = "pep440"
|
|
13
12
|
version_provider = "pep621"
|
|
14
|
-
update_changelog_on_bump =
|
|
13
|
+
update_changelog_on_bump = false
|
|
15
14
|
[project]
|
|
16
15
|
name = "pytestomatio"
|
|
17
|
-
version = "2.8.2.
|
|
16
|
+
version = "2.8.2.dev38"
|
|
18
17
|
|
|
19
18
|
dependencies = [
|
|
20
19
|
"requests>=2.29.0",
|
|
@@ -22,8 +21,7 @@ dependencies = [
|
|
|
22
21
|
"boto3>=1.28.28",
|
|
23
22
|
"libcst==1.1.0",
|
|
24
23
|
"commitizen>=3.18.1",
|
|
25
|
-
"autopep8>=2.1.0"
|
|
26
|
-
"pytest-xdist>=3.6.1"
|
|
24
|
+
"autopep8>=2.1.0"
|
|
27
25
|
]
|
|
28
26
|
|
|
29
27
|
authors = [
|
|
File without changes
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from requests.exceptions import HTTPError, ConnectionError
|
|
3
|
+
import logging
|
|
4
|
+
from os.path import join, normpath
|
|
5
|
+
from os import getenv
|
|
6
|
+
from pytestomatio.utils.helper import safe_string_list
|
|
7
|
+
from pytestomatio.testing.testItem import TestItem
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger('pytestomatio')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Connector:
|
|
13
|
+
def __init__(self, base_url: str = '', api_key: str = None):
|
|
14
|
+
self.base_url = base_url
|
|
15
|
+
self.session = requests.Session()
|
|
16
|
+
self.session.verify = True
|
|
17
|
+
self.jwt: str = ''
|
|
18
|
+
self.api_key = api_key
|
|
19
|
+
|
|
20
|
+
def load_tests(
|
|
21
|
+
self,
|
|
22
|
+
tests: list[TestItem],
|
|
23
|
+
no_empty: bool = False,
|
|
24
|
+
no_detach: bool = False,
|
|
25
|
+
structure: bool = False,
|
|
26
|
+
create: bool = False,
|
|
27
|
+
directory: str = None
|
|
28
|
+
):
|
|
29
|
+
request = {
|
|
30
|
+
"framework": "pytest",
|
|
31
|
+
"language": "python",
|
|
32
|
+
"noempty": no_empty,
|
|
33
|
+
"no-detach": no_detach,
|
|
34
|
+
"structure": structure if not no_empty else False,
|
|
35
|
+
"create": create,
|
|
36
|
+
"sync": True,
|
|
37
|
+
"tests": []
|
|
38
|
+
}
|
|
39
|
+
for test in tests:
|
|
40
|
+
request['tests'].append({
|
|
41
|
+
"name": test.sync_title,
|
|
42
|
+
"suites": [
|
|
43
|
+
test.class_name
|
|
44
|
+
],
|
|
45
|
+
"code": test.source_code,
|
|
46
|
+
"file": test.file_path if structure else (
|
|
47
|
+
test.file_name if directory is None else normpath(join(directory, test.file_name))),
|
|
48
|
+
"labels": safe_string_list(getenv('TESTOMATIO_SYNC_LABELS')),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = self.session.post(f'{self.base_url}/api/load?api_key={self.api_key}', json=request)
|
|
53
|
+
except ConnectionError:
|
|
54
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
55
|
+
return
|
|
56
|
+
except HTTPError:
|
|
57
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
58
|
+
return
|
|
59
|
+
except Exception as e:
|
|
60
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if response.status_code < 400:
|
|
64
|
+
log.info(f'Tests loaded to {self.base_url}')
|
|
65
|
+
else:
|
|
66
|
+
log.error(f'Failed to load tests to {self.base_url}. Status code: {response.status_code}')
|
|
67
|
+
|
|
68
|
+
def get_tests(self, test_metadata: list[TestItem]) -> dict:
|
|
69
|
+
# with safe_request('Failed to get test ids from testomat.io'):
|
|
70
|
+
response = self.session.get(f'{self.base_url}/api/test_data?api_key={self.api_key}')
|
|
71
|
+
return response.json()
|
|
72
|
+
|
|
73
|
+
def create_test_run(self, title: str, group_title, env: str, label: str, shared_run: bool, parallel, ci_build_url: str) -> dict | None:
|
|
74
|
+
request = {
|
|
75
|
+
"api_key": self.api_key,
|
|
76
|
+
"title": title,
|
|
77
|
+
"group_title": group_title,
|
|
78
|
+
"env": env,
|
|
79
|
+
"label": label,
|
|
80
|
+
"parallel": parallel,
|
|
81
|
+
"ci_build_url": ci_build_url,
|
|
82
|
+
}
|
|
83
|
+
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
84
|
+
try:
|
|
85
|
+
response = self.session.post(f'{self.base_url}/api/reporter', json=filtered_request)
|
|
86
|
+
except ConnectionError:
|
|
87
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
88
|
+
return
|
|
89
|
+
except HTTPError:
|
|
90
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
91
|
+
return
|
|
92
|
+
except Exception as e:
|
|
93
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if response.status_code == 200:
|
|
97
|
+
log.info(f'Test run created {response.json()["uid"]}')
|
|
98
|
+
return response.json()
|
|
99
|
+
|
|
100
|
+
def update_test_run(self, id: str, title: str, group_title,
|
|
101
|
+
env: str, label: str, shared_run: bool, parallel, ci_build_url: str) -> dict | None:
|
|
102
|
+
request = {
|
|
103
|
+
"api_key": self.api_key,
|
|
104
|
+
"title": title,
|
|
105
|
+
"group_title": group_title,
|
|
106
|
+
# "env": env, TODO: enabled when bug with 500 response fixed
|
|
107
|
+
# "label": label, TODO: enabled when bug with 500 response fixed
|
|
108
|
+
"parallel": parallel,
|
|
109
|
+
"ci_build_url": ci_build_url,
|
|
110
|
+
}
|
|
111
|
+
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
response = self.session.put(f'{self.base_url}/api/reporter/{id}', json=filtered_request)
|
|
115
|
+
except ConnectionError:
|
|
116
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
117
|
+
return
|
|
118
|
+
except HTTPError:
|
|
119
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
120
|
+
return
|
|
121
|
+
except Exception as e:
|
|
122
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if response.status_code == 200:
|
|
126
|
+
log.info(f'Test run updated {response.json()["uid"]}')
|
|
127
|
+
return response.json()
|
|
128
|
+
|
|
129
|
+
def update_test_status(self, run_id: str,
|
|
130
|
+
status: str,
|
|
131
|
+
title: str,
|
|
132
|
+
suite_title: str,
|
|
133
|
+
suite_id: str,
|
|
134
|
+
test_id: str,
|
|
135
|
+
message: str,
|
|
136
|
+
stack: str,
|
|
137
|
+
run_time: float,
|
|
138
|
+
artifacts: list[str],
|
|
139
|
+
steps: str,
|
|
140
|
+
code: str,
|
|
141
|
+
example: dict) -> None:
|
|
142
|
+
|
|
143
|
+
request = {
|
|
144
|
+
"status": status, # Enum: "passed" "failed" "skipped"
|
|
145
|
+
"title": title,
|
|
146
|
+
"suite_title": suite_title,
|
|
147
|
+
"suite_id": suite_id,
|
|
148
|
+
"test_id": test_id,
|
|
149
|
+
"message": message,
|
|
150
|
+
"stack": stack,
|
|
151
|
+
"run_time": run_time,
|
|
152
|
+
"example": example,
|
|
153
|
+
"artifacts": artifacts,
|
|
154
|
+
"steps": steps,
|
|
155
|
+
"code": code
|
|
156
|
+
}
|
|
157
|
+
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
158
|
+
try:
|
|
159
|
+
response = self.session.post(f'{self.base_url}/api/reporter/{run_id}/testrun?api_key={self.api_key}',
|
|
160
|
+
json=filtered_request)
|
|
161
|
+
except ConnectionError:
|
|
162
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
163
|
+
return
|
|
164
|
+
except HTTPError:
|
|
165
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
166
|
+
return
|
|
167
|
+
except Exception as e:
|
|
168
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
169
|
+
return
|
|
170
|
+
if response.status_code == 200:
|
|
171
|
+
log.info('Test status updated')
|
|
172
|
+
|
|
173
|
+
# TODO: I guess this class should be just an API client and used within testRun (testRunConfig)
|
|
174
|
+
def finish_test_run(self, run_id: str, is_final=False) -> None:
|
|
175
|
+
status_event = 'finish_parallel' if is_final else 'finish'
|
|
176
|
+
try:
|
|
177
|
+
self.session.put(f'{self.base_url}/api/reporter/{run_id}?api_key={self.api_key}',
|
|
178
|
+
json={"status_event": status_event})
|
|
179
|
+
except ConnectionError:
|
|
180
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
181
|
+
return
|
|
182
|
+
except HTTPError:
|
|
183
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
184
|
+
return
|
|
185
|
+
except Exception as e:
|
|
186
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
def disconnect(self):
|
|
190
|
+
self.session.close()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import logging
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
log.setLevel('INFO')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_endpoint(endpoint: str or None) -> str or None:
|
|
10
|
+
if endpoint is None:
|
|
11
|
+
return
|
|
12
|
+
if endpoint.startswith('https://'):
|
|
13
|
+
return endpoint[8:]
|
|
14
|
+
elif endpoint.startswith('http://'):
|
|
15
|
+
return endpoint[7:]
|
|
16
|
+
return endpoint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class S3Connector:
|
|
20
|
+
def __init__(self, aws_access_key_id: str or None = None,
|
|
21
|
+
aws_secret_access_key: str or None = None,
|
|
22
|
+
endpoint: str or None = None,
|
|
23
|
+
bucket_name: str or None = None):
|
|
24
|
+
|
|
25
|
+
self.endpoint = parse_endpoint(endpoint)
|
|
26
|
+
self.bucket_name = bucket_name
|
|
27
|
+
self.client = None
|
|
28
|
+
self._is_logged_in = False
|
|
29
|
+
self.aws_access_key_id = aws_access_key_id
|
|
30
|
+
self.aws_secret_access_key = aws_secret_access_key
|
|
31
|
+
|
|
32
|
+
def login(self):
|
|
33
|
+
log.debug('creating s3 session')
|
|
34
|
+
self.client = boto3.client(
|
|
35
|
+
's3',
|
|
36
|
+
endpoint_url=f'https://{self.endpoint}',
|
|
37
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
38
|
+
aws_secret_access_key=self.aws_secret_access_key)
|
|
39
|
+
self._is_logged_in = True
|
|
40
|
+
log.info('s3 session created')
|
|
41
|
+
|
|
42
|
+
def upload_file(self, file_path: str, key: str = None, bucket_name: str = None) -> str or None:
|
|
43
|
+
if not self._is_logged_in:
|
|
44
|
+
log.warning('s3 session is not created, creating new one')
|
|
45
|
+
return
|
|
46
|
+
if not key:
|
|
47
|
+
key = file_path
|
|
48
|
+
if not bucket_name:
|
|
49
|
+
bucket_name = self.bucket_name
|
|
50
|
+
if bucket_name is None:
|
|
51
|
+
raise Exception('bucket name is not defined')
|
|
52
|
+
log.info(f'uploading artifact {file_path} to s3://{bucket_name}/{key}')
|
|
53
|
+
self.client.upload_file(file_path, bucket_name, key)
|
|
54
|
+
log.info(f'artifact {file_path} uploaded to s3://{bucket_name}/{key}')
|
|
55
|
+
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
56
|
+
|
|
57
|
+
def upload_file_object(self, file_bytes: bytes, key: str, bucket_name: str = None) -> str or None:
|
|
58
|
+
if not self._is_logged_in:
|
|
59
|
+
log.warning('s3 session is not created, creating new one')
|
|
60
|
+
return
|
|
61
|
+
file = BytesIO(file_bytes)
|
|
62
|
+
if not bucket_name:
|
|
63
|
+
bucket_name = self.bucket_name
|
|
64
|
+
if bucket_name is None:
|
|
65
|
+
raise Exception('bucket name is not defined')
|
|
66
|
+
log.info(f'uploading artifact {key} to s3://{bucket_name}/{key}')
|
|
67
|
+
self.client.upload_fileobj(file, bucket_name, key)
|
|
68
|
+
log.info(f'artifact {key} uploaded to s3://{bucket_name}/{key}')
|
|
69
|
+
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pytestomatio.decor.pep8 import update_tests as update_tests_pep8
|
|
3
|
+
from pytestomatio.decor.default import update_tests as update_tests_default
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def update_tests(file: str,
|
|
7
|
+
mapped_tests: list[tuple[str, int]],
|
|
8
|
+
all_tests: list[str],
|
|
9
|
+
decorator_name: str,
|
|
10
|
+
remove=False):
|
|
11
|
+
code_style = os.getenv('TESTOMATIO_CODE_STYLE', 'default')
|
|
12
|
+
if code_style == 'pep8':
|
|
13
|
+
update_tests_pep8(file, mapped_tests, all_tests, decorator_name, remove)
|
|
14
|
+
else:
|
|
15
|
+
update_tests_default(file, mapped_tests, all_tests, decorator_name, remove)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import libcst as cst
|
|
2
|
+
from typing import List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DecoratorUpdater(cst.CSTTransformer):
|
|
6
|
+
def __init__(self, mapped_tests: List[Tuple[str, int]], all_tests: List[str], decorator_name: str):
|
|
7
|
+
self.mapped_tests = mapped_tests
|
|
8
|
+
self.all_tests = all_tests
|
|
9
|
+
self.decorator_name = decorator_name
|
|
10
|
+
|
|
11
|
+
def _get_id_by_title(self, title: str):
|
|
12
|
+
for pair in self.mapped_tests:
|
|
13
|
+
if pair[0] == title:
|
|
14
|
+
return pair[1]
|
|
15
|
+
|
|
16
|
+
def _remove_decorator(self, node: cst.FunctionDef) -> cst.FunctionDef:
|
|
17
|
+
node.decorator_list = [decorator for decorator in node.decorator_list if
|
|
18
|
+
not (isinstance(decorator, cst.Call) and decorator.func.attr == self.decorator_name)]
|
|
19
|
+
return node
|
|
20
|
+
|
|
21
|
+
def remove_decorators(self, tree: cst.Module) -> cst.Module:
|
|
22
|
+
for node in cst.walk(tree):
|
|
23
|
+
if isinstance(node, cst.FunctionDef):
|
|
24
|
+
self.visit_FunctionDef(node, remove=True)
|
|
25
|
+
return tree
|
|
26
|
+
|
|
27
|
+
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef:
|
|
28
|
+
if original_node.name.value in self.all_tests:
|
|
29
|
+
test_id = self._get_id_by_title(original_node.name.value)
|
|
30
|
+
if test_id is None:
|
|
31
|
+
return updated_node
|
|
32
|
+
|
|
33
|
+
deco_name = f'pytest.mark.{self.decorator_name}("{test_id}")'
|
|
34
|
+
decorator = cst.Decorator(decorator=cst.parse_expression(deco_name))
|
|
35
|
+
|
|
36
|
+
# Check if the decorator already exists
|
|
37
|
+
for existing_decorator in original_node.decorators:
|
|
38
|
+
if isinstance(existing_decorator.decorator, cst.Call) and \
|
|
39
|
+
isinstance(existing_decorator.decorator.func, cst.Attribute) and \
|
|
40
|
+
existing_decorator.decorator.func.attr.value == self.decorator_name:
|
|
41
|
+
# The decorator already exists, so we don't add it
|
|
42
|
+
return updated_node
|
|
43
|
+
|
|
44
|
+
# The decorator doesn't exist, so we add it
|
|
45
|
+
return updated_node.with_changes(decorators=[decorator] + list(updated_node.decorators))
|
|
46
|
+
return updated_node
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DecoratorRemover(cst.CSTTransformer):
|
|
50
|
+
def __init__(self, decorator_name: str):
|
|
51
|
+
self.decorator_name = decorator_name
|
|
52
|
+
|
|
53
|
+
def leave_Decorator(self, original_node: cst.Decorator, updated_node: cst.Decorator) -> Union[
|
|
54
|
+
cst.Decorator, cst.RemovalSentinel]:
|
|
55
|
+
if isinstance(original_node.decorator, cst.Call) and \
|
|
56
|
+
isinstance(original_node.decorator.func, cst.Attribute) and \
|
|
57
|
+
original_node.decorator.func.attr.value == self.decorator_name and \
|
|
58
|
+
isinstance(original_node.decorator.func.value, cst.Attribute) and \
|
|
59
|
+
original_node.decorator.func.value.attr.value == 'mark' and \
|
|
60
|
+
isinstance(original_node.decorator.func.value.value, cst.Name) and \
|
|
61
|
+
original_node.decorator.func.value.value.value == 'pytest':
|
|
62
|
+
return cst.RemovalSentinel.REMOVE
|
|
63
|
+
return updated_node
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_tests(file: str,
|
|
67
|
+
mapped_tests: List[Tuple[str, int]],
|
|
68
|
+
all_tests: List[str],
|
|
69
|
+
decorator_name: str,
|
|
70
|
+
remove=False):
|
|
71
|
+
with open(file, 'r') as f:
|
|
72
|
+
source_code = f.read()
|
|
73
|
+
|
|
74
|
+
tree = cst.parse_module(source_code)
|
|
75
|
+
transform = DecoratorUpdater(mapped_tests, all_tests, decorator_name)
|
|
76
|
+
if remove:
|
|
77
|
+
transform = DecoratorRemover(decorator_name)
|
|
78
|
+
tree = tree.visit(transform)
|
|
79
|
+
else:
|
|
80
|
+
transform = DecoratorUpdater(mapped_tests, all_tests, decorator_name)
|
|
81
|
+
tree = tree.visit(transform)
|
|
82
|
+
updated_source_code = tree.code
|
|
83
|
+
|
|
84
|
+
with open(file, "w") as file:
|
|
85
|
+
file.write(updated_source_code)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import autopep8
|
|
3
|
+
|
|
4
|
+
pytest_mark = 'pytest', 'mark'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DecoratorUpdater(ast.NodeTransformer):
|
|
8
|
+
def __init__(self, mapped_tests: list[tuple[str, int]], all_tests: list[str], decorator_name: str):
|
|
9
|
+
self.mapped_tests = mapped_tests
|
|
10
|
+
self.all_tests = all_tests
|
|
11
|
+
self.decorator_name = decorator_name
|
|
12
|
+
|
|
13
|
+
def _get_id_by_title(self, title: str):
|
|
14
|
+
for pair in self.mapped_tests:
|
|
15
|
+
if pair[0] == title:
|
|
16
|
+
return pair[1]
|
|
17
|
+
|
|
18
|
+
def _remove_decorator(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
19
|
+
node.decorator_list = [decorator for decorator in node.decorator_list if
|
|
20
|
+
not (isinstance(decorator, ast.Call) and decorator.func.attr == self.decorator_name)]
|
|
21
|
+
return node
|
|
22
|
+
|
|
23
|
+
def remove_decorators(self, tree: ast.Module) -> ast.Module:
|
|
24
|
+
for node in ast.walk(tree):
|
|
25
|
+
if isinstance(node, ast.FunctionDef):
|
|
26
|
+
self.visit_FunctionDef(node, remove=True)
|
|
27
|
+
return tree
|
|
28
|
+
|
|
29
|
+
def visit_FunctionDef(self, node: ast.FunctionDef, remove=False) -> ast.FunctionDef:
|
|
30
|
+
if remove:
|
|
31
|
+
return self._remove_decorator(node)
|
|
32
|
+
else:
|
|
33
|
+
if node.name in self.all_tests:
|
|
34
|
+
if not any(isinstance(decorator, ast.Call) and
|
|
35
|
+
decorator.func.attr == self.decorator_name
|
|
36
|
+
for decorator in node.decorator_list):
|
|
37
|
+
test_id = self._get_id_by_title(node.name)
|
|
38
|
+
deco_name = f'mark.{self.decorator_name}(\'{test_id}\')'
|
|
39
|
+
decorator = ast.Name(id=deco_name, ctx=ast.Load())
|
|
40
|
+
node.decorator_list = [decorator] + node.decorator_list
|
|
41
|
+
return node
|
|
42
|
+
|
|
43
|
+
def insert_pytest_mark_import(self, tree: ast.Module, module_name: str, decorator_name: str) -> None:
|
|
44
|
+
# Check if the import statement already exists
|
|
45
|
+
if not any(
|
|
46
|
+
isinstance(node, ast.ImportFrom) and
|
|
47
|
+
node.module == module_name and
|
|
48
|
+
any(alias.name == decorator_name for alias in node.names)
|
|
49
|
+
for node in tree.body
|
|
50
|
+
):
|
|
51
|
+
import_node = ast.ImportFrom(
|
|
52
|
+
module=module_name,
|
|
53
|
+
names=[ast.alias(name=decorator_name, asname=None)],
|
|
54
|
+
level=0
|
|
55
|
+
)
|
|
56
|
+
tree.body.insert(0, import_node)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def update_tests(file: str,
|
|
60
|
+
mapped_tests: list[tuple[str, int]],
|
|
61
|
+
all_tests: list[str],
|
|
62
|
+
decorator_name: str,
|
|
63
|
+
remove=False):
|
|
64
|
+
with open(file, 'r') as f:
|
|
65
|
+
source_code = f.read()
|
|
66
|
+
|
|
67
|
+
tree = ast.parse(source_code)
|
|
68
|
+
transform = DecoratorUpdater(mapped_tests, all_tests, decorator_name)
|
|
69
|
+
if remove:
|
|
70
|
+
transform.remove_decorators(tree)
|
|
71
|
+
else:
|
|
72
|
+
tree = transform.visit(tree)
|
|
73
|
+
transform.insert_pytest_mark_import(tree, *pytest_mark)
|
|
74
|
+
updated_source_code = ast.unparse(tree)
|
|
75
|
+
|
|
76
|
+
pep8_source_code = autopep8.fix_code(updated_source_code)
|
|
77
|
+
|
|
78
|
+
with open(file, "w") as file:
|
|
79
|
+
file.write(pep8_source_code)
|
|
@@ -2,13 +2,13 @@ import os, pytest, logging, json, time
|
|
|
2
2
|
|
|
3
3
|
from pytest import Parser, Session, Config, Item, CallInfo
|
|
4
4
|
from pytestomatio.connect.connector import Connector
|
|
5
|
-
from pytestomatio.decor.decorator_updater import update_tests
|
|
6
|
-
from pytestomatio.testing.testItem import TestItem
|
|
7
5
|
from pytestomatio.connect.s3_connector import S3Connector
|
|
6
|
+
from pytestomatio.testing.testItem import TestItem
|
|
7
|
+
from pytestomatio.decor.decorator_updater import update_tests
|
|
8
|
+
|
|
8
9
|
from pytestomatio.utils.helper import add_and_enrich_tests, get_test_mapping, collect_tests, read_env_s3_keys
|
|
9
10
|
from pytestomatio.utils.parser_setup import parser_options
|
|
10
11
|
from pytestomatio.utils import validations
|
|
11
|
-
from xdist.plugin import is_xdist_controller, get_xdist_worker_id
|
|
12
12
|
|
|
13
13
|
from pytestomatio.testomatio.testRunConfig import TestRunConfig
|
|
14
14
|
from pytestomatio.testomatio.testomatio import Testomatio
|
|
@@ -20,6 +20,7 @@ log.setLevel('INFO')
|
|
|
20
20
|
metadata_file = 'metadata.json'
|
|
21
21
|
decorator_name = 'testomatio'
|
|
22
22
|
testomatio = 'testomatio'
|
|
23
|
+
TESTOMATIO_URL = 'https://app.testomat.io'
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def pytest_addoption(parser: Parser) -> None:
|
|
@@ -42,11 +43,11 @@ def pytest_configure(config: Config):
|
|
|
42
43
|
if option == 'debug':
|
|
43
44
|
return
|
|
44
45
|
|
|
45
|
-
is_parallel = config.
|
|
46
|
+
is_parallel = hasattr(config.option, 'numprocesses')
|
|
46
47
|
|
|
47
48
|
pytest.testomatio = Testomatio(TestRunConfig(is_parallel))
|
|
48
49
|
|
|
49
|
-
url = os.environ.get('TESTOMATIO_URL') or config.getini('testomatio_url')
|
|
50
|
+
url = os.environ.get('TESTOMATIO_URL') or config.getini('testomatio_url') or TESTOMATIO_URL
|
|
50
51
|
project = os.environ.get('TESTOMATIO')
|
|
51
52
|
|
|
52
53
|
pytest.testomatio.connector = Connector(url, project)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_functions_source_by_name(abs_file_path: str, all_tests: list[str]):
|
|
6
|
+
spec = importlib.util.spec_from_file_location('name', abs_file_path)
|
|
7
|
+
module = importlib.util.module_from_spec(spec)
|
|
8
|
+
spec.loader.exec_module(module)
|
|
9
|
+
functions = inspect.getmembers(module, inspect.isfunction)
|
|
10
|
+
classes = inspect.getmembers(module, inspect.isclass)
|
|
11
|
+
for class_name, cls in classes:
|
|
12
|
+
functions += inspect.getmembers(cls, inspect.isfunction)
|
|
13
|
+
for function_name, function in functions:
|
|
14
|
+
if function_name in all_tests:
|
|
15
|
+
yield function_name, inspect.getsource(function)
|