Skip to content

Commit 04ee48f

Browse files
author
Fred Ross
committed
Merge pull request #44 from splunk/feature/testsuite
First round of Ace compatibility changes, and a completely revised test suite.
2 parents 2081b5a + 0ff3a98 commit 04ee48f

18 files changed

+1451
-1547
lines changed

splunklib/binding.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import ssl
3232
import urllib
3333
import functools
34+
import logging
35+
from datetime import datetime
3436

3537
from contextlib import contextmanager
3638

@@ -53,6 +55,13 @@
5355
DEFAULT_PORT = "8089"
5456
DEFAULT_SCHEME = "https"
5557

58+
@contextmanager
59+
def log_duration():
60+
start_time = datetime.now()
61+
yield
62+
end_time = datetime.now()
63+
logging.debug("Operation took %s", end_time-start_time)
64+
5665
class AuthenticationError(Exception):
5766
pass
5867

@@ -131,6 +140,8 @@ def __mod__(self, fields):
131140
``TypeError``.
132141
"""
133142
raise TypeError("Cannot interpolate into a UrlEncoded object.")
143+
def __repr__(self):
144+
return "UrlEncoded('%s')" % urllib.unquote(self)
134145

135146
@contextmanager
136147
def _handle_auth_error(msg):
@@ -461,7 +472,10 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
461472
"""
462473
path = self.authority + self._abspath(path_segment, owner=owner,
463474
app=app, sharing=sharing)
464-
return self.http.delete(path, self._auth_headers, **query)
475+
logging.debug("DELETE request to %s (body: %s)", path, repr(query))
476+
with log_duration():
477+
response = self.http.delete(path, self._auth_headers, **query)
478+
return response
465479

466480
@_authentication
467481
def get(self, path_segment, owner=None, app=None, sharing=None, **query):
@@ -508,7 +522,10 @@ def get(self, path_segment, owner=None, app=None, sharing=None, **query):
508522
"""
509523
path = self.authority + self._abspath(path_segment, owner=owner,
510524
app=app, sharing=sharing)
511-
return self.http.get(path, self._auth_headers, **query)
525+
logging.debug("GET request to %s (body: %s)", path, repr(query))
526+
with log_duration():
527+
response = self.http.get(path, self._auth_headers, **query)
528+
return response
512529

513530
@_authentication
514531
def post(self, path_segment, owner=None, app=None, sharing=None, **query):
@@ -558,7 +575,10 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
558575
"""
559576
path = self.authority + self._abspath(path_segment, owner=owner,
560577
app=app, sharing=sharing)
561-
return self.http.post(path, self._auth_headers, **query)
578+
logging.debug("POST request to %s (body: %s)", path, repr(query))
579+
with log_duration():
580+
response = self.http.post(path, self._auth_headers, **query)
581+
return response
562582

