spl2-testing-framework 1.0.0b1__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.0b1.dist-info/LICENSE +201 -0
  29. spl2_testing_framework-1.0.0b1.dist-info/METADATA +190 -0
  30. spl2_testing_framework-1.0.0b1.dist-info/RECORD +32 -0
  31. spl2_testing_framework-1.0.0b1.dist-info/WHEEL +4 -0
  32. spl2_testing_framework-1.0.0b1.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,135 @@
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 json
18
+ from collections import UserDict
19
+ from datetime import datetime, timezone
20
+ from typing import Any
21
+
22
+
23
+ class Results(UserDict):
24
+ """Class responsible for parsing and storing results from Splunk, IP and CLI.
25
+ It's necessary to parse nested jsons and other python types to strings in order to compare them
26
+ """
27
+
28
+ def __init__(self, dictionary: dict = None, **kwargs):
29
+ super().__init__(dictionary, **kwargs)
30
+ self.data = self._cast_types(self.data)
31
+ self.data = self._remove_all_string_nulls_from_dicts(
32
+ self.data
33
+ ) # TODO -> remove
34
+
35
+ self._remove_timestamps_if_necessary()
36
+
37
+ @staticmethod
38
+ def _cast_types(data: Any):
39
+ """Casts everything into dicts / lists / strings.
40
+ This is necessary because Splunk, IP and CLI often return different types of data.
41
+ """
42
+ if isinstance(data, str) and (data.startswith("[") or data.startswith("{")):
43
+ try:
44
+ data = json.loads(data)
45
+ except Exception:
46
+ pass # If it fails, it's not a json, so it's just a normal string
47
+
48
+ if isinstance(data, list):
49
+ data = [Results._cast_types(item) for item in data]
50
+ if isinstance(data, dict):
51
+ for key, value in data.items():
52
+ data[key] = Results._cast_types(value)
53
+ if key == "_time":
54
+ data[key] = Results._convert_timestamp(value)
55
+ if isinstance(data, (int, float)):
56
+ data = str(data)
57
+
58
+ return data
59
+
60
+ @staticmethod
61
+ def _convert_timestamp(timestamp):
62
+ """We need to convert timestamp, as there are different formats returned by cloud / CLI / splunk"""
63
+ try:
64
+ ts = datetime.fromisoformat(timestamp)
65
+ except ValueError:
66
+ ts = datetime.fromtimestamp(float(timestamp), timezone.utc)
67
+
68
+ return ts.isoformat()
69
+
70
+ def __hash__(self):
71
+ return hash(json.dumps(self.data, sort_keys=True))
72
+
73
+ def _remove_timestamps_if_necessary(self):
74
+ """Compatibility workaround
75
+ Because IP is adding _has_time field and automatically adds timestamp=now() if there is no timestamp,
76
+ but CLI doesn't, we need to remove it from comparison.
77
+ """
78
+
79
+ if self.data.get("_has_time", "1") == "0":
80
+ self.data.pop("_has_time", None)
81
+ self.data.pop("_time", None)
82
+ else:
83
+ self.data.pop("_has_time", None)
84
+
85
+ @staticmethod
86
+ def _remove_all_string_nulls_from_dicts(
87
+ data,
88
+ ): # TODO -> remove, it's just a workaround for different behaviour of splunk, IP and CLI
89
+ """Because of different behaviour of Splunk, IP and CLI, we need to remove all "null" values,
90
+ which are returned by CLI
91
+ """
92
+ for k, v in list(
93
+ data.items()
94
+ ): # list(...) is to avoid changing dict size during iteration
95
+ if v == "null":
96
+ del data[k]
97
+ if isinstance(v, dict):
98
+ Results._remove_all_string_nulls_from_dicts(v)
99
+ if isinstance(v, list):
100
+ for item in v[:]:
101
+ if isinstance(item, dict):
102
+ Results._remove_all_string_nulls_from_dicts(item)
103
+
104
+ return data
105
+
106
+ def __lt__(self, other):
107
+ if "_raw" in self.data:
108
+ s = self.data.get("_raw")
109
+ o = other.data.get("_raw")
110
+ else:
111
+ s = self.data.get("name") # just a "workaround" for log_to_metrics func
112
+ o = other.data.get("name")
113
+ return json.dumps(s, sort_keys=True) < json.dumps(o, sort_keys=True)
114
+
115
+
116
+ class Metrics(Results):
117
+ """Class responsible for storing metrics from Splunk, IP and CLI.
118
+ Its main responsibility is to remove metric_ prefix from keys in order to compare
119
+ actual metric results with expected ones.
120
+ """
121
+
122
+ def __init__(self, dictionary: dict = None, **kwargs):
123
+ if not dictionary:
124
+ dictionary = {}
125
+ modified_dict = self._remove_metric_prefix(dictionary)
126
+ super().__init__(modified_dict, **kwargs)
127
+
128
+ @staticmethod
129
+ def _remove_metric_prefix(dictionary: dict = None):
130
+ new_data = {}
131
+ for key, value in dictionary.items():
132
+ if key.startswith("metric_"):
133
+ new_key = key[7:]
134
+ new_data[new_key] = value
135
+ return new_data
@@ -0,0 +1,28 @@
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
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ #
@@ -0,0 +1,87 @@
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
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ #
29
+
30
+
31
+ import json
32
+ import logging
33
+ import subprocess
34
+ from ..jobs.cli_job import CLIAssertionJob, CLISimpleJob
35
+ from ..jobs.job import Job
36
+ from .search_client import SearchClient
37
+
38
+ _LOGGER = logging.getLogger(__name__)
39
+
40
+
41
+ class CLISearchClient(SearchClient):
42
+ """Search client responsible for running jobs on CLI."""
43
+
44
+ jobs_dict = {}
45
+
46
+ @staticmethod
47
+ def create_job(
48
+ test_module: str, code_file_name: str, code_to_test: str, test_statement: str
49
+ ) -> Job:
50
+ """Creates a job for unit test run."""
51
+ return CLIAssertionJob(
52
+ test_module, code_file_name, code_to_test, test_statement
53
+ ).create()
54
+
55
+ @staticmethod
56
+ def create_simple_job(
57
+ source: str, code_module_name: str, code_module_content, test_name
58
+ ) -> Job:
59
+ """Creates a job for box test run."""
60
+
61
+ return CLISimpleJob(
62
+ source, code_module_name, code_module_content, test_name
63
+ ).create()
64
+
65
+ def run_job(self, job):
66
+ """Run a job on CLI."""
67
+ result = subprocess.run(
68
+ job.job_content["command"],
69
+ input=job.job_content["input_data"], # Pass data to stdin
70
+ text=True, # Encode input_data as string
71
+ capture_output=True,
72
+ )
73
+
74
+ if result.stderr:
75
+ raise RuntimeError(result.stderr)
76
+
77
+ job.result = json.loads(result.stdout)
78
+ return
79
+
80
+ def get_job_results(self, job: Job) -> dict:
81
+ """Get results of a job."""
82
+ tmp = { # remove job_ prefix
83
+ k.replace("job_", ""): v for k, v in job.result.items()
84
+ } # TODO for python>3.9 use str.remove_prefix
85
+
86
+ results = self._cast_results(tmp)
87
+ return results
@@ -0,0 +1,84 @@
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
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ #
29
+
30
+ #
31
+ #
32
+
33
+ import time
34
+ import requests
35
+ from requests import Response
36
+
37
+ from ..jobs.job import Job
38
+ from ..jobs.cloud_job import CloudJob, CloudSimpleJob
39
+ from .search_client import HTTPSSearchClient
40
+
41
+
42
+ class CloudSearchClient(HTTPSSearchClient):
43
+ """Search client responsible for running jobs on cloud tenant."""
44
+
45
+ DISPATCH_SERVICE = "search/v3alpha1/dispatch"
46
+ JOBS_SERVICE = "search/v3alpha1/jobs"
47
+
48
+ def _get_job_results_url(self, job_id: str) -> str:
49
+ return f"{self.jobs_url}/{job_id}/results"
50
+
51
+ @staticmethod
52
+ def create_job(
53
+ test_module: str, code_file_name: str, code_to_test: str, test_statement: str
54
+ ) -> Job:
55
+ """Creates a job for unit test run."""
56
+ return CloudJob(
57
+ test_module, code_file_name, code_to_test, test_statement
58
+ ).create()
59
+
60
+ @staticmethod
61
+ def create_simple_job(
62
+ source: str, code_module_name: str, code_module_content, test_name
63
+ ) -> Job:
64
+ """Creates a job for box test run."""
65
+ return CloudSimpleJob(
66
+ source, code_module_name, code_module_content, test_name
67
+ ).create()
68
+
69
+ def _wait_for_job_status(self, job_id: str) -> Response:
70
+ time.sleep(2)
71
+
72
+ while True:
73
+ response = requests.get(
74
+ self._get_job_details_url(job_id), verify=False, auth=self.auth
75
+ )
76
+ if response.status_code != 200:
77
+ if "GET_PIPELINE_NOT_FOUND" in response.text:
78
+ continue
79
+ raise Exception(response.text)
80
+ if response.json()["status"] == "failed":
81
+ raise Exception(f"Job failed, id: {job_id}")
82
+ if response.json()["status"] == "done":
83
+ return response
84
+ time.sleep(1)
@@ -0,0 +1,150 @@
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
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ #
29
+
30
+ #
31
+ #
32
+
33
+ import json
34
+ import time
35
+ from typing import Dict, Union, List
36
+
37
+ import requests
38
+ from requests import Response
39
+
40
+ from ..jobs.job import Job
41
+ from ..results import Results, Metrics
42
+ from abc import ABC, abstractmethod
43
+
44
+
45
+ class SearchClient(ABC):
46
+ def __init__(self):
47
+ pass
48
+
49
+ @abstractmethod
50
+ def run_job(self, job: Job):
51
+ pass
52
+
53
+ @abstractmethod
54
+ def get_job_results(self, job: Job) -> Dict[str, Results]:
55
+ pass
56
+
57
+ @staticmethod
58
+ @abstractmethod
59
+ def create_job(*args, **kwargs):
60
+ pass
61
+
62
+ @staticmethod
63
+ @abstractmethod
64
+ def create_simple_job(*args, **kwargs):
65
+ pass
66
+
67
+ @staticmethod
68
+ def _cast_results(results: dict) -> Dict[str, List[Union[Results, Metrics]]]:
69
+ """Cast results to Results / Metrics instances"""
70
+ rs = {}
71
+ for key, value in results.items():
72
+ if key == "metrics_destination":
73
+ rs[key] = [Metrics(x) for x in value]
74
+ else:
75
+ rs[key] = [Results(x) for x in value]
76
+
77
+ return rs
78
+
79
+
80
+ class HTTPSSearchClient(SearchClient):
81
+ """Base class for HTTP based search clients."""
82
+
83
+ @property
84
+ @abstractmethod
85
+ def DISPATCH_SERVICE(self):
86
+ pass
87
+
88
+ @property
89
+ @abstractmethod
90
+ def JOBS_SERVICE(self):
91
+ pass
92
+
93
+ def __init__(self, url: str, auth=None):
94
+ super().__init__()
95
+ self._base_url = url
96
+ self.auth = auth
97
+
98
+ def run_job(self, job: Job) -> Response:
99
+ """Run a job."""
100
+ response = requests.post(
101
+ self.dispatch_url,
102
+ data=json.dumps(job.job_content),
103
+ verify=False,
104
+ auth=self.auth,
105
+ )
106
+
107
+ job._assign_ids_from_response(response)
108
+
109
+ return response
110
+
111
+ @property
112
+ def url(self) -> str:
113
+ """Base URL of a Splunk instance / cloud tenant."""
114
+ return self._base_url
115
+
116
+ @property
117
+ def dispatch_url(self) -> str:
118
+ """URL of the dispatch service."""
119
+ return f"{self.url}/{self.DISPATCH_SERVICE}"
120
+
121
+ @property
122
+ def jobs_url(self) -> str:
123
+ """URL of the job service."""
124
+ return f"{self.url}/{self.JOBS_SERVICE}"
125
+
126
+ def _get_job_details_url(self, job_id: str) -> str:
127
+ return f"{self.jobs_url}/{job_id}"
128
+
129
+ def get_job_results(self, job: Job) -> Dict[str, Union[Results, Metrics]]:
130
+ """Get results of a job."""
131
+ for job_name, job_id in job.ids.items():
132
+ self._wait_for_job_status(job_id)
133
+
134
+ job_url = self._get_job_results_url(job_id)
135
+
136
+ response = requests.get(job_url, verify=False, auth=self.auth)
137
+ while response.status_code == 204:
138
+ time.sleep(1)
139
+ response = requests.get(job_url, verify=False, auth=self.auth)
140
+ if response.status_code != 200:
141
+ raise Exception(response.status_code, response.content)
142
+
143
+ job.result[job_name] = response.json()["results"]
144
+
145
+ return self._cast_results(job.result)
146
+
147
+ @staticmethod
148
+ @abstractmethod
149
+ def create_job(*args, **kwargs):
150
+ pass
@@ -0,0 +1,81 @@
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
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ #
29
+
30
+ #
31
+ #
32
+
33
+ import time
34
+ import requests
35
+ from requests import Response
36
+
37
+ from ..jobs.job import Job
38
+ from ..jobs.splunk_job import SplunkAssertionJob, SplunkSimpleJob
39
+ from .search_client import HTTPSSearchClient
40
+
41
+
42
+ class SplunkSearchClient(HTTPSSearchClient):
43
+ """Search client responsible for running jobs on Splunk instance"""
44
+
45
+ DISPATCH_SERVICE = "servicesNS/admin/-/search/spl2-module-dispatch"
46
+ JOBS_SERVICE = "servicesNS/admin/-/search/jobs"
47
+
48
+ @staticmethod
49
+ def create_job(
50
+ test_module: str, code_file_name: str, code_to_test: str, test_statement: str
51
+ ) -> Job:
52
+ """Creates a job for unit test run."""
53
+ return SplunkAssertionJob(
54
+ test_module, code_file_name, code_to_test, test_statement
55
+ ).create()
56
+
57
+ @staticmethod
58
+ def create_simple_job(
59
+ source: str, code_module_name: str, code_module_content, test_name
60
+ ) -> Job:
61
+ """Creates a job for box test run."""
62
+ return SplunkSimpleJob(
63
+ source, code_module_name, code_module_content, test_name
64
+ ).create()
65
+
66
+ def _get_job_results_url(self, job_id: str) -> str:
67
+ return f"{self.jobs_url}/{job_id}/results?output_mode=json"
68
+
69
+ def _wait_for_job_status(self, job_id: str) -> Response:
70
+ job_url = self._get_job_details_url(job_id)
71
+
72
+ counter = 0
73
+ while True:
74
+ response = requests.get(job_url, verify=False, auth=self.auth)
75
+ if response.status_code != 200:
76
+ counter += 1
77
+ if response.status_code == 200:
78
+ return response
79
+ time.sleep(1)
80
+ if counter == 5:
81
+ raise Exception(response.text)