Skip to content

Commit b00183d

Browse files
committed
Always load steps from all the features directories
1 parent 64a0acb commit b00183d

File tree

8 files changed

+67
-85
lines changed

8 files changed

+67
-85
lines changed

README.md

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ outlines, doc strings, data tables and internationalization.
8686

8787
Feature and scenario tags can be used, but aren't currently doing anything.
8888

89+
If `aloe` is run without specifying the features, every file with the extension
90+
of `.feature` in a directory called `features` (and its subdirectories) will be
91+
run.
92+
8993
Steps
9094
=====
9195

@@ -113,21 +117,8 @@ clean-up must be done by the callbacks.
113117
### Step loading
114118

115119
Steps can and should be defined in separate modules to the main application
116-
code. Aloe searches for modules upwards from the directory of the feature, for
117-
example, given a feature of `features/calculator/basic/addition.feature`, the
118-
following directories will be searched for modules to import:
119-
120-
* `features/calculator/basic`
121-
* `features/calculator`
122-
* `features`
123-
124-
and so on (the search does not stop at the current directory).
125-
126-
The first directory containing any Python modules stops the search, all the
127-
modules from it are imported and then the feature is run.
128-
129-
If multiple features are given, steps are searched relative to all of them, but
130-
any given module is only imported once.
120+
code. Aloe searches for modules to load steps from inside the `features`
121+
directories found.
131122

132123
### Step objects
133124

aloe/fs.py

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,51 +30,27 @@
3030
import sys
3131
import fnmatch
3232

33-
from glob import glob
3433
from importlib import import_module
3534
try:
3635
reload
3736
except NameError:
3837
# pylint:disable=no-name-in-module,redefined-builtin
3938
from importlib import reload
4039
# pylint:enable=no-name-in-module,redefined-builtin
41-
from os import walk
42-
from os.path import abspath, join, dirname
40+
from os.path import join, dirname
4341

4442

4543
class FeatureLoader(object):
4644
"""Loader class responsible for findind features and step
4745
definitions along a given path on filesystem"""
4846

49-
@classmethod
50-
def find_steps_dir(cls, feature_file):
51-
"""
52-
Find the steps directory corresponding to the feature file.
53-
54-
This is the first directory upwards of the feature file
55-
containing any Python files.
56-
57-
If no steps directory can be found, return the filesystem root.
58-
"""
59-
60-
base_dir = dirname(abspath(feature_file))
61-
62-
while base_dir != '/':
63-
files = cls.locate(base_dir, '*.py')
64-
if files:
65-
break
66-
base_dir = abspath(join(base_dir, '..'))
67-
68-
return base_dir
69-
7047
@classmethod
7148
def find_and_load_step_definitions(cls, steps_dir):
7249
"""
7350
Load the steps from the specified directory.
7451
"""
75-
files = cls.locate(steps_dir, '*.py')
7652

77-
for filename in files:
53+
for filename in cls.locate(steps_dir, '*.py'):
7854
root = dirname(filename)
7955
sys.path.insert(0, root)
8056
to_load = cls.filename(filename)
@@ -83,16 +59,11 @@ def find_and_load_step_definitions(cls, steps_dir):
8359
sys.path.remove(root)
8460

8561
@staticmethod
86-
def locate(root_path, match, recursive=True):
87-
"""Locate files recursively in a given path"""
88-
if recursive:
89-
return_files = []
90-
for path, _, files in walk(root_path):
91-
for filename in fnmatch.filter(files, match):
92-
return_files.append(join(path, filename))
93-
return return_files
94-
else:
95-
return glob(join(root_path, match))
62+
def locate(root_path, match):
63+
"""Locate files/directories recursively in a given path"""
64+
for path, dirs, files in os.walk(root_path):
65+
for filename in fnmatch.filter(dirs + files, match):
66+
yield join(path, filename)
9667

9768
@classmethod
9869
def filename(cls, path):

aloe/plugin.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ class GherkinPlugin(Plugin):
5050

5151
def begin(self):
5252
"""
53-
Start the test suite, resetting internal state.
53+
Start the test suite, loading all the step definitions.
5454
"""
5555

56-
self.steps_loaded = []
57-
self.first_test = True
56+
self.feature_dirs = [
57+
os.path.abspath(dir_)
58+
for dir_ in FeatureLoader.locate('.', 'features')
59+
]
60+
for feature_dir in self.feature_dirs:
61+
FeatureLoader.find_and_load_step_definitions(feature_dir)
5862

