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.
- spl2_testing_framework/__init__.py +15 -0
- spl2_testing_framework/conftest.py +217 -0
- spl2_testing_framework/launch_remotely_spl2.py +85 -0
- spl2_testing_framework/logger_manager.py +26 -0
- spl2_testing_framework/pytest.ini.sample +16 -0
- spl2_testing_framework/single_spl2_file_runner.py +52 -0
- spl2_testing_framework/spl2_test_config.json +9 -0
- spl2_testing_framework/spl2_utils/assertions.spl2 +157 -0
- spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +36 -0
- spl2_testing_framework/test_runner.py +59 -0
- spl2_testing_framework/tools/__init__.py +15 -0
- spl2_testing_framework/tools/jobs/__init__.py +15 -0
- spl2_testing_framework/tools/jobs/cli_job.py +72 -0
- spl2_testing_framework/tools/jobs/cloud_job.py +107 -0
- spl2_testing_framework/tools/jobs/job.py +65 -0
- spl2_testing_framework/tools/jobs/splunk_job.py +112 -0
- spl2_testing_framework/tools/performance.py +252 -0
- spl2_testing_framework/tools/results.py +135 -0
- spl2_testing_framework/tools/search_clients/__init__.py +28 -0
- spl2_testing_framework/tools/search_clients/cli_search_client.py +87 -0
- spl2_testing_framework/tools/search_clients/cloud_search_client.py +84 -0
- spl2_testing_framework/tools/search_clients/search_client.py +150 -0
- spl2_testing_framework/tools/search_clients/splunk_search_client.py +81 -0
- spl2_testing_framework/tools/spl2test_runner.py +143 -0
- spl2_testing_framework/tools/test_discovery.py +220 -0
- spl2_testing_framework/tools/test_types.py +84 -0
- spl2_testing_framework/tools/utils.py +52 -0
- spl2_testing_framework-1.0.0.dist-info/LICENSE +201 -0
- spl2_testing_framework-1.0.0.dist-info/METADATA +190 -0
- spl2_testing_framework-1.0.0.dist-info/RECORD +32 -0
- spl2_testing_framework-1.0.0.dist-info/WHEEL +4 -0
- 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
|