spl2-testing-framework 1.0.0__py3-none-any.whl

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 (32) hide show
  1. spl2_testing_framework/__init__.py +15 -0
  2. spl2_testing_framework/conftest.py +217 -0
  3. spl2_testing_framework/launch_remotely_spl2.py +85 -0
  4. spl2_testing_framework/logger_manager.py +26 -0
  5. spl2_testing_framework/pytest.ini.sample +16 -0
  6. spl2_testing_framework/single_spl2_file_runner.py +52 -0
  7. spl2_testing_framework/spl2_test_config.json +9 -0
  8. spl2_testing_framework/spl2_utils/assertions.spl2 +157 -0
  9. spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +36 -0
  10. spl2_testing_framework/test_runner.py +59 -0
  11. spl2_testing_framework/tools/__init__.py +15 -0
  12. spl2_testing_framework/tools/jobs/__init__.py +15 -0
  13. spl2_testing_framework/tools/jobs/cli_job.py +72 -0
  14. spl2_testing_framework/tools/jobs/cloud_job.py +107 -0
  15. spl2_testing_framework/tools/jobs/job.py +65 -0
  16. spl2_testing_framework/tools/jobs/splunk_job.py +112 -0
  17. spl2_testing_framework/tools/performance.py +252 -0
  18. spl2_testing_framework/tools/results.py +135 -0
  19. spl2_testing_framework/tools/search_clients/__init__.py +28 -0
  20. spl2_testing_framework/tools/search_clients/cli_search_client.py +87 -0
  21. spl2_testing_framework/tools/search_clients/cloud_search_client.py +84 -0
  22. spl2_testing_framework/tools/search_clients/search_client.py +150 -0
  23. spl2_testing_framework/tools/search_clients/splunk_search_client.py +81 -0
  24. spl2_testing_framework/tools/spl2test_runner.py +143 -0
  25. spl2_testing_framework/tools/test_discovery.py +220 -0
  26. spl2_testing_framework/tools/test_types.py +84 -0
  27. spl2_testing_framework/tools/utils.py +52 -0
  28. spl2_testing_framework-1.0.0.dist-info/LICENSE +201 -0
  29. spl2_testing_framework-1.0.0.dist-info/METADATA +190 -0
  30. spl2_testing_framework-1.0.0.dist-info/RECORD +32 -0
  31. spl2_testing_framework-1.0.0.dist-info/WHEEL +4 -0
  32. spl2_testing_framework-1.0.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