5963
def options(self, parser, env=None):
6064
"""
@@ -115,10 +119,10 @@ def wantDirectory(self, directory):
115119
"""
116120

117121
directory = os.path.abspath(directory)
118-
while directory != '/':
119-
if os.path.basename(directory) == 'features':
120-
return True
121-
directory = os.path.dirname(directory)
122+
if any(feature_dir.startswith(directory) or
123+
directory.startswith(feature_dir)
124+
for feature_dir in self.feature_dirs):
125+
return True
122126

123127
def wantFile(self, file_):
124128
"""
@@ -143,14 +147,9 @@ def loadTestsFromFile(self, file_):
143147
Load a feature from the feature file.
144148
"""
145149

146-
# Ensure the steps corresponding to the feature file are loaded
147-
steps_dir = FeatureLoader.find_steps_dir(file_)
148-
if steps_dir not in self.steps_loaded:
149-
FeatureLoader.find_and_load_step_definitions(steps_dir)
150-
self.steps_loaded.append(steps_dir)
151-
152150
test = self.test_class.from_file(file_)
153151

152+
# About to run a feature - ensure "before all" callbacks have run
154153
self.ensure_before_callbacks()
155154

156155
# Filter the scenarios, if asked
@@ -164,13 +163,11 @@ def ensure_before_callbacks(self):
164163
Before the first test, run the "before all" callbacks.
165164
"""
166165

167-
if self.first_test:
166+
if not hasattr(self, 'after_hook'):
168167
before_all, after_all = CALLBACK_REGISTRY.before_after('all')
169168
before_all()
170169
self.after_hook = after_all
171170

172-
self.first_test = False
173-
174171
def finalize(self, result):
175172
"""
176173
After the last test, run the "after all" callbacks.

tests/functional/test_callbacks.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,26 +215,27 @@ def test_step_failed(self):
215215

216216

217217
@in_directory('tests/multiple_steps_app')
218-
class MultipleAppsCallbackTest(FeatureTest):
218+
class MultipleDirectoriesCallbackTest(FeatureTest):
219219
"""
220-
Test before.all callbacks when running multiple apps.
220+
Test before.all callbacks when running features from multiple directories.
221221
"""
222222

223223
def test_single_app(self):
224224
"""
225-
Test what is loaded and called when only running a single app.
225+
Test all the callbacks are called when running a single feature.
226226
"""
227227

228228
self.assert_feature_success('one/features/check_started.feature')
229229

230230
self.assertEquals(world.started_callbacks_one, {
231231
'one': True,
232-
'two': None,
232+
'two': True,
233233
})
234234

235235
def test_multiple_apps(self):
236236
"""
237-
Test what is loaded and called when running multiple apps.
237+
Test what is loaded and called when running features from multiple
238+
directories.
238239
"""
239240

240241
self.assert_feature_success()
File renamed without changes.
File renamed without changes.

tests/unit/features/steps.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Aloe - Cucumber runner for Python based on Lettuce and Nose
2+
# Copyright (C) <2015> Alexey Kotlyarov <a@koterpillar.com>
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
from __future__ import unicode_literals
18+
from __future__ import print_function
19+
from __future__ import division
20+
from __future__ import absolute_import
21+
from future import standard_library
22+
standard_library.install_aliases()
23+
from aloe import step
24+
25+
26+
@step(r'I do nothing')
27+
def trivial_step(self): # pylint:disable=unused-argument
28+
"""Trivial passing step."""
29+
pass
30+
31+
32+
@step(r'I fail')
33+
def failing_step(self): # pylint:disable=unused-argument
34+
"""Trivial failing step."""
35+
raise AssertionError("This step is meant to fail.")

tests/unit/test_testing.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,12 @@
2828
import os
2929
import unittest
3030

31-
from aloe import step
3231
from aloe.testing import (
3332
FeatureTest,
3433
in_directory,
3534
)
3635

3736

38-
@step(r'I do nothing')
39-
def trivial_step(self): # pylint:disable=unused-argument
40-
"""Trivial passing step."""
41-
pass
42-
43-
44-
@step(r'I fail')
45-
def failing_step(self): # pylint:disable=unused-argument
46-
"""Trivial failing step."""
47-
raise AssertionError("This step is meant to fail.")
48-
49-
5037
@in_directory('tests/unit')
5138
class FeatureTestTest(FeatureTest):
5239
"""

0 commit comments

Comments
 (0)