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.
 
 
 

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