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.
 
 
 

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