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