Skip to content

Commit 71e28f6

Browse files
committed
Cleanup new running/suite building code
1 parent d13c6aa commit 71e28f6

File tree

13 files changed

+789
-551
lines changed

13 files changed

+789
-551
lines changed

src/robot/model/keyword.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ def setup(self, kw):
147147
if kw is not None:
148148
self.insert(0, kw)
149149

150+
def create_setup(self, *args, **kwargs):
151+
self.setup = self._item_class(*args, type='setup', **kwargs)
152+
150153
@property
151154
def teardown(self):
152155
"""Keyword used as the teardown or ``None`` if no teardown.
@@ -165,6 +168,9 @@ def teardown(self, kw):
165168
if kw is not None:
166169
self.append(kw)
167170

171+
def create_teardown(self, *args, **kwargs):
172+
self.teardown = self._item_class(*args, type='teardown', **kwargs)
173+
168174
@property
169175
def all(self):
170176
"""Iterates over all keywords, including setup and teardown."""

src/robot/parsing/lexer/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def lex(self, statement):
3535
name_token.error = err.args[0]
3636
else:
3737
name_token.type = getattr(Token, normalized.replace(' ', '_'))
38-
self.settings[name.upper()] = statement[1:]
38+
self.settings[normalized] = statement[1:]
3939
for token in statement[1:]:
4040
token.type = Token.ARGUMENT
4141

src/robot/parsing/newparser/nodes.py

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
from ast import AST
2+
import re
23

34

45
class Node(AST):
56
_fields = ()
67

8+
def _add_joiners(self, values):
9+
for index, item in enumerate(values):
10+
yield item
11+
if index < len(values) - 1:
12+
yield self._joiner_based_on_eol_escapes(item)
13+
14+
def _joiner_based_on_eol_escapes(self, item):
15+
_end_of_line_escapes = re.compile(r'(\\+)n?$')
16+
match = _end_of_line_escapes.search(item)
17+
if match and len(match.group(1)) % 2 == 1:
18+
return ''
19+
return '\n'
20+
21+
22+
class Value(Node):
23+
_fields = ('value',)
24+
25+
def __init__(self, value):
26+
self.value = value
27+
728

829
class DataFile(Node):
930
_fields = ('sections',)
@@ -50,12 +71,13 @@ def __init__(self, name, value):
5071

5172

5273
class KeywordCall(Node):
74+
# TODO: consider `keyword` -> `name`, as in Fixture
5375
_fields = ('assign', 'keyword', 'args')
5476

5577
def __init__(self, assign, keyword, args=None):
56-
self.assign = assign
78+
self.assign = assign or ()
5779
self.keyword = keyword
58-
self.args = args or []
80+
self.args = args or ()
5981

6082

6183
class ForLoop(Node):
@@ -91,13 +113,6 @@ def __init__(self, args):
91113
self.args = args
92114

93115

94-
class Setting(Node):
95-
_fields = ('value',)
96-
97-
def __init__(self, value):
98-
self.value = value
99-
100-
101116
class ImportSetting(Node):
102117
_fields = ('name', 'args')
103118

@@ -106,30 +121,60 @@ def __init__(self, name, args):
106121
self.args = args
107122

108123

124+
class LibrarySetting(ImportSetting): pass
125+
class ResourceSetting(ImportSetting): pass
126+
class VariablesSetting(ImportSetting): pass
127+
128+
109129
class MetadataSetting(Node):
110130
_fields = ('name', 'value')
111131

112132
def __init__(self, name, value):
113133
self.name = name
114-
self.value = value
134+
self.value = ''.join(self._add_joiners(value))
115135

116136

117-
class DocumentationSetting(Setting): pass
118-
class SuiteSetupSetting(Setting): pass
119-
class SuiteTeardownSetting(Setting): pass
120-
class TestSetupSetting(Setting): pass
121-
class TestTeardownSetting(Setting): pass
122-
class TestTemplateSetting(Setting): pass
123-
class TestTimeoutSetting(Setting): pass
124-
class ForceTagsSetting(Setting): pass
125-
class DefaultTagsSetting(Setting): pass
126-
class LibrarySetting(ImportSetting): pass
127-
class ResourceSetting(ImportSetting): pass
128-
class VariablesSetting(ImportSetting): pass
129-
class SetupSetting(Setting): pass
130-
class TeardownSetting(Setting): pass
131-
class TemplateSetting(Setting): pass
132-
class TimeoutSetting(Setting): pass
133-
class TagsSetting(Setting): pass
134-
class ArgumentsSetting(Setting): pass
135-
class ReturnSetting(Setting): pass
137+
class DocumentationSetting(Value):
138+
139+
def __init__(self, value):
140+
doc = ''.join(self._add_joiners(value))
141+
Value.__init__(self, doc)
142+
143+
144+
class Fixture(Node):
145+
_fields = ('name', 'args')
146+
147+
def __init__(self, value):
148+
if value and value[0].upper() != 'NONE':
149+
self.name = value[0]
150+
self.args = tuple(value[1:])
151+
else:
152+
self.name = None
153+
self.args = ()
154+
155+
156+
class SuiteSetupSetting(Fixture): pass
157+
class SuiteTeardownSetting(Fixture): pass
158+
class TestSetupSetting(Fixture): pass
159+
class TestTeardownSetting(Fixture): pass
160+
class SetupSetting(Fixture): pass
161+
class TeardownSetting(Fixture): pass
162+
163+
164+
class TestTemplateSetting(Value):
165+
166+
def __init__(self, value):
167+
value = value[0] if value and value[0].upper() != 'NONE' else None
168+
Value.__init__(self, value)
169+
170+
171+
class TemplateSetting(TestTemplateSetting): pass
172+
173+
174+
class TestTimeoutSetting(Value): pass
175+
class ForceTagsSetting(Value): pass
176+
class DefaultTagsSetting(Value): pass
177+
class TimeoutSetting(Value): pass
178+
class TagsSetting(Value): pass
179+
class ArgumentsSetting(Value): pass
180+
class ReturnSetting(Value): pass

src/robot/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from robot.model import ModelModifier
4343
from robot.output import LOGGER, pyloggingconf
4444
from robot.reporting import ResultWriter
45-
from robot.running.newbuilder import TestSuiteBuilder
45+
from robot.running.builder import TestSuiteBuilder
4646
from robot.utils import Application, unic, text
4747

4848

src/robot/running/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
ResultWriter('skynet.xml').write_results()
9696
"""
9797

