spl2-testing-framework 1.6.0__tar.gz → 1.7.1__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.
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/PKG-INFO +49 -3
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/README.md +48 -2
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/pyproject.toml +1 -1
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/conftest.py +8 -2
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/spl2test_runner.py +15 -3
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/splunk_integration/splunk_handler.py +68 -19
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/test_discovery.py +144 -16
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/utils.py +9 -4
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/LICENSE +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/__init__.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/launch_remotely_spl2.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/logger_manager.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/pytest.ini.sample +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/single_spl2_file_runner.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_test_config.json +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_utils/assertions.spl2 +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/test_runner.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/__init__.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/create_diff_sheet.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/__init__.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/cli_job.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/cloud_job.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/job.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/splunk_job.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/test_cloud_job.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/performance.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/results.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/__init__.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/cli_search_client.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/cloud_search_client.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/search_client.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/splunk_search_client.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/splunk_integration/__init__.py +0 -0
- {spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spl2-testing-framework
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: SPL2 Testing Framework
|
|
5
5
|
Home-page: https://github.com/splunk/spl2-testing-framework
|
|
6
6
|
Keywords: SPL2 testing framework
|
|
@@ -131,6 +131,8 @@ It is possible to pass more options to the command, which works also with pytest
|
|
|
131
131
|
|
|
132
132
|
Additionally, the following options are supported:
|
|
133
133
|
|
|
134
|
+
* `--test_dir` - directory where test files (`.test.json`, `.test.spl2`, `module.json`) are located. Defaults to the current working directory. Discovery is recursive.
|
|
135
|
+
* `--code_dir` - directory where SPL2 code modules (`.spl2`) are located. Defaults to the current working directory. Falls back to `--test_dir` if the module is not found. See [Separate code and test directories](#separate-code-and-test-directories) for details.
|
|
134
136
|
* `--ignore_empty_strings` - to ignore empty strings in the results
|
|
135
137
|
* `--ignore_additional_fields_in_actual` - to ignore fields present in actual results but not in expected results (useful when actual results contain extra fields that should not affect comparison)
|
|
136
138
|
* `--create_comparison_sheet` - to create a comparison sheet in `comparison_box_test` folder using actual and expected outputs (works only when running box tests)
|
|
@@ -152,10 +154,53 @@ Sample configuration which may be used:
|
|
|
152
154
|
- Parameters:
|
|
153
155
|
`--type [cli | splunk | cloud] --test_dir /tests/resources -o log_cli=true --log-cli-level=INFO --verbose`
|
|
154
156
|
- If test dir is not specified, current working directory will be used
|
|
157
|
+
- Use `--code_dir` if SPL2 code modules are in a separate directory from tests
|
|
155
158
|
- If necessary another pytest options can be added
|
|
156
159
|
|
|
157
160
|
Note: It's necessary to set "pytest" as default test runner in PyCharm settings
|
|
158
161
|
|
|
162
|
+
### Separate code and test directories
|
|
163
|
+
|
|
164
|
+
By default, the framework expects SPL2 code modules (`.spl2` files) to live alongside their test files. The `--code_dir` option allows you to keep code and tests in separate directory trees.
|
|
165
|
+
|
|
166
|
+
#### How it works
|
|
167
|
+
|
|
168
|
+
| What is searched | Primary directory | Fallback directory |
|
|
169
|
+
|------------------|-------------------|--------------------|
|
|
170
|
+
| **Code modules** (`.spl2`) | `--code_dir` | `--test_dir` |
|
|
171
|
+
| **Test files** (`.test.json`, `.test.spl2`) | `--test_dir` | `--code_dir` |
|
|
172
|
+
|
|
173
|
+
Both searches are recursive. If the primary directory has no results, the framework falls back to the other directory and logs the action.
|
|
174
|
+
|
|
175
|
+
When multiple files with the same name are found within a directory, a warning is logged and the lexicographically first match is used.
|
|
176
|
+
|
|
177
|
+
#### Example project layout
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
my-project/
|
|
181
|
+
├── src/ # --code_dir
|
|
182
|
+
│ ├── network_traffic.spl2
|
|
183
|
+
│ └── dns_lookup.spl2
|
|
184
|
+
└── tests/ # --test_dir
|
|
185
|
+
├── network_traffic.test.json
|
|
186
|
+
├── dns_lookup.test.json
|
|
187
|
+
└── dns_lookup.test.spl2
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Run with:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
spl2_tests_run cli --test_dir tests --code_dir src
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Backward compatibility
|
|
197
|
+
|
|
198
|
+
When `--code_dir` is not specified (or both flags point to the same directory), the framework behaves exactly as before — code modules are found next to their test files via the same recursive search.
|
|
199
|
+
|
|
200
|
+
#### Applies to all test types
|
|
201
|
+
|
|
202
|
+
The `--code_dir` option works for box tests, unit tests, and single SPL2 file execution.
|
|
203
|
+
|
|
159
204
|
## Executing a spl2 file
|
|
160
205
|
|
|
161
206
|
This framework also supports executing a single spl2 file and prints the results in command line as well as a log file.
|
|
@@ -179,6 +224,7 @@ It is possible to pass more options to the command, which works also with pytest
|
|
|
179
224
|
|
|
180
225
|
* `--test_dir` - Path in which the template file and module.json are available. If not provided, it will look for the
|
|
181
226
|
current directory for the template file and module.json file
|
|
227
|
+
* `--code_dir` - Path where SPL2 code modules are located, if different from `--test_dir`. Falls back to `--test_dir` when not set. See [Separate code and test directories](#separate-code-and-test-directories).
|
|
182
228
|
* `--template_file` - The spl2 template file to execute
|
|
183
229
|
* `--sample_file` - A file containing all the samples required for the template file. If not provided, it will look for
|
|
184
230
|
the samples in module.json file of the corresponding template file
|
|
@@ -214,8 +260,8 @@ When set (`--check_splunk_results=<option>`), box tests (after running the SPL2
|
|
|
214
260
|
|
|
215
261
|
| Value | Behavior |
|
|
216
262
|
|-------|----------|
|
|
217
|
-
| **CIM** | For each output event,
|
|
218
|
-
| **TA** | For each output event,
|
|
263
|
+
| **CIM** | For each output event, ingest via HEC, load back from Splunk, then check CIM fields from `expected_cim_fields.cim_fields` with `validate_compatibility(actual, expected)`. Skips if the box test has no `cim_fields`. |
|
|
264
|
+
| **TA** | For each output event, ingest via HEC, load back from Splunk, then assert equality with the expected row for that index. |
|
|
219
265
|
|
|
220
266
|
For **TA** only, you can name top-level fields to drop on **both** the queried event and each `expected_destination_result` row by adding **`ignore_fields_in_splunk_check`**: an object mapping **field name → short reason**. Use the same key under `test` in each entry in `*.test.json` (same file as `expected_destination_result`):
|
|
221
267
|
|
|
@@ -98,6 +98,8 @@ It is possible to pass more options to the command, which works also with pytest
|
|
|
98
98
|
|
|
99
99
|
Additionally, the following options are supported:
|
|
100
100
|
|
|
101
|
+
* `--test_dir` - directory where test files (`.test.json`, `.test.spl2`, `module.json`) are located. Defaults to the current working directory. Discovery is recursive.
|
|
102
|
+
* `--code_dir` - directory where SPL2 code modules (`.spl2`) are located. Defaults to the current working directory. Falls back to `--test_dir` if the module is not found. See [Separate code and test directories](#separate-code-and-test-directories) for details.
|
|
101
103
|
* `--ignore_empty_strings` - to ignore empty strings in the results
|
|
102
104
|
* `--ignore_additional_fields_in_actual` - to ignore fields present in actual results but not in expected results (useful when actual results contain extra fields that should not affect comparison)
|
|
103
105
|
* `--create_comparison_sheet` - to create a comparison sheet in `comparison_box_test` folder using actual and expected outputs (works only when running box tests)
|
|
@@ -119,10 +121,53 @@ Sample configuration which may be used:
|
|
|
119
121
|
- Parameters:
|
|
120
122
|
`--type [cli | splunk | cloud] --test_dir /tests/resources -o log_cli=true --log-cli-level=INFO --verbose`
|
|
121
123
|
- If test dir is not specified, current working directory will be used
|
|
124
|
+
- Use `--code_dir` if SPL2 code modules are in a separate directory from tests
|
|
122
125
|
- If necessary another pytest options can be added
|
|
123
126
|
|
|
124
127
|
Note: It's necessary to set "pytest" as default test runner in PyCharm settings
|
|
125
128
|
|
|
129
|
+
### Separate code and test directories
|
|
130
|
+
|
|
131
|
+
By default, the framework expects SPL2 code modules (`.spl2` files) to live alongside their test files. The `--code_dir` option allows you to keep code and tests in separate directory trees.
|
|
132
|
+
|
|
133
|
+
#### How it works
|
|
134
|
+
|
|
135
|
+
| What is searched | Primary directory | Fallback directory |
|
|
136
|
+
|------------------|-------------------|--------------------|
|
|
137
|
+
| **Code modules** (`.spl2`) | `--code_dir` | `--test_dir` |
|
|
138
|
+
| **Test files** (`.test.json`, `.test.spl2`) | `--test_dir` | `--code_dir` |
|
|
139
|
+
|
|
140
|
+
Both searches are recursive. If the primary directory has no results, the framework falls back to the other directory and logs the action.
|
|
141
|
+
|
|
142
|
+
When multiple files with the same name are found within a directory, a warning is logged and the lexicographically first match is used.
|
|
143
|
+
|
|
144
|
+
#### Example project layout
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
my-project/
|
|
148
|
+
├── src/ # --code_dir
|
|
149
|
+
│ ├── network_traffic.spl2
|
|
150
|
+
│ └── dns_lookup.spl2
|
|
151
|
+
└── tests/ # --test_dir
|
|
152
|
+
├── network_traffic.test.json
|
|
153
|
+
├── dns_lookup.test.json
|
|
154
|
+
└── dns_lookup.test.spl2
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Run with:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
spl2_tests_run cli --test_dir tests --code_dir src
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Backward compatibility
|
|
164
|
+
|
|
165
|
+
When `--code_dir` is not specified (or both flags point to the same directory), the framework behaves exactly as before — code modules are found next to their test files via the same recursive search.
|
|
166
|
+
|
|
167
|
+
#### Applies to all test types
|
|
168
|
+
|
|
169
|
+
The `--code_dir` option works for box tests, unit tests, and single SPL2 file execution.
|
|
170
|
+
|
|
126
171
|
## Executing a spl2 file
|
|
127
172
|
|
|
128
173
|
This framework also supports executing a single spl2 file and prints the results in command line as well as a log file.
|
|
@@ -146,6 +191,7 @@ It is possible to pass more options to the command, which works also with pytest
|
|
|
146
191
|
|
|
147
192
|
* `--test_dir` - Path in which the template file and module.json are available. If not provided, it will look for the
|
|
148
193
|
current directory for the template file and module.json file
|
|
194
|
+
* `--code_dir` - Path where SPL2 code modules are located, if different from `--test_dir`. Falls back to `--test_dir` when not set. See [Separate code and test directories](#separate-code-and-test-directories).
|
|
149
195
|
* `--template_file` - The spl2 template file to execute
|
|
150
196
|
* `--sample_file` - A file containing all the samples required for the template file. If not provided, it will look for
|
|
151
197
|
the samples in module.json file of the corresponding template file
|
|
@@ -181,8 +227,8 @@ When set (`--check_splunk_results=<option>`), box tests (after running the SPL2
|
|
|
181
227
|
|
|
182
228
|
| Value | Behavior |
|
|
183
229
|
|-------|----------|
|
|
184
|
-
| **CIM** | For each output event,
|
|
185
|
-
| **TA** | For each output event,
|
|
230
|
+
| **CIM** | For each output event, ingest via HEC, load back from Splunk, then check CIM fields from `expected_cim_fields.cim_fields` with `validate_compatibility(actual, expected)`. Skips if the box test has no `cim_fields`. |
|
|
231
|
+
| **TA** | For each output event, ingest via HEC, load back from Splunk, then assert equality with the expected row for that index. |
|
|
186
232
|
|
|
187
233
|
For **TA** only, you can name top-level fields to drop on **both** the queried event and each `expected_destination_result` row by adding **`ignore_fields_in_splunk_check`**: an object mapping **field name → short reason**. Use the same key under `test` in each entry in `*.test.json` (same file as `expected_destination_result`):
|
|
188
234
|
|
{spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/conftest.py
RENAMED
|
@@ -55,6 +55,7 @@ CHECK_SPLUNK_RESULTS = "check_splunk_results"
|
|
|
55
55
|
PERFORMANCE_CHECK = "performance_check"
|
|
56
56
|
TEST_TYPE = "type"
|
|
57
57
|
TEST_DIR = "test_dir"
|
|
58
|
+
CODE_DIR = "code_dir"
|
|
58
59
|
LIMIT_TESTS = "limit_tests"
|
|
59
60
|
CLI_BENCH = "cli_bench"
|
|
60
61
|
|
|
@@ -70,6 +71,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
70
71
|
def pytest_addoption(parser):
|
|
71
72
|
parser.addoption(f"--{TEST_TYPE}", action="store", default="cli")
|
|
72
73
|
parser.addoption(f"--{TEST_DIR}", action="store", default=".")
|
|
74
|
+
parser.addoption(f"--{CODE_DIR}", action="store", default=".")
|
|
73
75
|
parser.addoption(
|
|
74
76
|
f"--{PERFORMANCE_CHECK}",
|
|
75
77
|
action="store",
|
|
@@ -93,13 +95,16 @@ def pytest_generate_tests(metafunc):
|
|
|
93
95
|
"""This function is used for dynamic generation of tests based on the test files in the directory specified by the
|
|
94
96
|
--test_dir option"""
|
|
95
97
|
test_dir = metafunc.config.getoption(TEST_DIR)
|
|
98
|
+
code_dir = metafunc.config.getoption(CODE_DIR)
|
|
96
99
|
performance_check = metafunc.config.getoption(PERFORMANCE_CHECK)
|
|
97
100
|
limit_tests = metafunc.config.getoption(LIMIT_TESTS)
|
|
98
101
|
_set_results_options(metafunc)
|
|
99
102
|
|
|
100
103
|
if BOX_TEST in metafunc.fixturenames:
|
|
101
104
|
_LOGGER.info("Collecting Box Tests")
|
|
102
|
-
module_tests = BoxTestDiscovery(
|
|
105
|
+
module_tests = BoxTestDiscovery(
|
|
106
|
+
test_dir, performance_check, limit_tests, code_dir=code_dir
|
|
107
|
+
)
|
|
103
108
|
module_tests.discover_tests()
|
|
104
109
|
metafunc.parametrize(
|
|
105
110
|
"%s" % BOX_TEST, module_tests.tests, ids=module_tests.tests.get_ids()
|
|
@@ -119,6 +124,7 @@ def pytest_generate_tests(metafunc):
|
|
|
119
124
|
sample_delimiter,
|
|
120
125
|
performance_check=performance_check,
|
|
121
126
|
limit_tests=limit_tests,
|
|
127
|
+
code_dir=code_dir,
|
|
122
128
|
)
|
|
123
129
|
try:
|
|
124
130
|
spl2_pipelines.discover_tests()
|
|
@@ -132,7 +138,7 @@ def pytest_generate_tests(metafunc):
|
|
|
132
138
|
|
|
133
139
|
if UNIT_TEST in metafunc.fixturenames:
|
|
134
140
|
_LOGGER.info("Collecting Unit Tests")
|
|
135
|
-
unit_tests = UTDiscovery(test_dir, limit_tests)
|
|
141
|
+
unit_tests = UTDiscovery(test_dir, limit_tests, code_dir=code_dir)
|
|
136
142
|
unit_tests.discover_tests()
|
|
137
143
|
metafunc.parametrize(
|
|
138
144
|
UNIT_TEST, unit_tests.tests, ids=unit_tests.tests.get_ids()
|
|
@@ -23,6 +23,9 @@ from spl2_testing_framework.tools.performance import PerformanceCheck
|
|
|
23
23
|
from spl2_testing_framework.tools.search_clients.cli_search_client import (
|
|
24
24
|
CLISearchClient,
|
|
25
25
|
)
|
|
26
|
+
from spl2_testing_framework.tools.splunk_integration.splunk_handler import (
|
|
27
|
+
SplunkEventNotFoundError,
|
|
28
|
+
)
|
|
26
29
|
from spl2_testing_framework.tools.test_types import UnitTest, BoxTest, SingleSPL2
|
|
27
30
|
from spl2_testing_framework.tools.utils import (
|
|
28
31
|
_make_functions_visible_for_testing,
|
|
@@ -227,6 +230,15 @@ class SPL2TestRunner:
|
|
|
227
230
|
return row
|
|
228
231
|
return {k: v for k, v in row.items() if k not in ignore_keys}
|
|
229
232
|
|
|
233
|
+
def _splunk_process_event(self, box_test: BoxTest, event):
|
|
234
|
+
try:
|
|
235
|
+
return self.splunk_handler.process_event(event)
|
|
236
|
+
except SplunkEventNotFoundError as e:
|
|
237
|
+
raise SplunkEventNotFoundError(
|
|
238
|
+
e.event_uuid,
|
|
239
|
+
context=f"box_test={box_test.name!r}",
|
|
240
|
+
) from e
|
|
241
|
+
|
|
230
242
|
def _check_splunk_results(self, box_test, job):
|
|
231
243
|
output_result = job.result.get("destination", [])
|
|
232
244
|
if self.check_splunk_results == "CIM":
|
|
@@ -234,13 +246,13 @@ class SPL2TestRunner:
|
|
|
234
246
|
pytest.skip("CIM output not available for this test")
|
|
235
247
|
|
|
236
248
|
for event in output_result:
|
|
237
|
-
processed_event = self.
|
|
238
|
-
validate_compatibility(box_test.cim_fields
|
|
249
|
+
processed_event = self._splunk_process_event(box_test, event)
|
|
250
|
+
validate_compatibility(processed_event, box_test.cim_fields)
|
|
239
251
|
elif self.check_splunk_results == "TA":
|
|
240
252
|
ignore_spec = box_test.ignore_fields_in_splunk_check
|
|
241
253
|
ignore_keys = frozenset(ignore_spec.keys())
|
|
242
254
|
for event, expected in zip(output_result, box_test.output):
|
|
243
|
-
processed_event = self.
|
|
255
|
+
processed_event = self._splunk_process_event(box_test, event)
|
|
244
256
|
actual = self._strip_ignored_fields_for_splunk_compare(
|
|
245
257
|
processed_event, ignore_keys
|
|
246
258
|
)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import uuid
|
|
17
|
+
from typing import Optional
|
|
17
18
|
|
|
18
19
|
import requests
|
|
19
20
|
import splunklib.client as client
|
|
@@ -26,6 +27,21 @@ from requests.auth import HTTPBasicAuth
|
|
|
26
27
|
_LOGGER = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
SPL2_TF_EVENT_UUID_FIELD = "spl2_testing_framework_event_uuid"
|
|
31
|
+
SPL2_TF_DEFAULT_HEC_HOST = "spl2-testing-framework"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SplunkEventNotFoundError(Exception):
|
|
35
|
+
"""Raised when HEC ingest succeeded but search returned no matching event."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, event_uuid: str, *, context: Optional[str] = None):
|
|
38
|
+
self.event_uuid = event_uuid
|
|
39
|
+
msg = f"No event found after HEC submit (event_uuid={event_uuid!r})"
|
|
40
|
+
if context:
|
|
41
|
+
msg = f"{context}: {msg}"
|
|
42
|
+
super().__init__(msg)
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
class SplunkHandler:
|
|
30
46
|
def __init__(self, config, auth):
|
|
31
47
|
self._config = config
|
|
@@ -36,18 +52,45 @@ class SplunkHandler:
|
|
|
36
52
|
self._create_index()
|
|
37
53
|
|
|
38
54
|
def process_event(self, event):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
event_uuid = str(uuid.uuid4())
|
|
56
|
+
metadata_host = event.get("host")
|
|
57
|
+
if metadata_host is not None and str(metadata_host).strip() != "":
|
|
58
|
+
hec_host = str(metadata_host).strip()
|
|
59
|
+
else:
|
|
60
|
+
hec_host = SPL2_TF_DEFAULT_HEC_HOST
|
|
61
|
+
|
|
62
|
+
self.push_to_hec(event, hec_host, event_uuid)
|
|
63
|
+
res = self.get_events_by_event_uuid(event_uuid)
|
|
43
64
|
if not res:
|
|
44
|
-
raise
|
|
65
|
+
raise SplunkEventNotFoundError(event_uuid)
|
|
45
66
|
|
|
46
67
|
if len(res) > 1:
|
|
47
68
|
_LOGGER.error("Found more than one event, the result may not be correct")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
out = dict(res[0])
|
|
70
|
+
out.pop(SPL2_TF_EVENT_UUID_FIELD, None)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _hec_event_url(raw_url: str) -> str:
|
|
75
|
+
if "/services/collector/raw" in raw_url:
|
|
76
|
+
return raw_url.replace(
|
|
77
|
+
"/services/collector/raw", "/services/collector/event"
|
|
78
|
+
)
|
|
79
|
+
return raw_url
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _raw_as_event_string(raw):
|
|
83
|
+
"""Normalize pipeline `_raw` for HEC JSON `event` (must be a string).
|
|
84
|
+
|
|
85
|
+
``str`` is passed through ``str()``; ``bytes`` are decoded as UTF-8 with
|
|
86
|
+
replacement on invalid sequences (avoids :class:`UnicodeDecodeError` for
|
|
87
|
+
odd binary payloads; prefer valid UTF-8 log text in normal use).
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(raw, bytes):
|
|
90
|
+
return raw.decode("utf-8", errors="replace")
|
|
91
|
+
return str(raw)
|
|
92
|
+
|
|
93
|
+
def push_to_hec(self, data, host, event_uuid):
|
|
51
94
|
sourcetype = data.get("sourcetype", "UNKNOWN")
|
|
52
95
|
|
|
53
96
|
if sourcetype == "UNKNOWN":
|
|
@@ -55,29 +98,31 @@ class SplunkHandler:
|
|
|
55
98
|
|
|
56
99
|
source = data.get("source", None)
|
|
57
100
|
|
|
58
|
-
|
|
101
|
+
payload = {
|
|
102
|
+
"event": SplunkHandler._raw_as_event_string(data["_raw"]),
|
|
59
103
|
"sourcetype": sourcetype,
|
|
60
104
|
"index": self.splunk_config["index"],
|
|
61
105
|
"host": host,
|
|
106
|
+
"fields": {SPL2_TF_EVENT_UUID_FIELD: event_uuid},
|
|
62
107
|
}
|
|
63
|
-
|
|
64
108
|
if source is not None:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
event_data = data["_raw"].encode("utf-8")
|
|
109
|
+
payload["source"] = source
|
|
68
110
|
|
|
69
111
|
response = requests.post(
|
|
70
|
-
url=self.splunk_config["url"],
|
|
71
|
-
|
|
72
|
-
params=params,
|
|
112
|
+
url=self._hec_event_url(self.splunk_config["url"]),
|
|
113
|
+
json=payload,
|
|
73
114
|
auth=self._auth,
|
|
74
115
|
verify=False,
|
|
75
116
|
)
|
|
76
117
|
|
|
77
118
|
response.raise_for_status()
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _splunk_search_term(value: str) -> str:
|
|
122
|
+
return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
123
|
+
|
|
124
|
+
def get_events_by_event_uuid(self, event_uuid):
|
|
125
|
+
SPL_TEMPLATE = "| search index={index} {uuid_field}={uuid} | fields *"
|
|
81
126
|
# Connect to Splunk
|
|
82
127
|
service = client.connect(
|
|
83
128
|
host=self.splunk_config["ip"],
|
|
@@ -86,7 +131,11 @@ class SplunkHandler:
|
|
|
86
131
|
password=self.splunk_config["password"],
|
|
87
132
|
app="search",
|
|
88
133
|
)
|
|
89
|
-
SPL = SPL_TEMPLATE.format(
|
|
134
|
+
SPL = SPL_TEMPLATE.format(
|
|
135
|
+
index=self.splunk_config["index"],
|
|
136
|
+
uuid_field=SPL2_TF_EVENT_UUID_FIELD,
|
|
137
|
+
uuid=self._splunk_search_term(event_uuid),
|
|
138
|
+
)
|
|
90
139
|
|
|
91
140
|
results_all = []
|
|
92
141
|
retry = 0
|
|
@@ -19,6 +19,8 @@ import logging
|
|
|
19
19
|
import os
|
|
20
20
|
import pathlib
|
|
21
21
|
import re
|
|
22
|
+
from collections import Counter
|
|
23
|
+
from typing import Optional
|
|
22
24
|
|
|
23
25
|
from spl2_testing_framework.tools.results import Metrics, Results
|
|
24
26
|
from spl2_testing_framework.tools.test_types import (
|
|
@@ -34,33 +36,151 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
34
36
|
class SamplesNotFoundException(Exception):
|
|
35
37
|
"""An Exception to be raised if samples are not found"""
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
class CodeModuleNotFoundError(Exception):
|
|
41
|
+
"""Raised when a code module cannot be found in any search directory"""
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
class TestDiscovery(abc.ABC):
|
|
41
45
|
"""Base class for test discovery classes"""
|
|
42
46
|
|
|
43
|
-
def __init__(
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
path: str,
|
|
50
|
+
performance_check: str,
|
|
51
|
+
limit_tests: Optional[int] = None,
|
|
52
|
+
code_dir: Optional[str] = None,
|
|
53
|
+
):
|
|
44
54
|
self.path = pathlib.Path(path)
|
|
55
|
+
self.code_dir = pathlib.Path(code_dir) if code_dir else self.path
|
|
45
56
|
self._performance_check = performance_check
|
|
46
57
|
self.limit_tests = limit_tests
|
|
47
58
|
self.tests = TestSuite()
|
|
48
59
|
|
|
60
|
+
@property
|
|
61
|
+
def _dirs_differ(self) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
return self._dirs_differ_cached
|
|
64
|
+
except AttributeError:
|
|
65
|
+
self._dirs_differ_cached = self.code_dir.resolve() != self.path.resolve()
|
|
66
|
+
return self._dirs_differ_cached
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _warn_duplicate_basenames(
|
|
70
|
+
paths: list, directory: pathlib.Path, label: str
|
|
71
|
+
) -> None:
|
|
72
|
+
names = [p.name for p in paths]
|
|
73
|
+
dupes = [name for name, count in Counter(names).items() if count > 1]
|
|
74
|
+
if dupes:
|
|
75
|
+
_LOGGER.warning(
|
|
76
|
+
"Multiple %s files with the same name found in '%s': %s",
|
|
77
|
+
label,
|
|
78
|
+
directory,
|
|
79
|
+
dupes,
|
|
80
|
+
)
|
|
81
|
+
|
|
49
82
|
def _read_tests(self, test_name: str) -> None:
|
|
50
|
-
|
|
83
|
+
_LOGGER.info(
|
|
84
|
+
"Searching for test files '%s' in test directory: %s",
|
|
85
|
+
test_name,
|
|
86
|
+
self.path,
|
|
87
|
+
)
|
|
88
|
+
tests_paths = list(self.path.rglob(test_name))
|
|
89
|
+
if tests_paths:
|
|
90
|
+
self._warn_duplicate_basenames(tests_paths, self.path, "test")
|
|
91
|
+
_LOGGER.info(
|
|
92
|
+
"Found %d test file(s) in test directory: %s",
|
|
93
|
+
len(tests_paths),
|
|
94
|
+
[str(p) for p in tests_paths],
|
|
95
|
+
)
|
|
96
|
+
elif self._dirs_differ:
|
|
97
|
+
_LOGGER.info(
|
|
98
|
+
"No test files '%s' found in test directory '%s', "
|
|
99
|
+
"falling back to code directory: %s",
|
|
100
|
+
test_name,
|
|
101
|
+
self.path,
|
|
102
|
+
self.code_dir,
|
|
103
|
+
)
|
|
104
|
+
tests_paths = list(self.code_dir.rglob(test_name))
|
|
105
|
+
if tests_paths:
|
|
106
|
+
self._warn_duplicate_basenames(tests_paths, self.code_dir, "test")
|
|
107
|
+
_LOGGER.info(
|
|
108
|
+
"Found %d test file(s) in code directory: %s",
|
|
109
|
+
len(tests_paths),
|
|
110
|
+
[str(p) for p in tests_paths],
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
_LOGGER.warning(
|
|
114
|
+
"No test files '%s' found in code directory either.", test_name
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
_LOGGER.warning("No test files '%s' found in test directory.", test_name)
|
|
118
|
+
|
|
51
119
|
self._raw_tests = {
|
|
52
120
|
x: pathlib.Path(x).read_text(encoding="utf-8") for x in tests_paths
|
|
53
121
|
}
|
|
54
122
|
|
|
123
|
+
def _search_directory(
|
|
124
|
+
self, filename: str, directory: pathlib.Path, label: str
|
|
125
|
+
) -> Optional[pathlib.Path]:
|
|
126
|
+
"""Search a directory recursively for *filename*.
|
|
127
|
+
|
|
128
|
+
Returns the first match (sorted lexicographically) or ``None``.
|
|
129
|
+
Warns when multiple matches exist.
|
|
130
|
+
"""
|
|
131
|
+
matches = sorted(directory.rglob(filename))
|
|
132
|
+
if not matches:
|
|
133
|
+
return None
|
|
134
|
+
if len(matches) > 1:
|
|
135
|
+
_LOGGER.warning(
|
|
136
|
+
"Multiple matches for code module '%s' in %s '%s': %s. "
|
|
137
|
+
"Using first match.",
|
|
138
|
+
filename,
|
|
139
|
+
label,
|
|
140
|
+
directory,
|
|
141
|
+
[str(m) for m in matches],
|
|
142
|
+
)
|
|
143
|
+
_LOGGER.info("Resolved code module (%s): %s", label, matches[0])
|
|
144
|
+
return matches[0]
|
|
145
|
+
|
|
146
|
+
def _resolve_code_module(self, filename: str) -> pathlib.Path:
|
|
147
|
+
"""Resolve a code module by filename.
|
|
148
|
+
|
|
149
|
+
Searches code_dir first (recursive), falls back to test_dir.
|
|
150
|
+
Warns when multiple files share the same name in a directory.
|
|
151
|
+
"""
|
|
152
|
+
_LOGGER.info(
|
|
153
|
+
"Searching for code module '%s' in code directory: %s",
|
|
154
|
+
filename,
|
|
155
|
+
self.code_dir,
|
|
156
|
+
)
|
|
157
|
+
result = self._search_directory(filename, self.code_dir, "code directory")
|
|
158
|
+
if result is not None:
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
if self._dirs_differ:
|
|
162
|
+
_LOGGER.info(
|
|
163
|
+
"Code module '%s' not found in code directory '%s', "
|
|
164
|
+
"falling back to test directory: %s",
|
|
165
|
+
filename,
|
|
166
|
+
self.code_dir,
|
|
167
|
+
self.path,
|
|
168
|
+
)
|
|
169
|
+
result = self._search_directory(filename, self.path, "test directory")
|
|
170
|
+
if result is not None:
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
raise CodeModuleNotFoundError(
|
|
174
|
+
f"Code module '{filename}' not found in code directory '{self.code_dir}' "
|
|
175
|
+
f"or test directory '{self.path}'"
|
|
176
|
+
)
|
|
177
|
+
|
|
55
178
|
|
|
56
179
|
class BoxTestDiscovery(TestDiscovery):
|
|
57
180
|
"""Discovers tests for box tests, looking into module.test.json files"""
|
|
58
181
|
|
|
59
182
|
_IGNORE_FIELDS_IN_SPLUNK_CHECK_KEY = "ignore_fields_in_splunk_check"
|
|
60
183
|
|
|
61
|
-
def __init__(self, path: str, performance_check: str, limit_tests: int = None):
|
|
62
|
-
super().__init__(path, performance_check, limit_tests)
|
|
63
|
-
|
|
64
184
|
@classmethod
|
|
65
185
|
def _parse_ignore_fields_in_splunk_check(cls, test_block: dict) -> dict:
|
|
66
186
|
raw = test_block.get(cls._IGNORE_FIELDS_IN_SPLUNK_CHECK_KEY)
|
|
@@ -110,7 +230,6 @@ class BoxTestDiscovery(TestDiscovery):
|
|
|
110
230
|
}
|
|
111
231
|
|
|
112
232
|
for module_path, content in self._parsed_modules.items():
|
|
113
|
-
path = module_path.parent
|
|
114
233
|
tests_added = 0
|
|
115
234
|
for entry in content:
|
|
116
235
|
if self.limit_tests is not None and tests_added >= self.limit_tests:
|
|
@@ -121,7 +240,7 @@ class BoxTestDiscovery(TestDiscovery):
|
|
|
121
240
|
test_block = entry["test"]
|
|
122
241
|
name = entry["filename"].replace(".spl2", "")
|
|
123
242
|
test_input = test_block["source"]
|
|
124
|
-
code_module_path =
|
|
243
|
+
code_module_path = self._resolve_code_module(entry["filename"])
|
|
125
244
|
output = [
|
|
126
245
|
Results(x)
|
|
127
246
|
for x in test_block.get("expected_destination_result", [])
|
|
@@ -161,9 +280,10 @@ class SingleSPL2Discovery(TestDiscovery):
|
|
|
161
280
|
sample_file=None,
|
|
162
281
|
sample_delimiter="\n",
|
|
163
282
|
performance_check="no",
|
|
164
|
-
limit_tests: int = None,
|
|
283
|
+
limit_tests: Optional[int] = None,
|
|
284
|
+
code_dir: Optional[str] = None,
|
|
165
285
|
):
|
|
166
|
-
super().__init__(path, performance_check, limit_tests)
|
|
286
|
+
super().__init__(path, performance_check, limit_tests, code_dir=code_dir)
|
|
167
287
|
self.template_file = template_file
|
|
168
288
|
self.sample_file = sample_file
|
|
169
289
|
self.sample_delimiter = sample_delimiter
|
|
@@ -180,7 +300,9 @@ class SingleSPL2Discovery(TestDiscovery):
|
|
|
180
300
|
_LOGGER.info("Reading samples from provided sample file.")
|
|
181
301
|
events = self._read_samples_from_file(delimiter=self.sample_delimiter)
|
|
182
302
|
name = self.template_file.replace(".spl2", "")
|
|
183
|
-
code_module_path =
|
|
303
|
+
code_module_path = self._resolve_code_module(
|
|
304
|
+
os.path.basename(self.template_file)
|
|
305
|
+
)
|
|
184
306
|
else:
|
|
185
307
|
_LOGGER.info("Reading samples from corresponding module.json file")
|
|
186
308
|
self._parsed_modules = {
|
|
@@ -188,12 +310,11 @@ class SingleSPL2Discovery(TestDiscovery):
|
|
|
188
310
|
}
|
|
189
311
|
events = None
|
|
190
312
|
for module_path, content in self._parsed_modules.items():
|
|
191
|
-
path = module_path.parent
|
|
192
313
|
break_outer = False
|
|
193
314
|
for test in content:
|
|
194
315
|
if test["filename"] == os.path.basename(self.template_file):
|
|
195
316
|
name = test["filename"].replace(".spl2", "")
|
|
196
|
-
code_module_path =
|
|
317
|
+
code_module_path = self._resolve_code_module(test["filename"])
|
|
197
318
|
events = test["context"]["events"]
|
|
198
319
|
if isinstance(events, str):
|
|
199
320
|
events = events.split("\r\n")
|
|
@@ -246,8 +367,15 @@ class SingleSPL2Discovery(TestDiscovery):
|
|
|
246
367
|
class UTDiscovery(TestDiscovery):
|
|
247
368
|
"""Discovers tests for unit tests, looking into .test.spl2 files"""
|
|
248
369
|
|
|
249
|
-
def __init__(
|
|
250
|
-
|
|
370
|
+
def __init__(
|
|
371
|
+
self,
|
|
372
|
+
path: str,
|
|
373
|
+
limit_tests: Optional[int] = None,
|
|
374
|
+
code_dir: Optional[str] = None,
|
|
375
|
+
):
|
|
376
|
+
super().__init__(
|
|
377
|
+
path, performance_check="no", limit_tests=limit_tests, code_dir=code_dir
|
|
378
|
+
)
|
|
251
379
|
|
|
252
380
|
def discover_tests(self) -> None:
|
|
253
381
|
self._read_tests("*.test.spl2")
|
|
@@ -264,7 +392,7 @@ class UTDiscovery(TestDiscovery):
|
|
|
264
392
|
file_name = path.name
|
|
265
393
|
content = path.read_text(encoding="utf-8")
|
|
266
394
|
code_module_name = file_name.replace(".test.", ".")
|
|
267
|
-
code_module_path =
|
|
395
|
+
code_module_path = self._resolve_code_module(code_module_name)
|
|
268
396
|
|
|
269
397
|
tests_added = 0
|
|
270
398
|
for test in tests:
|
{spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/utils.py
RENAMED
|
@@ -67,13 +67,18 @@ def calculate_reduction_rate(original_event, reduced_event):
|
|
|
67
67
|
return reduction_rates
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def validate_compatibility(
|
|
71
|
-
"""
|
|
70
|
+
def validate_compatibility(actual, expected):
|
|
71
|
+
"""Check that every field in ``expected`` exists in ``actual`` with the same value.
|
|
72
|
+
|
|
73
|
+
Order matches the usual test convention ``assert actual == expected`` (observed
|
|
74
|
+
value first, reference second). For CIM checks, ``actual`` is the Splunk-processed
|
|
75
|
+
event and ``expected`` is ``box_test.cim_fields``.
|
|
76
|
+
"""
|
|
72
77
|
extracted_but_wrong = {}
|
|
73
78
|
not_extracted = {}
|
|
74
79
|
for field, value in expected.items():
|
|
75
|
-
if field in
|
|
76
|
-
if
|
|
80
|
+
if field in actual:
|
|
81
|
+
if actual[field] == value:
|
|
77
82
|
_LOGGER.info(f"FIELD {field} in reduced event")
|
|
78
83
|
continue
|
|
79
84
|
else:
|
|
File without changes
|
{spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spl2_testing_framework-1.6.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/test_runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|