1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2010 Google Inc. All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15# implied. See the License for the specific language governing
16# permissions and limitations under the License.
17
18"""Unittest for textfsm module."""
19from __future__ import absolute_import
20from __future__ import division
21from __future__ import print_function
22from __future__ import unicode_literals
23
24from builtins import str
25import unittest
26from io import StringIO
27
28
29
30import textfsm
31
32
33class UnitTestFSM(unittest.TestCase):
34  """Tests the FSM engine."""
35
36  def testFSMValue(self):
37    # Check basic line is parsed.
38    line = r'Value beer (\S+)'
39    v = textfsm.TextFSMValue()
40    v.Parse(line)
41    self.assertEqual(v.name, 'beer')
42    self.assertEqual(v.regex, r'(\S+)')
43    self.assertEqual(v.template, r'(?P<beer>\S+)')
44    self.assertFalse(v.options)
45
46    # Test options
47    line = r'Value Filldown,Required beer (\S+)'
48    v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
49    v.Parse(line)
50    self.assertEqual(v.name, 'beer')
51    self.assertEqual(v.regex, r'(\S+)')
52    self.assertEqual(v.OptionNames(), ['Filldown', 'Required'])
53
54    # Multiple parenthesis.
55    v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
56    v.Parse('Value Required beer (boo(hoo))')
57    self.assertEqual(v.name, 'beer')
58    self.assertEqual(v.regex, '(boo(hoo))')
59    self.assertEqual(v.template, '(?P<beer>boo(hoo))')
60    self.assertEqual(v.OptionNames(), ['Required'])
61
62    # regex must be bounded by parenthesis.
63    self.assertRaises(textfsm.TextFSMTemplateError,
64                      v.Parse,
65                      'Value beer (boo(hoo)))boo')
66    self.assertRaises(textfsm.TextFSMTemplateError,
67                      v.Parse,
68                      'Value beer boo(boo(hoo)))')
69    self.assertRaises(textfsm.TextFSMTemplateError,
70                      v.Parse,
71                      'Value beer (boo)hoo)')
72
73    # Unbalanced parenthesis can exist if within square "[]" braces.
74    v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
75    v.Parse('Value beer (boo[(]hoo)')
76    self.assertEqual(v.name, 'beer')
77    self.assertEqual(v.regex, '(boo[(]hoo)')
78
79    # Escaped braces don't count.
80    self.assertRaises(textfsm.TextFSMTemplateError,
81                      v.Parse,
82                      'Value beer (boo\[)\]hoo)')
83
84    # String function.
85    v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
86    v.Parse('Value Required beer (boo(hoo))')
87    self.assertEqual(str(v), 'Value Required beer (boo(hoo))')
88    v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
89    v.Parse(
90        r'Value Required,Filldown beer (bo\S+(hoo))')
91    self.assertEqual(str(v), r'Value Required,Filldown beer (bo\S+(hoo))')
92
93  def testFSMRule(self):
94
95    # Basic line, no action
96    line = '  ^A beer called ${beer}'
97    r = textfsm.TextFSMRule(line)
98    self.assertEqual(r.match, '^A beer called ${beer}')
99    self.assertEqual(r.line_op, '')
100    self.assertEqual(r.new_state, '')
101    self.assertEqual(r.record_op, '')
102    # Multiple matches
103    line = '  ^A $hi called ${beer}'
104    r = textfsm.TextFSMRule(line)
105    self.assertEqual(r.match, '^A $hi called ${beer}')
106    self.assertEqual(r.line_op, '')
107    self.assertEqual(r.new_state, '')
108    self.assertEqual(r.record_op, '')
109
110    # Line with action.
111    line = '  ^A beer called ${beer} -> Next'
112    r = textfsm.TextFSMRule(line)
113    self.assertEqual(r.match, '^A beer called ${beer}')
114    self.assertEqual(r.line_op, 'Next')
115    self.assertEqual(r.new_state, '')
116    self.assertEqual(r.record_op, '')
117
118    # Line with record.
119    line = '  ^A beer called ${beer} -> Continue.Record'
120    r = textfsm.TextFSMRule(line)
121    self.assertEqual(r.match, '^A beer called ${beer}')
122    self.assertEqual(r.line_op, 'Continue')
123    self.assertEqual(r.new_state, '')
124    self.assertEqual(r.record_op, 'Record')
125
126    # Line with new state.
127    line = '  ^A beer called ${beer} -> Next.NoRecord End'
128    r = textfsm.TextFSMRule(line)
129    self.assertEqual(r.match, '^A beer called ${beer}')
130    self.assertEqual(r.line_op, 'Next')
131    self.assertEqual(r.new_state, 'End')
132    self.assertEqual(r.record_op, 'NoRecord')
133
134    # Bad syntax tests.
135    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
136                      '  ^A beer called ${beer} -> Next Next Next')
137    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
138                      '  ^A beer called ${beer} -> Boo.hoo')
139    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
140                      '  ^A beer called ${beer} -> Continue.Record $Hi')
141
142  def testRulePrefixes(self):
143    """Test valid and invalid rule prefixes."""
144
145    # Bad syntax tests.
146    for prefix in (' ', '.^', ' \t', ''):
147      f = StringIO('Value unused (.)\n\nStart\n' + prefix + 'A simple string.')
148      self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
149
150    # Good syntax tests.
151    for prefix in (' ^', '  ^', '\t^'):
152      f = StringIO('Value unused (.)\n\nStart\n' + prefix + 'A simple string.')
153      self.assertIsNotNone(textfsm.TextFSM(f))
154
155  def testImplicitDefaultRules(self):
156
157    for line in ('  ^A beer called ${beer} -> Record End',
158                 '  ^A beer called ${beer} -> End',
159                 '  ^A beer called ${beer} -> Next.NoRecord End',
160                 '  ^A beer called ${beer} -> Clear End',
161                 '  ^A beer called ${beer} -> Error "Hello World"'):
162      r = textfsm.TextFSMRule(line)
163      self.assertEqual(str(r), line)
164
165    for line in ('  ^A beer called ${beer} -> Next "Hello World"',
166                 '  ^A beer called ${beer} -> Record.Next',
167                 '  ^A beer called ${beer} -> Continue End',
168                 '  ^A beer called ${beer} -> Beer End'):
169      self.assertRaises(textfsm.TextFSMTemplateError,
170                        textfsm.TextFSMRule, line)
171
172  def testSpacesAroundAction(self):
173    for line in ('  ^Hello World -> Boo',
174                 '  ^Hello World ->  Boo',
175                 '  ^Hello World ->   Boo'):
176      self.assertEqual(
177          str(textfsm.TextFSMRule(line)), '  ^Hello World -> Boo')
178
179    # A '->' without a leading space is considered part of the matching line.
180    self.assertEqual('  A simple line-> Boo -> Next',
181                     str(textfsm.TextFSMRule('  A simple line-> Boo -> Next')))
182
183  def testParseFSMVariables(self):
184    # Trivial template to initiate object.
185    f = StringIO('Value unused (.)\n\nStart\n')
186    t = textfsm.TextFSM(f)
187
188    # Trivial entry
189    buf = 'Value Filldown Beer (beer)\n\n'
190    f = StringIO(buf)
191    t._ParseFSMVariables(f)
192
193    # Single variable with commented header.
194    buf = '# Headline\nValue Filldown Beer (beer)\n\n'
195    f = StringIO(buf)
196    t._ParseFSMVariables(f)
197    self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
198
199    # Multiple variables.
200    buf = ('# Headline\n'
201           'Value Filldown Beer (beer)\n'
202           'Value Required Spirits (whiskey)\n'
203           'Value Filldown Wine (claret)\n'
204           '\n')
205    t._line_num = 0
206    f = StringIO(buf)
207    t._ParseFSMVariables(f)
208    self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
209    self.assertEqual(
210        str(t._GetValue('Spirits')), 'Value Required Spirits (whiskey)')
211    self.assertEqual(str(t._GetValue('Wine')), 'Value Filldown Wine (claret)')
212
213    # Multiple variables.
214    buf = ('# Headline\n'
215           'Value Filldown Beer (beer)\n'
216           ' # A comment\n'
217           'Value Spirits ()\n'
218           'Value Filldown,Required Wine ((c|C)laret)\n'
219           '\n')
220
221    f = StringIO(buf)
222    t._ParseFSMVariables(f)
223    self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
224    self.assertEqual(
225        str(t._GetValue('Spirits')), 'Value Spirits ()')
226    self.assertEqual(str(t._GetValue('Wine')),
227                     'Value Filldown,Required Wine ((c|C)laret)')
228
229    # Malformed variables.
230    buf = 'Value Beer (beer) beer'
231    f = StringIO(buf)
232    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
233
234    buf = 'Value Filldown, Required Spirits ()'
235    f = StringIO(buf)
236    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
237    buf = 'Value filldown,Required Wine ((c|C)laret)'
238    f = StringIO(buf)
239    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
240
241    # Values that look bad but are OK.
242    buf = ('# Headline\n'
243           'Value Filldown Beer (bee(r), (and) (M)ead$)\n'
244           '# A comment\n'
245           'Value Spirits,and,some ()\n'
246           'Value Filldown,Required Wine ((c|C)laret)\n'
247           '\n')
248    f = StringIO(buf)
249    t._ParseFSMVariables(f)
250    self.assertEqual(str(t._GetValue('Beer')),
251                     'Value Filldown Beer (bee(r), (and) (M)ead$)')
252    self.assertEqual(
253        str(t._GetValue('Spirits,and,some')), 'Value Spirits,and,some ()')
254    self.assertEqual(str(t._GetValue('Wine')),
255                     'Value Filldown,Required Wine ((c|C)laret)')
256
257    # Variable name too long.
258    buf = ('Value Filldown '
259           'nametoolong_nametoolong_nametoolo_nametoolong_nametoolong '
260           '(beer)\n\n')
261    f = StringIO(buf)
262    self.assertRaises(textfsm.TextFSMTemplateError,
263                      t._ParseFSMVariables, f)
264
265  def testParseFSMState(self):
266
267    f = StringIO('Value Beer (.)\nValue Wine (\\w)\n\nStart\n')
268    t = textfsm.TextFSM(f)
269
270    # Fails as we already have 'Start' state.
271    buf = 'Start\n  ^.\n'
272    f = StringIO(buf)
273    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
274
275    # Remove start so we can test new Start state.
276    t.states = {}
277
278    # Single state.
279    buf = '# Headline\nStart\n  ^.\n\n'
280    f = StringIO(buf)
281    t._ParseFSMState(f)
282    self.assertEqual(str(t.states['Start'][0]), '  ^.')
283    try:
284      _ = t.states['Start'][1]
285    except IndexError:
286      pass
287
288    # Multiple states.
289    buf = '# Headline\nStart\n  ^.\n  ^Hello World\n  ^Last-[Cc]ha$$nge\n'
290    f = StringIO(buf)
291    t._line_num = 0
292    t.states = {}
293    t._ParseFSMState(f)
294    self.assertEqual(str(t.states['Start'][0]), '  ^.')
295    self.assertEqual(str(t.states['Start'][1]), '  ^Hello World')
296    self.assertEqual(t.states['Start'][1].line_num, 4)
297    self.assertEqual(str(t.states['Start'][2]), '  ^Last-[Cc]ha$$nge')
298    try:
299      _ = t.states['Start'][3]
300    except IndexError:
301      pass
302
303    t.states = {}
304    # Malformed states.
305    buf = 'St%art\n  ^.\n  ^Hello World\n'
306    f = StringIO(buf)
307    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
308
309    buf = 'Start\n^.\n  ^Hello World\n'
310    f = StringIO(buf)
311    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
312
313    buf = '  Start\n  ^.\n  ^Hello World\n'
314    f = StringIO(buf)
315    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
316
317    # Multiple variables and substitution (depends on _ParseFSMVariables).
318    buf = ('# Headline\nStart\n  ^.${Beer}${Wine}.\n'
319           '  ^Hello $Beer\n  ^Last-[Cc]ha$$nge\n')
320    f = StringIO(buf)
321    t.states = {}
322    t._ParseFSMState(f)
323    self.assertEqual(str(t.states['Start'][0]), '  ^.${Beer}${Wine}.')
324    self.assertEqual(str(t.states['Start'][1]), '  ^Hello $Beer')
325    self.assertEqual(str(t.states['Start'][2]), '  ^Last-[Cc]ha$$nge')
326    try:
327      _ = t.states['Start'][3]
328    except IndexError:
329      pass
330
331    t.states['bogus'] = []
332
333    # State name too long (>32 char).
334    buf = 'rnametoolong_nametoolong_nametoolong_nametoolong_nametoolo\n  ^.\n\n'
335    f = StringIO(buf)
336    self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
337
338  def testInvalidStates(self):
339
340    # 'Continue' should not accept a destination.
341    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
342                      '^.* -> Continue Start')
343
344    # 'Error' accepts a text string but "next' state does not.
345    self.assertEqual(str(textfsm.TextFSMRule('  ^ -> Error "hi there"')),
346                     '  ^ -> Error "hi there"')
347    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
348                      '^.* -> Next "Hello World"')
349
350  def testRuleStartsWithCarrot(self):
351
352    f = StringIO(
353        'Value Beer (.)\nValue Wine (\\w)\n\nStart\n  A Simple line')
354    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
355
356  def testValidateFSM(self):
357
358    # No Values.
359    f = StringIO('\nNotStart\n')
360    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
361
362    # No states.
363    f = StringIO('Value unused (.)\n\n')
364    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
365
366    # No 'Start' state.
367    f = StringIO('Value unused (.)\n\nNotStart\n')
368    self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
369
370    # Has 'Start' state with valid destination
371    f = StringIO('Value unused (.)\n\nStart\n')
372    t = textfsm.TextFSM(f)
373    t.states['Start'] = []
374    t.states['Start'].append(textfsm.TextFSMRule('^.* -> Start'))
375    t._ValidateFSM()
376
377    # Invalid destination.
378    t.states['Start'].append(textfsm.TextFSMRule('^.* -> bogus'))
379    self.assertRaises(textfsm.TextFSMTemplateError, t._ValidateFSM)
380
381    # Now valid again.
382    t.states['bogus'] = []
383    t.states['bogus'].append(textfsm.TextFSMRule('^.* -> Start'))
384    t._ValidateFSM()
385
386    # Valid destination with options.
387    t.states['bogus'] = []
388    t.states['bogus'].append(textfsm.TextFSMRule('^.* -> Next.Record Start'))
389    t._ValidateFSM()
390
391    # Error with and without messages string.
392    t.states['bogus'] = []
393    t.states['bogus'].append(textfsm.TextFSMRule('^.* -> Error'))
394    t._ValidateFSM()
395    t.states['bogus'].append(textfsm.TextFSMRule('^.* -> Error "Boo hoo"'))
396    t._ValidateFSM()
397
398  def testTextFSM(self):
399
400    # Trivial template
401    buf = 'Value Beer (.*)\n\nStart\n  ^\\w\n'
402    buf_result = buf
403    f = StringIO(buf)
404    t = textfsm.TextFSM(f)
405    self.assertEqual(str(t), buf_result)
406
407    # Slightly more complex, multple vars.
408    buf = 'Value A (.*)\nValue B (.*)\n\nStart\n  ^\\w\n\nState1\n  ^.\n'
409    buf_result = buf
410    f = StringIO(buf)
411    t = textfsm.TextFSM(f)
412    self.assertEqual(str(t), buf_result)
413
414  def testParseText(self):
415
416    # Trivial FSM, no records produced.
417    tplt = 'Value unused (.)\n\nStart\n  ^Trivial SFM\n'
418    t = textfsm.TextFSM(StringIO(tplt))
419
420    data = 'Non-matching text\nline1\nline 2\n'
421    self.assertFalse(t.ParseText(data))
422    # Matching.
423    data = 'Matching text\nTrivial SFM\nline 2\n'
424    self.assertFalse(t.ParseText(data))
425
426    # Simple FSM, One Variable no options.
427    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next.Record\n\nEOF\n'
428    t = textfsm.TextFSM(StringIO(tplt))
429
430    # Matching one line.
431    # Tests 'Next' & 'Record' actions.
432    data = 'Matching text'
433    result = t.ParseText(data)
434    self.assertListEqual(result, [['Matching text']])
435
436    # Matching two lines. Reseting FSM before Parsing.
437    t.Reset()
438    data = 'Matching text\nAnd again'
439    result = t.ParseText(data)
440    self.assertListEqual(result, [['Matching text'], ['And again']])
441
442    # Two Variables and singular options.
443    tplt = ('Value Required boo (one)\nValue Filldown hoo (two)\n\n'
444            'Start\n  ^$boo -> Next.Record\n  ^$hoo -> Next.Record\n\n'
445            'EOF\n')
446    t = textfsm.TextFSM(StringIO(tplt))
447
448    # Matching two lines. Only one records returned due to 'Required' flag.
449    # Tests 'Filldown' and 'Required' options.
450    data = 'two\none'
451    result = t.ParseText(data)
452    self.assertListEqual(result, [['one', 'two']])
453
454    t = textfsm.TextFSM(StringIO(tplt))
455    # Matching two lines. Two records returned due to 'Filldown' flag.
456    data = 'two\none\none'
457    t.Reset()
458    result = t.ParseText(data)
459    self.assertListEqual(result, [['one', 'two'], ['one', 'two']])
460
461    # Multiple Variables and options.
462    tplt = ('Value Required,Filldown boo (one)\n'
463            'Value Filldown,Required hoo (two)\n\n'
464            'Start\n  ^$boo -> Next.Record\n  ^$hoo -> Next.Record\n\n'
465            'EOF\n')
466    t = textfsm.TextFSM(StringIO(tplt))
467    data = 'two\none\none'
468    result = t.ParseText(data)
469    self.assertListEqual(result, [['one', 'two'], ['one', 'two']])
470
471  def testParseTextToDicts(self):
472
473    # Trivial FSM, no records produced.
474    tplt = 'Value unused (.)\n\nStart\n  ^Trivial SFM\n'
475    t = textfsm.TextFSM(StringIO(tplt))
476
477    data = 'Non-matching text\nline1\nline 2\n'
478    self.assertFalse(t.ParseText(data))
479    # Matching.
480    data = 'Matching text\nTrivial SFM\nline 2\n'
481    self.assertFalse(t.ParseText(data))
482
483    # Simple FSM, One Variable no options.
484    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next.Record\n\nEOF\n'
485    t = textfsm.TextFSM(StringIO(tplt))
486
487    # Matching one line.
488    # Tests 'Next' & 'Record' actions.
489    data = 'Matching text'
490    result = t.ParseTextToDicts(data)
491    self.assertListEqual(result, [{'boo': 'Matching text'}])
492
493    # Matching two lines. Reseting FSM before Parsing.
494    t.Reset()
495    data = 'Matching text\nAnd again'
496    result = t.ParseTextToDicts(data)
497    self.assertListEqual(result,
498                     [{'boo': 'Matching text'}, {'boo': 'And again'}])
499
500    # Two Variables and singular options.
501    tplt = ('Value Required boo (one)\nValue Filldown hoo (two)\n\n'
502            'Start\n  ^$boo -> Next.Record\n  ^$hoo -> Next.Record\n\n'
503            'EOF\n')
504    t = textfsm.TextFSM(StringIO(tplt))
505
506    # Matching two lines. Only one records returned due to 'Required' flag.
507    # Tests 'Filldown' and 'Required' options.
508    data = 'two\none'
509    result = t.ParseTextToDicts(data)
510    self.assertListEqual(result, [{'hoo': 'two', 'boo': 'one'}])
511
512    t = textfsm.TextFSM(StringIO(tplt))
513    # Matching two lines. Two records returned due to 'Filldown' flag.
514    data = 'two\none\none'
515    t.Reset()
516    result = t.ParseTextToDicts(data)
517    self.assertListEqual(
518        result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}])
519
520    # Multiple Variables and options.
521    tplt = ('Value Required,Filldown boo (one)\n'
522            'Value Filldown,Required hoo (two)\n\n'
523            'Start\n  ^$boo -> Next.Record\n  ^$hoo -> Next.Record\n\n'
524            'EOF\n')
525    t = textfsm.TextFSM(StringIO(tplt))
526    data = 'two\none\none'
527    result = t.ParseTextToDicts(data)
528    self.assertListEqual(
529        result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}])
530
531  def testParseNullText(self):
532
533    # Simple FSM, One Variable no options.
534    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next.Record\n\n'
535    t = textfsm.TextFSM(StringIO(tplt))
536
537    # Null string
538    data = ''
539    result = t.ParseText(data)
540    self.assertListEqual(result, [])
541
542  def testReset(self):
543
544    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next.Record\n\nEOF\n'
545    t = textfsm.TextFSM(StringIO(tplt))
546    data = 'Matching text'
547    result1 = t.ParseText(data)
548    t.Reset()
549    result2 = t.ParseText(data)
550    self.assertListEqual(result1, result2)
551
552    tplt = ('Value boo (one)\nValue hoo (two)\n\n'
553            'Start\n  ^$boo -> State1\n\n'
554            'State1\n  ^$hoo -> Start\n\n'
555            'EOF')
556    t = textfsm.TextFSM(StringIO(tplt))
557
558    data = 'one'
559    t.ParseText(data)
560    t.Reset()
561    self.assertEqual(t._cur_state[0].match, '^$boo')
562    self.assertEqual(t._GetValue('boo').value, None)
563    self.assertEqual(t._GetValue('hoo').value, None)
564    self.assertEqual(t._result, [])
565
566  def testClear(self):
567
568    # Clear Filldown variable.
569    # Tests 'Clear'.
570    tplt = ('Value Required boo (on.)\n'
571            'Value Filldown,Required hoo (tw.)\n\n'
572            'Start\n  ^$boo -> Next.Record\n  ^$hoo -> Next.Clear')
573
574    t = textfsm.TextFSM(StringIO(tplt))
575    data = 'one\ntwo\nonE\ntwO'
576    result = t.ParseText(data)
577    self.assertListEqual(result, [['onE', 'two']])
578
579    # Clearall, with Filldown variable.
580    # Tests 'Clearall'.
581    tplt = ('Value Filldown boo (on.)\n'
582            'Value Filldown hoo (tw.)\n\n'
583            'Start\n  ^$boo -> Next.Clearall\n'
584            '  ^$hoo')
585
586    t = textfsm.TextFSM(StringIO(tplt))
587    data = 'one\ntwo'
588    result = t.ParseText(data)
589    self.assertListEqual(result, [['', 'two']])
590
591  def testContinue(self):
592
593    tplt = ('Value Required boo (on.)\n'
594            'Value Filldown,Required hoo (on.)\n\n'
595            'Start\n  ^$boo -> Continue\n  ^$hoo -> Continue.Record')
596
597    t = textfsm.TextFSM(StringIO(tplt))
598    data = 'one\non0'
599    result = t.ParseText(data)
600    self.assertListEqual(result, [['one', 'one'], ['on0', 'on0']])
601
602  def testError(self):
603
604    tplt = ('Value Required boo (on.)\n'
605            'Value Filldown,Required hoo (on.)\n\n'
606            'Start\n  ^$boo -> Continue\n  ^$hoo -> Error')
607
608    t = textfsm.TextFSM(StringIO(tplt))
609    data = 'one'
610    self.assertRaises(textfsm.TextFSMError, t.ParseText, data)
611
612    tplt = ('Value Required boo (on.)\n'
613            'Value Filldown,Required hoo (on.)\n\n'
614            'Start\n  ^$boo -> Continue\n  ^$hoo -> Error "Hello World"')
615
616    t = textfsm.TextFSM(StringIO(tplt))
617    self.assertRaises(textfsm.TextFSMError, t.ParseText, data)
618
619  def testKey(self):
620    tplt = ('Value Required boo (on.)\n'
621            'Value Required,Key hoo (on.)\n\n'
622            'Start\n  ^$boo -> Continue\n  ^$hoo -> Record')
623
624    t = textfsm.TextFSM(StringIO(tplt))
625    self.assertTrue('Key' in t._GetValue('hoo').OptionNames())
626    self.assertTrue('Key' not in t._GetValue('boo').OptionNames())
627
628  def testList(self):
629
630    tplt = ('Value List boo (on.)\n'
631            'Value hoo (tw.)\n\n'
632            'Start\n  ^$boo\n  ^$hoo -> Next.Record\n\n'
633            'EOF')
634
635    t = textfsm.TextFSM(StringIO(tplt))
636    data = 'one\ntwo\non0\ntw0'
637    result = t.ParseText(data)
638    self.assertListEqual(result, [[['one'], 'two'], [['on0'], 'tw0']])
639
640    tplt = ('Value List,Filldown boo (on.)\n'
641            'Value hoo (on.)\n\n'
642            'Start\n  ^$boo -> Continue\n  ^$hoo -> Next.Record\n\n'
643            'EOF')
644
645    t = textfsm.TextFSM(StringIO(tplt))
646    data = 'one\non0\non1'
647    result = t.ParseText(data)
648    self.assertEqual(result, ([[['one'], 'one'],
649                               [['one', 'on0'], 'on0'],
650                               [['one', 'on0', 'on1'], 'on1']]))
651
652    tplt = ('Value List,Required boo (on.)\n'
653            'Value hoo (tw.)\n\n'
654            'Start\n  ^$boo -> Continue\n  ^$hoo -> Next.Record\n\n'
655            'EOF')
656
657    t = textfsm.TextFSM(StringIO(tplt))
658    data = 'one\ntwo\ntw2'
659    result = t.ParseText(data)
660    self.assertListEqual(result, [[['one'], 'two']])
661
662
663  def testNestedMatching(self):
664      """
665      Ensures that List-type values with nested regex capture groups are parsed
666      correctly as a list of dictionaries.
667
668      Additionaly, another value is used with the same group-name as one of the
669      nested groups to ensure that there are no conflicts when the same name is
670      used.
671      """
672      tplt = (
673          # A nested group is called "name"
674          r"Value List foo ((?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\w{2})\s*)"
675          "\n"
676          # A regular value is called "name"
677          r"Value name (\w+)"
678          # "${name}" here refers to the Value called "name"
679          "\n\nStart\n"
680          r"  ^\s*${foo}"
681          "\n"
682          r"  ^\s*${name}"
683          "\n"
684          r"  ^\s*$$ -> Record"
685      )
686      t = textfsm.TextFSM(StringIO(tplt))
687      # Julia should be parsed as "name" separately
688      data = " Bob: 32 NC\n Alice: 27 NY\n Jeff: 45 CA\nJulia\n\n"
689      result = t.ParseText(data)
690      self.assertListEqual(
691          result, (
692              [[[
693                  {'name': 'Bob', 'age': '32', 'state': 'NC'},
694                  {'name': 'Alice', 'age': '27', 'state': 'NY'},
695                  {'name': 'Jeff', 'age': '45', 'state': 'CA'}
696              ], 'Julia']]
697          )
698      )
699
700  def testNestedNameConflict(self):
701      tplt = (
702          # Two nested groups are called "name"
703          r"Value List foo ((?P<name>\w+)\s+(?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\w{2})\s*)"
704          "\nStart\n"
705          r"^\s*${foo}"
706          "\n  ^"
707          r"\s*$$ -> Record"
708      )
709      self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, StringIO(tplt))
710
711
712  def testGetValuesByAttrib(self):
713
714    tplt = ('Value Required boo (on.)\n'
715            'Value Required,List hoo (on.)\n\n'
716            'Start\n  ^$boo -> Continue\n  ^$hoo -> Record')
717
718    # Explicit default.
719    t = textfsm.TextFSM(StringIO(tplt))
720    self.assertEqual(t.GetValuesByAttrib('List'), ['hoo'])
721    self.assertEqual(t.GetValuesByAttrib('Filldown'), [])
722    result = t.GetValuesByAttrib('Required')
723    result.sort()
724    self.assertListEqual(result, ['boo', 'hoo'])
725
726  def testStateChange(self):
727
728    # Sinple state change, no actions
729    tplt = ('Value boo (one)\nValue hoo (two)\n\n'
730            'Start\n  ^$boo -> State1\n\nState1\n  ^$hoo -> Start\n\n'
731            'EOF')
732    t = textfsm.TextFSM(StringIO(tplt))
733
734    data = 'one'
735    t.ParseText(data)
736    self.assertEqual(t._cur_state[0].match, '^$hoo')
737    self.assertEqual('one', t._GetValue('boo').value)
738    self.assertEqual(None, t._GetValue('hoo').value)
739    self.assertEqual(t._result, [])
740
741    # State change with actions.
742    tplt = ('Value boo (one)\nValue hoo (two)\n\n'
743            'Start\n  ^$boo -> Next.Record State1\n\n'
744            'State1\n  ^$hoo -> Start\n\n'
745            'EOF')
746    t = textfsm.TextFSM(StringIO(tplt))
747
748    data = 'one'
749    t.ParseText(data)
750    self.assertEqual(t._cur_state[0].match, '^$hoo')
751    self.assertEqual(None, t._GetValue('boo').value)
752    self.assertEqual(None, t._GetValue('hoo').value)
753    self.assertEqual(t._result, [['one', '']])
754
755  def testEOF(self):
756
757    # Implicit EOF.
758    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next\n'
759    t = textfsm.TextFSM(StringIO(tplt))
760
761    data = 'Matching text'
762    result = t.ParseText(data)
763    self.assertListEqual(result, [['Matching text']])
764
765    # EOF explicitly suppressed in template.
766    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next\n\nEOF\n'
767    t = textfsm.TextFSM(StringIO(tplt))
768
769    result = t.ParseText(data)
770    self.assertListEqual(result, [])
771
772    # Implicit EOF suppressed by argument.
773    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next\n'
774    t = textfsm.TextFSM(StringIO(tplt))
775
776    result = t.ParseText(data, eof=False)
777    self.assertListEqual(result, [])
778
779  def testEnd(self):
780
781    # End State, EOF is skipped.
782    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> End\n  ^$boo -> Record\n'
783    t = textfsm.TextFSM(StringIO(tplt))
784    data = 'Matching text A\nMatching text B'
785
786    result = t.ParseText(data)
787    self.assertListEqual(result, [])
788
789    # End State, with explicit Record.
790    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Record End\n'
791    t = textfsm.TextFSM(StringIO(tplt))
792
793    result = t.ParseText(data)
794    self.assertListEqual(result, [['Matching text A']])
795
796    # EOF state transition is followed by implicit End State.
797    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> EOF\n  ^$boo -> Record\n'
798    t = textfsm.TextFSM(StringIO(tplt))
799
800    result = t.ParseText(data)
801    self.assertListEqual(result, [['Matching text A']])
802
803  def testInvalidRegexp(self):
804
805    tplt = 'Value boo (.$*)\n\nStart\n  ^$boo -> Next\n'
806    self.assertRaises(textfsm.TextFSMTemplateError,
807                      textfsm.TextFSM, StringIO(tplt))
808
809  def testValidRegexp(self):
810    """RegexObjects uncopyable in Python 2.6."""
811
812    tplt = 'Value boo (fo*)\n\nStart\n  ^$boo -> Record\n'
813    t = textfsm.TextFSM(StringIO(tplt))
814    data = 'f\nfo\nfoo\n'
815    result = t.ParseText(data)
816    self.assertListEqual(result, [['f'], ['fo'], ['foo']])
817
818  def testReEnteringState(self):
819    """Issue 2. TextFSM should leave file pointer at top of template file."""
820
821    tplt = 'Value boo (.*)\n\nStart\n  ^$boo -> Next Stop\n\nStop\n  ^abc\n'
822    output_text = 'one\ntwo'
823    tmpl_file = StringIO(tplt)
824
825    t = textfsm.TextFSM(tmpl_file)
826    t.ParseText(output_text)
827    t = textfsm.TextFSM(tmpl_file)
828    t.ParseText(output_text)
829
830  def testFillup(self):
831    """Fillup should work ok."""
832    tplt = """Value Required Col1 ([^-]+)
833Value Fillup Col2 ([^-]+)
834Value Fillup Col3 ([^-]+)
835
836Start
837  ^$Col1 -- -- -> Record
838  ^$Col1 $Col2 -- -> Record
839  ^$Col1 -- $Col3 -> Record
840  ^$Col1 $Col2 $Col3 -> Record
841"""
842    data = """
8431 -- B1
8442 A2 --
8453 -- B3
846"""
847    t = textfsm.TextFSM(StringIO(tplt))
848    result = t.ParseText(data)
849    self.assertListEqual(
850        result, [['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']])
851
852
853class UnitTestUnicode(unittest.TestCase):
854  """Tests the FSM engine."""
855
856  def testFSMValue(self):
857    # Check basic line is parsed.
858    line = 'Value beer (\\S+Δ)'
859    v = textfsm.TextFSMValue()
860    v.Parse(line)
861    self.assertEqual(v.name, 'beer')
862    self.assertEqual(v.regex, '(\\S+Δ)')
863    self.assertEqual(v.template, '(?P<beer>\\S+Δ)')
864    self.assertFalse(v.options)
865
866  def testFSMRule(self):
867    # Basic line, no action
868    line = '  ^A beer called ${beer}Δ'
869    r = textfsm.TextFSMRule(line)
870    self.assertEqual(r.match, '^A beer called ${beer}Δ')
871    self.assertEqual(r.line_op, '')
872    self.assertEqual(r.new_state, '')
873    self.assertEqual(r.record_op, '')
874
875  def testTemplateValue(self):
876    # Complex template, multiple vars and states with comments (no var options).
877    buf = """# Header
878# Header 2
879Value Beer (.*)
880Value Wine (\\w+)
881
882# An explanation with a unicode character Δ
883Start
884  ^hi there ${Wine}. -> Next.Record State1
885
886State1
887  ^\\wΔ
888  ^$Beer .. -> Start
889  # Some comments
890  ^$$ -> Next
891  ^$$ -> End
892
893End
894# Tail comment.
895"""
896
897    buf_result = """Value Beer (.*)
898Value Wine (\\w+)
899
900Start
901  ^hi there ${Wine}. -> Next.Record State1
902
903State1
904  ^\\wΔ
905  ^$Beer .. -> Start
906  ^$$ -> Next
907  ^$$ -> End
908"""
909    f = StringIO(buf)
910    t = textfsm.TextFSM(f)
911    self.assertEqual(str(t), buf_result)
912
913
914if __name__ == '__main__':
915  unittest.main()
916