PY-86174 Fix behave step definitions detection in nested directories

Well, here's the thing. We clear step registry after dry run (because
we'll reload steps again after running behave for real). Technically we
run behave twice, one for dry run (to get the number of tests and some
context) and then we invoke their runner again.

Here's how behave loads step definitions. It creates a decorator for
each step type, then under the hood it compiles all the python scripts
with steps (in steps directory) and executes the code with definitions
of those decorators as context to execute the script.

When we don't have subfolders, it all works as expected. But here's a
twist with the subfolder. We now have an import for a subfolder script,
but the problem is that sys.modules lives globally for the whole
lifecycle of our runner. So when we execute the same script second time,
we only go through the import statement, we see that our module is
already loaded, and hence don't run the nested steps script, missing the
steps from there.

The fix involves clearing the modules that were loaded during the dry
run, so that we can reimport modules with steps in nested directories.

GitOrigin-RevId: 9a6e77819aa0ba5d5789049034f2c0e2af4f3b2c
This commit is contained in:
Alexey Katsman
2025-12-11 22:47:11 +00:00
committed by intellij-monorepo-bot
parent a3fd2fb32e
commit d46ac5c37e
+9
View File
@@ -235,8 +235,17 @@ class _BehaveRunner(_bdd_utils.BddRunner):
return isinstance(expected_tags, TagExpression) and expected_tags.check(scenario.tags)
def _get_features_to_run(self):
old_modules = sys.modules.copy()
self.__real_runner.dry_run = True
self.__real_runner.run()
# During the dry run we can import some modules with steps in nested
# directories. And since we then clear step registry, there's no way to
# get those steps back without reimport. So we clear up the modules that
# were imported during the dry run to support such scenario.
new_modules = sys.modules.copy()
for module in new_modules.keys():
if module not in old_modules:
del sys.modules[module]
features_to_run = self.__real_runner.features
self.__real_runner.clean() # To make sure nothing left after dry run