You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

579 lines
23 KiB

  1. # -*- coding: utf-8 -*-
  2. import nilmdb
  3. from nilmdb.utils.printf import *
  4. from nilmdb.utils import timestamper
  5. from nilmdb.client import ClientError, ServerError
  6. from nilmdb.utils import datetime_tz
  7. from nose.tools import *
  8. from nose.tools import assert_raises
  9. import itertools
  10. import distutils.version
  11. import os
  12. import sys
  13. import threading
  14. import cStringIO
  15. import simplejson as json
  16. import unittest
  17. import warnings
  18. import resource
  19. import time
  20. from testutil.helpers import *
  21. testdb = "tests/client-testdb"
  22. testurl = "http://localhost:12380/"
  23. def setup_module():
  24. global test_server, test_db
  25. # Clear out DB
  26. recursive_unlink(testdb)
  27. # Start web app on a custom port
  28. test_db = nilmdb.utils.serializer_proxy(nilmdb.NilmDB)(testdb, sync = False)
  29. test_server = nilmdb.Server(test_db, host = "127.0.0.1",
  30. port = 12380, stoppable = False,
  31. fast_shutdown = True,
  32. force_traceback = False)
  33. test_server.start(blocking = False)
  34. def teardown_module():
  35. global test_server, test_db
  36. # Close web app
  37. test_server.stop()
  38. test_db.close()
  39. class TestClient(object):
  40. def test_client_01_basic(self):
  41. # Test a fake host
  42. client = nilmdb.Client(url = "http://localhost:1/")
  43. with assert_raises(nilmdb.client.ServerError):
  44. client.version()
  45. client.close()
  46. # Trigger same error with a PUT request
  47. client = nilmdb.Client(url = "http://localhost:1/")
  48. with assert_raises(nilmdb.client.ServerError):
  49. client.version()
  50. client.close()
  51. # Then a fake URL on a real host
  52. client = nilmdb.Client(url = "http://localhost:12380/fake/")
  53. with assert_raises(nilmdb.client.ClientError):
  54. client.version()
  55. client.close()
  56. # Now a real URL with no http:// prefix
  57. client = nilmdb.Client(url = "localhost:12380")
  58. version = client.version()
  59. client.close()
  60. # Now use the real URL
  61. client = nilmdb.Client(url = testurl)
  62. version = client.version()
  63. eq_(distutils.version.LooseVersion(version),
  64. distutils.version.LooseVersion(test_server.version))
  65. # Bad URLs should give 404, not 500
  66. with assert_raises(ClientError):
  67. client.http.get("/stream/create")
  68. client.close()
  69. def test_client_02_createlist(self):
  70. # Basic stream tests, like those in test_nilmdb:test_stream
  71. client = nilmdb.Client(url = testurl)
  72. # Database starts empty
  73. eq_(client.stream_list(), [])
  74. # Bad path
  75. with assert_raises(ClientError):
  76. client.stream_create("foo/bar/baz", "PrepData")
  77. with assert_raises(ClientError):
  78. client.stream_create("/foo", "PrepData")
  79. # Bad layout type
  80. with assert_raises(ClientError):
  81. client.stream_create("/newton/prep", "NoSuchLayout")
  82. # Create three streams
  83. client.stream_create("/newton/prep", "PrepData")
  84. client.stream_create("/newton/raw", "RawData")
  85. client.stream_create("/newton/zzz/rawnotch", "RawNotchedData")
  86. # Verify we got 3 streams
  87. eq_(client.stream_list(), [ ["/newton/prep", "PrepData"],
  88. ["/newton/raw", "RawData"],
  89. ["/newton/zzz/rawnotch", "RawNotchedData"]
  90. ])
  91. # Match just one type or one path
  92. eq_(client.stream_list(layout="RawData"),
  93. [ ["/newton/raw", "RawData"] ])
  94. eq_(client.stream_list(path="/newton/raw"),
  95. [ ["/newton/raw", "RawData"] ])
  96. # Try messing with resource limits to trigger errors and get
  97. # more coverage. Here, make it so we can only create files 1
  98. # byte in size, which will trigger an IOError in the server when
  99. # we create a table.
  100. limit = resource.getrlimit(resource.RLIMIT_FSIZE)
  101. resource.setrlimit(resource.RLIMIT_FSIZE, (1, limit[1]))
  102. with assert_raises(ServerError) as e:
  103. client.stream_create("/newton/hello", "RawData")
  104. resource.setrlimit(resource.RLIMIT_FSIZE, limit)
  105. client.close()
  106. def test_client_03_metadata(self):
  107. client = nilmdb.Client(url = testurl)
  108. # Set / get metadata
  109. eq_(client.stream_get_metadata("/newton/prep"), {})
  110. eq_(client.stream_get_metadata("/newton/raw"), {})
  111. meta1 = { "description": "The Data",
  112. "v_scale": "1.234" }
  113. meta2 = { "description": "The Data" }
  114. meta3 = { "v_scale": "1.234" }
  115. client.stream_set_metadata("/newton/prep", meta1)
  116. client.stream_update_metadata("/newton/prep", {})
  117. client.stream_update_metadata("/newton/raw", meta2)
  118. client.stream_update_metadata("/newton/raw", meta3)
  119. eq_(client.stream_get_metadata("/newton/prep"), meta1)
  120. eq_(client.stream_get_metadata("/newton/raw"), meta1)
  121. eq_(client.stream_get_metadata("/newton/raw",
  122. [ "description" ] ), meta2)
  123. eq_(client.stream_get_metadata("/newton/raw",
  124. [ "description", "v_scale" ] ), meta1)
  125. # missing key
  126. eq_(client.stream_get_metadata("/newton/raw", "descr"),
  127. { "descr": None })
  128. eq_(client.stream_get_metadata("/newton/raw", [ "descr" ]),
  129. { "descr": None })
  130. # test wrong types (list instead of dict)
  131. with assert_raises(ClientError):
  132. client.stream_set_metadata("/newton/prep", [1,2,3])
  133. with assert_raises(ClientError):
  134. client.stream_update_metadata("/newton/prep", [1,2,3])
  135. client.close()
  136. def test_client_04_insert(self):
  137. client = nilmdb.Client(url = testurl)
  138. datetime_tz.localtz_set("America/New_York")
  139. testfile = "tests/data/prep-20120323T1000"
  140. start = datetime_tz.datetime_tz.smartparse("20120323T1000")
  141. start = start.totimestamp()
  142. rate = 120
  143. # First try a nonexistent path
  144. data = timestamper.TimestamperRate(testfile, start, 120)
  145. with assert_raises(ClientError) as e:
  146. result = client.stream_insert("/newton/no-such-path", data)
  147. in_("404 Not Found", str(e.exception))
  148. # Now try reversed timestamps
  149. data = timestamper.TimestamperRate(testfile, start, 120)
  150. data = reversed(list(data))
  151. with assert_raises(ClientError) as e:
  152. result = client.stream_insert("/newton/prep", data)
  153. in_("400 Bad Request", str(e.exception))
  154. in_("timestamp is not monotonically increasing", str(e.exception))
  155. # Now try empty data (no server request made)
  156. empty = cStringIO.StringIO("")
  157. data = timestamper.TimestamperRate(empty, start, 120)
  158. result = client.stream_insert("/newton/prep", data)
  159. eq_(result, None)
  160. # It's OK to insert an empty interval
  161. client.http.put("stream/insert", "", { "path": "/newton/prep",
  162. "start": 1, "end": 2 })
  163. eq_(list(client.stream_intervals("/newton/prep")), [[1, 2]])
  164. client.stream_remove("/newton/prep")
  165. eq_(list(client.stream_intervals("/newton/prep")), [])
  166. # Timestamps can be negative too
  167. client.http.put("stream/insert", "", { "path": "/newton/prep",
  168. "start": -2, "end": -1 })
  169. eq_(list(client.stream_intervals("/newton/prep")), [[-2, -1]])
  170. client.stream_remove("/newton/prep")
  171. eq_(list(client.stream_intervals("/newton/prep")), [])
  172. # Intervals that end at zero shouldn't be any different
  173. client.http.put("stream/insert", "", { "path": "/newton/prep",
  174. "start": -1, "end": 0 })
  175. eq_(list(client.stream_intervals("/newton/prep")), [[-1, 0]])
  176. client.stream_remove("/newton/prep")
  177. eq_(list(client.stream_intervals("/newton/prep")), [])
  178. # Try forcing a server request with equal start and end
  179. with assert_raises(ClientError) as e:
  180. client.http.put("stream/insert", "", { "path": "/newton/prep",
  181. "start": 0, "end": 0 })
  182. in_("400 Bad Request", str(e.exception))
  183. in_("start must precede end", str(e.exception))
  184. # Specify start/end (starts too late)
  185. data = timestamper.TimestamperRate(testfile, start, 120)
  186. with assert_raises(ClientError) as e:
  187. result = client.stream_insert("/newton/prep", data,
  188. start + 5, start + 120)
  189. in_("400 Bad Request", str(e.exception))
  190. in_("Data timestamp 1332511200.0 < start time 1332511205.0",
  191. str(e.exception))
  192. # Specify start/end (ends too early)
  193. data = timestamper.TimestamperRate(testfile, start, 120)
  194. with assert_raises(ClientError) as e:
  195. result = client.stream_insert("/newton/prep", data,
  196. start, start + 1)
  197. in_("400 Bad Request", str(e.exception))
  198. # Client chunks the input, so the exact timestamp here might change
  199. # if the chunk positions change.
  200. in_("Data timestamp 1332511271.016667 >= end time 1332511201.0",
  201. str(e.exception))
  202. # Now do the real load
  203. data = timestamper.TimestamperRate(testfile, start, 120)
  204. result = client.stream_insert("/newton/prep", data,
  205. start, start + 119.999777)
  206. # Verify the intervals. Should be just one, even if the data
  207. # was inserted in chunks, due to nilmdb interval concatenation.
  208. intervals = list(client.stream_intervals("/newton/prep"))
  209. eq_(intervals, [[start, start + 119.999777]])
  210. # Try some overlapping data -- just insert it again
  211. data = timestamper.TimestamperRate(testfile, start, 120)
  212. with assert_raises(ClientError) as e:
  213. result = client.stream_insert("/newton/prep", data)
  214. in_("400 Bad Request", str(e.exception))
  215. in_("verlap", str(e.exception))
  216. client.close()
  217. def test_client_05_extractremove(self):
  218. # Misc tests for extract and remove. Most of them are in test_cmdline.
  219. client = nilmdb.Client(url = testurl)
  220. for x in client.stream_extract("/newton/prep", 999123, 999124):
  221. raise AssertionError("shouldn't be any data for this request")
  222. with assert_raises(ClientError) as e:
  223. client.stream_remove("/newton/prep", 123, 120)
  224. # Test the exception we get if we nest requests
  225. with assert_raises(Exception) as e:
  226. for data in client.stream_extract("/newton/prep"):
  227. x = client.stream_intervals("/newton/prep")
  228. in_("nesting calls is not supported", str(e.exception))
  229. # Test count
  230. eq_(client.stream_count("/newton/prep"), 14400)
  231. client.close()
  232. def test_client_06_generators(self):
  233. # A lot of the client functionality is already tested by test_cmdline,
  234. # but this gets a bit more coverage that cmdline misses.
  235. client = nilmdb.Client(url = testurl)
  236. # Trigger a client error in generator
  237. start = datetime_tz.datetime_tz.smartparse("20120323T2000")
  238. end = datetime_tz.datetime_tz.smartparse("20120323T1000")
  239. for function in [ client.stream_intervals, client.stream_extract ]:
  240. with assert_raises(ClientError) as e:
  241. function("/newton/prep",
  242. start.totimestamp(),
  243. end.totimestamp()).next()
  244. in_("400 Bad Request", str(e.exception))
  245. in_("start must precede end", str(e.exception))
  246. # Trigger a curl error in generator
  247. with assert_raises(ServerError) as e:
  248. client.http.get_gen("http://nosuchurl/").next()
  249. # Trigger a curl error in generator
  250. with assert_raises(ServerError) as e:
  251. client.http.get_gen("http://nosuchurl/").next()
  252. # Check non-json version of string output
  253. eq_(json.loads(client.http.get("/stream/list",retjson=False)),
  254. client.http.get("/stream/list",retjson=True))
  255. # Check non-json version of generator output
  256. for (a, b) in itertools.izip(
  257. client.http.get_gen("/stream/list",retjson=False),
  258. client.http.get_gen("/stream/list",retjson=True)):
  259. eq_(json.loads(a), b)
  260. # Check PUT with generator out
  261. with assert_raises(ClientError) as e:
  262. client.http.put_gen("stream/insert", "",
  263. { "path": "/newton/prep",
  264. "start": 0, "end": 0 }).next()
  265. in_("400 Bad Request", str(e.exception))
  266. in_("start must precede end", str(e.exception))
  267. # Check 404 for missing streams
  268. for function in [ client.stream_intervals, client.stream_extract ]:
  269. with assert_raises(ClientError) as e:
  270. function("/no/such/stream").next()
  271. in_("404 Not Found", str(e.exception))
  272. in_("No such stream", str(e.exception))
  273. client.close()
  274. def test_client_07_headers(self):
  275. # Make sure that /stream/intervals and /stream/extract
  276. # properly return streaming, chunked, text/plain response.
  277. # Pokes around in client.http internals a bit to look at the
  278. # response headers.
  279. client = nilmdb.Client(url = testurl)
  280. http = client.http
  281. # Use a warning rather than returning a test failure, so that we can
  282. # still disable chunked responses for debugging.
  283. # Intervals
  284. x = http.get("stream/intervals", { "path": "/newton/prep" },
  285. retjson=False)
  286. lines_(x, 1)
  287. if "Transfer-Encoding: chunked" not in http._headers:
  288. warnings.warn("Non-chunked HTTP response for /stream/intervals")
  289. if "Content-Type: text/plain;charset=utf-8" not in http._headers:
  290. raise AssertionError("/stream/intervals is not text/plain:\n" +
  291. http._headers)
  292. # Extract
  293. x = http.get("stream/extract",
  294. { "path": "/newton/prep",
  295. "start": "123",
  296. "end": "124" }, retjson=False)
  297. if "Transfer-Encoding: chunked" not in http._headers:
  298. warnings.warn("Non-chunked HTTP response for /stream/extract")
  299. if "Content-Type: text/plain;charset=utf-8" not in http._headers:
  300. raise AssertionError("/stream/extract is not text/plain:\n" +
  301. http._headers)
  302. # Make sure Access-Control-Allow-Origin gets set
  303. if "Access-Control-Allow-Origin: " not in http._headers:
  304. raise AssertionError("No Access-Control-Allow-Origin (CORS) "
  305. "header in /stream/extract response:\n" +
  306. http._headers)
  307. client.close()
  308. def test_client_08_unicode(self):
  309. # Basic Unicode tests
  310. client = nilmdb.Client(url = testurl)
  311. # Delete streams that exist
  312. for stream in client.stream_list():
  313. client.stream_destroy(stream[0])
  314. # Database is empty
  315. eq_(client.stream_list(), [])
  316. # Create Unicode stream, match it
  317. raw = [ u"/düsseldorf/raw", u"uint16_6" ]
  318. prep = [ u"/düsseldorf/prep", u"uint16_6" ]
  319. client.stream_create(*raw)
  320. eq_(client.stream_list(), [raw])
  321. eq_(client.stream_list(layout=raw[1]), [raw])
  322. eq_(client.stream_list(path=raw[0]), [raw])
  323. client.stream_create(*prep)
  324. eq_(client.stream_list(), [prep, raw])
  325. # Set / get metadata with Unicode keys and values
  326. eq_(client.stream_get_metadata(raw[0]), {})
  327. eq_(client.stream_get_metadata(prep[0]), {})
  328. meta1 = { u"alpha": u"α",
  329. u"β": u"beta" }
  330. meta2 = { u"alpha": u"α" }
  331. meta3 = { u"β": u"beta" }
  332. client.stream_set_metadata(prep[0], meta1)
  333. client.stream_update_metadata(prep[0], {})
  334. client.stream_update_metadata(raw[0], meta2)
  335. client.stream_update_metadata(raw[0], meta3)
  336. eq_(client.stream_get_metadata(prep[0]), meta1)
  337. eq_(client.stream_get_metadata(raw[0]), meta1)
  338. eq_(client.stream_get_metadata(raw[0], [ "alpha" ]), meta2)
  339. eq_(client.stream_get_metadata(raw[0], [ "alpha", "β" ]), meta1)
  340. client.close()
  341. def test_client_09_closing(self):
  342. # Make sure we actually close sockets correctly. New
  343. # connections will block for a while if they're not, since the
  344. # server will stop accepting new connections.
  345. for test in [1, 2]:
  346. start = time.time()
  347. for i in range(50):
  348. if time.time() - start > 15:
  349. raise AssertionError("Connections seem to be blocking... "
  350. "probably not closing properly.")
  351. if test == 1:
  352. # explicit close
  353. client = nilmdb.Client(url = testurl)
  354. with assert_raises(ClientError) as e:
  355. client.stream_remove("/newton/prep", 123, 120)
  356. client.close() # remove this to see the failure
  357. elif test == 2:
  358. # use the context manager
  359. with nilmdb.Client(url = testurl) as c:
  360. with assert_raises(ClientError) as e:
  361. c.stream_remove("/newton/prep", 123, 120)
  362. def test_client_10_context(self):
  363. # Test using the client's stream insertion context manager to
  364. # insert data.
  365. client = nilmdb.Client(testurl)
  366. client.stream_create("/context/test", "uint16_1")
  367. with client.stream_insert_context("/context/test") as ctx:
  368. # override _max_data to trigger frequent server updates
  369. ctx._max_data = 15
  370. with assert_raises(ValueError):
  371. ctx.insert_line("100 1")
  372. ctx.insert_line("100 1\n")
  373. ctx.insert_iter([ "101 1\n",
  374. "102 1\n",
  375. "103 1\n" ])
  376. ctx.insert_line("104 1\n")
  377. ctx.insert_line("105 1\n")
  378. ctx.finalize()
  379. ctx.insert_line("106 1\n")
  380. ctx.update_end(106.5)
  381. ctx.finalize()
  382. ctx.update_start(106.8)
  383. ctx.insert_line("107 1\n")
  384. ctx.insert_line("108 1\n")
  385. ctx.insert_line("109 1\n")
  386. ctx.insert_line("110 1\n")
  387. ctx.insert_line("111 1\n")
  388. ctx.update_end(113)
  389. ctx.insert_line("112 1\n")
  390. ctx.update_end(114)
  391. ctx.insert_line("113 1\n")
  392. ctx.update_end(115)
  393. ctx.insert_line("114 1\n")
  394. ctx.finalize()
  395. with assert_raises(ClientError):
  396. with client.stream_insert_context("/context/test", 100, 200) as ctx:
  397. ctx.insert_line("115 1\n")
  398. with assert_raises(ClientError):
  399. with client.stream_insert_context("/context/test", 200, 300) as ctx:
  400. ctx.insert_line("115 1\n")
  401. with client.stream_insert_context("/context/test", 200, 300) as ctx:
  402. # make sure our override wasn't permanent
  403. ne_(ctx._max_data, 15)
  404. ctx.insert_line("225 1\n")
  405. ctx.finalize()
  406. eq_(list(client.stream_intervals("/context/test")),
  407. [ [ 100, 105.000001 ],
  408. [ 106, 106.5 ],
  409. [ 106.8, 115 ],
  410. [ 200, 300 ] ])
  411. client.stream_destroy("/context/test")
  412. client.close()
  413. def test_client_11_emptyintervals(self):
  414. # Empty intervals are ok! If recording detection events
  415. # by inserting rows into the database, we want to be able to
  416. # have an interval where no events occurred. Test them here.
  417. client = nilmdb.Client(testurl)
  418. client.stream_create("/empty/test", "uint16_1")
  419. def info():
  420. result = []
  421. for interval in list(client.stream_intervals("/empty/test")):
  422. result.append((client.stream_count("/empty/test", *interval),
  423. interval))
  424. return result
  425. eq_(info(), [])
  426. # Insert a region with just a few points
  427. with client.stream_insert_context("/empty/test") as ctx:
  428. ctx.update_start(100)
  429. ctx.insert_line("140 1\n")
  430. ctx.insert_line("150 1\n")
  431. ctx.insert_line("160 1\n")
  432. ctx.update_end(200)
  433. ctx.finalize()
  434. eq_(info(), [(3, [100, 200])])
  435. # Delete chunk, which will leave one data point and two intervals
  436. client.stream_remove("/empty/test", 145, 175)
  437. eq_(info(), [(1, [100, 145]),
  438. (0, [175, 200])])
  439. # Try also creating a completely empty interval from scratch,
  440. # in a few different ways.
  441. client.stream_insert_block("/empty/test", "", 300, 350)
  442. client.stream_insert("/empty/test", [], 400, 450)
  443. with client.stream_insert_context("/empty/test", 500, 550):
  444. pass
  445. # If enough timestamps aren't provided, empty streams won't be created.
  446. client.stream_insert("/empty/test", [])
  447. with client.stream_insert_context("/empty/test"):
  448. pass
  449. client.stream_insert("/empty/test", [], start = 600)
  450. with client.stream_insert_context("/empty/test", start = 700):
  451. pass
  452. client.stream_insert("/empty/test", [], end = 850)
  453. with client.stream_insert_context("/empty/test", end = 950):
  454. pass
  455. # Try various things that might cause problems
  456. with client.stream_insert_context("/empty/test", 1000, 1050):
  457. ctx.finalize() # inserts [1000, 1050]
  458. ctx.finalize() # nothing
  459. ctx.finalize() # nothing
  460. ctx.insert_line("1100 1\n")
  461. ctx.finalize() # inserts [1100, 1100.000001]
  462. ctx.update_start(1199)
  463. ctx.insert_line("1200 1\n")
  464. ctx.update_end(1250)
  465. ctx.finalize() # inserts [1199, 1250]
  466. ctx.update_start(1299)
  467. ctx.finalize() # nothing
  468. ctx.update_end(1350)
  469. ctx.finalize() # nothing
  470. ctx.update_start(1400)
  471. ctx.update_end(1450)
  472. ctx.finalize()
  473. # implicit last finalize inserts [1400, 1450]
  474. # Check everything
  475. eq_(info(), [(1, [100, 145]),
  476. (0, [175, 200]),
  477. (0, [300, 350]),
  478. (0, [400, 450]),
  479. (0, [500, 550]),
  480. (0, [1000, 1050]),
  481. (1, [1100, 1100.000001]),
  482. (1, [1199, 1250]),
  483. (0, [1400, 1450]),
  484. ])
  485. # Clean up
  486. client.stream_destroy("/empty/test")
  487. client.close()