spl2-testing-framework 1.1.1__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/PKG-INFO +12 -3
  2. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/README.md +1 -0
  3. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/pyproject.toml +15 -2
  4. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/conftest.py +19 -3
  5. spl2_testing_framework-1.2.0/spl2_testing_framework/tools/create_diff_sheet.py +228 -0
  6. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/spl2test_runner.py +7 -1
  7. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/LICENSE +0 -0
  8. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/__init__.py +0 -0
  9. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/launch_remotely_spl2.py +0 -0
  10. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/logger_manager.py +0 -0
  11. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/pytest.ini.sample +0 -0
  12. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/single_spl2_file_runner.py +0 -0
  13. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/spl2_test_config.json +0 -0
  14. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/spl2_utils/assertions.spl2 +0 -0
  15. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +0 -0
  16. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/test_runner.py +0 -0
  17. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/__init__.py +0 -0
  18. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/__init__.py +0 -0
  19. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/cli_job.py +0 -0
  20. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/cloud_job.py +0 -0
  21. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/job.py +0 -0
  22. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/splunk_job.py +0 -0
  23. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/jobs/test_cloud_job.py +0 -0
  24. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/performance.py +0 -0
  25. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/results.py +0 -0
  26. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/search_clients/__init__.py +0 -0
  27. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/search_clients/cli_search_client.py +0 -0
  28. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/search_clients/cloud_search_client.py +0 -0
  29. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/search_clients/search_client.py +0 -0
  30. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/search_clients/splunk_search_client.py +0 -0
  31. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/test_discovery.py +0 -0
  32. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/test_types.py +0 -0
  33. {spl2_testing_framework-1.1.1 → spl2_testing_framework-1.2.0}/spl2_testing_framework/tools/utils.py +0 -0
@@ -1,18 +1,26 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spl2-testing-framework
3
- Version: 1.1.1
3
+ Version: 1.2.0
4
4
  Summary: SPL2 Testing Framework
5
5
  Home-page: https://cd.splunkdev.com/data-availability/spl2-content-tools
6
6
  Keywords: SPL2 testing framework
7
7
  Author: mgazda-splunk
8
8
  Author-email: mgazda@splunk.com
9
- Requires-Python: >=3.7,<4.0
9
+ Requires-Python: >=3.7.1,<4.0.0
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.7
12
11
  Classifier: Programming Language :: Python :: 3.8
13
12
  Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Requires-Dist: numpy (>=1.21,<1.22) ; python_version < "3.8"
16
+ Requires-Dist: numpy (>=1.21,<1.24) ; python_version >= "3.8" and python_version < "3.10"
17
+ Requires-Dist: numpy (>=1.25,<2.0) ; python_version >= "3.10" and python_version < "3.13"
18
+ Requires-Dist: numpy (>=1.26,<2.0) ; python_version >= "3.13"
19
+ Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
20
+ Requires-Dist: pandas (==1.3.5) ; python_version < "3.8"
21
+ Requires-Dist: pandas (==1.5.3) ; python_version >= "3.8" and python_version < "3.10"
22
+ Requires-Dist: pandas (>=2.0.3,<3.0.0) ; python_version >= "3.10" and python_version < "3.13"
23
+ Requires-Dist: pandas (>=2.1.0,<3.0.0) ; python_version >= "3.13"
16
24
  Requires-Dist: pytest (==7.4.4)
17
25
  Requires-Dist: pytest-subtests (==0.13.1)
18
26
  Requires-Dist: pytest-xdist (==3.5.0)
@@ -123,6 +131,7 @@ It is possible to pass more options to the command, which works also with pytest
123
131
  Additionally, the following options are supported:
124
132
 
125
133
  * `--ignore_empty_strings` - to ignore empty strings in the results
134
+ * `--create_comparison_sheet` - to create a comparison sheet in `comparison_box_test` folder using actual and expected outputs (works only when running box tests)
126
135
 
127
136
  Note: The `pytest.ini.sample` file allows you to define command parameters. Just update the configurations, rename the
128
137
  file by removing the `.sample` extension, and execute the command.
@@ -99,6 +99,7 @@ It is possible to pass more options to the command, which works also with pytest
99
99
  Additionally, the following options are supported:
100
100
 
101
101
  * `--ignore_empty_strings` - to ignore empty strings in the results
102
+ * `--create_comparison_sheet` - to create a comparison sheet in `comparison_box_test` folder using actual and expected outputs (works only when running box tests)
102
103
 
103
104
  Note: The `pytest.ini.sample` file allows you to define command parameters. Just update the configurations, rename the
104
105
  file by removing the `.sample` extension, and execute the command.
@@ -16,7 +16,7 @@
16
16
 