98-
from .newbuilder import TestSuiteBuilder, ResourceFileBuilder
98+
from .builder import TestSuiteBuilder, ResourceFileBuilder
9999
from .context import EXECUTION_CONTEXTS
100100
from .model import Keyword, TestCase, TestSuite
101101
from .testlibraries import TestLibrary

src/robot/running/builder/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
16+
from .builders import TestSuiteBuilder, ResourceFileBuilder

src/robot/running/builder/builders.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
16+
import os
17+
18+
from robot.parsing.newparser.nodes import TestCaseSection
19+
from robot.errors import DataError
20+
from robot.parsing.newparser import builder
21+
from robot.utils import abspath
22+
from robot.output import LOGGER
23+
24+
from .testsettings import TestDefaults
25+
from .transformers import SuiteBuilder, SettingsBuilder, ResourceBuilder
26+
from .suitestructure import SuiteStructureBuilder, SuiteStructureVisitor
27+
from ..model import TestSuite, ResourceFile
28+
29+
30+
class TestSuiteBuilder(object):
31+
32+
def __init__(self, include_suites=None, extension=None, rpa=None):
33+
self.rpa = rpa
34+
self.include_suites = include_suites
35+
self.extension = extension
36+
37+
def build(self, *paths):
38+
"""
39+
:param paths: Paths to test data files or directories.
40+
:return: :class:`~robot.running.model.TestSuite` instance.
41+
"""
42+
if not paths:
43+
raise DataError('One or more source paths required.')
44+
paths = [abspath(p) for p in paths]
45+
structure = SuiteStructureBuilder(self.include_suites, self.extension).build(paths)
46+
parser = SuiteStructureParser(self.rpa)
47+
parser.parse(structure)
48+
suite = parser.suite
49+
suite.rpa = parser.rpa
50+
suite.remove_empty_suites()
51+
return suite
52+
53+
54+
class ResourceFileBuilder(object):
55+
56+
def build(self, path):
57+
data = builder.get_resource_file_ast(path)
58+
return build_resource(data, path)
59+
60+
61+
class SuiteStructureParser(SuiteStructureVisitor):
62+
63+
def __init__(self, rpa=None):
64+
self.rpa = rpa
65+
self._rpa_given = rpa is not None
66+
self.suite = None
67+
self._stack = []
68+
69+
def parse(self, structure):
70+
structure.visit(self)
71+
72+
def visit_file(self, structure):
73+
LOGGER.info("Parsing file '%s'." % structure.source)
74+
suite, _ = self._build_suite(structure)
75+
if self._stack:
76+
self._stack[-1][0].suites.append(suite)
77+
else:
78+
self.suite = suite
79+
80+
def start_directory(self, structure):
81+
if structure.source:
82+
LOGGER.info("Parsing directory '%s'." % structure.source)
83+
suite, defaults = self._build_suite(structure)
84+
if self.suite is None:
85+
self.suite = suite
86+
else:
87+
self._stack[-1][0].suites.append(suite)
88+
self._stack.append((suite, defaults))
89+
90+
def end_directory(self, structure):
91+
self._stack.pop()
92+
93+
def _build_suite(self, structure):
94+
defaults = self._stack[-1][-1] if self._stack else None
95+
source = structure.source
96+
datapath = source if not structure.is_directory else structure.init_file
97+
try:
98+
suite, defaults = build_suite(source, datapath, defaults)
99+
self._validate_execution_mode(suite.rpa)
100+
except DataError as err:
101+
raise DataError("Parsing '%s' failed: %s" % (source, err.message))
102+
return suite, defaults
103+
104+
def _validate_execution_mode(self, rpa):
105+
if self._rpa_given or rpa is None:
106+
return
107+
if self.rpa is None:
108+
self.rpa = rpa
109+
elif self.rpa is not rpa:
110+
this, that = ('tasks', 'tests') if rpa else ('tests', 'tasks')
111+
raise DataError("Conflicting execution modes. File has %s "
112+
"but files parsed earlier have %s. Fix headers "
113+
"or use '--rpa' or '--norpa' options to set the "
114+
"execution mode explicitly." % (this, that))
115+
116+
117+
def build_suite(source, datapath=None, parent_defaults=None):
118+
suite = TestSuite(name=format_name(source), source=source)
119+
defaults = TestDefaults(parent_defaults)
120+
if datapath:
121+
ast = builder.get_test_case_file_ast(datapath)
122+
#print(ast.dump(ast))
123+
SettingsBuilder(suite, defaults).visit(ast)
124+
SuiteBuilder(suite, defaults).visit(ast)
125+
suite.rpa = _get_rpa_mode(ast)
126+
return suite, defaults
127+
128+
129+
def _get_rpa_mode(data):
130+
if not data:
131+
return None
132+
modes = [s.header.lower() in ('task', 'tasks')
133+
for s in data.sections if isinstance(s, TestCaseSection)]
134+
if all(modes) or not any(modes):
135+
return modes[0] if modes else None
136+
raise DataError('One file cannot have both tests and tasks.')
137+
138+
139+
def build_resource(data, source):
140+
resource = ResourceFile(source=source)
141+
if data.sections:
142+
ResourceBuilder(resource).visit(data)
143+
else:
144+
LOGGER.warn("Imported resource file '%s' is empty." % source)
145+
return resource
146+
147+
148+
def format_name(source):
149+
def strip_possible_prefix_from_name(name):
150+
return name.split('__', 1)[-1]
151+
152+
def format_name(name):
153+
name = strip_possible_prefix_from_name(name)
154+
name = name.replace('_', ' ').strip()
155+
return name.title() if name.islower() else name
156+
157+
if source is None:
158+
return None
159+
if os.path.isdir(source):
160+
basename = os.path.basename(source)
161+
else:
162+
basename = os.path.splitext(os.path.basename(source))[0]
163+
return format_name(basename)

0 commit comments

Comments
 (0)