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,143 @@
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
+
19
+ from tabulate import tabulate
20
+
21
+ from spl2_testing_framework.tools.performance import PerformanceCheck
22
+ from spl2_testing_framework.tools.test_types import UnitTest, BoxTest, SingleSPL2
23
+ from spl2_testing_framework.tools.utils import _make_functions_visible_for_testing
24
+
25
+ _LOGGER = logging.getLogger(__name__)
26
+
27
+
28
+ class SPL2TestRunner:
29
+ """Main class responsible for running tests"""
30
+
31
+ def __init__(self, search_client):
32
+ self._search_client = search_client
33
+
34
+ def run_single_spl2_file(self, single_spl2_file: SingleSPL2) -> None:
35
+ """Run a single spl2 file and print the output"""
36
+ code_to_test = _make_functions_visible_for_testing(
37
+ single_spl2_file.code_module_path
38
+ )
39
+ perf = PerformanceCheck(single_spl2_file)
40
+ code_to_test = perf.apply_code_transformations(code_to_test)
41
+
42
+ job = self._search_client.create_simple_job(
43
+ single_spl2_file.input,
44
+ single_spl2_file.code_module_path.stem,
45
+ code_to_test,
46
+ single_spl2_file.name,
47
+ )
48
+ self._search_client.run_job(job)
49
+ job_result = self._search_client.get_job_results(job)
50
+ output_result = job_result.get("destination", [])
51
+
52
+ perf.check_performance(output_result)
53
+
54
+ self.print_results(output_result, "RESULTS")
55
+ metrics_result = job_result.get("metrics_destination", [])
56
+ if len(metrics_result) > 0:
57
+ self.print_results(metrics_result, "METRICS RESULTS")
58
+
59
+ def print_results(self, results, type):
60
+ """Prints the results in cli and log file"""
61
+ separator = "#"
62
+ _LOGGER.info(f"\n\n{separator * 50} {type} {separator * 50}\n\n")
63
+ for item in results:
64
+ table = tabulate(item.items(), headers=["Field", "Value"], tablefmt="plain")
65
+ _LOGGER.info("\n" + "-" * 20 + "\n")
66
+ _LOGGER.info("\n" + table + "\n")
67
+ _LOGGER.info("\n" + "-" * 20 + "\n")
68
+
69
+ def run_unit_test(self, template_test: UnitTest) -> None:
70
+ """Run a unit test"""
71
+ code_to_test = _make_functions_visible_for_testing(
72
+ template_test.code_module_path
73
+ )
74
+
75
+ job = self._search_client.create_job(
76
+ template_test.content,
77
+ template_test.code_module_path.stem,
78
+ code_to_test,
79
+ template_test.name,
80
+ )
81
+
82
+ self._search_client.run_job(job)
83
+ job_results = self._search_client.get_job_results(job)
84
+
85
+ errors = []
86
+
87
+ for result in job_results[template_test.name]:
88
+ for case_result in result["_testResults"]["results"]:
89
+ received, expected = (
90
+ case_result.get("received", None),
91
+ case_result.get("expected", None),
92
+ )
93
+ _LOGGER.debug("Check details: %s", case_result)
94
+
95
+ if expected == received:
96
+ _LOGGER.info(
97
+ f'Check passed: {template_test.file_name}/{template_test.name}/{case_result["test"]}'
98
+ )
99
+ else:
100
+ errors.append(
101
+ {
102
+ "TEST_STATEMENT": case_result["test"],
103
+ "ACTUAL": received,
104
+ "EXPECTED": expected,
105
+ }
106
+ )
107
+
108
+ assert errors == [], f"Assertion errors: \n{errors}"
109
+
110
+ def run_box_test(self, box_test: BoxTest) -> None:
111
+ """Run a box test"""
112
+ code_to_test = _make_functions_visible_for_testing(box_test.code_module_path)
113
+
114
+ perf = PerformanceCheck(box_test)
115
+ code_to_test = perf.apply_code_transformations(code_to_test)
116
+
117
+ job = self._search_client.create_simple_job(
118
+ box_test.input, box_test.code_module_path.stem, code_to_test, box_test.name
119
+ )
120
+
121
+ self._search_client.run_job(job)
122
+ job_result = self._search_client.get_job_results(job)
123
+
124
+ output_result = job_result.get("destination", [])
125
+ metrics_result = job_result.get("metrics_destination", [])
126
+
127
+ perf.check_performance(output_result)
128
+
129
+ if box_test.output:
130
+ assert sorted(output_result) == sorted(box_test.output)
131
+ _LOGGER.info(f"Output check passed: {box_test.name}")
132
+ _LOGGER.debug("Received: \n%s", str(output_result))
133
+ _LOGGER.debug("Expected: \n%s", str(box_test.output))
134
+ else:
135
+ _LOGGER.info(f"Output check skipped (expected empty): {box_test.name}")
136
+
137
+ if box_test.metrics:
138
+ assert sorted(metrics_result) == sorted(box_test.metrics)
139
+ _LOGGER.info(f"Metric check passed: {box_test.name}")
140
+ _LOGGER.debug("Received: \n%s", str(metrics_result))
141
+ _LOGGER.debug("Expected: \n%s", str(box_test.metrics))
142
+ else:
143
+ _LOGGER.info(f"Metric check skipped (expected empty): {box_test.name}")
@@ -0,0 +1,220 @@
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 abc
18
+ import json
19
+ import logging
20
+ import os
21
+ import pathlib
22
+ import re
23
+
24
+ from spl2_testing_framework.tools.results import Metrics, Results
25
+ from spl2_testing_framework.tools.test_types import (
26
+ BoxTest,
27
+ SingleSPL2,
28
+ TestSuite,
29
+ UnitTest,
30
+ )
31
+
32
+ _LOGGER = logging.getLogger(__name__)
33
+
34
+
35
+ class SamplesNotFoundException(Exception):
36
+ """An Exception to be raised if samples are not found"""
37
+
38
+ pass
39
+
40
+
41
+ class TestDiscovery(abc.ABC):
42
+ """Base class for test discovery classes"""
43
+
44
+ def __init__(self, path: str, performance_check: str):
45
+ self.path = pathlib.Path(path)
46
+ self._performance_check = performance_check
47
+ self.tests = TestSuite()
48
+
49
+ def _read_tests(self, test_name: str) -> None:
50
+ tests_paths = self.path.rglob(test_name)
51
+ self._raw_tests = {
52
+ x: pathlib.Path(x).read_text(encoding="utf-8") for x in tests_paths
53
+ }
54
+
55
+
56
+ class BoxTestDiscovery(TestDiscovery):
57
+ """Discovers tests for box tests, looking into module.test.json files"""
58
+
59
+ def __init__(self, path: str, performance_check: str):
60
+ super().__init__(path, performance_check)
61
+
62
+ def discover_tests(self) -> None:
63
+ self._read_tests("module.test.json")
64
+ self._parse_tests()
65
+
66
+ def _parse_tests(self) -> None:
67
+ self._parsed_modules = {
68
+ x: json.loads(content) for x, content in self._raw_tests.items()
69
+ }
70
+
71
+ for module_path, content in self._parsed_modules.items():
72
+ path = module_path.parent
73
+ for test in content:
74
+ name = test["filename"].replace(".spl2", "")
75
+ test_input = test["test"]["source"]
76
+ code_module_path = path / test["filename"]
77
+ output = [
78
+ Results(x)
79
+ for x in test["test"].get("expected_destination_result", [])
80
+ ]
81
+ metrics = [
82
+ Metrics(x)
83
+ for x in test["test"].get("expected_metrics_destination_result", [])
84
+ ]
85
+ test = BoxTest(
86
+ name=name,
87
+ input=test_input,
88
+ output=output,
89
+ metrics=metrics,
90
+ module_path=module_path,
91
+ code_module_path=code_module_path,
92
+ )
93
+ test.meta["performance"] = self._performance_check
94
+ self.tests.add(test)
95
+
96
+
97
+ class SingleSPL2Discovery(TestDiscovery):
98
+ """Discovers the template file and its samples"""
99
+
100
+ def __init__(
101
+ self,
102
+ path: str,
103
+ template_file,
104
+ sample_file=None,
105
+ sample_delimiter="\n",
106
+ performance_check="no",
107
+ ):
108
+ super().__init__(path, performance_check)
109
+ self.template_file = template_file
110
+ self.sample_file = sample_file
111
+ self.sample_delimiter = sample_delimiter
112
+
113
+ def discover_tests(self) -> None:
114
+ if not self.sample_file:
115
+ self._read_tests("module.json")
116
+ self._parse_tests()
117
+
118
+ def _parse_tests(self) -> None:
119
+ name = None
120
+ code_module_path = None
121
+ if self.sample_file:
122
+ _LOGGER.info("Reading samples from provided sample file.")
123
+ events = self._read_samples_from_file(delimiter=self.sample_delimiter)
124
+ name = self.template_file.replace(".spl2", "")
125
+ code_module_path = next(self.path.rglob(self.template_file), None)
126
+ else:
127
+ _LOGGER.info("Reading samples from corresponding module.json file")
128
+ self._parsed_modules = {
129
+ x: json.loads(content) for x, content in self._raw_tests.items()
130
+ }
131
+ events = None
132
+ for module_path, content in self._parsed_modules.items():
133
+ path = module_path.parent
134
+ break_outer = False
135
+ for test in content:
136
+ if test["filename"] == os.path.basename(self.template_file):
137
+ name = test["filename"].replace(".spl2", "")
138
+ code_module_path = path / test["filename"]
139
+ events = test["context"]["events"]
140
+ if isinstance(events, str):
141
+ events = events.split("\r\n")
142
+ elif isinstance(events, list):
143
+ events = [event["_raw"] for event in events]
144
+ else:
145
+ _LOGGER.error("Invalid events found.")
146
+ raise SamplesNotFoundException(
147
+ "No valid sample events found. Exiting.."
148
+ )
149
+ break_outer = True
150
+ break
151
+ if break_outer:
152
+ break
153
+ if not events:
154
+ _LOGGER.error("No events found.")
155
+ raise SamplesNotFoundException("No valid sample events found. Exiting..")
156
+ _LOGGER.debug(f"Events collected: {events}")
157
+ test_input = self._parse_and_append_events(events)
158
+
159
+ test = SingleSPL2(
160
+ name=name,
161
+ input=test_input,
162
+ code_module_path=code_module_path,
163
+ )
164
+ test.meta["performance"] = self._performance_check
165
+
166
+ self.tests.add(test)
167
+
168
+ def _read_samples_from_file(self, delimiter):
169
+ with open(self.sample_file) as file:
170
+ events = filter(lambda ev: len(ev) > 0, file.read().split(delimiter))
171
+ updated_events = []
172
+ for event in events:
173
+ event = "".join([delimiter, event])
174
+ updated_events.append(event)
175
+ final_events = [
176
+ event.encode().decode("unicode_escape") for event in updated_events
177
+ ]
178
+ return final_events
179
+
180
+ def _parse_and_append_events(self, events):
181
+ formatted_events = ",".join(
182
+ [f"{{_raw: {json.dumps(event)} }}" for event in events]
183
+ )
184
+ template_module = f"[{formatted_events}]"
185
+ return template_module
186
+
187
+
188
+ class UTDiscovery(TestDiscovery):
189
+ """Discovers tests for unit tests, looking into .test.spl2 files"""
190
+
191
+ def __init__(self, path: str):
192
+ super().__init__(path, performance_check="no")
193
+
194
+ def discover_tests(self) -> None:
195
+ self._read_tests("*.test.spl2")
196
+ self._parse_tests()
197
+
198
+ def _parse_tests(self) -> None:
199
+ self.parsed = {
200
+ filename: re.findall(r"\$?(.*?__test)", test_file_content)
201
+ for filename, test_file_content in self._raw_tests.items()
202
+ }
203
+
204
+ for file, tests in self.parsed.items():
205
+ path = pathlib.Path(file)
206
+ file_name = path.name
207
+ content = path.read_text(encoding="utf-8")
208
+ code_module_name = file_name.replace(".test.", ".")
209
+ code_module_path = path.parent / code_module_name
210
+
211
+ for test in tests:
212
+ self.tests.add(
213
+ UnitTest(
214
+ name=test,
215
+ file_name=file_name,
216
+ file_path=path,
217
+ content=content,
218
+ code_module_path=code_module_path,
219
+ )
220
+ )
@@ -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
+ import logging
18
+ from collections import UserDict
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Dict, Iterator
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class Test:
28
+ name: str
29
+ meta: dict = field(default_factory=dict, init=False)
30
+
31
+ @property
32
+ def test_id(self) -> str:
33
+ return self.name
34
+
35
+
36
+ @dataclass
37
+ class SingleSPL2(Test):
38
+ input: str
39
+ code_module_path: Path
40
+
41
+
42
+ @dataclass
43
+ class BoxTest(Test):
44
+ input: str
45
+ output: list
46
+ module_path: Path
47
+ code_module_path: Path
48
+ metrics: list = field(default_factory=list)
49
+
50
+
51
+ @dataclass
52
+ class UnitTest(Test):
53
+ file_path: Path
54
+ file_name: str
55
+ content: str
56
+ code_module_path: Path
57
+
58
+ @property
59
+ def test_id(self) -> str:
60
+ return self.file_name + "__" + self.name
61
+
62
+
63
+ @dataclass
64
+ class TestSuite(UserDict):
65
+ tests: Dict[str, Test] = field(default_factory=dict)
66
+
67
+ def add(self, test: Test) -> None:
68
+ if test.test_id not in self.tests:
69
+ self.tests[test.test_id] = test
70
+ else:
71
+ _LOGGER.warning(
72
+ f"Test {test.test_id} already exists, name will be modified"
73
+ )
74
+ test_name = self.__calculate_new_name(test.test_id)
75
+ self.tests[test_name] = test
76
+
77
+ def __calculate_new_name(self, name: str) -> str:
78
+ return name + "__" + str(len(self.tests))
79
+
80
+ def __iter__(self):
81
+ return iter(self.tests.values())
82
+
83
+ def get_ids(self) -> Iterator[str]:
84
+ return iter(self.tests)
@@ -0,0 +1,52 @@
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 pathlib
18
+ import re
19
+ from functools import lru_cache
20
+
21
+
22
+ def get_framework_root() -> pathlib.Path:
23
+ framework_root = pathlib.Path(__file__).parents[1]
24
+ return framework_root
25
+
26
+
27
+ def _make_functions_visible_for_testing(code_module_path):
28
+ code_module = code_module_path.read_text(encoding="utf-8")
29
+ functions = re.findall(r"^function\s+([^(]+)\(", code_module, flags=re.M)
30
+ for function in functions:
31
+ if not re.search(r"export\s" + function, code_module):
32
+ code_module = re.sub(
33
+ r"^function\s+" + function,
34
+ f"export function {function}",
35
+ code_module,
36
+ flags=re.M,
37
+ )
38
+ return code_module
39
+
40
+
41
+ @lru_cache(None)
42
+ def read_assertions_module():
43
+ root_folder = get_framework_root()
44
+ assertions_file = root_folder / "spl2_utils" / "assertions.spl2"
45
+ return assertions_file.read_text(encoding="utf-8")
46
+
47
+
48
+ @lru_cache(None)
49
+ def read_commands_modules():
50
+ root_folder = get_framework_root()
51
+ commands_module = root_folder / "spl2_utils" / "logs_to_metrics.spl2"
52
+ return commands_module.read_text(encoding="utf-8")