17
17
  [tool.poetry]
18
18
  name = "spl2-testing-framework"
19
- version = "1.1.1"
19
+ version = "1.2.0"
20
20
  description = "SPL2 Testing Framework"
21
21
  authors = ["mgazda-splunk <mgazda@splunk.com>"]
22
22
  readme = "README.md"
@@ -33,13 +33,26 @@ spl2_tests_run = "spl2_testing_framework.test_runner:run"
33
33
  single_spl2_file_run = "spl2_testing_framework.single_spl2_file_runner:run"
34
34
 
35
35
  [tool.poetry.dependencies]
36
- python = "^3.7"
36
+ python = "^3.7.1"
37
37
  pytest = "7.4.4"
38
38
  pytest-xdist = "3.5.0"
39
39
  pytest-subtests = "0.13.1"
40
40
  requests = "^2.31.0"
41
41
  tabulate = "0.9.0"
42
42
  urllib3 = "^1.26.20"
43
+ pandas = [
44
+ { version = "1.3.5", python = "<3.8" },
45
+ { version = "1.5.3", python = ">=3.8,<3.10" },
46
+ { version = "^2.0.3", python = ">=3.10,<3.13" },
47
+ { version = "^2.1.0", python = ">=3.13" }
48
+ ]
49
+ numpy = [
50
+ { version = ">=1.21,<1.22", python = "<3.8" },
51
+ { version = ">=1.21,<1.24", python = ">=3.8,<3.10" },
52
+ { version = ">=1.25,<2.0", python = ">=3.10,<3.13" },
53
+ { version = ">=1.26,<2.0", python = ">=3.13" }
54
+ ]
55
+ openpyxl = "^3.1.2"
43
56
 
44
57
  [tool.poetry.group.dev.dependencies]
45
58
  pre-commit = "*"
@@ -47,6 +47,7 @@ BOX_TEST = "box_test"
47
47
  SINGLE_SPL2_FILE = "single_spl2_file"
48
48
 
49
49
  IGNORE_EMPTY_STRINGS = "ignore_empty_strings"
50
+ CREATE_COMPARISON_SHEET = "create_comparison_sheet"
50
51
  PERFORMANCE_CHECK = "performance_check"
51
52
  TEST_TYPE = "type"
52
53
  TEST_DIR = "test_dir"
@@ -72,6 +73,7 @@ def pytest_addoption(parser):
72
73
  parser.addoption("--template_file", action="store", default=None)
73
74
  parser.addoption("--sample_file", action="store", default=None)
74
75
  parser.addoption("--sample_delimiter", action="store", default="\n")
76
+ parser.addoption(f"--{CREATE_COMPARISON_SHEET}", action="store_true", default=False)
75
77
  parser.addoption(f"--{IGNORE_EMPTY_STRINGS}", action="store_true")
76
78
 
77
79
 
@@ -123,9 +125,13 @@ def pytest_generate_tests(metafunc):
123
125
  )
124
126
 
125
127
 
126
- @pytest.fixture()
128
+ @pytest.fixture(scope="session")
127
129
  def spl2_test_runner(
128
- request, config, basic_authentication, bearer_token_authentication
130
+ request,
131
+ config,
132
+ basic_authentication,
133
+ bearer_token_authentication,
134
+ run_configurations,
129
135
  ):
130
136
  """This fixture is used to create a test runner object based on the test type specified by the --type option
131
137
  3 types of tests are supported: 'splunk', 'cloud', 'cli'
@@ -146,10 +152,20 @@ def spl2_test_runner(
146
152
  )
147
153
  )
148
154
 
149
- x = SPL2TestRunner(search_client)
155
+ x = SPL2TestRunner(search_client, run_configurations)
150
156
  return x
151
157
 
152
158
 
159
+ @pytest.fixture(scope="session")
160
+ def run_configurations(request):
161
+ """Returns all the configuration options in one object (dict). Add more as necessary."""
162
+ return {
163
+ CREATE_COMPARISON_SHEET: request.config.getoption(
164
+ f"--{CREATE_COMPARISON_SHEET}"
165
+ ),
166
+ }
167
+
168
+
153
169
  @pytest.fixture(scope="session")
154
170
  def config():
155
171
  """This fixture is used to read the configuration from the environment variables or from the spl2_test_config.json
