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.
 
 
 

1062 lines
40 KiB

  1. # -*- coding: utf-8 -*-
  2. import nilmdb.server
  3. from nilmdb.utils.printf import *
  4. import nilmdb.cmdline
  5. from nilmdb.utils import datetime_tz
  6. import unittest
  7. from nose.tools import *
  8. from nose.tools import assert_raises
  9. import itertools
  10. import os
  11. import re
  12. import sys
  13. import StringIO
  14. import shlex
  15. import warnings
  16. from testutil.helpers import *
  17. testdb = "tests/cmdline-testdb"
  18. def server_start(max_results = None, bulkdata_args = {}):
  19. global test_server, test_db
  20. # Start web app on a custom port
  21. test_db = nilmdb.utils.serializer_proxy(nilmdb.server.NilmDB)(
  22. testdb,
  23. max_results = max_results,
  24. bulkdata_args = bulkdata_args)
  25. test_server = nilmdb.server.Server(test_db, host = "127.0.0.1",
  26. port = 32180, stoppable = False,
  27. fast_shutdown = True,
  28. force_traceback = False)
  29. test_server.start(blocking = False)
  30. def server_stop():
  31. global test_server, test_db
  32. # Close web app
  33. test_server.stop()
  34. test_db.close()
  35. def setup_module():
  36. global test_server, test_db
  37. # Clear out DB
  38. recursive_unlink(testdb)
  39. server_start()
  40. def teardown_module():
  41. server_stop()
  42. # Add an encoding property to StringIO so Python will convert Unicode
  43. # properly when writing or reading.
  44. class UTF8StringIO(StringIO.StringIO):
  45. encoding = 'utf-8'
  46. class TestCmdline(object):
  47. def run(self, arg_string, infile=None, outfile=None):
  48. """Run a cmdline client with the specified argument string,
  49. passing the given input. Returns a tuple with the output and
  50. exit code"""
  51. # printf("TZ=UTC ./nilmtool.py %s\n", arg_string)
  52. os.environ['NILMDB_URL'] = "http://localhost:32180/"
  53. class stdio_wrapper:
  54. def __init__(self, stdin, stdout, stderr):
  55. self.io = (stdin, stdout, stderr)
  56. def __enter__(self):
  57. self.saved = ( sys.stdin, sys.stdout, sys.stderr )
  58. ( sys.stdin, sys.stdout, sys.stderr ) = self.io
  59. def __exit__(self, type, value, traceback):
  60. ( sys.stdin, sys.stdout, sys.stderr ) = self.saved
  61. # Empty input if none provided
  62. if infile is None:
  63. infile = UTF8StringIO("")
  64. # Capture stderr
  65. errfile = UTF8StringIO()
  66. if outfile is None:
  67. # If no output file, capture stdout with stderr
  68. outfile = errfile
  69. with stdio_wrapper(infile, outfile, errfile) as s:
  70. try:
  71. # shlex doesn't support Unicode very well. Encode the
  72. # string as UTF-8 explicitly before splitting.
  73. args = shlex.split(arg_string.encode('utf-8'))
  74. nilmdb.cmdline.Cmdline(args).run()
  75. sys.exit(0)
  76. except SystemExit as e:
  77. exitcode = e.code
  78. captured = outfile.getvalue()
  79. self.captured = captured
  80. self.exitcode = exitcode
  81. def ok(self, arg_string, infile = None):
  82. self.run(arg_string, infile)
  83. if self.exitcode != 0:
  84. self.dump()
  85. eq_(self.exitcode, 0)
  86. def fail(self, arg_string, infile = None,
  87. exitcode = None, require_error = True):
  88. self.run(arg_string, infile)
  89. if exitcode is not None and self.exitcode != exitcode:
  90. # Wrong exit code
  91. self.dump()
  92. eq_(self.exitcode, exitcode)
  93. if self.exitcode == 0:
  94. # Success, when we wanted failure
  95. self.dump()
  96. ne_(self.exitcode, 0)
  97. # Make sure the output contains the word "error" at the
  98. # beginning of a line, but only if an exitcode wasn't
  99. # specified.
  100. if require_error and not re.search("^error",
  101. self.captured, re.MULTILINE):
  102. raise AssertionError("command failed, but output doesn't "
  103. "contain the string 'error'")
  104. def contain(self, checkstring):
  105. in_(checkstring, self.captured)
  106. def match(self, checkstring):
  107. eq_(checkstring, self.captured)
  108. def matchfile(self, file):
  109. # Captured data should match file contents exactly
  110. with open(file) as f:
  111. contents = f.read()
  112. if contents != self.captured:
  113. print "--- reference file (first 1000 bytes):\n"
  114. print contents[0:1000] + "\n"
  115. print "--- captured data (first 1000 bytes):\n"
  116. print self.captured[0:1000] + "\n"
  117. zipped = itertools.izip_longest(contents, self.captured)
  118. for (n, (a, b)) in enumerate(zipped):
  119. if a != b:
  120. print "--- first difference is at offset", n
  121. print "--- reference:", repr(a)
  122. print "--- captured:", repr(b)
  123. break
  124. raise AssertionError("captured data doesn't match " + file)
  125. def matchfilecount(self, file):
  126. # Last line of captured data should match the number of
  127. # non-commented lines in file
  128. count = 0
  129. with open(file) as f:
  130. for line in f:
  131. if line[0] != '#':
  132. count += 1
  133. eq_(self.captured.splitlines()[-1], sprintf("%d", count))
  134. def dump(self):
  135. printf("-----dump start-----\n%s-----dump end-----\n", self.captured)
  136. def test_01_basic(self):
  137. # help
  138. self.ok("--help")
  139. self.contain("usage:")
  140. # fail for no args
  141. self.fail("")
  142. # fail for no such option
  143. self.fail("--nosuchoption")
  144. # fail for bad command
  145. self.fail("badcommand")
  146. # try some URL constructions
  147. self.fail("--url http://nosuchurl/ info")
  148. self.contain("error connecting to server")
  149. self.fail("--url nosuchurl info")
  150. self.contain("error connecting to server")
  151. self.fail("-u nosuchurl/foo info")
  152. self.contain("error connecting to server")
  153. self.fail("-u localhost:1 info")
  154. self.contain("error connecting to server")
  155. self.ok("-u localhost:32180 info")
  156. self.ok("info")
  157. # Duplicated arguments should fail, but this isn't implemented
  158. # due to it being kind of a pain with argparse.
  159. if 0:
  160. self.fail("-u url1 -u url2 info")
  161. self.contain("duplicated argument")
  162. self.fail("list --detail --detail")
  163. self.contain("duplicated argument")
  164. self.fail("list --detail --path path1 --path path2")
  165. self.contain("duplicated argument")
  166. self.fail("extract --start 2000-01-01 --start 2001-01-02")
  167. self.contain("duplicated argument")
  168. # Verify that "help command" and "command --help" are identical
  169. # for all commands.
  170. self.fail("")
  171. m = re.search(r"{(.*)}", self.captured)
  172. for command in [""] + m.group(1).split(','):
  173. self.ok(command + " --help")
  174. cap1 = self.captured
  175. self.ok("help " + command)
  176. cap2 = self.captured
  177. self.ok("help " + command + " asdf --url --zxcv -")
  178. cap3 = self.captured
  179. eq_(cap1, cap2)
  180. eq_(cap2, cap3)
  181. def test_02_parsetime(self):
  182. os.environ['TZ'] = "America/New_York"
  183. test = datetime_tz.datetime_tz.now()
  184. u2ts = nilmdb.utils.time.unix_to_timestamp
  185. parse_time = nilmdb.utils.time.parse_time
  186. eq_(parse_time(str(test)), u2ts(test.totimestamp()))
  187. test = u2ts(datetime_tz.datetime_tz.smartparse("20120405 1400-0400").
  188. totimestamp())
  189. eq_(parse_time("hi there 20120405 1400-0400 testing! 123"), test)
  190. eq_(parse_time("20120405 1800 UTC"), test)
  191. eq_(parse_time("20120405 1400-0400 UTC"), test)
  192. for badtime in [ "20120405 1400-9999", "hello", "-", "", "4:00" ]:
  193. with assert_raises(ValueError):
  194. x = parse_time(badtime)
  195. x = parse_time("now")
  196. eq_(parse_time("snapshot-20120405-140000.raw.gz"), test)
  197. eq_(parse_time("prep-20120405T1400"), test)
  198. eq_(parse_time("1333648800.0"), test)
  199. try:
  200. eq_(parse_time("1333648800000000"), test)
  201. eq_(parse_time("@1333648800000000"), test)
  202. except AssertionError:
  203. warnings.warn("parsing int64 timestamps isn't working")
  204. def test_03_info(self):
  205. self.ok("info")
  206. self.contain("Server URL: http://localhost:32180/")
  207. self.contain("Client version: " + nilmdb.__version__)
  208. self.contain("Server version: " + test_server.version)
  209. self.contain("Server database path")
  210. self.contain("Server database size")
  211. self.contain("Server database free space")
  212. def test_04_createlist(self):
  213. # Basic stream tests, like those in test_client.
  214. # No streams
  215. self.ok("list")
  216. self.match("")
  217. # Bad paths
  218. self.fail("create foo/bar/baz float32_8")
  219. self.contain("paths must start with /")
  220. self.fail("create /foo float32_8")
  221. self.contain("invalid path")
  222. self.fail("create /newton/prep/ float32_8")
  223. self.contain("invalid path")
  224. self.fail("create /newton/_format/prep float32_8")
  225. self.contain("path name is invalid")
  226. self.fail("create /_format/newton/prep float32_8")
  227. self.contain("path name is invalid")
  228. self.fail("create /newton/prep/_format float32_8")
  229. self.contain("path name is invalid")
  230. # Bad layout type
  231. self.fail("create /newton/prep NoSuchLayout")
  232. self.contain("no such layout")
  233. self.fail("create /newton/prep float32_0")
  234. self.contain("no such layout")
  235. self.fail("create /newton/prep float33_1")
  236. self.contain("no such layout")
  237. # Create a few streams
  238. self.ok("create /newton/zzz/rawnotch uint16_9")
  239. self.ok("create /newton/prep float32_8")
  240. self.ok("create /newton/raw uint16_6")
  241. # Create a stream that already exists
  242. self.fail("create /newton/raw uint16_6")
  243. self.contain("stream already exists at this path")
  244. # Should not be able to create a stream with another stream as
  245. # its parent
  246. self.fail("create /newton/prep/blah float32_8")
  247. self.contain("path is subdir of existing node")
  248. # Should not be able to create a stream at a location that
  249. # has other nodes as children
  250. self.fail("create /newton/zzz float32_8")
  251. self.contain("subdirs of this path already exist")
  252. # Verify we got those 3 streams and they're returned in
  253. # alphabetical order.
  254. self.ok("list")
  255. self.match("/newton/prep float32_8\n"
  256. "/newton/raw uint16_6\n"
  257. "/newton/zzz/rawnotch uint16_9\n")
  258. # Match just one type or one path. Also check
  259. # that --path is optional
  260. self.ok("list --path /newton/raw")
  261. self.match("/newton/raw uint16_6\n")
  262. self.ok("list /newton/raw")
  263. self.match("/newton/raw uint16_6\n")
  264. self.fail("list -p /newton/raw /newton/raw")
  265. self.contain("too many paths")
  266. self.ok("list --layout uint16_6")
  267. self.match("/newton/raw uint16_6\n")
  268. # Wildcard matches
  269. self.ok("list --layout uint16*")
  270. self.match("/newton/raw uint16_6\n"
  271. "/newton/zzz/rawnotch uint16_9\n")
  272. self.ok("list --path *zzz* --layout uint16*")
  273. self.match("/newton/zzz/rawnotch uint16_9\n")
  274. self.ok("list *zzz* --layout uint16*")
  275. self.match("/newton/zzz/rawnotch uint16_9\n")
  276. self.ok("list --path *zzz* --layout float32*")
  277. self.match("")
  278. # reversed range
  279. self.fail("list /newton/prep --start 2020-01-01 --end 2000-01-01")
  280. self.contain("start must precede end")
  281. def test_05_metadata(self):
  282. # Set / get metadata
  283. self.fail("metadata")
  284. self.fail("metadata --get")
  285. self.ok("metadata /newton/prep")
  286. self.match("")
  287. self.ok("metadata /newton/raw --get")
  288. self.match("")
  289. self.ok("metadata /newton/prep --set "
  290. "'description=The Data' "
  291. "v_scale=1.234")
  292. self.ok("metadata /newton/raw --update "
  293. "'description=The Data'")
  294. self.ok("metadata /newton/raw --update "
  295. "v_scale=1.234")
  296. # various parsing tests
  297. self.ok("metadata /newton/raw --update foo=")
  298. self.fail("metadata /newton/raw --update =bar")
  299. self.fail("metadata /newton/raw --update foo==bar")
  300. self.fail("metadata /newton/raw --update foo;bar")
  301. # errors
  302. self.fail("metadata /newton/nosuchstream foo=bar")
  303. self.contain("unrecognized arguments")
  304. self.fail("metadata /newton/nosuchstream")
  305. self.contain("No stream at path")
  306. self.fail("metadata /newton/nosuchstream --set foo=bar")
  307. self.contain("No stream at path")
  308. self.ok("metadata /newton/prep")
  309. self.match("description=The Data\nv_scale=1.234\n")
  310. self.ok("metadata /newton/prep --get")
  311. self.match("description=The Data\nv_scale=1.234\n")
  312. self.ok("metadata /newton/prep --get descr")
  313. self.match("descr=\n")
  314. self.ok("metadata /newton/prep --get description")
  315. self.match("description=The Data\n")
  316. self.ok("metadata /newton/prep --get description v_scale")
  317. self.match("description=The Data\nv_scale=1.234\n")
  318. self.ok("metadata /newton/prep --set "
  319. "'description=The Data'")
  320. self.ok("metadata /newton/prep --get")
  321. self.match("description=The Data\n")
  322. self.fail("metadata /newton/nosuchpath")
  323. self.contain("No stream at path /newton/nosuchpath")
  324. def test_06_insert(self):
  325. self.ok("insert --help")
  326. self.fail("insert -s 2000 -e 2001 /foo/bar baz")
  327. self.contain("error getting stream info")
  328. self.fail("insert -s 2000 -e 2001 /newton/prep baz")
  329. self.match("error opening input file baz\n")
  330. self.fail("insert /newton/prep --timestamp -f -r 120")
  331. self.contain("error extracting start time")
  332. self.fail("insert /newton/prep --timestamp -r 120")
  333. self.contain("need --start or --filename")
  334. self.fail("insert /newton/prep "
  335. "tests/data/prep-20120323T1000")
  336. # insert pre-timestamped data, with bad times (non-monotonic)
  337. os.environ['TZ'] = "UTC"
  338. with open("tests/data/prep-20120323T1004-badtimes") as input:
  339. self.fail("insert -s 20120323T1004 -e 20120323T1006 /newton/prep",
  340. input)
  341. self.contain("error parsing input data")
  342. self.contain("line 7")
  343. self.contain("timestamp is not monotonically increasing")
  344. # insert pre-timestamped data, from stdin
  345. os.environ['TZ'] = "UTC"
  346. with open("tests/data/prep-20120323T1004-timestamped") as input:
  347. self.ok("insert -s 20120323T1004 -e 20120323T1006 /newton/prep",
  348. input)
  349. # insert data with normal timestamper from filename
  350. os.environ['TZ'] = "UTC"
  351. self.ok("insert --timestamp -f --rate 120 /newton/prep "
  352. "tests/data/prep-20120323T1000")
  353. self.fail("insert -t --filename /newton/prep "
  354. "tests/data/prep-20120323T1002")
  355. self.contain("rate is needed")
  356. self.ok("insert -t --filename --rate 120 /newton/prep "
  357. "tests/data/prep-20120323T1002")
  358. # overlap
  359. os.environ['TZ'] = "UTC"
  360. self.fail("insert --timestamp -f --rate 120 /newton/prep "
  361. "tests/data/prep-20120323T1004")
  362. self.contain("overlap")
  363. # Just to help test more situations -- stop and restart
  364. # the server now. This tests nilmdb's interval caching,
  365. # at the very least.
  366. server_stop()
  367. server_start()
  368. # still an overlap if we specify a different start
  369. os.environ['TZ'] = "America/New_York"
  370. self.fail("insert -t -r 120 --start '03/23/2012 06:05:00' /newton/prep"
  371. " tests/data/prep-20120323T1004")
  372. self.contain("overlap")
  373. # wrong format
  374. os.environ['TZ'] = "UTC"
  375. self.fail("insert -t -r 120 -f /newton/raw "
  376. "tests/data/prep-20120323T1004")
  377. self.contain("error parsing input data")
  378. self.contain("can't parse value")
  379. # too few rows per line
  380. self.ok("create /insert/test float32_20")
  381. self.fail("insert -t -r 120 -f /insert/test "
  382. "tests/data/prep-20120323T1004")
  383. self.contain("error parsing input data")
  384. self.contain("wrong number of values")
  385. self.ok("destroy /insert/test")
  386. # empty data does nothing
  387. self.ok("insert -t -r 120 --start '03/23/2012 06:05:00' /newton/prep "
  388. "/dev/null")
  389. # bad start time
  390. self.fail("insert -t -r 120 --start 'whatever' /newton/prep /dev/null")
  391. def test_07_detail_extended(self):
  392. # Just count the number of lines, it's probably fine
  393. self.ok("list --detail")
  394. lines_(self.captured, 8)
  395. self.ok("list --detail --path *prep")
  396. lines_(self.captured, 4)
  397. self.ok("list --detail --path *prep --start='23 Mar 2012 10:02'")
  398. lines_(self.captured, 3)
  399. self.ok("list --detail --path *prep --start='23 Mar 2012 10:05'")
  400. lines_(self.captured, 2)
  401. self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15'")
  402. lines_(self.captured, 2)
  403. self.contain("10:05:15.000")
  404. self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'")
  405. lines_(self.captured, 2)
  406. self.contain("10:05:15.500")
  407. self.ok("list --detail --path *prep --start='23 Mar 2012 19:05:15.50'")
  408. lines_(self.captured, 2)
  409. self.contain("no intervals")
  410. self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'"
  411. + " --end='23 Mar 2012 10:05:15.51'")
  412. lines_(self.captured, 2)
  413. self.contain("10:05:15.500")
  414. self.ok("list --detail")
  415. lines_(self.captured, 8)
  416. # Verify the "raw timestamp" output
  417. self.ok("list --detail --path *prep --timestamp-raw "
  418. "--start='23 Mar 2012 10:05:15.50'")
  419. lines_(self.captured, 2)
  420. self.contain("[ 1332497115500000 -> 1332497160000000 ]")
  421. # bad time
  422. self.fail("list --detail --path *prep -T --start='9332497115.612'")
  423. # good time
  424. self.ok("list --detail --path *prep -T --start='1332497115.612'")
  425. lines_(self.captured, 2)
  426. self.contain("[ 1332497115612000 -> 1332497160000000 ]")
  427. # Check --ext output
  428. self.ok("list --ext")
  429. lines_(self.captured, 9)
  430. self.ok("list -E -T")
  431. c = self.contain
  432. c("\n interval extents: 1332496800000000 -> 1332497160000000\n")
  433. c("\n total data: 43200 rows, 359.983336 seconds\n")
  434. c("\n interval extents: (no data)\n")
  435. c("\n total data: 0 rows, 0.000000 seconds\n")
  436. # Misc
  437. self.fail("list --ext --start='23 Mar 2012 10:05:15.50'")
  438. self.contain("--start and --end only make sense with --detail")
  439. def test_08_extract(self):
  440. # nonexistent stream
  441. self.fail("extract /no/such/foo --start 2000-01-01 --end 2020-01-01")
  442. self.contain("error getting stream info")
  443. # reversed range
  444. self.fail("extract -a /newton/prep --start 2020-01-01 --end 2000-01-01")
  445. self.contain("start is after end")
  446. # empty ranges return error 2
  447. self.fail("extract -a /newton/prep " +
  448. "--start '23 Mar 2012 20:00:30' " +
  449. "--end '23 Mar 2012 20:00:31'",
  450. exitcode = 2, require_error = False)
  451. self.contain("no data")
  452. self.fail("extract -a /newton/prep " +
  453. "--start '23 Mar 2012 20:00:30.000001' " +
  454. "--end '23 Mar 2012 20:00:30.000002'",
  455. exitcode = 2, require_error = False)
  456. self.contain("no data")
  457. self.fail("extract -a /newton/prep " +
  458. "--start '23 Mar 2022 10:00:30' " +
  459. "--end '23 Mar 2022 10:00:31'",
  460. exitcode = 2, require_error = False)
  461. self.contain("no data")
  462. # but are ok if we're just counting results
  463. self.ok("extract --count /newton/prep " +
  464. "--start '23 Mar 2012 20:00:30' " +
  465. "--end '23 Mar 2012 20:00:31'")
  466. self.match("0\n")
  467. self.ok("extract -c /newton/prep " +
  468. "--start '23 Mar 2012 20:00:30.000001' " +
  469. "--end '23 Mar 2012 20:00:30.000002'")
  470. self.match("0\n")
  471. # Check various dumps against stored copies of how they should appear
  472. def test(file, start, end, extra=""):
  473. self.ok("extract " + extra + " /newton/prep " +
  474. "--start '23 Mar 2012 " + start + "' " +
  475. "--end '23 Mar 2012 " + end + "'")
  476. self.matchfile("tests/data/extract-" + str(file))
  477. self.ok("extract --count " + extra + " /newton/prep " +
  478. "--start '23 Mar 2012 " + start + "' " +
  479. "--end '23 Mar 2012 " + end + "'")
  480. self.matchfilecount("tests/data/extract-" + str(file))
  481. test(1, "10:00:30", "10:00:31", extra="-a")
  482. test(1, "10:00:30.000000", "10:00:31", extra="-a")
  483. test(2, "10:00:30.000001", "10:00:31")
  484. test(2, "10:00:30.008333", "10:00:31")
  485. test(3, "10:00:30.008333", "10:00:30.008334")
  486. test(3, "10:00:30.008333", "10:00:30.016667")
  487. test(4, "10:00:30.008333", "10:00:30.025")
  488. test(5, "10:00:30", "10:00:31", extra="--annotate --bare")
  489. test(6, "10:00:30", "10:00:31", extra="-b")
  490. test(7, "10:00:30", "10:00:30.999", extra="-a -T")
  491. test(7, "10:00:30", "10:00:30.999", extra="-a --timestamp-raw")
  492. # all data put in by tests
  493. self.ok("extract -a /newton/prep --start 2000-01-01 --end 2020-01-01")
  494. lines_(self.captured, 43204)
  495. self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
  496. self.match("43200\n")
  497. def test_09_truncated(self):
  498. # Test truncated responses by overriding the nilmdb max_results
  499. server_stop()
  500. server_start(max_results = 2)
  501. self.ok("list --detail")
  502. lines_(self.captured, 8)
  503. server_stop()
  504. server_start()
  505. def test_10_remove(self):
  506. # Removing data
  507. # Try nonexistent stream
  508. self.fail("remove /no/such/foo --start 2000-01-01 --end 2020-01-01")
  509. self.contain("No stream at path")
  510. # empty or backward ranges return errors
  511. self.fail("remove /newton/prep --start 2020-01-01 --end 2000-01-01")
  512. self.contain("start must precede end")
  513. self.fail("remove /newton/prep " +
  514. "--start '23 Mar 2012 10:00:30' " +
  515. "--end '23 Mar 2012 10:00:30'")
  516. self.contain("start must precede end")
  517. self.fail("remove /newton/prep " +
  518. "--start '23 Mar 2012 10:00:30.000001' " +
  519. "--end '23 Mar 2012 10:00:30.000001'")
  520. self.contain("start must precede end")
  521. self.fail("remove /newton/prep " +
  522. "--start '23 Mar 2022 10:00:30' " +
  523. "--end '23 Mar 2022 10:00:30'")
  524. self.contain("start must precede end")
  525. # Verbose
  526. self.ok("remove -c /newton/prep " +
  527. "--start '23 Mar 2022 20:00:30' " +
  528. "--end '23 Mar 2022 20:00:31'")
  529. self.match("0\n")
  530. self.ok("remove --count /newton/prep " +
  531. "--start '23 Mar 2022 20:00:30' " +
  532. "--end '23 Mar 2022 20:00:31'")
  533. self.match("0\n")
  534. # Make sure we have the data we expect
  535. self.ok("list --detail /newton/prep")
  536. self.match("/newton/prep float32_8\n" +
  537. " [ Fri, 23 Mar 2012 10:00:00.000000 +0000"
  538. " -> Fri, 23 Mar 2012 10:01:59.991668 +0000 ]\n"
  539. " [ Fri, 23 Mar 2012 10:02:00.000000 +0000"
  540. " -> Fri, 23 Mar 2012 10:03:59.991668 +0000 ]\n"
  541. " [ Fri, 23 Mar 2012 10:04:00.000000 +0000"
  542. " -> Fri, 23 Mar 2012 10:06:00.000000 +0000 ]\n")
  543. # Remove various chunks of prep data and make sure
  544. # they're gone.
  545. self.ok("remove -c /newton/prep " +
  546. "--start '23 Mar 2012 10:00:30' " +
  547. "--end '23 Mar 2012 10:00:40'")
  548. self.match("1200\n")
  549. self.ok("remove -c /newton/prep " +
  550. "--start '23 Mar 2012 10:00:10' " +
  551. "--end '23 Mar 2012 10:00:20'")
  552. self.match("1200\n")
  553. self.ok("remove -c /newton/prep " +
  554. "--start '23 Mar 2012 10:00:05' " +
  555. "--end '23 Mar 2012 10:00:25'")
  556. self.match("1200\n")
  557. self.ok("remove -c /newton/prep " +
  558. "--start '23 Mar 2012 10:03:50' " +
  559. "--end '23 Mar 2012 10:06:50'")
  560. self.match("15600\n")
  561. self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
  562. self.match("24000\n")
  563. # See the missing chunks in list output
  564. self.ok("list --detail /newton/prep")
  565. self.match("/newton/prep float32_8\n" +
  566. " [ Fri, 23 Mar 2012 10:00:00.000000 +0000"
  567. " -> Fri, 23 Mar 2012 10:00:05.000000 +0000 ]\n"
  568. " [ Fri, 23 Mar 2012 10:00:25.000000 +0000"
  569. " -> Fri, 23 Mar 2012 10:00:30.000000 +0000 ]\n"
  570. " [ Fri, 23 Mar 2012 10:00:40.000000 +0000"
  571. " -> Fri, 23 Mar 2012 10:01:59.991668 +0000 ]\n"
  572. " [ Fri, 23 Mar 2012 10:02:00.000000 +0000"
  573. " -> Fri, 23 Mar 2012 10:03:50.000000 +0000 ]\n")
  574. # Remove all data, verify it's missing
  575. self.ok("remove /newton/prep --start 2000-01-01 --end 2020-01-01")
  576. self.match("") # no count requested this time
  577. self.ok("list --detail /newton/prep")
  578. self.match("/newton/prep float32_8\n" +
  579. " (no intervals)\n")
  580. # Reinsert some data, to verify that no overlaps with deleted
  581. # data are reported
  582. os.environ['TZ'] = "UTC"
  583. self.ok("insert --timestamp -f --rate 120 /newton/prep "
  584. "tests/data/prep-20120323T1000")
  585. self.ok("insert -t --filename --rate 120 /newton/prep "
  586. "tests/data/prep-20120323T1002")
  587. def test_11_destroy(self):
  588. # Delete records
  589. self.ok("destroy --help")
  590. self.fail("destroy")
  591. self.contain("too few arguments")
  592. self.fail("destroy /no/such/stream")
  593. self.contain("No stream at path")
  594. self.fail("destroy asdfasdf")
  595. self.contain("No stream at path")
  596. # From previous tests, we have:
  597. self.ok("list")
  598. self.match("/newton/prep float32_8\n"
  599. "/newton/raw uint16_6\n"
  600. "/newton/zzz/rawnotch uint16_9\n")
  601. # Notice how they're not empty
  602. self.ok("list --detail")
  603. lines_(self.captured, 7)
  604. # Delete some
  605. self.ok("destroy /newton/prep")
  606. self.ok("list")
  607. self.match("/newton/raw uint16_6\n"
  608. "/newton/zzz/rawnotch uint16_9\n")
  609. self.ok("destroy /newton/zzz/rawnotch")
  610. self.ok("list")
  611. self.match("/newton/raw uint16_6\n")
  612. self.ok("destroy /newton/raw")
  613. self.ok("create /newton/raw uint16_6")
  614. self.ok("destroy /newton/raw")
  615. self.ok("list")
  616. self.match("")
  617. # Re-create a previously deleted location, and some new ones
  618. rebuild = [ "/newton/prep", "/newton/zzz",
  619. "/newton/raw", "/newton/asdf/qwer" ]
  620. for path in rebuild:
  621. # Create the path
  622. self.ok("create " + path + " float32_8")
  623. self.ok("list")
  624. self.contain(path)
  625. # Make sure it was created empty
  626. self.ok("list --detail --path " + path)
  627. self.contain("(no intervals)")
  628. def test_12_unicode(self):
  629. # Unicode paths.
  630. self.ok("destroy /newton/asdf/qwer")
  631. self.ok("destroy /newton/prep")
  632. self.ok("destroy /newton/raw")
  633. self.ok("destroy /newton/zzz")
  634. self.ok(u"create /düsseldorf/raw uint16_6")
  635. self.ok("list --detail")
  636. self.contain(u"/düsseldorf/raw uint16_6")
  637. self.contain("(no intervals)")
  638. # Unicode metadata
  639. self.ok(u"metadata /düsseldorf/raw --set α=beta 'γ=δ'")
  640. self.ok(u"metadata /düsseldorf/raw --update 'α=β ε τ α'")
  641. self.ok(u"metadata /düsseldorf/raw")
  642. self.match(u"α=β ε τ α\nγ=δ\n")
  643. self.ok(u"destroy /düsseldorf/raw")
  644. def test_13_files(self):
  645. # Test BulkData's ability to split into multiple files,
  646. # by forcing the file size to be really small.
  647. server_stop()
  648. server_start(bulkdata_args = { "file_size" : 920, # 23 rows per file
  649. "files_per_dir" : 3 })
  650. # Fill data
  651. self.ok("create /newton/prep float32_8")
  652. os.environ['TZ'] = "UTC"
  653. with open("tests/data/prep-20120323T1004-timestamped") as input:
  654. self.ok("insert -s 20120323T1004 -e 20120323T1006 /newton/prep",
  655. input)
  656. # Extract it
  657. self.ok("extract /newton/prep --start '2000-01-01' " +
  658. "--end '2012-03-23 10:04:01'")
  659. lines_(self.captured, 120)
  660. self.ok("extract /newton/prep --start '2000-01-01' " +
  661. "--end '2022-03-23 10:04:01'")
  662. lines_(self.captured, 14400)
  663. # Make sure there were lots of files generated in the database
  664. # dir
  665. nfiles = 0
  666. for (dirpath, dirnames, filenames) in os.walk(testdb):
  667. nfiles += len(filenames)
  668. assert(nfiles > 500)
  669. # Make sure we can restart the server with a different file
  670. # size and have it still work
  671. server_stop()
  672. server_start()
  673. self.ok("extract /newton/prep --start '2000-01-01' " +
  674. "--end '2022-03-23 10:04:01'")
  675. lines_(self.captured, 14400)
  676. # Now recreate the data one more time and make sure there are
  677. # fewer files.
  678. self.ok("destroy /newton/prep")
  679. self.fail("destroy /newton/prep") # already destroyed
  680. self.ok("create /newton/prep float32_8")
  681. os.environ['TZ'] = "UTC"
  682. with open("tests/data/prep-20120323T1004-timestamped") as input:
  683. self.ok("insert -s 20120323T1004 -e 20120323T1006 /newton/prep",
  684. input)
  685. nfiles = 0
  686. for (dirpath, dirnames, filenames) in os.walk(testdb):
  687. nfiles += len(filenames)
  688. lt_(nfiles, 50)
  689. self.ok("destroy /newton/prep") # destroy again
  690. def test_14_remove_files(self):
  691. # Test BulkData's ability to remove when data is split into
  692. # multiple files. Should be a fairly comprehensive test of
  693. # remove functionality.
  694. server_stop()
  695. server_start(bulkdata_args = { "file_size" : 920, # 23 rows per file
  696. "files_per_dir" : 3 })
  697. # Insert data. Just for fun, insert out of order
  698. self.ok("create /newton/prep float32_8")
  699. os.environ['TZ'] = "UTC"
  700. self.ok("insert -t --filename --rate 120 /newton/prep "
  701. "tests/data/prep-20120323T1002")
  702. self.ok("insert -t --filename --rate 120 /newton/prep "
  703. "tests/data/prep-20120323T1000")
  704. # Should take up about 2.8 MB here (including directory entries)
  705. du_before = nilmdb.utils.diskusage.du(testdb)
  706. # Make sure we have the data we expect
  707. self.ok("list --detail")
  708. self.match("/newton/prep float32_8\n" +
  709. " [ Fri, 23 Mar 2012 10:00:00.000000 +0000"
  710. " -> Fri, 23 Mar 2012 10:01:59.991668 +0000 ]\n"
  711. " [ Fri, 23 Mar 2012 10:02:00.000000 +0000"
  712. " -> Fri, 23 Mar 2012 10:03:59.991668 +0000 ]\n")
  713. # Remove various chunks of prep data and make sure
  714. # they're gone.
  715. self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
  716. self.match("28800\n")
  717. self.ok("remove -c /newton/prep " +
  718. "--start '23 Mar 2012 10:00:30' " +
  719. "--end '23 Mar 2012 10:03:30'")
  720. self.match("21600\n")
  721. self.ok("remove -c /newton/prep " +
  722. "--start '23 Mar 2012 10:00:10' " +
  723. "--end '23 Mar 2012 10:00:20'")
  724. self.match("1200\n")
  725. self.ok("remove -c /newton/prep " +
  726. "--start '23 Mar 2012 10:00:05' " +
  727. "--end '23 Mar 2012 10:00:25'")
  728. self.match("1200\n")
  729. self.ok("remove -c /newton/prep " +
  730. "--start '23 Mar 2012 10:03:50' " +
  731. "--end '23 Mar 2012 10:06:50'")
  732. self.match("1200\n")
  733. self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
  734. self.match("3600\n")
  735. # See the missing chunks in list output
  736. self.ok("list --detail")
  737. self.match("/newton/prep float32_8\n" +
  738. " [ Fri, 23 Mar 2012 10:00:00.000000 +0000"
  739. " -> Fri, 23 Mar 2012 10:00:05.000000 +0000 ]\n"
  740. " [ Fri, 23 Mar 2012 10:00:25.000000 +0000"
  741. " -> Fri, 23 Mar 2012 10:00:30.000000 +0000 ]\n"
  742. " [ Fri, 23 Mar 2012 10:03:30.000000 +0000"
  743. " -> Fri, 23 Mar 2012 10:03:50.000000 +0000 ]\n")
  744. # We have 1/8 of the data that we had before, so the file size
  745. # should have dropped below 1/4 of what it used to be
  746. du_after = nilmdb.utils.diskusage.du(testdb)
  747. lt_(du_after, (du_before / 4))
  748. # Remove anything that came from the 10:02 data file
  749. self.ok("remove /newton/prep " +
  750. "--start '23 Mar 2012 10:02:00' --end '2020-01-01'")
  751. # Re-insert 19 lines from that file, then remove them again.
  752. # With the specific file_size above, this will cause the last
  753. # file in the bulk data storage to be exactly file_size large,
  754. # so removing the data should also remove that last file.
  755. self.ok("insert --timestamp -f --rate 120 /newton/prep " +
  756. "tests/data/prep-20120323T1002-first19lines")
  757. self.ok("remove /newton/prep " +
  758. "--start '23 Mar 2012 10:02:00' --end '2020-01-01'")
  759. # Shut down and restart server, to force nrows to get refreshed.
  760. server_stop()
  761. server_start()
  762. # Re-add the full 10:02 data file. This tests adding new data once
  763. # we removed data near the end.
  764. self.ok("insert -t -f -r 120 /newton/prep "
  765. "tests/data/prep-20120323T1002")
  766. # See if we can extract it all
  767. self.ok("extract /newton/prep --start 2000-01-01 --end 2020-01-01")
  768. lines_(self.captured, 15600)
  769. def test_15_intervals_diff(self):
  770. # Test "intervals" and "intervals --diff" command.
  771. os.environ['TZ'] = "UTC"
  772. self.ok("create /diff/1 uint8_1")
  773. self.match("")
  774. self.ok("intervals /diff/1")
  775. self.match("")
  776. self.ok("intervals /diff/1 --diff /diff/1")
  777. self.match("")
  778. self.ok("intervals --diff /diff/1 /diff/1")
  779. self.match("")
  780. self.fail("intervals /diff/2")
  781. self.fail("intervals /diff/1 -d /diff/2")
  782. self.ok("create /diff/2 uint8_1")
  783. self.ok("intervals -T /diff/1 -d /diff/2")
  784. self.match("")
  785. self.ok("insert -s 01-01-2000 -e 01-01-2001 /diff/1 /dev/null")
  786. self.ok("intervals /diff/1")
  787. self.match("[ Sat, 01 Jan 2000 00:00:00.000000 +0000 -"
  788. "> Mon, 01 Jan 2001 00:00:00.000000 +0000 ]\n")
  789. self.ok("intervals /diff/1 -d /diff/2")
  790. self.match("[ Sat, 01 Jan 2000 00:00:00.000000 +0000 -"
  791. "> Mon, 01 Jan 2001 00:00:00.000000 +0000 ]\n")
  792. self.ok("insert -s 01-01-2000 -e 01-01-2001 /diff/2 /dev/null")
  793. self.ok("intervals /diff/1 -d /diff/2")
  794. self.match("")
  795. self.ok("insert -s 01-01-2001 -e 01-01-2002 /diff/1 /dev/null")
  796. self.ok("insert -s 01-01-2002 -e 01-01-2003 /diff/2 /dev/null")
  797. self.ok("intervals /diff/1 -d /diff/2")
  798. self.match("[ Mon, 01 Jan 2001 00:00:00.000000 +0000 -"
  799. "> Tue, 01 Jan 2002 00:00:00.000000 +0000 ]\n")
  800. self.ok("insert -s 01-01-2004 -e 01-01-2005 /diff/1 /dev/null")
  801. self.ok("intervals /diff/1 -d /diff/2")
  802. self.match("[ Mon, 01 Jan 2001 00:00:00.000000 +0000 -"
  803. "> Tue, 01 Jan 2002 00:00:00.000000 +0000 ]\n"
  804. "[ Thu, 01 Jan 2004 00:00:00.000000 +0000 -"
  805. "> Sat, 01 Jan 2005 00:00:00.000000 +0000 ]\n")
  806. self.fail("intervals -s 01-01-2003 -e 01-01-2000 /diff/1 -d /diff/2")
  807. self.ok("intervals -s 01-01-2003 -e 01-01-2008 /diff/1 -d /diff/2")
  808. self.match("[ Thu, 01 Jan 2004 00:00:00.000000 +0000 -"
  809. "> Sat, 01 Jan 2005 00:00:00.000000 +0000 ]\n")
  810. self.ok("destroy /diff/1")
  811. self.ok("destroy /diff/2")
  812. def test_16_rename(self):
  813. # Test renaming. Force file size smaller so we get more files
  814. server_stop()
  815. recursive_unlink(testdb)
  816. server_start(bulkdata_args = { "file_size" : 920, # 23 rows per file
  817. "files_per_dir" : 3 })
  818. # Fill data
  819. self.ok("create /newton/prep float32_8")
  820. os.environ['TZ'] = "UTC"
  821. with open("tests/data/prep-20120323T1004-timestamped") as input:
  822. self.ok("insert -s 20120323T1004 -e 20120323T1006 /newton/prep",
  823. input)
  824. # Extract it
  825. self.ok("extract /newton/prep --start '2000-01-01' " +
  826. "--end '2012-03-23 10:04:01'")
  827. extract_before = self.captured
  828. def check_path(*components):
  829. # Verify the paths look right on disk
  830. seek = os.path.join(testdb, "data", *components)
  831. for (dirpath, dirnames, filenames) in os.walk(testdb):
  832. if "_format" in filenames:
  833. if dirpath == seek:
  834. break
  835. raise AssertionError("data also found at " + dirpath)
  836. else:
  837. raise AssertionError("data not found at " + seek)
  838. # Verify "list" output
  839. self.ok("list")
  840. self.match("/" + "/".join(components) + " float32_8\n")
  841. # Lots of renames
  842. check_path("newton", "prep")
  843. self.fail("rename /newton/prep /newton/prep")
  844. self.contain("old and new paths are the same")
  845. check_path("newton", "prep")
  846. self.fail("rename /newton/prep /newton")
  847. self.contain("subdirs of this path already exist")
  848. self.fail("rename /newton/prep /newton/prep/")
  849. self.contain("invalid path")
  850. self.ok("rename /newton/prep /newton/foo")
  851. check_path("newton", "foo")
  852. self.ok("rename /newton/foo /totally/different/thing")
  853. check_path("totally", "different", "thing")
  854. self.ok("rename /totally/different/thing /totally/something")
  855. check_path("totally", "something")
  856. self.ok("rename /totally/something /totally/something/cool")
  857. check_path("totally", "something", "cool")
  858. self.ok("rename /totally/something/cool /foo/bar")
  859. check_path("foo", "bar")
  860. self.ok("create /xxx/yyy/zzz float32_8")
  861. self.fail("rename /foo/bar /xxx/yyy")
  862. self.contain("subdirs of this path already exist")
  863. self.fail("rename /foo/bar /xxx/yyy/zzz")
  864. self.contain("stream already exists at this path")
  865. self.fail("rename /foo/bar /xxx/yyy/zzz/www")
  866. self.contain("path is subdir of existing node")
  867. self.ok("rename /foo/bar /xxx/yyy/mmm")
  868. self.ok("destroy /xxx/yyy/zzz")
  869. check_path("xxx", "yyy", "mmm")
  870. # Extract it at the final path
  871. self.ok("extract /xxx/yyy/mmm --start '2000-01-01' " +
  872. "--end '2012-03-23 10:04:01'")
  873. eq_(self.captured, extract_before)
  874. self.ok("destroy /xxx/yyy/mmm")
  875. # Make sure temporary rename dirs weren't left around
  876. for (dirpath, dirnames, filenames) in os.walk(testdb):
  877. if "rename-" in dirpath:
  878. raise AssertionError("temporary directories not cleaned up")
  879. if "totally" in dirpath or "newton" in dirpath:
  880. raise AssertionError("old directories not cleaned up")
  881. server_stop()
  882. server_start()