563583
@_authentication
564584
def request(self, path_segment, method="GET", headers=[], body="",
@@ -621,10 +641,14 @@ def request(self, path_segment, method="GET", headers=[], body="",
621641
# f's local namespace or g's, and cannot switch between them
622642
# during the run of the function.
623643
all_headers = headers + self._auth_headers
624-
return self.http.request(path,
625-
{'method': method,
626-
'headers': all_headers,
627-
'body': body})
644+
logging.debug("%s request to %s (headers: %s, body: %s)",
645+
method, path, str(all_headers), repr(body))
646+
with log_duration():
647+
response = self.http.request(path,
648+
{'method': method,
649+
'headers': all_headers,
650+
'body': body})
651+
return response
628652

629653
def login(self):
630654
"""Log into the Splunk instance referred to by this ``Context``.

splunklib/client.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
import datetime
6565
import json
6666
import urllib
67-
67+
import logging
6868
from time import sleep
6969

7070
from binding import Context, HTTPError, AuthenticationError, namespace, UrlEncoded
@@ -107,6 +107,32 @@
107107

108108
MATCH_ENTRY_CONTENT = "%s/%s/*" % (XNAME_ENTRY, XNAME_CONTENT)
109109

110+
capabilities = record({k:k for k in [
111+
"admin_all_objects", "change_authentication",
112+
"change_own_password", "delete_by_keyword",
113+
"edit_deployment_client", "edit_deployment_server",
114+
"edit_dist_peer", "edit_forwarders", "edit_httpauths",
115+
"edit_input_defaults", "edit_monitor", "edit_roles",
116+
"edit_scripted", "edit_search_server", "edit_server",
117+
"edit_splunktcp", "edit_splunktcp_ssl", "edit_tcp",
118+
"edit_udp", "edit_user", "edit_web_settings", "get_metadata",
119+
"get_typeahead", "indexes_edit", "license_edit", "license_tab",
120+
"list_deployment_client", "list_forwarders", "list_httpauths",
121+
"list_inputs", "request_remote_tok", "rest_apps_management",
122+
"rest_apps_view", "rest_properties_get", "rest_properties_set",
123+
"restart_splunkd", "rtsearch", "schedule_search", "search",
124+
"use_file_operator"]})
125+
126+
127+
class NoSuchUserException(Exception):
128+
pass
129+
130+
class NoSuchApplicationException(Exception):
131+
pass
132+
133+
class IllegalOperationException(Exception):
134+
pass
135+
110136
class IncomparableException(Exception):
111137
pass
112138

@@ -116,6 +142,9 @@ class JobNotReadyException(Exception):
116142
class AmbiguousReferenceException(ValueError):
117143
pass
118144

145+
class EntityDeletedException(Exception):
146+
pass
147+
119148
def trailing(template, *targets):
120149
"""Substring of *template* following all *targets*.
121150
@@ -1002,7 +1031,6 @@ def __len__(self):
10021031
saved_searches = c.saved_searches
10031032
n = len(saved_searches)
10041033
"""
1005-
m = self.list(count=1)
10061034
return len(self.list())
10071035

10081036
def _entity_path(self, state):
@@ -1225,6 +1253,7 @@ def iter(self, offset=0, count=None, pagesize=None, **kwargs):
12251253
# server.
12261254
...
12271255
"""
1256+
assert pagesize is None or pagesize > 0
12281257
if count is None:
12291258
count = self.null_count
12301259
fetched = 0
@@ -1238,6 +1267,7 @@ def iter(self, offset=0, count=None, pagesize=None, **kwargs):
12381267
if pagesize is None or N < pagesize:
12391268
break
12401269
offset += N
1270+
logging.debug("pagesize=%d, fetched=%d, offset=%d, N=%d", pagesize, fetched, offset, N)
12411271

12421272
# kwargs: count, offset, search, sort_dir, sort_key, sort_mode
12431273
def list(self, count=None, **kwargs):
@@ -1930,6 +1960,9 @@ def export(self, query, **params):
19301960
else:
19311961
raise
19321962

1963+
def itemmeta(self):
1964+
raise NotSupportedError()
1965+
19331966

19341967
def oneshot(self, query, **params):
19351968
"""Run a search and directly return an InputStream IO handle over the results.

tests/test_app.py

Lines changed: 58 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,89 +15,78 @@
1515
# under the License.
1616

1717
import testlib
18+
import logging
1819

1920
import splunklib.client as client
2021
import splunklib.data as data
2122

22-
class TestCase(testlib.TestCase):
23-
def check_app(self, app):
24-
self.check_entity(app)
25-
cfu = int(app.check_for_updates)
26-
self.assertTrue(cfu == 1 or cfu == 0)
27-
28-
def test_read(self):
29-
service = client.connect(**self.opts.kwargs)
30-
31-
disabled_app = 'sdk-test-app-disabled'
32-
if (disabled_app in service.apps):
33-
service.apps.delete(disabled_app)
34-
disabled = service.apps.create(disabled_app)
35-
disabled.disable()
36-
37-
for app in service.apps:
38-
self.check_app(app)
39-
app.refresh()
40-
self.check_app(app)
41-
42-
service.apps.delete(disabled_app)
43-
44-
def test_crud(self):
45-
service = client.connect(**self.opts.kwargs)
46-
47-
appname = "sdk-test-app"
48-
49-
testlib.delete_app(service, appname)
50-
self.assertFalse(appname in service.apps)
51-
23+
class TestApp(testlib.TestCase):
24+
app = None
25+
app_name = None
26+
def setUp(self):
27+
testlib.TestCase.setUp(self)
28+
if self.app is None:
29+
for app in self.service.apps:
30+
if app.name.startswith('delete-me'):
31+
self.service.apps.delete(app.name)
32+
# Creating apps takes 0.8s, which is too long to wait for
33+
# each test in this test suite. Therefore we create one
34+
# app and reuse it. Since apps are rather less fraught
35+
# than entities like indexes, this is okay.
36+
TestApp.app_name = testlib.tmpname()
37+
TestApp.app = self.service.apps.create(self.app_name)
38+
logging.debug("Creating app %s", self.app_name)
39+
else:
40+
logging.debug("App %s already exists. Skipping creation.", self.app_name)
41+
42+
class TestAppIntegrity(TestApp):
43+
def test_app_integrity(self):
44+
self.check_entity(self.app)
45+
46+
class TestDisableEnable(TestApp):
47+
def test_disable_enable(self):
48+
self.app.disable()
49+
self.app.refresh()
50+
self.assertEqual(self.app['disabled'], '1')
51+
self.app.enable()
52+
self.app.refresh()
53+
self.assertEqual(self.app['disabled'], '0')
54+
55+
class TestUpdate(TestApp):
56+
def test_update(self):
5257
kwargs = {
5358
'author': "Me",
5459
'description': "Test app description",
5560
'label': "SDK Test",
5661
'manageable': False,
57-
'template': "barebones",
5862
'visible': True,
5963
}
60-
service.apps.create(appname, **kwargs)
61-
self.assertTrue(appname in service.apps)
62-
app = service.apps[appname]
63-
self.assertTrue(isinstance(app.state.content, data.Record))
64-
self.assertEqual(app['author'], "Me")
65-
self.assertEqual(app['label'], "SDK Test")
66-
self.assertEqual(app['manageable'], "0")
67-
self.assertEqual(app['visible'], "1")
68-
69-
self.assertEqual(app.author, "Me")
70-
self.assertEqual(app.label, "SDK Test")
71-
self.assertEqual(app.manageable, "0")
72-
self.assertEqual(app.visible, "1")
73-
74-
kwargs = {
75-
'author': "SDK",
76-
'visible': False,
77-
}
78-
app = service.apps[appname]
79-
app.update(**kwargs)
80-
app.refresh()
81-
self.assertEqual(app['author'], "SDK")
82-
self.assertEqual(app['label'], "SDK Test")
83-
self.assertEqual(app['manageable'], "0")
84-
self.assertEqual(app['visible'], "0")
85-
86-
testlib.delete_app(service, appname)
87-
self.assertFalse(appname in service.apps)
88-
64+
self.app.update(**kwargs)
65+
# self.app.refresh() <-- should be automatically refreshed
66+
# after update.
67+
self.assertEqual(self.app['author'], "Me")
68+
self.assertEqual(self.app['label'], "SDK Test")
69+
self.assertEqual(self.app['manageable'], "0")
70+
self.assertEqual(self.app['visible'], "1")
71+
72+
class TestDelete(TestApp):
73+
def test_delete(self):
74+
name = testlib.tmpname()
75+
app = self.service.apps.create(name)
76+
self.assertTrue(name in self.service.apps)
77+
self.service.apps.delete(name)
78+
self.assertFalse(name in self.service.apps)
79+
80+
class TestPackage(TestApp):
8981
def test_package(self):
90-
service = client.connect(**self.opts.kwargs)
91-
app = service.apps['search']
92-
p = app.package()
93-
self.assertEqual(p.name, 'search')
94-
self.assertTrue(p.path.endswith('search.spl'))
95-
self.assertTrue(p.url.endswith('search.spl'))
82+
p = self.app.package()
83+
self.assertEqual(p.name, self.app_name)
84+
self.assertTrue(p.path.endswith(self.app_name + '.spl'))
85+
self.assertTrue(p.url.endswith(self.app_name + '.spl'))
9686

87+
class TestUpdateInfo(TestApp):
9788
def test_updateInfo(self):
98-
service = client.connect(**self.opts.kwargs)
99-
app = service.apps['search']
100-
p = app.updateInfo()
89+
p = self.app.updateInfo()
10190
self.assertTrue(p is not None)
10291

10392
if __name__ == "__main__":

0 commit comments

Comments
 (0)