@@ -0,0 +1,228 @@
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 pandas as pd
18
+ from openpyxl import load_workbook
19
+ from openpyxl.styles import PatternFill, Border, Side, Alignment
20
+ import json, os
21
+ from typing import Union, List, Dict
22
+ import logging
23
+
24
+ _LOGGER = logging.getLogger(__name__)
25
+
26
+
27
+ class ComparisonSheet:
28
+ """Class to create a comparison Excel sheet between expected and actual data."""
29
+
30
+ def __init__(self):
31
+ self.actual_json_list = []
32
+ self.expected_json_list = []
33
+ self.expected_path = "comparison_box_test/expected.json"
34
+ self.actual_path = "comparison_box_test/actual.json"
35
+ self.output_path = "comparison_box_test/comparison.xlsx"
36
+ os.makedirs("comparison_box_test", exist_ok=True)
37
+
38
+ def create_comparison_sheet(self, output_result, box_test):
39
+ """
40
+ 1. Creates a comparison_box_test directory.
41
+ 2. Then creates actual.json, expected.json and comparison.xlsx sheet in that folder.
42
+
43
+ Parameters:
44
+ output_result (list): The output result got from the execution.
45
+ box_test (object): Box test object.
46
+
47
+ """
48
+ self.actual_json_list.append(output_result[0])
49
+ self.expected_json_list.append(box_test.output[0])
50
+ with open(self.expected_path, "w") as f:
51
+ json.dump([r.__dict__ for r in self.expected_json_list], f, indent=2)
52
+ with open(self.actual_path, "w") as f:
53
+ json.dump([r.__dict__ for r in self.actual_json_list], f, indent=2)
54
+ self._create_comparison_sheet()
55
+
56
+ def _create_comparison_sheet(self):
57
+ """
58
+ Creates a comparison Excel sheet between expected and actual data.
59
+ """
60
+
61
+ _LOGGER.info("Starting comparison generation...")
62
+
63
+ expected_list = self._load_json(self.expected_path)
64
+ actual_list = self._load_json(self.actual_path)
65
+ df = self._compare_events(expected_list, actual_list)
66
+ df.to_excel(self.output_path, index=False)
67
+
68
+ self._apply_excel_styles(self.output_path)
69
+
70
+ _LOGGER.info(f"✅ Comparison sheet created at: {self.output_path}")
71
+
72
+ def _load_json(self, json_input: Union[str, list]) -> List[Dict]:
73
+ """Load JSON data from a file path or return list directly."""
74
+ if isinstance(json_input, str):
75
+ with open(json_input) as f:
76
+ return json.load(f)
77
+ return json_input
78
+
79
+ def _compare_events(self, list1: List[Dict], list2: List[Dict]) -> pd.DataFrame:
80
+ """Compare expected and actual events, returning a formatted DataFrame."""
81
+
82
+ self.fields = []
83
+ self.expected_vals = []
84
+ self.actual_vals = []
85
+ self.problems = []
86
+ self.solutions = []
87
+
88
+ for idx, (event_expected, event_actual) in enumerate(
89
+ zip(list1, list2), start=1
90
+ ):
91
+ dict_expected = event_expected.get("data", {})
92
+ dict_actual = event_actual.get("data", {})
93
+
94
+ all_fields = sorted(set(dict_expected.keys()) | set(dict_actual.keys()))
95
+ equal_fields = []
96
+
97
+ for field in all_fields:
98
+ exp_val = dict_expected.get(field)
99
+ act_val = dict_actual.get(field)
100
+
101
+ if exp_val is None and act_val is not None:
102
+ self._add_row(
103
+ field,
104
+ exp_val,
105
+ act_val,
106
+ problem="Extracted additionally from SPL2 pipeline",
107
+ solution="Possible to ignore",
108
+ )
109
+ elif act_val is None and exp_val is not None:
110
+ self._add_row(
111
+ field,
112
+ exp_val,
113
+ act_val,
114
+ problem="Missing from SPL2 pipeline. Need to fix.",
115
+ solution="",
116
+ )
117
+ elif exp_val != act_val:
118
+ self._add_row(
119
+ field,
120
+ exp_val,
121
+ act_val,
122
+ problem="Value mismatch",
123
+ solution="Check this in SPL2 pipeline and fix if needed.",
124
+ )
125
+ else:
126
+ if field == "_raw":
127
+ self._add_row(field, exp_val, act_val)
128
+ else:
129
+ equal_fields.append(field)
130
+
131
+ if equal_fields:
132
+ self._add_row(
133
+ field="Equal fields for this event",
134
+ exp_val=", ".join(equal_fields),
135
+ act_val="",
136
+ )
137
+
138
+ self._add_row(
139
+ field="All Expected Fields", exp_val=dict_expected, act_val=""
140
+ )
141
+ self._add_row(field="All Actual Fields", exp_val=dict_actual, act_val="")
142
+ self._add_row(field="", exp_val="", act_val="")
143
+
144
+ return pd.DataFrame(
145
+ {
146
+ "Field": self.fields,
147
+ "Expected": self.expected_vals,
148
+ "Actual": self.actual_vals,
149
+ "Problem": self.problems,
150
+ "Solution": self.solutions,
151
+ }
152
+ )
153
+
154
+ def _add_row(self, field, exp_val, act_val, problem="", solution=""):
155
+ """Append a single comparison row to the running lists."""
156
+ self.fields.append(field)
157
+ self.expected_vals.append(exp_val)
158
+ self.actual_vals.append(act_val)
159
+ self.problems.append(problem)
160
+ self.solutions.append(solution)
161
+
162
+ def _apply_excel_styles(self, output_path: str) -> None:
163
+ """Apply Excel formatting and highlighting."""
164
+ wb = load_workbook(output_path)
165
+ ws = wb.active
166
+
167
+ diff_fill = PatternFill(
168
+ start_color="FFFF00", end_color="FFFF00", fill_type="solid"
169
+ )
170
+ missing_fill = PatternFill(
171
+ start_color="FF9999", end_color="FF9999", fill_type="solid"
172
+ )
173
+ equal_fill = PatternFill(
174
+ start_color="CCFFCC", end_color="CCFFCC", fill_type="solid"
175
+ )
176
+
177
+ thin_border = Border(
178
+ left=Side(style="thin", color="000000"),
179
+ right=Side(style="thin", color="000000"),
180
+ top=Side(style="thin", color="000000"),
181
+ bottom=Side(style="thin", color="000000"),
182
+ )
183
+
184
+ alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
185
+
186
+ for r, row in enumerate(
187
+ ws.iter_rows(
188
+ min_row=1, max_row=ws.max_row, min_col=1, max_col=ws.max_column
189
+ ),
190
+ start=1,
191
+ ):
192
+ ws.row_dimensions[r].height = 15
193
+
194
+ field_name = row[0].value
195
+ expected_val = row[1].value if len(row) > 1 else None
196
+ actual_val = row[2].value if len(row) > 2 else None
197
+
198
+ for cell in row:
199
+ cell.border = thin_border
200
+ cell.alignment = alignment
201
+
202
+ if r > 1: # skip header row
203
+ if expected_val is None and actual_val not in (None, ""):
204
+ row[1].fill = missing_fill
205
+ elif actual_val is None and expected_val not in (None, ""):
206
+ row[2].fill = missing_fill
207
+ elif expected_val != actual_val and field_name not in (
208
+ "",
209
+ "Equal fields for this event",
210
+ "All Expected Fields",
211
+ "All Actual Fields",
212
+ ):
213
+ row[1].fill = diff_fill
214
+ row[2].fill = diff_fill
215
+
216
+ if field_name in (
217
+ "All Expected Fields",
218
+ "All Actual Fields",
219
+ "Equal fields for this event",
220
+ ):
221
+ for cell in row:
222
+ cell.fill = equal_fill
223
+
224
+ for col in ["A", "B", "C", "D", "E"]:
225
+ ws.column_dimensions[col].width = 50
226
+ ws.freeze_panes = "B2"
227
+
228
+ wb.save(output_path)
@@ -21,6 +21,8 @@ from tabulate import tabulate
21
21
  from spl2_testing_framework.tools.performance import PerformanceCheck
