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.
Files changed (45) hide show
  1. {pytestomatio-2.8.2.dev36/pytestomatio.egg-info → pytestomatio-2.8.2.dev38}/PKG-INFO +1 -2
  2. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pyproject.toml +3 -5
  3. pytestomatio-2.8.2.dev38/pytestomatio/connect/__init__.py +0 -0
  4. pytestomatio-2.8.2.dev38/pytestomatio/connect/connector.py +190 -0
  5. pytestomatio-2.8.2.dev38/pytestomatio/connect/s3_connector.py +69 -0
  6. pytestomatio-2.8.2.dev38/pytestomatio/decor/__init__.py +0 -0
  7. pytestomatio-2.8.2.dev38/pytestomatio/decor/decorator_updater.py +15 -0
  8. pytestomatio-2.8.2.dev38/pytestomatio/decor/default.py +85 -0
  9. pytestomatio-2.8.2.dev38/pytestomatio/decor/pep8.py +79 -0
  10. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio/main.py +6 -5
  11. pytestomatio-2.8.2.dev38/pytestomatio/testing/__init__.py +0 -0
  12. pytestomatio-2.8.2.dev38/pytestomatio/testing/code_collector.py +15 -0
  13. pytestomatio-2.8.2.dev38/pytestomatio/testing/testItem.py +144 -0
  14. pytestomatio-2.8.2.dev38/pytestomatio/testomatio/__init__.py +0 -0
  15. pytestomatio-2.8.2.dev38/pytestomatio/testomatio/filter_plugin.py +52 -0
  16. pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testRunConfig.py +80 -0
  17. pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testomat_item.py +18 -0
  18. pytestomatio-2.8.2.dev38/pytestomatio/testomatio/testomatio.py +29 -0
  19. pytestomatio-2.8.2.dev38/pytestomatio/utils/__init__.py +0 -0
  20. pytestomatio-2.8.2.dev38/pytestomatio/utils/helper.py +98 -0
  21. pytestomatio-2.8.2.dev38/pytestomatio/utils/parser_setup.py +77 -0
  22. pytestomatio-2.8.2.dev38/pytestomatio/utils/validations.py +23 -0
  23. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38/pytestomatio.egg-info}/PKG-INFO +1 -2
  24. pytestomatio-2.8.2.dev38/pytestomatio.egg-info/SOURCES.txt +42 -0
  25. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/requires.txt +0 -1
  26. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/top_level.txt +1 -0
  27. pytestomatio-2.8.2.dev38/tests/sub/__init__.py +0 -0
  28. pytestomatio-2.8.2.dev38/tests/sub/sub_mob/__init__.py +0 -0
  29. pytestomatio-2.8.2.dev38/tests/sub/sub_mob/sub_sub_class_test.py +21 -0
  30. pytestomatio-2.8.2.dev38/tests/sub/sub_mob/sub_sub_test.py +30 -0
  31. pytestomatio-2.8.2.dev38/tests/sub/test_class_sub.py +20 -0
  32. pytestomatio-2.8.2.dev38/tests/sub/test_sub.py +30 -0
  33. pytestomatio-2.8.2.dev36/pytestomatio.egg-info/SOURCES.txt +0 -17
  34. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/LICENSE +0 -0
  35. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/README.md +0 -0
  36. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio/__init__.py +0 -0
  37. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/dependency_links.txt +0 -0
  38. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/pytestomatio.egg-info/entry_points.txt +0 -0
  39. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/setup.cfg +0 -0
  40. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_class_root.py +0 -0
  41. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_cli_param_test_id.py +0 -0
  42. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_cli_params.py +0 -0
  43. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_decorators.py +0 -0
  44. {pytestomatio-2.8.2.dev36 → pytestomatio-2.8.2.dev38}/tests/test_root.py +0 -0
  45. {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.dev36
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
  [![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](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 = true
13
+ update_changelog_on_bump = false
15
14
  [project]
16
15
  name = "pytestomatio"
17
- version = "2.8.2.dev36"
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 = [
@@ -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}'
@@ -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.getoption('numprocesses') is not None
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)
@@ -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)