test-matching.coffee 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. test = require 'tape'
  2. ROOT = "../src"
  3. if process.env.ROOT
  4. ROOT = process.env.ROOT
  5. matching = require (ROOT + '/matching')
  6. adjacency_graphs = require (ROOT + '/adjacency_graphs')
  7. # takes a pattern and list of prefixes/suffixes
  8. # returns a bunch of variants of that pattern embedded
  9. # with each possible prefix/suffix combination, including no prefix/suffix
  10. # returns a list of triplets [variant, i, j] where [i,j] is the start/end of the pattern, inclusive
  11. genpws = (pattern, prefixes, suffixes) ->
  12. prefixes = prefixes.slice()
  13. suffixes = suffixes.slice()
  14. for lst in [prefixes, suffixes]
  15. lst.unshift '' if '' not in lst
  16. result = []
  17. for prefix in prefixes
  18. for suffix in suffixes
  19. [i, j] = [prefix.length, prefix.length + pattern.length - 1]
  20. result.push [prefix + pattern + suffix, i, j]
  21. result
  22. check_matches = (prefix, t, matches, pattern_names, patterns, ijs, props) ->
  23. if typeof pattern_names is "string"
  24. # shortcut: if checking for a list of the same type of patterns,
  25. # allow passing a string 'pat' instead of array ['pat', 'pat', ...]
  26. pattern_names = (pattern_names for i in [0...patterns.length])
  27. is_equal_len_args = pattern_names.length == patterns.length == ijs.length
  28. for prop, lst of props
  29. # props is structured as: keys that points to list of values
  30. is_equal_len_args = is_equal_len_args and (lst.length == patterns.length)
  31. throw 'unequal argument lists to check_matches' unless is_equal_len_args
  32. msg = "#{prefix}: matches.length == #{patterns.length}"
  33. t.equal matches.length, patterns.length, msg
  34. for k in [0...patterns.length]
  35. match = matches[k]
  36. pattern_name = pattern_names[k]
  37. pattern = patterns[k]
  38. [i, j] = ijs[k]
  39. msg = "#{prefix}: matches[#{k}].pattern == '#{pattern_name}'"
  40. t.equal match.pattern, pattern_name, msg
  41. msg = "#{prefix}: matches[#{k}] should have [i, j] of [#{i}, #{j}]"
  42. t.deepEqual [match.i, match.j], [i, j], msg
  43. msg = "#{prefix}: matches[#{k}].token == '#{pattern}'"
  44. t.equal match.token, pattern, msg
  45. for prop_name, prop_list of props
  46. prop_msg = prop_list[k]
  47. prop_msg = "'#{prop_msg}'" if typeof(prop_msg) == 'string'
  48. msg = "#{prefix}: matches[#{k}].#{prop_name} == #{prop_msg}"
  49. t.deepEqual match[prop_name], prop_list[k], msg
  50. test 'matching utils', (t) ->
  51. return t.end() if matching.no_util
  52. t.ok matching.empty([]), ".empty returns true for an empty array"
  53. t.ok matching.empty({}), ".empty returns true for an empty object"
  54. for obj in [
  55. [1]
  56. [1, 2]
  57. [[]]
  58. {a: 1}
  59. {0: {}}
  60. ]
  61. t.notOk matching.empty(obj), ".empty returns false for non-empty objects and arrays"
  62. lst = []
  63. matching.extend lst, []
  64. t.deepEqual lst, [], "extending an empty list with an empty list leaves it empty"
  65. matching.extend lst, [1]
  66. t.deepEqual lst, [1], "extending an empty list with another makes it equal to the other"
  67. matching.extend lst, [2, 3]
  68. t.deepEqual lst, [1, 2, 3], "extending a list with another adds each of the other's elements"
  69. [lst1, lst2] = [[1], [2]]
  70. matching.extend lst1, lst2
  71. t.deepEqual lst2, [2], "extending a list by another doesn't affect the other"
  72. chr_map = {a: 'A', b: 'B'}
  73. for [string, map, result] in [
  74. ['a', chr_map, 'A']
  75. ['c', chr_map, 'c']
  76. ['ab', chr_map, 'AB']
  77. ['abc', chr_map, 'ABc']
  78. ['aa', chr_map, 'AA']
  79. ['abab', chr_map, 'ABAB']
  80. ['', chr_map, '']
  81. ['', {}, '']
  82. ['abc', {}, 'abc']
  83. ]
  84. msg = "translates '#{string}' to '#{result}' with provided charmap"
  85. t.equal matching.translate(string, map), result, msg
  86. for [[dividend, divisor], remainder] in [
  87. [[ 0, 1], 0]
  88. [[ 1, 1], 0]
  89. [[-1, 1], 0]
  90. [[ 5, 5], 0]
  91. [[ 3, 5], 3]
  92. [[-1, 5], 4]
  93. [[-5, 5], 0]
  94. [[ 6, 5], 1]
  95. ]
  96. msg = "mod(#{dividend}, #{divisor}) == #{remainder}"
  97. t.equal matching.mod(dividend, divisor), remainder, msg
  98. t.deepEqual matching.sorted([]), [], "sorting an empty list leaves it empty"
  99. [m1, m2, m3, m4, m5, m6] = [
  100. {i: 5, j: 5}
  101. {i: 6, j: 7}
  102. {i: 2, j: 5}
  103. {i: 0, j: 0}
  104. {i: 2, j: 3}
  105. {i: 0, j: 3}
  106. ]
  107. msg = "matches are sorted on i index primary, j secondary"
  108. t.deepEqual matching.sorted([m1, m2, m3, m4, m5, m6]), [m4, m6, m5, m3, m1, m2], msg
  109. t.end()
  110. test 'dictionary matching', (t) ->
  111. dm = (pw) -> matching.dictionary_match pw, test_dicts
  112. test_dicts =
  113. d1:
  114. motherboard: 1
  115. mother: 2
  116. board: 3
  117. abcd: 4
  118. cdef: 5
  119. d2:
  120. 'z': 1
  121. '8': 2
  122. '99': 3
  123. '$': 4
  124. 'asdf1234&*': 5
  125. matches = dm 'motherboard'
  126. patterns = ['mother', 'motherboard', 'board']
  127. msg = "matches words that contain other words"
  128. check_matches msg, t, matches, 'dictionary', patterns, [[0,5], [0,10], [6,10]],
  129. matched_word: ['mother', 'motherboard', 'board']
  130. rank: [2, 1, 3]
  131. dictionary_name: ['d1', 'd1', 'd1']
  132. matches = dm 'abcdef'
  133. patterns = ['abcd', 'cdef']
  134. msg = "matches multiple words when they overlap"
  135. check_matches msg, t, matches, 'dictionary', patterns, [[0,3], [2,5]],
  136. matched_word: ['abcd', 'cdef']
  137. rank: [4, 5]
  138. dictionary_name: ['d1', 'd1']
  139. matches = dm 'BoaRdZ'
  140. patterns = ['BoaRd', 'Z']
  141. msg = "ignores uppercasing"
  142. check_matches msg, t, matches, 'dictionary', patterns, [[0,4], [5,5]],
  143. matched_word: ['board', 'z']
  144. rank: [3, 1]
  145. dictionary_name: ['d1', 'd2']
  146. prefixes = ['q', '%%']
  147. suffixes = ['%', 'qq']
  148. word = 'asdf1234&*'
  149. for [password, i, j] in genpws word, prefixes, suffixes
  150. matches = dm password
  151. msg = "identifies words surrounded by non-words"
  152. check_matches msg, t, matches, 'dictionary', [word], [[i,j]],
  153. matched_word: [word]
  154. rank: [5]
  155. dictionary_name: ['d2']
  156. for name, dict of test_dicts
  157. for word, rank of dict
  158. continue if word is 'motherboard' # skip words that contain others
  159. matches = dm word
  160. msg = "matches against all words in provided dictionaries"
  161. check_matches msg, t, matches, 'dictionary', [word], [[0, word.length - 1]],
  162. matched_word: [word]
  163. rank: [rank]
  164. dictionary_name: [name]
  165. # test the default dictionaries
  166. matches = matching.dictionary_match 'wow'
  167. patterns = ['wow']
  168. ijs = [[0,2]]
  169. msg = "default dictionaries"
  170. check_matches msg, t, matches, 'dictionary', patterns, ijs,
  171. matched_word: patterns
  172. rank: [322]
  173. dictionary_name: ['us_tv_and_film']
  174. matching.set_user_input_dictionary ['foo', 'bar']
  175. matches = matching.dictionary_match 'foobar'
  176. matches = matches.filter (match) ->
  177. match.dictionary_name == 'user_inputs'
  178. msg = "matches with provided user input dictionary"
  179. check_matches msg, t, matches, 'dictionary', ['foo', 'bar'], [[0, 2], [3, 5]],
  180. matched_word: ['foo', 'bar']
  181. rank: [1, 2]
  182. t.end()
  183. test 'reverse dictionary matching', (t) ->
  184. test_dicts =
  185. d1:
  186. 123: 1
  187. 321: 2
  188. 456: 3
  189. 654: 4
  190. password = '0123456789'
  191. matches = matching.reverse_dictionary_match password, test_dicts
  192. msg = 'matches against reversed words'
  193. check_matches msg, t, matches, 'dictionary', ['123', '456'], [[1, 3], [4, 6]],
  194. matched_word: ['321', '654']
  195. reversed: [true, true]
  196. dictionary_name: ['d1', 'd1']
  197. rank: [2, 4]
  198. t.end()
  199. test 'l33t matching', (t) ->
  200. test_table =
  201. a: ['4', '@']
  202. c: ['(', '{', '[', '<']
  203. g: ['6', '9']
  204. o: ['0']
  205. for [pw, expected] in [
  206. [ '', {} ]
  207. [ 'abcdefgo123578!#$&*)]}>', {} ]
  208. [ 'a', {} ]
  209. [ '4', {'a': ['4']} ]
  210. [ '4@', {'a': ['4','@']} ]
  211. [ '4({60', {'a': ['4'], 'c': ['(','{'], 'g': ['6'], 'o': ['0']} ]
  212. ]
  213. msg = "reduces l33t table to only the substitutions that a password might be employing"
  214. t.deepEquals matching.relevant_l33t_subtable(pw, test_table), expected, msg
  215. for [table, subs] in [
  216. [ {}, [{}] ]
  217. [ {a: ['@']}, [{'@': 'a'}] ]
  218. [ {a: ['@','4']}, [{'@': 'a'}, {'4': 'a'}] ]
  219. [ {a: ['@','4'], c: ['(']}, [{'@': 'a', '(': 'c' }, {'4': 'a', '(': 'c'}] ]
  220. ]
  221. msg = "enumerates the different sets of l33t substitutions a password might be using"
  222. t.deepEquals matching.enumerate_l33t_subs(table), subs, msg
  223. lm = (pw) -> matching.l33t_match pw, dicts, test_table
  224. dicts =
  225. words:
  226. aac: 1
  227. password: 3
  228. paassword: 4
  229. asdf0: 5
  230. words2:
  231. cgo: 1
  232. t.deepEquals lm(''), [], "doesn't match ''"
  233. t.deepEquals lm('password'), [], "doesn't match pure dictionary words"
  234. for [password, pattern, word, dictionary_name, rank, ij, sub] in [
  235. [ 'p4ssword', 'p4ssword', 'password', 'words', 3, [0,7], {'4': 'a'} ]
  236. [ 'p@ssw0rd', 'p@ssw0rd', 'password', 'words', 3, [0,7], {'@': 'a', '0': 'o'} ]
  237. [ 'aSdfO{G0asDfO', '{G0', 'cgo', 'words2', 1, [5, 7], {'{': 'c', '0': 'o'} ]
  238. ]
  239. msg = "matches against common l33t substitutions"
  240. check_matches msg, t, lm(password), 'dictionary', [pattern], [ij],
  241. l33t: [true]
  242. sub: [sub]
  243. matched_word: [word]
  244. rank: [rank]
  245. dictionary_name: [dictionary_name]
  246. matches = lm '@a(go{G0'
  247. msg = "matches against overlapping l33t patterns"
  248. check_matches msg, t, matches, 'dictionary', ['@a(', '(go', '{G0'], [[0,2], [2,4], [5,7]],
  249. l33t: [true, true, true]
  250. sub: [{'@': 'a', '(': 'c'}, {'(': 'c'}, {'{': 'c', '0': 'o'}]
  251. matched_word: ['aac', 'cgo', 'cgo']
  252. rank: [1, 1, 1]
  253. dictionary_name: ['words', 'words2', 'words2']
  254. msg = "doesn't match when multiple l33t substitutions are needed for the same letter"
  255. t.deepEqual lm('p4@ssword'), [], msg
  256. msg = "doesn't match single-character l33ted words"
  257. matches = matching.l33t_match '4 1 @'
  258. t.deepEqual matches, [], msg
  259. # known issue: subsets of substitutions aren't tried.
  260. # for long inputs, trying every subset of every possible substitution could quickly get large,
  261. # but there might be a performant way to fix.
  262. # (so in this example: {'4': a, '0': 'o'} is detected as a possible sub,
  263. # but the subset {'4': 'a'} isn't tried, missing the match for asdf0.)
  264. # TODO: consider partially fixing by trying all subsets of size 1 and maybe 2
  265. msg = "doesn't match with subsets of possible l33t substitutions"
  266. t.deepEqual lm('4sdf0'), [], msg
  267. t.end()
  268. test 'spatial matching', (t) ->
  269. for password in ['', '/', 'qw', '*/']
  270. msg = "doesn't match 1- and 2-character spatial patterns"
  271. t.deepEqual matching.spatial_match(password), [], msg
  272. # for testing, make a subgraph that contains a single keyboard
  273. _graphs = qwerty: adjacency_graphs.qwerty
  274. pattern = '6tfGHJ'
  275. matches = matching.spatial_match "rz!#{pattern}%z", _graphs
  276. msg = "matches against spatial patterns surrounded by non-spatial patterns"
  277. check_matches msg, t, matches, 'spatial', [pattern], [[3, 3 + pattern.length - 1]],
  278. graph: ['qwerty']
  279. turns: [2]
  280. shifted_count: [3]
  281. for [pattern, keyboard, turns, shifts] in [
  282. [ '12345', 'qwerty', 1, 0 ]
  283. [ '@WSX', 'qwerty', 1, 4 ]
  284. [ '6tfGHJ', 'qwerty', 2, 3 ]
  285. [ 'hGFd', 'qwerty', 1, 2 ]
  286. [ '/;p09876yhn', 'qwerty', 3, 0 ]
  287. [ 'Xdr%', 'qwerty', 1, 2 ]
  288. [ '159-', 'keypad', 1, 0 ]
  289. [ '*84', 'keypad', 1, 0 ]
  290. [ '/8520', 'keypad', 1, 0 ]
  291. [ '369', 'keypad', 1, 0 ]
  292. [ '/963.', 'mac_keypad', 1, 0 ]
  293. [ '*-632.0214', 'mac_keypad', 9, 0 ]
  294. [ 'aoEP%yIxkjq:', 'dvorak', 4, 5 ]
  295. [ ';qoaOQ:Aoq;a', 'dvorak', 11, 4 ]
  296. ]
  297. _graphs = {}
  298. _graphs[keyboard] = adjacency_graphs[keyboard]
  299. matches = matching.spatial_match pattern, _graphs
  300. msg = "matches '#{pattern}' as a #{keyboard} pattern"
  301. check_matches msg, t, matches, 'spatial', [pattern], [[0, pattern.length - 1]],
  302. graph: [keyboard]
  303. turns: [turns]
  304. shifted_count: [shifts]
  305. t.end()
  306. test 'sequence matching', (t) ->
  307. for password in ['', 'a', '1']
  308. msg = "doesn't match length-#{password.length} sequences"
  309. t.deepEqual matching.sequence_match(password), [], msg
  310. matches = matching.sequence_match 'abcbabc'
  311. msg = "matches overlapping patterns"
  312. check_matches msg, t, matches, 'sequence', ['abc', 'cba', 'abc'], [[0, 2], [2, 4], [4, 6]],
  313. ascending: [true, false, true]
  314. prefixes = ['!', '22']
  315. suffixes = ['!', '22']
  316. pattern = 'jihg'
  317. for [password, i, j] in genpws pattern, prefixes, suffixes
  318. matches = matching.sequence_match password
  319. msg = "matches embedded sequence patterns #{password}"
  320. check_matches msg, t, matches, 'sequence', [pattern], [[i, j]],
  321. sequence_name: ['lower']
  322. ascending: [false]
  323. for [pattern, name, is_ascending] in [
  324. ['ABC', 'upper', true]
  325. ['CBA', 'upper', false]
  326. ['PQR', 'upper', true]
  327. ['RQP', 'upper', false]
  328. ['XYZ', 'upper', true]
  329. ['ZYX', 'upper', false]
  330. ['abcd', 'lower', true]
  331. ['dcba', 'lower', false]
  332. ['jihg', 'lower', false]
  333. ['wxyz', 'lower', true]
  334. ['zxvt', 'lower', false]
  335. ['0369', 'digits', true]
  336. ['97531', 'digits', false]
  337. ]
  338. matches = matching.sequence_match pattern
  339. msg = "matches '#{pattern}' as a '#{name}' sequence"
  340. check_matches msg, t, matches, 'sequence', [pattern], [[0, pattern.length - 1]],
  341. sequence_name: [name]
  342. ascending: [is_ascending]
  343. t.end()
  344. test 'repeat matching', (t) ->
  345. for password in ['', '#']
  346. msg = "doesn't match length-#{password.length} repeat patterns"
  347. t.deepEqual matching.repeat_match(password), [], msg
  348. # test single-character repeats
  349. prefixes = ['@', 'y4@']
  350. suffixes = ['u', 'u%7']
  351. pattern = '&&&&&'
  352. for [password, i, j] in genpws pattern, prefixes, suffixes
  353. matches = matching.repeat_match password
  354. msg = "matches embedded repeat patterns"
  355. check_matches msg, t, matches, 'repeat', [pattern], [[i, j]],
  356. base_token: ['&']
  357. for length in [3, 12]
  358. for chr in ['a', 'Z', '4', '&']
  359. pattern = Array(length + 1).join(chr)
  360. matches = matching.repeat_match pattern
  361. msg = "matches repeats with base character '#{chr}'"
  362. check_matches msg, t, matches, 'repeat', [pattern], [[0, pattern.length - 1]],
  363. base_token: [chr]
  364. matches = matching.repeat_match 'BBB1111aaaaa@@@@@@'
  365. patterns = ['BBB','1111','aaaaa','@@@@@@']
  366. msg = 'matches multiple adjacent repeats'
  367. check_matches msg, t, matches, 'repeat', patterns, [[0, 2],[3, 6],[7, 11],[12, 17]],
  368. base_token: ['B', '1', 'a', '@']
  369. matches = matching.repeat_match '2818BBBbzsdf1111@*&@!aaaaaEUDA@@@@@@1729'
  370. msg = 'matches multiple repeats with non-repeats in-between'
  371. check_matches msg, t, matches, 'repeat', patterns, [[4, 6],[12, 15],[21, 25],[30, 35]],
  372. base_token: ['B', '1', 'a', '@']
  373. # test multi-character repeats
  374. pattern = 'abab'
  375. matches = matching.repeat_match pattern
  376. msg = 'matches multi-character repeat pattern'
  377. check_matches msg, t, matches, 'repeat', [pattern], [[0, pattern.length - 1]],
  378. base_token: ['ab']
  379. pattern = 'aabaab'
  380. matches = matching.repeat_match pattern
  381. msg = 'matches aabaab as a repeat instead of the aa prefix'
  382. check_matches msg, t, matches, 'repeat', [pattern], [[0, pattern.length - 1]],
  383. base_token: ['aab']
  384. pattern = 'abababab'
  385. matches = matching.repeat_match pattern
  386. msg = 'identifies ab as repeat string, even though abab is also repeated'
  387. check_matches msg, t, matches, 'repeat', [pattern], [[0, pattern.length - 1]],
  388. base_token: ['ab']
  389. t.end()
  390. test 'regex matching', (t) ->
  391. for [pattern, name] in [
  392. ['1922', 'recent_year']
  393. ['2017', 'recent_year']
  394. ]
  395. matches = matching.regex_match pattern
  396. msg = "matches #{pattern} as a #{name} pattern"
  397. check_matches msg, t, matches, 'regex', [pattern], [[0, pattern.length - 1]],
  398. regex_name: [name]
  399. t.end()
  400. test 'date matching', (t) ->
  401. for sep in ['', ' ', '-', '/', '\\', '_', '.']
  402. password = "13#{sep}2#{sep}1921"
  403. matches = matching.date_match password
  404. msg = "matches dates that use '#{sep}' as a separator"
  405. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  406. separator: [sep]
  407. year: [1921]
  408. month: [2]
  409. day: [13]
  410. for order in ['mdy', 'dmy', 'ymd', 'ydm']
  411. [d,m,y] = [8,8,88]
  412. password = order
  413. .replace 'y', y
  414. .replace 'm', m
  415. .replace 'd', d
  416. matches = matching.date_match password
  417. msg = "matches dates with '#{order}' format"
  418. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  419. separator: ['']
  420. year: [1988]
  421. month: [8]
  422. day: [8]
  423. password = '111504'
  424. matches = matching.date_match password
  425. msg = "matches the date with year closest to REFERENCE_YEAR when ambiguous"
  426. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  427. separator: ['']
  428. year: [2004] # picks '04' -> 2004 as year, not '1504'
  429. month: [11]
  430. day: [15]
  431. for [day, month, year] in [
  432. [1, 1, 1999]
  433. [11, 8, 2000]
  434. [9, 12, 2005]
  435. [22, 11, 1551]
  436. ]
  437. password = "#{year}#{month}#{day}"
  438. matches = matching.date_match password
  439. msg = "matches #{password}"
  440. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  441. separator: ['']
  442. year: [year]
  443. password = "#{year}.#{month}.#{day}"
  444. matches = matching.date_match password
  445. msg = "matches #{password}"
  446. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  447. separator: ['.']
  448. year: [year]
  449. password = "02/02/02"
  450. matches = matching.date_match password
  451. msg = "matches zero-padded dates"
  452. check_matches msg, t, matches, 'date', [password], [[0, password.length - 1]],
  453. separator: ['/']
  454. year: [2002]
  455. month: [2]
  456. day: [2]
  457. prefixes = ['a', 'ab']
  458. suffixes = ['!']
  459. pattern = '1/1/91'
  460. for [password, i, j] in genpws pattern, prefixes, suffixes
  461. matches = matching.date_match password
  462. msg = "matches embedded dates"
  463. check_matches msg, t, matches, 'date', [pattern], [[i, j]],
  464. year: [1991]
  465. month: [1]
  466. day: [1]
  467. matches = matching.date_match '12/20/1991.12.20'
  468. msg = "matches overlapping dates"
  469. check_matches msg, t, matches, 'date', ['12/20/1991', '1991.12.20'], [[0, 9], [6,15]],
  470. separator: ['/', '.']
  471. year: [1991, 1991]
  472. month: [12, 12]
  473. day: [20, 20]
  474. matches = matching.date_match '912/20/919'
  475. msg = "matches dates padded by non-ambiguous digits"
  476. check_matches msg, t, matches, 'date', ['12/20/91'], [[1, 8]],
  477. separator: ['/']
  478. year: [1991]
  479. month: [12]
  480. day: [20]
  481. t.end()
  482. test 'omnimatch', (t) ->
  483. t.deepEquals matching.omnimatch(''), [], "doesn't match ''"
  484. password = 'r0sebudmaelstrom11/20/91aaaa'
  485. matches = matching.omnimatch password
  486. for [pattern_name, [i, j]] in [
  487. [ 'dictionary', [0, 6] ]
  488. [ 'dictionary', [7, 15] ]
  489. [ 'date', [16, 23] ]
  490. [ 'repeat', [24, 27] ]
  491. ]
  492. included = false
  493. for match in matches
  494. included = true if match.i == i and match.j == j and match.pattern == pattern_name
  495. msg = "for #{password}, matches a #{pattern_name} pattern at [#{i}, #{j}]"
  496. t.ok included, msg
  497. t.end()