1#  Copyright 2008-2015 Nokia Networks
2#  Copyright 2016-     Robot Framework Foundation
3#
4#  Licensed under the Apache License, Version 2.0 (the "License");
5#  you may not use this file except in compliance with the License.
6#  You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10#  Unless required by applicable law or agreed to in writing, software
11#  distributed under the License is distributed on an "AS IS" BASIS,
12#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13#  See the License for the specific language governing permissions and
14#  limitations under the License.
15
16import os
17import copy
18import warnings
19
20from robot.errors import DataError
21from robot.variables import is_var
22from robot.output import LOGGER
23from robot.writer import DataFileWriter
24from robot.utils import abspath, is_string, normalize, py2to3, NormalizedDict
25
26from .comments import Comment
27from .populators import FromFilePopulator, FromDirectoryPopulator, NoTestsFound
28from .settings import (Documentation, Fixture, Timeout, Tags, Metadata,
29                       Library, Resource, Variables, Arguments, Return,
30                       Template, MetadataList, ImportList)
31
32
33def TestData(parent=None, source=None, include_suites=None,
34             warn_on_skipped='DEPRECATED', extensions=None):
35    """Parses a file or directory to a corresponding model object.
36
37    :param parent: Optional parent to be used in creation of the model object.
38    :param source: Path where test data is read from.
39    :param warn_on_skipped: Deprecated.
40    :param extensions: List/set of extensions to parse. If None, all files
41        supported by Robot Framework are parsed when searching test cases.
42    :returns: :class:`~.model.TestDataDirectory`  if `source` is a directory,
43        :class:`~.model.TestCaseFile` otherwise.
44    """
45    # TODO: Remove in RF 3.2.
46    if warn_on_skipped != 'DEPRECATED':
47        warnings.warn("Option 'warn_on_skipped' is deprecated and has no "
48                      "effect.", DeprecationWarning)
49    if os.path.isdir(source):
50        return TestDataDirectory(parent, source).populate(include_suites,
51                                                          extensions)
52    return TestCaseFile(parent, source).populate()
53
54
55class _TestData(object):
56    _setting_table_names = 'Setting', 'Settings'
57    _variable_table_names = 'Variable', 'Variables'
58    _testcase_table_names = 'Test Case', 'Test Cases', 'Task', 'Tasks'
59    _keyword_table_names = 'Keyword', 'Keywords'
60    _comment_table_names = 'Comment', 'Comments'
61
62    def __init__(self, parent=None, source=None):
63        self.parent = parent
64        self.source = abspath(source) if source else None
65        self.children = []
66        self._tables = dict(self._get_tables())
67
68    def _get_tables(self):
69        for names, table in [(self._setting_table_names, self.setting_table),
70                             (self._variable_table_names, self.variable_table),
71                             (self._testcase_table_names, self.testcase_table),
72                             (self._keyword_table_names, self.keyword_table),
73                             (self._comment_table_names, None)]:
74            for name in names:
75                yield name, table
76
77    def start_table(self, header_row):
78        table = self._find_table(header_row)
79        if table is None or not self._table_is_allowed(table):
80            return None
81        table.set_header(header_row)
82        return table
83
84    def _find_table(self, header_row):
85        name = header_row[0] if header_row else ''
86        title = name.title()
87        if title not in self._tables:
88            title = self._resolve_deprecated_table(name)
89            if title is None:
90                self._report_unrecognized_table(name)
91                return None
92        return self._tables[title]
93
94    def _resolve_deprecated_table(self, used_name):
95        normalized = normalize(used_name)
96        for name in (self._setting_table_names + self._variable_table_names +
97                     self._testcase_table_names + self._keyword_table_names +
98                     self._comment_table_names):
99            if normalize(name) == normalized:
100                self._report_deprecated_table(used_name, name)
101                return name
102        return None
103
104    def _report_deprecated_table(self, deprecated, name):
105        self.report_invalid_syntax(
106            "Section name '%s' is deprecated. Use '%s' instead."
107            % (deprecated, name), level='WARN'
108        )
109
110    def _report_unrecognized_table(self, name):
111        self.report_invalid_syntax(
112            "Unrecognized table header '%s'. Available headers for data: "
113            "'Setting(s)', 'Variable(s)', 'Test Case(s)', 'Task(s)' and "
114            "'Keyword(s)'. Use 'Comment(s)' to embedded additional data."
115            % name
116        )
117
118    def _table_is_allowed(self, table):
119        return True
120
121    @property
122    def name(self):
123        return self._format_name(self._get_basename()) if self.source else None
124
125    def _get_basename(self):
126        return os.path.splitext(os.path.basename(self.source))[0]
127
128    def _format_name(self, name):
129        name = self._strip_possible_prefix_from_name(name)
130        name = name.replace('_', ' ').strip()
131        return name.title() if name.islower() else name
132
133    def _strip_possible_prefix_from_name(self, name):
134        return name.split('__', 1)[-1]
135
136    @property
137    def keywords(self):
138        return self.keyword_table.keywords
139
140    @property
141    def imports(self):
142        return self.setting_table.imports
143
144    def report_invalid_syntax(self, message, level='ERROR'):
145        initfile = getattr(self, 'initfile', None)
146        path = os.path.join(self.source, initfile) if initfile else self.source
147        LOGGER.write("Error in file '%s': %s" % (path, message), level)
148
149    def save(self, **options):
150        """Writes this datafile to disk.
151
152        :param options: Configuration for writing. These are passed to
153            :py:class:`~robot.writer.datafilewriter.WritingContext` as
154            keyword arguments.
155
156        See also :py:class:`robot.writer.datafilewriter.DataFileWriter`
157        """
158        return DataFileWriter(**options).write(self)
159
160
161@py2to3
162class TestCaseFile(_TestData):
163    """The parsed test case file object.
164
165    :param parent: parent object to be used in creation of the model object.
166    :param source: path where test data is read from.
167    """
168
169    def __init__(self, parent=None, source=None):
170        self.directory = os.path.dirname(source) if source else None
171        self.setting_table = TestCaseFileSettingTable(self)
172        self.variable_table = VariableTable(self)
173        self.testcase_table = TestCaseTable(self)
174        self.keyword_table = KeywordTable(self)
175        _TestData.__init__(self, parent, source)
176
177    def populate(self):
178        FromFilePopulator(self).populate(self.source)
179        self._validate()
180        return self
181
182    def _validate(self):
183        if not self.testcase_table.is_started():
184            raise NoTestsFound('File has no tests or tasks.')
185
186    def has_tests(self):
187        return True
188
189    def __iter__(self):
190        for table in [self.setting_table, self.variable_table,
191                      self.testcase_table, self.keyword_table]:
192            yield table
193
194    def __nonzero__(self):
195        return any(table for table in self)
196
197
198class ResourceFile(_TestData):
199    """The parsed resource file object.
200
201    :param source: path where resource file is read from.
202    """
203
204    def __init__(self, source=None):
205        self.directory = os.path.dirname(source) if source else None
206        self.setting_table = ResourceFileSettingTable(self)
207        self.variable_table = VariableTable(self)
208        self.testcase_table = TestCaseTable(self)
209        self.keyword_table = KeywordTable(self)
210        _TestData.__init__(self, source=source)
211
212    def populate(self):
213        FromFilePopulator(self).populate(self.source, resource=True)
214        self._report_status()
215        return self
216
217    def _report_status(self):
218        if self.setting_table or self.variable_table or self.keyword_table:
219            LOGGER.info("Imported resource file '%s' (%d keywords)."
220                        % (self.source, len(self.keyword_table.keywords)))
221        else:
222            LOGGER.warn("Imported resource file '%s' is empty." % self.source)
223
224    def _table_is_allowed(self, table):
225        if table is self.testcase_table:
226            raise DataError("Resource file '%s' cannot contain tests or "
227                            "tasks." % self.source)
228        return True
229
230    def __iter__(self):
231        for table in [self.setting_table, self.variable_table, self.keyword_table]:
232            yield table
233
234
235class TestDataDirectory(_TestData):
236    """The parsed test data directory object. Contains hiearchical structure
237    of other :py:class:`.TestDataDirectory` and :py:class:`.TestCaseFile`
238    objects.
239
240    :param parent: parent object to be used in creation of the model object.
241    :param source: path where test data is read from.
242    """
243
244    def __init__(self, parent=None, source=None):
245        self.directory = source
246        self.initfile = None
247        self.setting_table = InitFileSettingTable(self)
248        self.variable_table = VariableTable(self)
249        self.testcase_table = TestCaseTable(self)
250        self.keyword_table = KeywordTable(self)
251        _TestData.__init__(self, parent, source)
252
253    def populate(self, include_suites=None, extensions=None, recurse=True):
254        FromDirectoryPopulator().populate(self.source, self, include_suites,
255                                          extensions, recurse)
256        self.children = [ch for ch in self.children if ch.has_tests()]
257        return self
258
259    def _get_basename(self):
260        return os.path.basename(self.source)
261
262    def _table_is_allowed(self, table):
263        if table is self.testcase_table:
264            LOGGER.error("Test suite initialization file in '%s' cannot "
265                         "contain tests or tasks." % self.source)
266            return False
267        return True
268
269    def add_child(self, path, include_suites, extensions=None):
270        self.children.append(TestData(parent=self,
271                                      source=path,
272                                      include_suites=include_suites,
273                                      extensions=extensions))
274
275    def has_tests(self):
276        return any(ch.has_tests() for ch in self.children)
277
278    def __iter__(self):
279        for table in [self.setting_table, self.variable_table, self.keyword_table]:
280            yield table
281
282
283@py2to3
284class _Table(object):
285
286    def __init__(self, parent):
287        self.parent = parent
288        self._header = None
289
290    def set_header(self, header):
291        self._header = self._prune_old_style_headers(header)
292
293    def _prune_old_style_headers(self, header):
294        if len(header) < 3:
295            return header
296        if self._old_header_matcher.match(header):
297            return [header[0]]
298        return header
299
300    @property
301    def header(self):
302        return self._header or [self.type.title() + 's']
303
304    @property
305    def name(self):
306        return self.header[0]
307
308    @property
309    def source(self):
310        return self.parent.source
311
312    @property
313    def directory(self):
314        return self.parent.directory
315
316    def report_invalid_syntax(self, message, level='ERROR'):
317        self.parent.report_invalid_syntax(message, level)
318
319    def __nonzero__(self):
320        return bool(self._header or len(self))
321
322    def __len__(self):
323        return sum(1 for item in self)
324
325
326class _WithSettings(object):
327    _setters = {}
328    _aliases = {}
329
330    def get_setter(self, name):
331        if name[-1:] == ':':
332            name = name[:-1]
333        setter = self._get_setter(name)
334        if setter is not None:
335            return setter
336        setter = self._get_deprecated_setter(name)
337        if setter is not None:
338            return setter
339        self.report_invalid_syntax("Non-existing setting '%s'." % name)
340        return None
341
342    def _get_setter(self, name):
343        title = name.title()
344        if title in self._aliases:
345            title = self._aliases[name]
346        if title in self._setters:
347            return self._setters[title](self)
348        return None
349
350    def _get_deprecated_setter(self, name):
351        normalized = normalize(name)
352        for setting in list(self._setters) + list(self._aliases):
353            if normalize(setting) == normalized:
354                self._report_deprecated_setting(name, setting)
355                return self._get_setter(setting)
356        return None
357
358    def _report_deprecated_setting(self, deprecated, correct):
359        self.report_invalid_syntax(
360            "Setting '%s' is deprecated. Use '%s' instead."
361            % (deprecated, correct), level='WARN'
362        )
363
364    def report_invalid_syntax(self, message, level='ERROR'):
365        raise NotImplementedError
366
367
368class _SettingTable(_Table, _WithSettings):
369    type = 'setting'
370
371    def __init__(self, parent):
372        _Table.__init__(self, parent)
373        self.doc = Documentation('Documentation', self)
374        self.suite_setup = Fixture('Suite Setup', self)
375        self.suite_teardown = Fixture('Suite Teardown', self)
376        self.test_setup = Fixture('Test Setup', self)
377        self.test_teardown = Fixture('Test Teardown', self)
378        self.force_tags = Tags('Force Tags', self)
379        self.default_tags = Tags('Default Tags', self)
380        self.test_template = Template('Test Template', self)
381        self.test_timeout = Timeout('Test Timeout', self)
382        self.metadata = MetadataList(self)
383        self.imports = ImportList(self)
384
385    @property
386    def _old_header_matcher(self):
387        return OldStyleSettingAndVariableTableHeaderMatcher()
388
389    def add_metadata(self, name, value='', comment=None):
390        self.metadata.add(Metadata(self, name, value, comment))
391        return self.metadata[-1]
392
393    def add_library(self, name, args=None, comment=None):
394        self.imports.add(Library(self, name, args, comment=comment))
395        return self.imports[-1]
396
397    def add_resource(self, name, invalid_args=None, comment=None):
398        self.imports.add(Resource(self, name, invalid_args, comment=comment))
399        return self.imports[-1]
400
401    def add_variables(self, name, args=None, comment=None):
402        self.imports.add(Variables(self, name, args, comment=comment))
403        return self.imports[-1]
404
405    def __len__(self):
406        return sum(1 for setting in self if setting.is_set())
407
408
409class TestCaseFileSettingTable(_SettingTable):
410    _setters = {'Documentation': lambda s: s.doc.populate,
411                'Suite Setup': lambda s: s.suite_setup.populate,
412                'Suite Teardown': lambda s: s.suite_teardown.populate,
413                'Test Setup': lambda s: s.test_setup.populate,
414                'Test Teardown': lambda s: s.test_teardown.populate,
415                'Force Tags': lambda s: s.force_tags.populate,
416                'Default Tags': lambda s: s.default_tags.populate,
417                'Test Template': lambda s: s.test_template.populate,
418                'Test Timeout': lambda s: s.test_timeout.populate,
419                'Library': lambda s: s.imports.populate_library,
420                'Resource': lambda s: s.imports.populate_resource,
421                'Variables': lambda s: s.imports.populate_variables,
422                'Metadata': lambda s: s.metadata.populate}
423    _aliases = {'Task Setup': 'Test Setup',
424                'Task Teardown': 'Test Teardown',
425                'Task Template': 'Test Template',
426                'Task Timeout': 'Test Timeout'}
427
428    def __iter__(self):
429        for setting in [self.doc, self.suite_setup, self.suite_teardown,
430                        self.test_setup, self.test_teardown, self.force_tags,
431                        self.default_tags, self.test_template, self.test_timeout] \
432                        + self.metadata.data + self.imports.data:
433            yield setting
434
435
436class ResourceFileSettingTable(_SettingTable):
437    _setters = {'Documentation': lambda s: s.doc.populate,
438                'Library': lambda s: s.imports.populate_library,
439                'Resource': lambda s: s.imports.populate_resource,
440                'Variables': lambda s: s.imports.populate_variables}
441
442    def __iter__(self):
443        for setting in [self.doc] + self.imports.data:
444            yield setting
445
446
447class InitFileSettingTable(_SettingTable):
448    _setters = {'Documentation': lambda s: s.doc.populate,
449                'Suite Setup': lambda s: s.suite_setup.populate,
450                'Suite Teardown': lambda s: s.suite_teardown.populate,
451                'Test Setup': lambda s: s.test_setup.populate,
452                'Test Teardown': lambda s: s.test_teardown.populate,
453                'Test Timeout': lambda s: s.test_timeout.populate,
454                'Force Tags': lambda s: s.force_tags.populate,
455                'Library': lambda s: s.imports.populate_library,
456                'Resource': lambda s: s.imports.populate_resource,
457                'Variables': lambda s: s.imports.populate_variables,
458                'Metadata': lambda s: s.metadata.populate}
459
460    def __iter__(self):
461        for setting in [self.doc, self.suite_setup, self.suite_teardown,
462                        self.test_setup, self.test_teardown, self.force_tags,
463                        self.test_timeout] + self.metadata.data + self.imports.data:
464            yield setting
465
466
467class VariableTable(_Table):
468    type = 'variable'
469
470    def __init__(self, parent):
471        _Table.__init__(self, parent)
472        self.variables = []
473
474    @property
475    def _old_header_matcher(self):
476        return OldStyleSettingAndVariableTableHeaderMatcher()
477
478    def add(self, name, value, comment=None):
479        self.variables.append(Variable(self, name, value, comment))
480
481    def __iter__(self):
482        return iter(self.variables)
483
484
485class TestCaseTable(_Table):
486    type = 'test case'
487
488    def __init__(self, parent):
489        _Table.__init__(self, parent)
490        self.tests = []
491
492    def set_header(self, header):
493        if self._header and header:
494            self._validate_mode(self._header[0], header[0])
495        _Table.set_header(self, header)
496
497    def _validate_mode(self, name1, name2):
498        tasks1 = normalize(name1) in ('task', 'tasks')
499        tasks2 = normalize(name2) in ('task', 'tasks')
500        if tasks1 is not tasks2:
501            raise DataError('One file cannot have both tests and tasks.')
502
503    @property
504    def _old_header_matcher(self):
505        return OldStyleTestAndKeywordTableHeaderMatcher()
506
507    def add(self, name):
508        self.tests.append(TestCase(self, name))
509        return self.tests[-1]
510
511    def __iter__(self):
512        return iter(self.tests)
513
514    def is_started(self):
515        return bool(self._header)
516
517
518class KeywordTable(_Table):
519    type = 'keyword'
520
521    def __init__(self, parent):
522        _Table.__init__(self, parent)
523        self.keywords = []
524
525    @property
526    def _old_header_matcher(self):
527        return OldStyleTestAndKeywordTableHeaderMatcher()
528
529    def add(self, name):
530        self.keywords.append(UserKeyword(self, name))
531        return self.keywords[-1]
532
533    def __iter__(self):
534        return iter(self.keywords)
535
536
537@py2to3
538class Variable(object):
539
540    def __init__(self, parent, name, value, comment=None):
541        self.parent = parent
542        self.name = name.rstrip('= ')
543        if name.startswith('$') and value == []:
544            value = ''
545        if is_string(value):
546            value = [value]
547        self.value = value
548        self.comment = Comment(comment)
549
550    def as_list(self):
551        if self.has_data():
552            return [self.name] + self.value + self.comment.as_list()
553        return self.comment.as_list()
554
555    def is_set(self):
556        return True
557
558    def is_for_loop(self):
559        return False
560
561    def has_data(self):
562        return bool(self.name or ''.join(self.value))
563
564    def __nonzero__(self):
565        return self.has_data()
566
567    def report_invalid_syntax(self, message, level='ERROR'):
568        self.parent.report_invalid_syntax("Setting variable '%s' failed: %s"
569                                          % (self.name, message), level)
570
571
572class _WithSteps(object):
573
574    def add_step(self, content, comment=None):
575        self.steps.append(Step(content, comment))
576        return self.steps[-1]
577
578    def copy(self, name):
579        new = copy.deepcopy(self)
580        new.name = name
581        self._add_to_parent(new)
582        return new
583
584
585class TestCase(_WithSteps, _WithSettings):
586
587    def __init__(self, parent, name):
588        self.parent = parent
589        self.name = name
590        self.doc = Documentation('[Documentation]', self)
591        self.template = Template('[Template]', self)
592        self.tags = Tags('[Tags]', self)
593        self.setup = Fixture('[Setup]', self)
594        self.teardown = Fixture('[Teardown]', self)
595        self.timeout = Timeout('[Timeout]', self)
596        self.steps = []
597        if name == '...':
598            self.report_invalid_syntax(
599                "Using '...' as test case name is deprecated. It will be "
600                "considered line continuation in Robot Framework 3.2.",
601                level='WARN'
602            )
603
604    _setters = {'Documentation': lambda s: s.doc.populate,
605                'Template': lambda s: s.template.populate,
606                'Setup': lambda s: s.setup.populate,
607                'Teardown': lambda s: s.teardown.populate,
608                'Tags': lambda s: s.tags.populate,
609                'Timeout': lambda s: s.timeout.populate}
610
611    @property
612    def source(self):
613        return self.parent.source
614
615    @property
616    def directory(self):
617        return self.parent.directory
618
619    def add_for_loop(self, declaration, comment=None):
620        self.steps.append(ForLoop(self, declaration, comment))
621        return self.steps[-1]
622
623    def end_for_loop(self):
624        loop, steps = self._find_last_empty_for_and_steps_after()
625        if not loop:
626            return False
627        loop.steps.extend(steps)
628        self.steps[-len(steps):] = []
629        return True
630
631    def _find_last_empty_for_and_steps_after(self):
632        steps = []
633        for step in reversed(self.steps):
634            if isinstance(step, ForLoop):
635                if not step.steps:
636                    steps.reverse()
637                    return step, steps
638                break
639            steps.append(step)
640        return None, []
641
642    def report_invalid_syntax(self, message, level='ERROR'):
643        type_ = 'test case' if type(self) is TestCase else 'keyword'
644        message = "Invalid syntax in %s '%s': %s" % (type_, self.name, message)
645        self.parent.report_invalid_syntax(message, level)
646
647    def _add_to_parent(self, test):
648        self.parent.tests.append(test)
649
650    @property
651    def settings(self):
652        return [self.doc, self.tags, self.setup, self.template, self.timeout,
653                self.teardown]
654
655    def __iter__(self):
656        for element in [self.doc, self.tags, self.setup,
657                        self.template, self.timeout] \
658                        + self.steps + [self.teardown]:
659            yield element
660
661
662class UserKeyword(TestCase):
663
664    def __init__(self, parent, name):
665        self.parent = parent
666        self.name = name
667        self.doc = Documentation('[Documentation]', self)
668        self.args = Arguments('[Arguments]', self)
669        self.return_ = Return('[Return]', self)
670        self.timeout = Timeout('[Timeout]', self)
671        self.teardown = Fixture('[Teardown]', self)
672        self.tags = Tags('[Tags]', self)
673        self.steps = []
674        if name == '...':
675            self.report_invalid_syntax(
676                "Using '...' as keyword name is deprecated. It will be "
677                "considered line continuation in Robot Framework 3.2.",
678                level='WARN'
679            )
680
681    _setters = {'Documentation': lambda s: s.doc.populate,
682                'Arguments': lambda s: s.args.populate,
683                'Return': lambda s: s.return_.populate,
684                'Timeout': lambda s: s.timeout.populate,
685                'Teardown': lambda s: s.teardown.populate,
686                'Tags': lambda s: s.tags.populate}
687
688    def _add_to_parent(self, test):
689        self.parent.keywords.append(test)
690
691    @property
692    def settings(self):
693        return [self.args, self.doc, self.tags, self.timeout, self.teardown, self.return_]
694
695    def __iter__(self):
696        for element in [self.args, self.doc, self.tags, self.timeout] \
697                        + self.steps + [self.teardown, self.return_]:
698            yield element
699
700
701class ForLoop(_WithSteps):
702    """The parsed representation of a for-loop.
703
704    :param list declaration: The literal cell values that declare the loop
705                             (excluding ":FOR").
706    :param str comment: A comment, default None.
707    :ivar str flavor: The value of the 'IN' item, uppercased.
708                      Typically 'IN', 'IN RANGE', 'IN ZIP', or 'IN ENUMERATE'.
709    :ivar list vars: Variables set per-iteration by this loop.
710    :ivar list items: Loop values that come after the 'IN' item.
711    :ivar str comment: A comment, or None.
712    :ivar list steps: A list of steps in the loop.
713    """
714    flavors = {'IN', 'IN RANGE', 'IN ZIP', 'IN ENUMERATE'}
715    normalized_flavors = NormalizedDict((f, f) for f in flavors)
716
717    def __init__(self, parent, declaration, comment=None):
718        self.parent = parent
719        self.flavor, index = self._get_flavor_and_index(declaration)
720        self.vars = declaration[:index]
721        self.items = declaration[index+1:]
722        self.comment = Comment(comment)
723        self.steps = []
724
725    def _get_flavor_and_index(self, declaration):
726        for index, item in enumerate(declaration):
727            if item in self.flavors:
728                return item, index
729            if item in self.normalized_flavors:
730                correct = self.normalized_flavors[item]
731                self._report_deprecated_flavor_syntax(item, correct)
732                return correct, index
733            if normalize(item).startswith('in'):
734                return item.upper(), index
735        return 'IN', len(declaration)
736
737    def _report_deprecated_flavor_syntax(self, deprecated, correct):
738        self.parent.report_invalid_syntax(
739            "Using '%s' as a FOR loop separator is deprecated. "
740            "Use '%s' instead." % (deprecated, correct), level='WARN'
741        )
742
743    def is_comment(self):
744        return False
745
746    def is_for_loop(self):
747        return True
748
749    def as_list(self, indent=False, include_comment=True):
750        comments = self.comment.as_list() if include_comment else []
751        return ['FOR'] + self.vars + [self.flavor] + self.items + comments
752
753    def __iter__(self):
754        return iter(self.steps)
755
756    def is_set(self):
757        return True
758
759
760class Step(object):
761
762    def __init__(self, content, comment=None):
763        self.assign = self._get_assign(content)
764        self.name = content.pop(0) if content else None
765        self.args = content
766        self.comment = Comment(comment)
767
768    def _get_assign(self, content):
769        assign = []
770        while content and is_var(content[0].rstrip('= ')):
771            assign.append(content.pop(0))
772        return assign
773
774    def is_comment(self):
775        return not (self.assign or self.name or self.args)
776
777    def is_for_loop(self):
778        return False
779
780    def is_set(self):
781        return True
782
783    def as_list(self, indent=False, include_comment=True):
784        kw = [self.name] if self.name is not None else []
785        comments = self.comment.as_list() if include_comment else []
786        data = self.assign + kw + self.args + comments
787        if indent:
788            data.insert(0, '')
789        return data
790
791
792class OldStyleSettingAndVariableTableHeaderMatcher(object):
793
794    def match(self, header):
795        return all(value.lower() == 'value' for value in header[1:])
796
797
798class OldStyleTestAndKeywordTableHeaderMatcher(object):
799
800    def match(self, header):
801        if header[1].lower() != 'action':
802            return False
803        for arg in header[2:]:
804            if not arg.lower().startswith('arg'):
805                return False
806        return True
807