spl2-testing-framework 1.8.3__tar.gz → 1.8.4__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.8.3 → spl2_testing_framework-1.8.4}/PKG-INFO +61 -1
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/README.md +60 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/pyproject.toml +2 -1
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/conftest.py +13 -1
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/spl2_test_config.json +1 -1
- spl2_testing_framework-1.8.4/spl2_testing_framework/splunk_setup.py +356 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/cli_job.py +12 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/search_clients/cli_search_client.py +20 -6
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/spl2test_runner.py +26 -11
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/test_discovery.py +30 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/test_types.py +2 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/LICENSE +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/__init__.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/launch_remotely_spl2.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/logger_manager.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/pytest.ini.sample +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/single_spl2_file_runner.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/spl2_utils/assertions.spl2 +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/test_runner.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/__init__.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/create_diff_sheet.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/__init__.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/cloud_job.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/job.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/splunk_job.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/jobs/test_cloud_job.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/performance.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/results.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/search_clients/__init__.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/search_clients/cloud_search_client.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/search_clients/search_client.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/search_clients/splunk_search_client.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/splunk_integration/__init__.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/splunk_integration/splunk_handler.py +0 -0
- {spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spl2-testing-framework
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.4
|
|
4
4
|
Summary: SPL2 Testing Framework
|
|
5
5
|
Home-page: https://github.com/splunk/spl2-testing-framework
|
|
6
6
|
Keywords: SPL2 testing framework
|
|
@@ -137,6 +137,7 @@ Additionally, the following options are supported:
|
|
|
137
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)
|
|
138
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)
|
|
139
139
|
* `--cli_bench` - when running tests with CLI, this option enables benchmarking mode by adding `-b` and `-n` flags to the spl2-processor-cli command. The value specifies the number of events to test (e.g., `--cli_bench=1000` will add `-b -n 1000` to the CLI command). **In bench mode, all tests always succeed and CLI output is printed to logs** - this is useful for performance testing and benchmarking. This is only applicable when using `--type cli`.
|
|
140
|
+
* `--lookups_root_dir` - root directory passed to `spl2-processor-cli` as `--lookupsRootDir` for resolving lookup `transforms.conf`. Defaults to `.` (current working directory). For a standard TA layout use `--lookups_root_dir=package/default`. Only applicable when using `--type cli`.
|
|
140
141
|
* `--check_splunk_results` - option to validate box test output by sending events to a Splunk instance (using HEC) and querying them back, with support for **CIM** (Common Information Model) and **TA** (Technology Add-on) checks.
|
|
141
142
|
* `--reduction_stats` - when set, **box tests** record a reduction rate per run: input `_raw` vs output `_raw` (after pairing input/output events). Rates are grouped by **pipeline** (the `.spl2` code module stem) and **sourcetype** (from the input event if set, else the output event, else `UNKNOWN`). Off by default.
|
|
142
143
|
* `--reduction_stats_file` - path to a JSON file written at the **end of the pytest session** with collected rates (default `reduction_stats.json`). Only used when `--reduction_stats` is set.
|
|
@@ -307,6 +308,26 @@ You can name top-level fields to drop on **both** the pipeline/actual row and ea
|
|
|
307
308
|
|
|
308
309
|
If the key is omitted, no fields are ignored. It applies to the **default in-memory** box test assert (no `--check_splunk_results`) and to **`--check_splunk_results=TA`**. It does **not** apply to **`--check_splunk_results=CIM`**. If the file or key is missing, no fields are ignored.
|
|
309
310
|
|
|
311
|
+
#### Skipping a test for specific run modes
|
|
312
|
+
|
|
313
|
+
Use **`skip_on`** in the `test` block to skip an individual test entry depending on how the framework is invoked:
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
"test": {
|
|
317
|
+
"source": "...",
|
|
318
|
+
"expected_destination_result": [ ... ],
|
|
319
|
+
"skip_on": ["splunk_ta", "splunk_cim"]
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Value | Skipped when |
|
|
324
|
+
|-------|-------------|
|
|
325
|
+
| `"standard"` | Running without `--check_splunk_results` (default in-memory compare) |
|
|
326
|
+
| `"splunk_ta"` | Running with `--check_splunk_results=TA` |
|
|
327
|
+
| `"splunk_cim"` | Running with `--check_splunk_results=CIM` |
|
|
328
|
+
|
|
329
|
+
A single value can be given as a plain string instead of a list. Unrecognised values are ignored with a warning.
|
|
330
|
+
|
|
310
331
|
#### Configuration:
|
|
311
332
|
|
|
312
333
|
`SPLUNK_INSTANCE` in `spl2_test_config.json`
|
|
@@ -351,6 +372,45 @@ Any `SPLUNK_INSTANCE` field can be set via the environment; config file values o
|
|
|
351
372
|
| `SPL2_TF_SPLUNK_INSTANCE_HEC_TOKEN` | `hec_token` |
|
|
352
373
|
|
|
353
374
|
|
|
375
|
+
## Starting a local Splunk instance for TA testing
|
|
376
|
+
|
|
377
|
+
`spl2_splunk_start` spins up a `splunk/splunk` Docker container with the TA mounted as an installed app and writes a ready-to-use `spl2_test_config.json`.
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
spl2_splunk_start --ta-path /path/to/ta
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The command:
|
|
384
|
+
1. Detects the TA app directory — prefers `<ta-path>/package/` (standard TA layout), falls back to `<ta-path>` itself.
|
|
385
|
+
2. Reads `app.manifest` to derive the Splunk app name.
|
|
386
|
+
3. Starts the container with the app directory mounted as a volume.
|
|
387
|
+
4. Waits for Ansible provisioning (`PLAY RECAP`) to complete, then polls the HEC health endpoint until Splunk is ready.
|
|
388
|
+
5. Writes `spl2_test_config.json` in the current directory with all connection details pre-filled.
|
|
389
|
+
|
|
390
|
+
**Options**
|
|
391
|
+
|
|
392
|
+
| Option | Default | Description |
|
|
393
|
+
|--------|---------|-------------|
|
|
394
|
+
| `--ta-path` | `.` | Path to the TA root directory. |
|
|
395
|
+
| `--splunk-version` | `10.4` | Splunk Docker image tag. |
|
|
396
|
+
| `--no-config` | off | Skip writing `spl2_test_config.json`. |
|
|
397
|
+
| `--config-dest` | `spl2_test_config.json` | Where to write the config file. |
|
|
398
|
+
|
|
399
|
+
Once the container is up, run your tests with:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
spl2_tests_run splunk --check_splunk_results=TA
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Requirements:** Docker must be running. The container exposes ports `8088` (HEC) and `8089` (API). If a container named `spl2-splunk` already exists with a matching app mount and Splunk version it is reused (started if stopped); if it exists with different parameters it is removed and recreated.
|
|
406
|
+
|
|
407
|
+
Override the default credentials via environment variables if needed:
|
|
408
|
+
|
|
409
|
+
| Variable | Default | Purpose |
|
|
410
|
+
|----------|---------|---------|
|
|
411
|
+
| `SPL2_TF_LOCAL_SPLUNK_PASSWORD` | `Chang3d!` | Splunk admin password |
|
|
412
|
+
| `SPL2_TF_LOCAL_SPLUNK_HEC_TOKEN` | *(built-in token)* | HEC token |
|
|
413
|
+
|
|
354
414
|
## Format all files
|
|
355
415
|
To format all files run:
|
|
356
416
|
|
|
@@ -104,6 +104,7 @@ Additionally, the following options are supported:
|
|
|
104
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)
|
|
105
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)
|
|
106
106
|
* `--cli_bench` - when running tests with CLI, this option enables benchmarking mode by adding `-b` and `-n` flags to the spl2-processor-cli command. The value specifies the number of events to test (e.g., `--cli_bench=1000` will add `-b -n 1000` to the CLI command). **In bench mode, all tests always succeed and CLI output is printed to logs** - this is useful for performance testing and benchmarking. This is only applicable when using `--type cli`.
|
|
107
|
+
* `--lookups_root_dir` - root directory passed to `spl2-processor-cli` as `--lookupsRootDir` for resolving lookup `transforms.conf`. Defaults to `.` (current working directory). For a standard TA layout use `--lookups_root_dir=package/default`. Only applicable when using `--type cli`.
|
|
107
108
|
* `--check_splunk_results` - option to validate box test output by sending events to a Splunk instance (using HEC) and querying them back, with support for **CIM** (Common Information Model) and **TA** (Technology Add-on) checks.
|
|
108
109
|
* `--reduction_stats` - when set, **box tests** record a reduction rate per run: input `_raw` vs output `_raw` (after pairing input/output events). Rates are grouped by **pipeline** (the `.spl2` code module stem) and **sourcetype** (from the input event if set, else the output event, else `UNKNOWN`). Off by default.
|
|
109
110
|
* `--reduction_stats_file` - path to a JSON file written at the **end of the pytest session** with collected rates (default `reduction_stats.json`). Only used when `--reduction_stats` is set.
|
|
@@ -274,6 +275,26 @@ You can name top-level fields to drop on **both** the pipeline/actual row and ea
|
|
|
274
275
|
|
|
275
276
|
If the key is omitted, no fields are ignored. It applies to the **default in-memory** box test assert (no `--check_splunk_results`) and to **`--check_splunk_results=TA`**. It does **not** apply to **`--check_splunk_results=CIM`**. If the file or key is missing, no fields are ignored.
|
|
276
277
|
|
|
278
|
+
#### Skipping a test for specific run modes
|
|
279
|
+
|
|
280
|
+
Use **`skip_on`** in the `test` block to skip an individual test entry depending on how the framework is invoked:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
"test": {
|
|
284
|
+
"source": "...",
|
|
285
|
+
"expected_destination_result": [ ... ],
|
|
286
|
+
"skip_on": ["splunk_ta", "splunk_cim"]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
| Value | Skipped when |
|
|
291
|
+
|-------|-------------|
|
|
292
|
+
| `"standard"` | Running without `--check_splunk_results` (default in-memory compare) |
|
|
293
|
+
| `"splunk_ta"` | Running with `--check_splunk_results=TA` |
|
|
294
|
+
| `"splunk_cim"` | Running with `--check_splunk_results=CIM` |
|
|
295
|
+
|
|
296
|
+
A single value can be given as a plain string instead of a list. Unrecognised values are ignored with a warning.
|
|
297
|
+
|
|
277
298
|
#### Configuration:
|
|
278
299
|
|
|
279
300
|
`SPLUNK_INSTANCE` in `spl2_test_config.json`
|
|
@@ -318,6 +339,45 @@ Any `SPLUNK_INSTANCE` field can be set via the environment; config file values o
|
|
|
318
339
|
| `SPL2_TF_SPLUNK_INSTANCE_HEC_TOKEN` | `hec_token` |
|
|
319
340
|
|
|
320
341
|
|
|
342
|
+
## Starting a local Splunk instance for TA testing
|
|
343
|
+
|
|
344
|
+
`spl2_splunk_start` spins up a `splunk/splunk` Docker container with the TA mounted as an installed app and writes a ready-to-use `spl2_test_config.json`.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
spl2_splunk_start --ta-path /path/to/ta
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The command:
|
|
351
|
+
1. Detects the TA app directory — prefers `<ta-path>/package/` (standard TA layout), falls back to `<ta-path>` itself.
|
|
352
|
+
2. Reads `app.manifest` to derive the Splunk app name.
|
|
353
|
+
3. Starts the container with the app directory mounted as a volume.
|
|
354
|
+
4. Waits for Ansible provisioning (`PLAY RECAP`) to complete, then polls the HEC health endpoint until Splunk is ready.
|
|
355
|
+
5. Writes `spl2_test_config.json` in the current directory with all connection details pre-filled.
|
|
356
|
+
|
|
357
|
+
**Options**
|
|
358
|
+
|
|
359
|
+
| Option | Default | Description |
|
|
360
|
+
|--------|---------|-------------|
|
|
361
|
+
| `--ta-path` | `.` | Path to the TA root directory. |
|
|
362
|
+
| `--splunk-version` | `10.4` | Splunk Docker image tag. |
|
|
363
|
+
| `--no-config` | off | Skip writing `spl2_test_config.json`. |
|
|
364
|
+
| `--config-dest` | `spl2_test_config.json` | Where to write the config file. |
|
|
365
|
+
|
|
366
|
+
Once the container is up, run your tests with:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
spl2_tests_run splunk --check_splunk_results=TA
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Requirements:** Docker must be running. The container exposes ports `8088` (HEC) and `8089` (API). If a container named `spl2-splunk` already exists with a matching app mount and Splunk version it is reused (started if stopped); if it exists with different parameters it is removed and recreated.
|
|
373
|
+
|
|
374
|
+
Override the default credentials via environment variables if needed:
|
|
375
|
+
|
|
376
|
+
| Variable | Default | Purpose |
|
|
377
|
+
|----------|---------|---------|
|
|
378
|
+
| `SPL2_TF_LOCAL_SPLUNK_PASSWORD` | `Chang3d!` | Splunk admin password |
|
|
379
|
+
| `SPL2_TF_LOCAL_SPLUNK_HEC_TOKEN` | *(built-in token)* | HEC token |
|
|
380
|
+
|
|
321
381
|
## Format all files
|
|
322
382
|
To format all files run:
|
|
323
383
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[tool.poetry]
|
|
17
17
|
name = "spl2-testing-framework"
|
|
18
|
-
version = "1.8.
|
|
18
|
+
version = "1.8.4"
|
|
19
19
|
description = "SPL2 Testing Framework"
|
|
20
20
|
authors = ["mgazda-splunk <mgazda@splunk.com>"]
|
|
21
21
|
readme = "README.md"
|
|
@@ -30,6 +30,7 @@ include = "spl2_testing_framework"
|
|
|
30
30
|
[tool.poetry.scripts]
|
|
31
31
|
spl2_tests_run = "spl2_testing_framework.test_runner:run"
|
|
32
32
|
single_spl2_file_run = "spl2_testing_framework.single_spl2_file_runner:run"
|
|
33
|
+
spl2_splunk_start = "spl2_testing_framework.splunk_setup:run"
|
|
33
34
|
|
|
34
35
|
[tool.poetry.dependencies]
|
|
35
36
|
python = "^3.7.1"
|
{spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/conftest.py
RENAMED
|
@@ -60,6 +60,7 @@ TEST_DIR = "test_dir"
|
|
|
60
60
|
CODE_DIR = "code_dir"
|
|
61
61
|
LIMIT_TESTS = "limit_tests"
|
|
62
62
|
CLI_BENCH = "cli_bench"
|
|
63
|
+
LOOKUPS_ROOT_DIR = "lookups_root_dir"
|
|
63
64
|
REDUCTION_STATS = "reduction_stats"
|
|
64
65
|
|
|
65
66
|
setup_logging()
|
|
@@ -88,6 +89,13 @@ def pytest_addoption(parser):
|
|
|
88
89
|
parser.addoption(f"--{IGNORE_ADDITIONAL_FIELDS_IN_ACTUAL}", action="store_true")
|
|
89
90
|
parser.addoption(f"--{LIMIT_TESTS}", action="store", type=int, default=None)
|
|
90
91
|
parser.addoption(f"--{CLI_BENCH}", action="store", type=int, default=None)
|
|
92
|
+
parser.addoption(
|
|
93
|
+
f"--{LOOKUPS_ROOT_DIR}",
|
|
94
|
+
action="store",
|
|
95
|
+
type=str,
|
|
96
|
+
default=".",
|
|
97
|
+
help="Root directory for lookups transforms.conf passed to spl2-processor-cli as --lookupsRootDir. Defaults to '.' (current directory).",
|
|
98
|
+
)
|
|
91
99
|
parser.addoption(
|
|
92
100
|
f"--{REDUCTION_STATS}",
|
|
93
101
|
action="store_true",
|
|
@@ -181,7 +189,10 @@ def spl2_test_runner(
|
|
|
181
189
|
search_client = SplunkSearchClient(url=url, auth=basic_authentication)
|
|
182
190
|
elif test_type == "cli":
|
|
183
191
|
cli_bench = run_configurations.get(CLI_BENCH)
|
|
184
|
-
|
|
192
|
+
lookups_root_dir = run_configurations.get(LOOKUPS_ROOT_DIR)
|
|
193
|
+
search_client = CLISearchClient(
|
|
194
|
+
cli_bench=cli_bench, lookups_root_dir=lookups_root_dir
|
|
195
|
+
)
|
|
185
196
|
elif test_type == "cloud":
|
|
186
197
|
url = f"https://{config['tenant']}.api.{config['cloud_instance']}/{config['tenant']}"
|
|
187
198
|
search_client = CloudSearchClient(url=url, auth=bearer_token_authentication)
|
|
@@ -204,6 +215,7 @@ def run_configurations(request):
|
|
|
204
215
|
f"--{CREATE_COMPARISON_SHEET}"
|
|
205
216
|
),
|
|
206
217
|
CLI_BENCH: request.config.getoption(f"--{CLI_BENCH}"),
|
|
218
|
+
LOOKUPS_ROOT_DIR: request.config.getoption(f"--{LOOKUPS_ROOT_DIR}"),
|
|
207
219
|
CHECK_SPLUNK_RESULTS: request.config.getoption(f"--{CHECK_SPLUNK_RESULTS}"),
|
|
208
220
|
REDUCTION_STATS: request.config.getoption(
|
|
209
221
|
f"--{REDUCTION_STATS}", default=False
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Copyright 2026 Splunk Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
spl2_splunk_start — spin up a local Splunk container with a TA mounted and
|
|
17
|
+
dump a ready-to-use spl2_test_config.json.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
spl2_splunk_start [--ta-path /path/to/ta] [--splunk-version 10.4] [--no-config]
|
|
21
|
+
|
|
22
|
+
The TA's ``package/`` directory is mounted into the container as an installed
|
|
23
|
+
Splunk app. Splunk is pre-configured with a HEC token so you can immediately
|
|
24
|
+
run:
|
|
25
|
+
|
|
26
|
+
spl2_tests_run splunk --check_splunk_results=TA
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import select
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
_SPLUNK_PASSWORD = os.environ.get("SPL2_TF_LOCAL_SPLUNK_PASSWORD", "Chang3d!")
|
|
39
|
+
_HEC_TOKEN = os.environ.get(
|
|
40
|
+
"SPL2_TF_LOCAL_SPLUNK_HEC_TOKEN", "4a8a737d-5452-426c-a6f7-106dca4e813f"
|
|
41
|
+
)
|
|
42
|
+
_INDEX = "spl2tf"
|
|
43
|
+
_CONTAINER_NAME = "spl2-splunk"
|
|
44
|
+
_HEC_PORT = 8088
|
|
45
|
+
_API_PORT = 8089
|
|
46
|
+
_CONFIG_FILENAME = "spl2_test_config.json"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _find_ta_app_dir(ta_path: Path) -> Path:
|
|
50
|
+
"""Return the directory to mount as a Splunk app.
|
|
51
|
+
|
|
52
|
+
Prefers ``<ta_path>/package`` (standard TA layout). Falls back to
|
|
53
|
+
``ta_path`` itself so the command also works when pointed directly at a
|
|
54
|
+
built/extracted app directory.
|
|
55
|
+
"""
|
|
56
|
+
candidate = ta_path / "package"
|
|
57
|
+
if candidate.is_dir():
|
|
58
|
+
return candidate
|
|
59
|
+
return ta_path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_app_name(app_dir: Path) -> str:
|
|
63
|
+
"""Derive a Splunk app name from app.manifest or the directory name."""
|
|
64
|
+
manifest = app_dir / "app.manifest"
|
|
65
|
+
if manifest.exists():
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(manifest.read_text())
|
|
68
|
+
name = data.get("info", {}).get("id", {}).get("name") or data.get("app_id")
|
|
69
|
+
if name:
|
|
70
|
+
return name
|
|
71
|
+
except (json.JSONDecodeError, AttributeError):
|
|
72
|
+
pass
|
|
73
|
+
return app_dir.name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _docker(*args, check=True, capture=False):
|
|
77
|
+
cmd = ["docker", *args]
|
|
78
|
+
return subprocess.run(
|
|
79
|
+
cmd,
|
|
80
|
+
check=check,
|
|
81
|
+
capture_output=capture,
|
|
82
|
+
text=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _container_exists(name: str) -> bool:
|
|
87
|
+
result = _docker(
|
|
88
|
+
"ps",
|
|
89
|
+
"-a",
|
|
90
|
+
"--filter",
|
|
91
|
+
f"name=^{name}$",
|
|
92
|
+
"--format",
|
|
93
|
+
"{{.Names}}",
|
|
94
|
+
capture=True,
|
|
95
|
+
)
|
|
96
|
+
return name in result.stdout.strip().splitlines()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _container_inspect(name: str) -> dict:
|
|
100
|
+
result = _docker("inspect", name, capture=True, check=False)
|
|
101
|
+
if result.returncode != 0:
|
|
102
|
+
return {}
|
|
103
|
+
try:
|
|
104
|
+
return json.loads(result.stdout)[0]
|
|
105
|
+
except (json.JSONDecodeError, IndexError):
|
|
106
|
+
return {}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _container_matches(name: str, app_dir_abs: str, splunk_version: str) -> bool:
|
|
110
|
+
"""Return True if the existing container was started with the same app mount and image."""
|
|
111
|
+
info = _container_inspect(name)
|
|
112
|
+
if not info:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
image = info.get("Config", {}).get("Image", "")
|
|
116
|
+
if f"splunk/splunk:{splunk_version}" not in image:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
mounts = info.get("Mounts", [])
|
|
120
|
+
for mount in mounts:
|
|
121
|
+
if mount.get("Source") == app_dir_abs:
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _write_config(dest: Path) -> None:
|
|
127
|
+
config = {
|
|
128
|
+
"host": "https://localhost",
|
|
129
|
+
"cloud_instance": "",
|
|
130
|
+
"tenant": "",
|
|
131
|
+
"bearer_token": "",
|
|
132
|
+
"port": str(_API_PORT),
|
|
133
|
+
"user": "admin",
|
|
134
|
+
"password": _SPLUNK_PASSWORD,
|
|
135
|
+
"SPLUNK_INSTANCE": {
|
|
136
|
+
"ip": "localhost",
|
|
137
|
+
"port": _HEC_PORT,
|
|
138
|
+
"api_port": _API_PORT,
|
|
139
|
+
"username": "admin",
|
|
140
|
+
"password": _SPLUNK_PASSWORD,
|
|
141
|
+
"index": _INDEX,
|
|
142
|
+
"hec_token": _HEC_TOKEN,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
dest.write_text(json.dumps(config, indent=2))
|
|
146
|
+
print(f"Config written to {dest}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _wait_for_ansible(timeout: int = 300) -> bool:
|
|
150
|
+
"""Stream container logs until Ansible finishes (PLAY RECAP line appears).
|
|
151
|
+
|
|
152
|
+
Returns True on success, False on timeout or container stop.
|
|
153
|
+
Uses a polling read loop so the timeout is always enforced.
|
|
154
|
+
"""
|
|
155
|
+
print("Waiting for Splunk Ansible provisioning to finish", end="", flush=True)
|
|
156
|
+
deadline = time.time() + timeout
|
|
157
|
+
proc = subprocess.Popen(
|
|
158
|
+
["docker", "logs", "-f", _CONTAINER_NAME],
|
|
159
|
+
stdout=subprocess.PIPE,
|
|
160
|
+
stderr=subprocess.STDOUT,
|
|
161
|
+
)
|
|
162
|
+
try:
|
|
163
|
+
buf = b""
|
|
164
|
+
while time.time() < deadline:
|
|
165
|
+
remaining = deadline - time.time()
|
|
166
|
+
ready, _, _ = select.select([proc.stdout], [], [], min(remaining, 2.0))
|
|
167
|
+
if ready:
|
|
168
|
+
chunk = proc.stdout.read(4096)
|
|
169
|
+
if not chunk:
|
|
170
|
+
break
|
|
171
|
+
buf += chunk
|
|
172
|
+
lines = buf.split(b"\n")
|
|
173
|
+
buf = lines[-1]
|
|
174
|
+
for line in lines[:-1]:
|
|
175
|
+
text = line.decode("utf-8", errors="replace")
|
|
176
|
+
if "PLAY RECAP" in text:
|
|
177
|
+
print(" done.")
|
|
178
|
+
return True
|
|
179
|
+
print(".", end="", flush=True)
|
|
180
|
+
else:
|
|
181
|
+
if proc.poll() is not None:
|
|
182
|
+
break
|
|
183
|
+
finally:
|
|
184
|
+
try:
|
|
185
|
+
proc.kill()
|
|
186
|
+
except OSError:
|
|
187
|
+
pass
|
|
188
|
+
proc.wait()
|
|
189
|
+
|
|
190
|
+
print()
|
|
191
|
+
print(
|
|
192
|
+
"ERROR: Ansible provisioning did not complete within the timeout. "
|
|
193
|
+
"Check container logs with: docker logs spl2-splunk",
|
|
194
|
+
file=sys.stderr,
|
|
195
|
+
)
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _wait_for_hec(timeout: int = 60) -> bool:
|
|
200
|
+
"""Poll the HEC health endpoint — returns True once splunkd is fully up."""
|
|
201
|
+
import urllib.request
|
|
202
|
+
import ssl
|
|
203
|
+
|
|
204
|
+
hec_health = f"https://localhost:{_HEC_PORT}/services/collector/health"
|
|
205
|
+
ctx = ssl.create_default_context()
|
|
206
|
+
ctx.check_hostname = False
|
|
207
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
208
|
+
|
|
209
|
+
print("Waiting for Splunk HEC to become ready", end="", flush=True)
|
|
210
|
+
deadline = time.time() + timeout
|
|
211
|
+
while time.time() < deadline:
|
|
212
|
+
try:
|
|
213
|
+
with urllib.request.urlopen(hec_health, context=ctx, timeout=3) as resp:
|
|
214
|
+
if resp.status == 200:
|
|
215
|
+
print(" ready.")
|
|
216
|
+
return True
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
print(".", end="", flush=True)
|
|
220
|
+
time.sleep(3)
|
|
221
|
+
|
|
222
|
+
print()
|
|
223
|
+
print(
|
|
224
|
+
"ERROR: HEC did not become ready within the timeout. "
|
|
225
|
+
"Check container logs with: docker logs spl2-splunk",
|
|
226
|
+
file=sys.stderr,
|
|
227
|
+
)
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def start(
|
|
232
|
+
ta_path: Path, splunk_version: str, write_config: bool, config_dest: Path
|
|
233
|
+
) -> None:
|
|
234
|
+
app_dir = _find_ta_app_dir(ta_path)
|
|
235
|
+
if not app_dir.is_dir():
|
|
236
|
+
print(f"ERROR: TA app directory not found at {app_dir}", file=sys.stderr)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
app_name = _resolve_app_name(app_dir)
|
|
240
|
+
app_dir_abs = str(app_dir.resolve())
|
|
241
|
+
container_app_path = f"/opt/splunk/etc/apps/{app_name}"
|
|
242
|
+
|
|
243
|
+
print(f"TA app dir : {app_dir_abs}")
|
|
244
|
+
print(f"App name : {app_name}")
|
|
245
|
+
print(f"Container : {_CONTAINER_NAME}")
|
|
246
|
+
print(f"Splunk : splunk/splunk:{splunk_version}")
|
|
247
|
+
|
|
248
|
+
already_running = False
|
|
249
|
+
if _container_exists(_CONTAINER_NAME):
|
|
250
|
+
info = _container_inspect(_CONTAINER_NAME)
|
|
251
|
+
if _container_matches(_CONTAINER_NAME, app_dir_abs, splunk_version):
|
|
252
|
+
if info.get("State", {}).get("Running"):
|
|
253
|
+
print(
|
|
254
|
+
f"Container '{_CONTAINER_NAME}' already running with matching app and version."
|
|
255
|
+
)
|
|
256
|
+
already_running = True
|
|
257
|
+
else:
|
|
258
|
+
print(
|
|
259
|
+
f"Container '{_CONTAINER_NAME}' exists with matching config — starting it."
|
|
260
|
+
)
|
|
261
|
+
_docker("start", _CONTAINER_NAME)
|
|
262
|
+
else:
|
|
263
|
+
print(
|
|
264
|
+
f"Container '{_CONTAINER_NAME}' exists but has a different app or Splunk version — removing it."
|
|
265
|
+
)
|
|
266
|
+
_docker("rm", "-f", _CONTAINER_NAME)
|
|
267
|
+
_launch(app_dir_abs, container_app_path, splunk_version)
|
|
268
|
+
else:
|
|
269
|
+
_launch(app_dir_abs, container_app_path, splunk_version)
|
|
270
|
+
|
|
271
|
+
if not already_running and not _wait_for_ansible():
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
|
|
274
|
+
if not _wait_for_hec():
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
if write_config:
|
|
278
|
+
_write_config(config_dest)
|
|
279
|
+
|
|
280
|
+
print()
|
|
281
|
+
print("Splunk is up. Run your tests with:")
|
|
282
|
+
print(" spl2_tests_run splunk --check_splunk_results=TA")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _launch(app_dir_abs: str, container_app_path: str, splunk_version: str) -> None:
|
|
286
|
+
_docker(
|
|
287
|
+
"run",
|
|
288
|
+
"-d",
|
|
289
|
+
"--name",
|
|
290
|
+
_CONTAINER_NAME,
|
|
291
|
+
"--platform",
|
|
292
|
+
"linux/amd64",
|
|
293
|
+
"-p",
|
|
294
|
+
f"{_HEC_PORT}:{_HEC_PORT}",
|
|
295
|
+
"-p",
|
|
296
|
+
f"{_API_PORT}:{_API_PORT}",
|
|
297
|
+
"-e",
|
|
298
|
+
f"SPLUNK_PASSWORD={_SPLUNK_PASSWORD}",
|
|
299
|
+
"-e",
|
|
300
|
+
"SPLUNK_START_ARGS=--accept-license",
|
|
301
|
+
"-e",
|
|
302
|
+
"SPLUNK_GENERAL_TERMS=--accept-sgt-current-at-splunk-com",
|
|
303
|
+
"-e",
|
|
304
|
+
f"SPLUNK_HEC_TOKEN={_HEC_TOKEN}",
|
|
305
|
+
"-v",
|
|
306
|
+
f"{app_dir_abs}:{container_app_path}",
|
|
307
|
+
f"splunk/splunk:{splunk_version}",
|
|
308
|
+
)
|
|
309
|
+
print(f"Container '{_CONTAINER_NAME}' started.")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def run() -> None:
|
|
313
|
+
"""Entry point for the ``spl2_splunk_start`` console script."""
|
|
314
|
+
parser = argparse.ArgumentParser(
|
|
315
|
+
description=(
|
|
316
|
+
"Start a local Splunk container with a TA mounted and write "
|
|
317
|
+
"spl2_test_config.json ready for --check_splunk_results."
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
parser.add_argument(
|
|
321
|
+
"--ta-path",
|
|
322
|
+
default=".",
|
|
323
|
+
help=(
|
|
324
|
+
"Path to the TA root directory (must contain a 'package/' subdirectory "
|
|
325
|
+
"or be the app directory itself). Defaults to the current directory."
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
"--splunk-version",
|
|
330
|
+
default="10.4",
|
|
331
|
+
help="Splunk Docker image tag (default: 10.4).",
|
|
332
|
+
)
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
"--no-config",
|
|
335
|
+
action="store_true",
|
|
336
|
+
default=False,
|
|
337
|
+
help="Skip writing spl2_test_config.json.",
|
|
338
|
+
)
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--config-dest",
|
|
341
|
+
default=_CONFIG_FILENAME,
|
|
342
|
+
help=f"Where to write the config file (default: {_CONFIG_FILENAME} in CWD).",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
args = parser.parse_args()
|
|
346
|
+
|
|
347
|
+
start(
|
|
348
|
+
ta_path=Path(args.ta_path).resolve(),
|
|
349
|
+
splunk_version=args.splunk_version,
|
|
350
|
+
write_config=not args.no_config,
|
|
351
|
+
config_dest=Path(args.config_dest),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
run()
|
|
@@ -19,6 +19,10 @@ from .job import Job
|
|
|
19
19
|
class CLIAssertionJob(Job):
|
|
20
20
|
"""A job that is executed on CLI in unit tests."""
|
|
21
21
|
|
|
22
|
+
def __init__(self, *args, lookups_root_dir=None, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
self.lookups_root_dir = lookups_root_dir
|
|
25
|
+
|
|
22
26
|
def create(self) -> "CLIAssertionJob":
|
|
23
27
|
"""Creates a job. To run the job use run_job() method."""
|
|
24
28
|
|
|
@@ -30,6 +34,9 @@ class CLIAssertionJob(Job):
|
|
|
30
34
|
self.test_name,
|
|
31
35
|
]
|
|
32
36
|
|
|
37
|
+
if self.lookups_root_dir is not None:
|
|
38
|
+
command.extend(["--lookupsRootDir", self.lookups_root_dir])
|
|
39
|
+
|
|
33
40
|
input_data = "\n\n".join(
|
|
34
41
|
[
|
|
35
42
|
( # for assertion tests we don't add assertions module
|
|
@@ -56,10 +63,12 @@ class CLISimpleJob(Job):
|
|
|
56
63
|
code_module_content,
|
|
57
64
|
test_name,
|
|
58
65
|
cli_bench=None,
|
|
66
|
+
lookups_root_dir=None,
|
|
59
67
|
):
|
|
60
68
|
code_module_content += f"\n$source = from {str(source)};"
|
|
61
69
|
super().__init__("", code_module_name, code_module_content, test_name)
|
|
62
70
|
self.cli_bench = cli_bench
|
|
71
|
+
self.lookups_root_dir = lookups_root_dir
|
|
63
72
|
|
|
64
73
|
def create(self) -> "CLISimpleJob":
|
|
65
74
|
"""Creates a job. To run the job use run_job() method."""
|
|
@@ -71,6 +80,9 @@ class CLISimpleJob(Job):
|
|
|
71
80
|
if self.cli_bench is not None:
|
|
72
81
|
command.extend(["-b", "-n", str(self.cli_bench)])
|
|
73
82
|
|
|
83
|
+
if self.lookups_root_dir is not None:
|
|
84
|
+
command.extend(["--lookupsRootDir", self.lookups_root_dir])
|
|
85
|
+
|
|
74
86
|
input_data = "\n\n".join(
|
|
75
87
|
[
|
|
76
88
|
self.commands_module,
|
|
@@ -29,21 +29,30 @@ class CLISearchClient(SearchClient):
|
|
|
29
29
|
|
|
30
30
|
jobs_dict = {}
|
|
31
31
|
|
|
32
|
-
def __init__(self, cli_bench=None):
|
|
33
|
-
"""Initialize CLISearchClient with optional bench
|
|
32
|
+
def __init__(self, cli_bench=None, lookups_root_dir=None):
|
|
33
|
+
"""Initialize CLISearchClient with optional bench and lookups parameters.
|
|
34
34
|
|
|
35
35
|
Args:
|
|
36
36
|
cli_bench: Number of events to use for benchmarking (adds -b and -n flags)
|
|
37
|
+
lookups_root_dir: Root directory for lookups transforms.conf (adds --lookupsRootDir)
|
|
37
38
|
"""
|
|
38
39
|
self.cli_bench = cli_bench
|
|
40
|
+
self.lookups_root_dir = lookups_root_dir
|
|
39
41
|
|
|
40
|
-
@staticmethod
|
|
41
42
|
def create_job(
|
|
42
|
-
|
|
43
|
+
self,
|
|
44
|
+
test_module: str,
|
|
45
|
+
code_file_name: str,
|
|
46
|
+
code_to_test: str,
|
|
47
|
+
test_statement: str,
|
|
43
48
|
) -> Job:
|
|
44
49
|
"""Creates a job for unit test run."""
|
|
45
50
|
return CLIAssertionJob(
|
|
46
|
-
test_module,
|
|
51
|
+
test_module,
|
|
52
|
+
code_file_name,
|
|
53
|
+
code_to_test,
|
|
54
|
+
test_statement,
|
|
55
|
+
lookups_root_dir=self.lookups_root_dir,
|
|
47
56
|
).create()
|
|
48
57
|
|
|
49
58
|
def create_simple_job(
|
|
@@ -52,7 +61,12 @@ class CLISearchClient(SearchClient):
|
|
|
52
61
|
"""Creates a job for box test run."""
|
|
53
62
|
|
|
54
63
|
return CLISimpleJob(
|
|
55
|
-
source,
|
|
64
|
+
source,
|
|
65
|
+
code_module_name,
|
|
66
|
+
code_module_content,
|
|
67
|
+
test_name,
|
|
68
|
+
self.cli_bench,
|
|
69
|
+
self.lookups_root_dir,
|
|
56
70
|
).create()
|
|
57
71
|
|
|
58
72
|
def run_job(self, job):
|
|
@@ -159,6 +159,16 @@ class SPL2TestRunner:
|
|
|
159
159
|
|
|
160
160
|
def run_box_test(self, box_test: BoxTest) -> None:
|
|
161
161
|
"""Run a box test"""
|
|
162
|
+
if box_test.skip_on:
|
|
163
|
+
if self.check_splunk_results == "TA" and "splunk_ta" in box_test.skip_on:
|
|
164
|
+
pytest.skip("Skipped for splunk_ta (skip_on)")
|
|
165
|
+
elif (
|
|
166
|
+
self.check_splunk_results == "CIM" and "splunk_cim" in box_test.skip_on
|
|
167
|
+
):
|
|
168
|
+
pytest.skip("Skipped for splunk_cim (skip_on)")
|
|
169
|
+
elif not self.check_splunk_results and "standard" in box_test.skip_on:
|
|
170
|
+
pytest.skip("Skipped for standard (skip_on)")
|
|
171
|
+
|
|
162
172
|
code_to_test = _make_functions_visible_for_testing(box_test.code_module_path)
|
|
163
173
|
|
|
164
174
|
perf = PerformanceCheck(box_test)
|
|
@@ -203,9 +213,10 @@ class SPL2TestRunner:
|
|
|
203
213
|
ignore_keys = frozenset(
|
|
204
214
|
box_test.ignore_fields_in_destination_compare.keys()
|
|
205
215
|
)
|
|
206
|
-
for
|
|
207
|
-
|
|
208
|
-
|
|
216
|
+
for actual_result, expected_result in zip(
|
|
217
|
+
sorted(output_result), sorted(box_test.output)
|
|
218
|
+
):
|
|
219
|
+
actual_result.remove_fields_not_in_expected(expected_result)
|
|
209
220
|
|
|
210
221
|
if self.create_comparison_sheet:
|
|
211
222
|
self.comp_obj.create_comparison_sheet(output_result, box_test)
|
|
@@ -238,9 +249,10 @@ class SPL2TestRunner:
|
|
|
238
249
|
|
|
239
250
|
if box_test.metrics:
|
|
240
251
|
# Remove fields from actual metrics that are not in expected metrics if configured
|
|
241
|
-
for
|
|
242
|
-
|
|
243
|
-
|
|
252
|
+
for actual_metric, expected_metric in zip(
|
|
253
|
+
sorted(metrics_result), sorted(box_test.metrics)
|
|
254
|
+
):
|
|
255
|
+
actual_metric.remove_fields_not_in_expected(expected_metric)
|
|
244
256
|
|
|
245
257
|
assert sorted(metrics_result) == sorted(box_test.metrics)
|
|
246
258
|
_LOGGER.info(f"Metric check passed: {box_test.name}")
|
|
@@ -259,14 +271,13 @@ class SPL2TestRunner:
|
|
|
259
271
|
|
|
260
272
|
``Results._cast_types`` updates dicts in place. Splunk-shaped rows are
|
|
261
273
|
JSON-like; cloning via :func:`json.dumps` / :func:`json.loads` is
|
|
262
|
-
typically faster than :func:`copy.deepcopy`.
|
|
263
|
-
|
|
264
|
-
deepcopy only if JSON cloning fails.
|
|
274
|
+
typically faster than :func:`copy.deepcopy`. Falls back to deepcopy for
|
|
275
|
+
any row that contains non-JSON-serializable values.
|
|
265
276
|
"""
|
|
266
277
|
if not d:
|
|
267
278
|
return {}
|
|
268
279
|
try:
|
|
269
|
-
return json.loads(json.dumps(d
|
|
280
|
+
return json.loads(json.dumps(d))
|
|
270
281
|
except (TypeError, ValueError):
|
|
271
282
|
return copy.deepcopy(d)
|
|
272
283
|
|
|
@@ -339,7 +350,11 @@ class SPL2TestRunner:
|
|
|
339
350
|
expected_data = self._row_as_dict(expected)
|
|
340
351
|
# Splunk adds many search-time fields; mirror in-memory compare by
|
|
341
352
|
# dropping keys not present on the expected row before asserting.
|
|
342
|
-
|
|
353
|
+
# Apply the same Results normalization (e.g. "True"->"true") that
|
|
354
|
+
# the non-Splunk path gets automatically via Results(_cast_types).
|
|
355
|
+
actual_dict = Results._cast_types(
|
|
356
|
+
self._tree_copy_for_results(dict(processed_event))
|
|
357
|
+
)
|
|
343
358
|
Results._remove_extra_fields(actual_dict, expected_data)
|
|
344
359
|
actual_cmp = self._strip_ignored_fields_for_destination_compare(
|
|
345
360
|
actual_dict, ignore_keys
|
|
@@ -180,6 +180,34 @@ class BoxTestDiscovery(TestDiscovery):
|
|
|
180
180
|
"""Discovers tests for box tests, looking into module.test.json files"""
|
|
181
181
|
|
|
182
182
|
_IGNORE_FIELDS_IN_DESTINATION_COMPARE_KEY = "ignore_fields_in_destination_compare"
|
|
183
|
+
_VALID_SKIP_ON_VALUES = frozenset({"standard", "splunk_ta", "splunk_cim"})
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def _parse_skip_on(cls, test_block: dict) -> frozenset:
|
|
187
|
+
raw = test_block.get("skip_on")
|
|
188
|
+
if raw is None:
|
|
189
|
+
return frozenset()
|
|
190
|
+
if isinstance(raw, str):
|
|
191
|
+
raw = [raw]
|
|
192
|
+
if not isinstance(raw, list):
|
|
193
|
+
_LOGGER.warning(
|
|
194
|
+
"skip_on must be a string or list of strings; ignoring %r", raw
|
|
195
|
+
)
|
|
196
|
+
return frozenset()
|
|
197
|
+
result = set()
|
|
198
|
+
for v in raw:
|
|
199
|
+
if not isinstance(v, str):
|
|
200
|
+
_LOGGER.warning("skip_on entries must be strings; skipping %r", v)
|
|
201
|
+
continue
|
|
202
|
+
if v not in cls._VALID_SKIP_ON_VALUES:
|
|
203
|
+
_LOGGER.warning(
|
|
204
|
+
"Unknown skip_on value %r; valid values: %s",
|
|
205
|
+
v,
|
|
206
|
+
sorted(cls._VALID_SKIP_ON_VALUES),
|
|
207
|
+
)
|
|
208
|
+
continue
|
|
209
|
+
result.add(v)
|
|
210
|
+
return frozenset(result)
|
|
183
211
|
|
|
184
212
|
@classmethod
|
|
185
213
|
def _parse_ignore_fields_in_destination_compare(cls, test_block: dict) -> dict:
|
|
@@ -255,6 +283,7 @@ class BoxTestDiscovery(TestDiscovery):
|
|
|
255
283
|
ignore_fields_in_destination_compare = (
|
|
256
284
|
self._parse_ignore_fields_in_destination_compare(test_block)
|
|
257
285
|
)
|
|
286
|
+
skip_on = self._parse_skip_on(test_block)
|
|
258
287
|
box_test = BoxTest(
|
|
259
288
|
name=name,
|
|
260
289
|
input=test_input,
|
|
@@ -264,6 +293,7 @@ class BoxTestDiscovery(TestDiscovery):
|
|
|
264
293
|
code_module_path=code_module_path,
|
|
265
294
|
cim_fields=cim_fields,
|
|
266
295
|
ignore_fields_in_destination_compare=ignore_fields_in_destination_compare,
|
|
296
|
+
skip_on=skip_on,
|
|
267
297
|
)
|
|
268
298
|
box_test.meta["performance"] = self._performance_check
|
|
269
299
|
self.tests.add(box_test)
|
|
@@ -48,6 +48,8 @@ class BoxTest(Test):
|
|
|
48
48
|
cim_fields: dict = field(default_factory=dict)
|
|
49
49
|
# Field name -> reason; stripped on both sides before destination asserts (not used for CIM).
|
|
50
50
|
ignore_fields_in_destination_compare: dict = field(default_factory=dict)
|
|
51
|
+
# Set of test modes to skip: "standard", "splunk_ta", "splunk_cim"
|
|
52
|
+
skip_on: frozenset = field(default_factory=frozenset)
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
@dataclass
|
|
File without changes
|
{spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/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
|
{spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/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
|
{spl2_testing_framework-1.8.3 → spl2_testing_framework-1.8.4}/spl2_testing_framework/tools/utils.py
RENAMED
|
File without changes
|