@@ -0,0 +1,72 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ from .job import Job
18
+
19
+
20
+ class CLIAssertionJob(Job):
21
+ """A job that is executed on CLI in unit tests."""
22
+
23
+ def create(self) -> "CLIAssertionJob":
24
+ """Creates a job. To run the job use run_job() method."""
25
+
26
+ cli = "spl2-processor-cli"
27
+ command = [
28
+ cli,
29
+ "test",
30
+ "-s",
31
+ self.test_name,
32
+ ]
33
+
34
+ input_data = "\n\n".join(
35
+ [
36
+ ( # for assertion tests we don't add assertions module
37
+ self.assertions_module
38
+ if not self.code_module_name == "assertions"
39
+ else ""
40
+ ),
41
+ self.commands_module,
42
+ self.code_module_content,
43
+ self.test_module,
44
+ ]
45
+ )
46
+ self.job_content = {"command": command, "input_data": input_data}
47
+ return self
48
+
49
+
50
+ class CLISimpleJob(Job):
51
+ """A job that is executed on CLI in box tests."""
52
+
53
+ def __init__(
54
+ self, source: str, code_module_name: str, code_module_content, test_name
55
+ ):
56
+ code_module_content += f"\n$source = from {str(source)};"
57
+ super().__init__("", code_module_name, code_module_content, test_name)
58
+
59
+ def create(self) -> "CLISimpleJob":
60
+ """Creates a job. To run the job use run_job() method."""
61
+
62
+ cli = "spl2-processor-cli"
63
+ command = [cli, "test", "-s", "pipeline"]
64
+
65
+ input_data = "\n\n".join(
66
+ [
67
+ self.commands_module,
68
+ self.code_module_content,
69
+ ]
70
+ )
71
+ self.job_content = {"command": command, "input_data": input_data}
72
+ return self
@@ -0,0 +1,107 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ from requests import Response
18
+
19
+ from .job import Job
20
+
21
+
22
+ class CloudJob(Job):
23
+ """A job that is executed on the cloud tenant in unit tests."""
24
+
25
+ def create(self):
26
+ """Creates a job. To run the job use run_job() method."""
27
+ self.job_content = {
28
+ "wipModules": {
29
+ "gdi.addons.spl2_content.dmx_utilities.assertions": {
30
+ "name": "assertions",
31
+ "definition": str(self.assertions_module),
32
+ "namespace": "/gdi.addons.spl2_content.dmx_utilities.assertions",
33
+ },
34
+ self.code_module_name: {
35
+ "name": self.code_module_name,
36
+ "definition": str(self.code_module_content),
37
+ },
38
+ },
39
+ "module": self.test_module,
40
+ "namespace": "",
41
+ "queryParameters": {
42
+ "defaults": {
43
+ "earliest": "-30m@m",
44
+ "latest": "now",
45
+ "profile": "ingestProcessor",
46
+ "collectFieldSummary": True,
47
+ "collectTimeBuckets": True,
48
+ "extractFields": "all",
49
+ "enablePreview": True,
50
+ "allowSideEffects": False,
51
+ },
52
+ self.test_name: {"saveAllResults": True},
53
+ },
54
+ }
55
+
56
+ return self
57
+
58
+ def _assign_ids_from_response(self, response: Response, metrics=False) -> None:
59
+ if not response.ok:
60
+ raise Exception(response.status_code, response.content)
61
+
62
+ self.ids[self.test_name] = response.json()["queryParameters"][self.test_name][
63
+ "sid"
64
+ ]
65
+
66
+
67
+ class CloudSimpleJob(Job):
68
+ """A job that is executed on the cloud tenant in box tests."""
69
+
70
+ def __init__(
71
+ self, source: str, code_module_name: str, code_module_content, test_name
72
+ ):
73
+ code_module_content += f"\n$source = from {str(source)};"
74
+ super().__init__("", code_module_name, code_module_content, test_name)
75
+
76
+ def create(self) -> "CloudSimpleJob":
77
+ """Creates a job. To run the job use run_job() method."""
78
+ self.job_content = {
79
+ "wipModules": {},
80
+ "module": self.code_module_content,
81
+ "namespace": "shared.pipelines",
82
+ "queryParameters": {
83
+ "defaults": {
84
+ "earliest": "-30m@m",
85
+ "latest": "now",
86
+ "profile": "ingestProcessor",
87
+ "collectFieldSummary": True,
88
+ "collectTimeBuckets": True,
89
+ "extractFields": "all",
90
+ "enablePreview": True,
91
+ "allowSideEffects": False,
92
+ },
93
+ "pipeline": {"saveAllResults": False},
94
+ },
95
+ }
96
+
97
+ return self
98
+
99
+ def _assign_ids_from_response(self, response: Response, metrics=False) -> None:
100
+ if not response.ok:
101
+ raise Exception(response.status_code, response.content)
102
+
103
+ for metric in ["destination", "metrics_destination"]:
104
+ try:
105
+ self.ids[metric] = response.json()["queryParameters"][metric]["sid"]
106
+ except KeyError:
107
+ pass
@@ -0,0 +1,65 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ from requests import Response
18
+
19
+ from ..utils import read_assertions_module, read_commands_modules
20
+
21
+
22
+ class Job:
23
+ """A job that can be executed in the testing framework.
24
+ Every pipeline that is tested in the framework is represented by a job,
25
+ It contains tested code, test code, some helper code modules (assertions and commands)
26
+ and job results"""
27
+
28
+ def __init__(
29
+ self,
30
+ test_content: str,
31
+ code_module_name: str,
32
+ code_module_content: str,
33
+ test_name: str,
34
+ ):
35
+ self._result = {}
36
+ self._ids = {}
37
+ self.assertions_module = read_assertions_module()
38
+ self.commands_module = read_commands_modules()
39
+ self.job_content = None
40
+
41
+ self.test_module = f"import * from {code_module_name}\n{test_content}"
42
+ self.code_module_name = code_module_name
43
+ self.code_module_content = code_module_content
44
+ self.test_name = test_name
45
+
46
+ @property
47
+ def ids(self) -> dict:
48
+ """IDs of jobs run on the Splunk server, IP/EP or CLI"""
49
+ return self._ids
50
+
51
+ @ids.setter
52
+ def ids(self, value: dict):
53
+ self._ids = value
54
+
55
+ def _assign_ids_from_response(self, response: Response):
56
+ raise NotImplementedError()
57
+
58
+ @property
59
+ def result(self) -> dict:
60
+ return self._result
61
+
62
+ @result.setter
63
+ def result(self, value: dict):
64
+ """Results of jobs run on the Splunk server, IP/EP or CLI"""
65
+ self._result = value
@@ -0,0 +1,112 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ from requests import Response
18
+
19
+ from .job import Job
20
+
21
+
22
+ class SplunkAssertionJob(Job):
23
+ """A job that is executed on the Splunk server in unit tests."""
24
+
25
+ def create(self) -> "SplunkAssertionJob":
26
+ """Creates a job. To run the job use run_job() method."""
27
+ self.job_content = {
28
+ "wipModules": {
29
+ "assertions": {
30
+ "name": "assertions",
31
+ "definition": str(self.assertions_module),
32
+ "namespace": "/gdi.addons.spl2_content.dmx_utilities.assertions",
33
+ },
34
+ "commands": {
35
+ "name": "commands",
36
+ "definition": str(self.commands_module),
37
+ "namespace": "commands",
38
+ },
39
+ self.code_module_name: {
40
+ "name": self.code_module_name,
41
+ "definition": str(self.code_module_content),
42
+ },
43
+ },
44
+ "module": self.test_module,
45
+ "namespace": "",
46
+ "queryParameters": {
47
+ "defaults": {
48
+ "earliest": "-30m@m",
49
+ "latest": "now",
50
+ "profile": "ingestProcessor",
51
+ "collectFieldSummary": True,
52
+ "collectTimeBuckets": True,
53
+ "extractFields": "all",
54
+ "enablePreview": True,
55
+ "allowSideEffects": False,
56
+ },
57
+ self.test_name: {"saveAllResults": True},
58
+ },
59
+ }
60
+
61
+ return self
62
+
63
+ def _assign_ids_from_response(self, response: Response) -> None:
64
+ if not response.ok:
65
+ raise Exception(response.status_code, response.content)
66
+
67
+ self.ids = {x["name"]: x["sid"] for x in response.json() if x["sid"]}
68
+
69
+
70
+ class SplunkSimpleJob(Job):
71
+ """A job that is executed on the Splunk server in box tests."""
72
+
73
+ def __init__(
74
+ self, source: str, code_module_name: str, code_module_content, test_name
75
+ ):
76
+ code_module_content += f"\n$source = from {str(source)};"
77
+ super().__init__("", code_module_name, code_module_content, test_name)
78
+
79
+ def create(self) -> "SplunkSimpleJob":
80
+ """Creates a job. To run the job use run_job() method."""
81
+ self.job_content = {
82
+ "wipModules": {
83
+ "commands": {
84
+ "name": "commands",
85
+ "definition": str(self.commands_module),
86
+ "namespace": "commands",
87
+ },
88
+ },
89
+ "module": self.code_module_content,
90
+ "namespace": "",
91
+ "queryParameters": {
92
+ "defaults": {
93
+ "earliest": "-30m@m",
94
+ "latest": "now",
95
+ "profile": None,
96
+ "collectFieldSummary": True,
97
+ "collectTimeBuckets": True,
98
+ "extractFields": "all",
99
+ "enablePreview": True,
100
+ "allowSideEffects": False,
101
+ },
102
+ "pipeline": {"saveAllResults": False},
103
+ },
104
+ }
105
+
106
+ return self
107
+
108
+ def _assign_ids_from_response(self, response: Response, metrics=False) -> None:
109
+ if not response.ok:
110
+ raise Exception(response.status_code, response.content)
111
+
112
+ self.ids = {x["name"]: x["sid"] for x in response.json() if x["sid"]}
@@ -0,0 +1,252 @@
1
+ #
2
+ # Copyright 2025 Splunk Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ import logging
18
+ import os
19
+ import re
20
+ from copy import copy
21
+ from datetime import datetime
22
+ from itertools import groupby
23
+ from typing import Tuple, List, Dict
24
+
25
+ from spl2_testing_framework.tools.test_types import Test
26
+
27
+ _LOGGER = logging.getLogger(__name__)
28
+
29
+
30
+ class PerformanceCheck:
31
+ """
32
+ Class responsible for adding timestamps and validating performance of SPL2 pipelines
33
+ For basic usecase (_performance_check_type == "time") it's showing the time of spl2 pipeline execution
34
+ For _performance_check_type == "detailed_time" it's printing pipeline code with added execution time for every
35
+ line, which consists code statement. The result of "detailed_time" execution is also saved to the text file.
36
+ """
37
+
38
+ OUTPUT_DIR = "PERF_CHECK"
39
+ PIPELINE_PATTERN = re.compile(
40
+ r"(?P<fullmatch>"
41
+ r"^\s*?\$pipeline\s*?=\s*?\|?\s*?from\s*?\$source\s*?" # first line of pipeline
42
+ r"(.|\n)*?" # anything in between
43
+ r"(\|\s*?into\s*?\$destination)" # last line of pipeline definition
44
+ r")",
45
+ flags=re.M | re.VERBOSE,
46
+ )
47
+
48
+ FUNCTION_PATTERN = re.compile(
49
+ r"^(?P<fullmatch>"
50
+ r"(export\s+)?function\s+(?P<f_name>\S+)\((?P<source>\S+):" # function header
51
+ r".*{\n\s+return\s+(\|\s+)?from\s+(?P=source)\n*" # return statement
52
+ r"(?P<statements>(.|\n)*?)" # anything before closing bracket
53
+ r"(?P<closing_bracket>^})" # closing bracket [ASSUMED IT'S IN THE NEW LINE]
54
+ r")",
55
+ flags=re.M | re.VERBOSE,
56
+ )
57
+
58
+ STATEMENT_PATTERN = re.compile(r"^(?P<lx>\s*?\|.*)$", re.M)
59
+
60
+ def __init__(self, test: Test):
61
+ self._test = test
62
+ self._performance_check_type = test.meta.get("performance", None)
63
+ self.code_module = None
64
+
65
+ def apply_code_transformations(self, code_module):
66
+ """Apply code transformations necessary for measuring performance.
67
+ Depending on perfromance_check_type (assigned to instance) it's adding code
68
+ for calculating timestamps to spl2 pipeline"""
69
+ if self._performance_check_type == "time":
70
+ code_module = self._performance_time_of_execution_(code_module)
71
+ elif self._performance_check_type == "detailed_time":
72
+ code_module = self._performance_timestamps(code_module)
73
+
74
+ self.code_module = code_module
75
+ return code_module
76
+
77
+ def check_performance(self, output_result):
78
+ """Check performance basing on timestamps in the output.
79
+ Depending on performance_check_type (assigned to instance) it's calculating time of execution
80
+ of spl2 pipeline or injecting timestamp values into the pipeline code"""
81
+ if self._performance_check_type == "time":
82
+ return self._check_time_of_execution(output_result)
83
+ elif self._performance_check_type == "detailed_time":
84
+ return self._check_profiling(output_result)
85
+
86
+ def _check_time_of_execution(self, output_result):
87
+ """Log time of execution to stdout and cleanup results."""
88
+ _LOGGER.info("Checking time of execution... ")
89
+
90
+ for event_no, single_output in enumerate(output_result):
91
+ time_of_execution = single_output.get("_timestamp_perf_exec", None)
92
+ _LOGGER.info(
93
+ f"Time of pipeline execution for event no {event_no}: {float(time_of_execution) * 1000}ms"
94
+ )
95
+
96
+ self._cleanup_result(output_result)
97
+
98
+ @staticmethod
99
+ def __find_group(timestamp):
100
+ """Finds a group of timestamps basing on function name, as their name consists it"""
101
+ m = re.search(r"_timestamp_(.+?)_\d{3}", timestamp[0])
102
+ return m.group(1)
103
+
104
+ @staticmethod
105
+ def __calculate_diff_for_timestamps(timestamps: List[List]) -> List[Tuple]:
106
+ """Calculate a time diff between consecutive timestamps and the relative"""
107
+ diffs = []
108
+ ts = sorted(
109
+ timestamps, key=lambda x: x[0]
110
+ ) # sort by name, to have the order correct
111
+ time_of_func_exec = ts[-1][1] - ts[0][1]
112
+ for start, end in zip(ts[:-1], ts[1:]):
113
+ diff: float = end[1] - start[1]
114
+ relative_time: float = diff / time_of_func_exec
115
+ diffs.append((start[0], diff, relative_time))
116
+
117
+ return diffs
118
+
119
+ def _check_profiling(self, output_result: List[Dict[str, str]]) -> List:
120
+ """Add timestamp values to pipeline code and cleanup results.
121
+ Timestamps are modified to be relative to 0 - which is the start of the pipeline.
122
+ """
123
+ _LOGGER.info("Checking profiling results... ")
124
+ outputs = []
125
+ for event_no, single_output in enumerate(output_result):
126
+ perf = [
127
+ [field, float(value)]
128
+ for field, value in single_output.items()
129
+ if field.startswith("_timestamp_")
130
+ ]
131
+
132
+ calculated_diffs = []
133
+ for _, v in groupby(perf, self.__find_group):
134
+ calculated_diffs.extend(self.__calculate_diff_for_timestamps(list(v)))
135
+
136
+ output = copy(self.code_module)
137
+
138
+ for timestamp_name, value, relative in calculated_diffs:
139
+ output = re.sub(
140
+ rf"\| eval {timestamp_name}=time\(\)",
141
+ f">>> {(value * 1000):.6f}ms [{relative:.2%}] <<< ",
142
+ output,
143
+ )
144
+
145
+ output = re.sub(r"\| eval _timestamp_\w+_\d{3}=time\(\)", r"", output)
146
+
147
+ _LOGGER.info(output)
148
+
149
+ self._save_output(output, event_no)
150
+ outputs.append(output)
151
+
152
+ self._cleanup_result(output_result)
153
+ return outputs
154
+
155
+ def _save_output(self, output: str, event_number: int) -> None:
156
+ """Save output to text file"""
157
+ os.makedirs(self.OUTPUT_DIR, exist_ok=True)
158
+ filename = f"{self.OUTPUT_DIR}/performance_{self._test.name}_{event_number}_{datetime.now()}.txt"
159
+
160
+ with open(filename, "w") as f:
161
+ f.write(output)
162
+
163
+ @staticmethod
164
+ def _cleanup_result(output_result: List) -> List:
165
+ """Remove all timestamps from output not to brake assertions"""
166
+ for single_result in output_result:
167
+ for k in list(single_result.keys()):
168
+ if k.startswith("_timestamp_perf"):
169
+ del single_result[k]
170
+
171
+ return output_result
172
+
173
+ @staticmethod
174
+ def _performance_time_of_execution_(code_module: str) -> str:
175
+ """Injects 3 timestamps to code - for beginning and end of pipeline execution
176
+ and also the difference of these 2.
177
+ """
178
+ code_module = re.sub(
179
+ r"(^\s*?\$pipeline\s*?=\s*?\|?\s*?from\s*?\$source\s*?)",
180
+ r"\1 | eval _timestamp_perf_start=time()",
181
+ code_module,
182
+ flags=re.M,
183
+ )
184
+
185
+ code_module = re.sub(
186
+ r"(\|\s*?into\s*?\$destination)",
187
+ r"| eval _timestamp_perf_stop = time()"
188
+ r"| eval _timestamp_perf_exec = _timestamp_perf_stop - _timestamp_perf_start \1",
189
+ code_module,
190
+ flags=re.M,
191
+ )
192
+
193
+ return code_module
194
+
195
+ def _performance_timestamps(self, code_module: str) -> str:
196
+ """Injects time of execution just before every command (In practise in every line which starts from "|")"""
197
+
198
+ functions = self.__parse_functions(code_module)
199
+ pipeline = self.__parse_pipeline(code_module)
200
+
201
+ functions.append(pipeline)
202
+
203
+ for f in functions:
204
+ new_function = self.__add_timestamps_to_function(f)
205
+ code_module = code_module.replace(f["fullmatch"], new_function)
206
+
207
+ return code_module
208
+
209
+ def __parse_functions(self, code_module: str) -> List[Dict]:
210
+ """Parse all functions from code module. The finding contains groups necessary for parsing it
211
+ and adding timestamps"""
212
+ functions = [m.groupdict() for m in self.FUNCTION_PATTERN.finditer(code_module)]
213
+ return functions
214
+
215
+ def __parse_pipeline(self, code_module: str) -> Dict:
216
+ """Parse pipeline definition from code module. The finding contains groups necessary for parsing it
217
+ and adding timestamps"""
218
+ pipeline = [m.groupdict() for m in self.PIPELINE_PATTERN.finditer(code_module)][
219
+ 0
220
+ ]
221
+ pipeline[
222
+ "f_name"
223
+ ] = "__main_pipeline__" # just to make it working with function timestamp parser
224
+ pipeline["closing_bracket"] = "NOTHING_THERE" # ^^^
225
+
226
+ return pipeline
227
+
228
+ def __add_timestamps_to_function(self, f: Dict) -> str:
229
+ """Add timestamps to all functions (and also correctly parsed pipeline definition)
230
+ Timestamps are parametrized using function name and numbered consecutively"""
231
+ new_function = self.STATEMENT_PATTERN.sub(
232
+ rf"| eval _time_{f['f_name']}_placeholder=time() \g<lx>", f["fullmatch"]
233
+ )
234
+
235
+ new_function = re.sub(
236
+ rf"^{f['closing_bracket']}",
237
+ rf"| eval _time_{f['f_name']}_placeholder=time() {f['closing_bracket']}",
238
+ new_function,
239
+ flags=re.M,
240
+ )
241
+
242
+ func_timestamps = re.findall(rf"_time_{f['f_name']}_placeholder", new_function)
243
+
244
+ for n, timestamp in enumerate(func_timestamps):
245
+ new_function = re.sub(
246
+ timestamp,
247
+ rf"_timestamp_perf{f['f_name']}_{n:03d}",
248
+ new_function,
249
+ count=1,
250
+ )
251
+
252
+ return new_function