22
22
  from spl2_testing_framework.tools.test_types import UnitTest, BoxTest, SingleSPL2
23
23
  from spl2_testing_framework.tools.utils import _make_functions_visible_for_testing
24
+ from spl2_testing_framework.tools.create_diff_sheet import ComparisonSheet
25
+
24
26
 
25
27
  _LOGGER = logging.getLogger(__name__)
26
28
 
@@ -28,8 +30,10 @@ _LOGGER = logging.getLogger(__name__)
28
30
  class SPL2TestRunner:
29
31
  """Main class responsible for running tests"""
30
32
 
31
- def __init__(self, search_client):
33
+ def __init__(self, search_client, run_configurations):
32
34
  self._search_client = search_client
35
+ self.create_comparison_sheet = run_configurations.get("create_comparison_sheet")
36
+ self.comp_obj = ComparisonSheet()
33
37
 
34
38
  def run_single_spl2_file(self, single_spl2_file: SingleSPL2) -> None:
35
39
  """Run a single spl2 file and print the output"""
@@ -127,6 +131,8 @@ class SPL2TestRunner:
127
131
  perf.check_performance(output_result)
128
132
 
129
133
  if box_test.output:
134
+ if self.create_comparison_sheet:
135
+ self.comp_obj.create_comparison_sheet(output_result, box_test)
130
136
  assert sorted(output_result) == sorted(box_test.output)
131
137
  _LOGGER.info(f"Output check passed: {box_test.name}")
132
138
  _LOGGER.debug("Received: \n%s", str(output_result))