behavex 4.2.1__tar.gz → 4.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {behavex-4.2.1/behavex.egg-info → behavex-4.2.2}/PKG-INFO +7 -4
  2. {behavex-4.2.1 → behavex-4.2.2}/README.md +6 -3
  3. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/arguments.cpython-39.pyc +0 -0
  4. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/conf_mgr.cpython-39.pyc +0 -0
  5. behavex-4.2.2/behavex/__pycache__/environment.cpython-39.pyc +0 -0
  6. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/runner.cpython-39.pyc +0 -0
  7. behavex-4.2.2/behavex/__pycache__/utils.cpython-39.pyc +0 -0
  8. {behavex-4.2.1 → behavex-4.2.2}/behavex/environment.py +66 -26
  9. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/formatter_manager.cpython-39.pyc +0 -0
  10. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/output_strings.cpython-39.pyc +0 -0
  11. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/report_json.cpython-39.pyc +0 -0
  12. behavex-4.2.2/behavex/outputs/__pycache__/report_utils.cpython-39.pyc +0 -0
  13. behavex-4.2.2/behavex/outputs/__pycache__/report_xml.cpython-39.pyc +0 -0
  14. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/formatters/__pycache__/allure_behavex_formatter.cpython-39.pyc +0 -0
  15. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/formatters/allure_behavex_formatter.py +12 -1
  16. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/output_strings.py +16 -0
  17. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/report_json.py +2 -0
  18. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/report_utils.py +85 -19
  19. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/report_xml.py +43 -4
  20. {behavex-4.2.1 → behavex-4.2.2}/behavex/runner.py +45 -4
  21. {behavex-4.2.1 → behavex-4.2.2}/behavex/utils.py +7 -0
  22. {behavex-4.2.1 → behavex-4.2.2/behavex.egg-info}/PKG-INFO +7 -4
  23. {behavex-4.2.1 → behavex-4.2.2}/pyproject.toml +1 -1
  24. {behavex-4.2.1 → behavex-4.2.2}/setup.py +1 -1
  25. {behavex-4.2.1 → behavex-4.2.2}/tests/features/steps/allure_formatter_steps.py +2 -4
  26. behavex-4.2.1/behavex/__pycache__/environment.cpython-39.pyc +0 -0
  27. behavex-4.2.1/behavex/__pycache__/utils.cpython-39.pyc +0 -0
  28. behavex-4.2.1/behavex/outputs/__pycache__/report_utils.cpython-39.pyc +0 -0
  29. behavex-4.2.1/behavex/outputs/__pycache__/report_xml.cpython-39.pyc +0 -0
  30. {behavex-4.2.1 → behavex-4.2.2}/LICENSE +0 -0
  31. {behavex-4.2.1 → behavex-4.2.2}/MANIFEST.in +0 -0
  32. {behavex-4.2.1 → behavex-4.2.2}/behavex/.DS_Store +0 -0
  33. {behavex-4.2.1 → behavex-4.2.2}/behavex/__init__.py +0 -0
  34. {behavex-4.2.1 → behavex-4.2.2}/behavex/__main__.py +0 -0
  35. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/__init__.cpython-39.pyc +0 -0
  36. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/__main__.cpython-39.pyc +0 -0
  37. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/execution_singleton.cpython-39.pyc +0 -0
  38. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/global_vars.cpython-39.pyc +0 -0
  39. {behavex-4.2.1 → behavex-4.2.2}/behavex/__pycache__/progress_bar.cpython-39.pyc +0 -0
  40. {behavex-4.2.1 → behavex-4.2.2}/behavex/arguments.py +0 -0
  41. {behavex-4.2.1 → behavex-4.2.2}/behavex/conf_behavex.cfg +0 -0
  42. {behavex-4.2.1 → behavex-4.2.2}/behavex/conf_logging.cfg +0 -0
  43. {behavex-4.2.1 → behavex-4.2.2}/behavex/conf_mgr.py +0 -0
  44. {behavex-4.2.1 → behavex-4.2.2}/behavex/execution_singleton.py +0 -0
  45. {behavex-4.2.1 → behavex-4.2.2}/behavex/global_vars.py +0 -0
  46. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__init__.py +0 -0
  47. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/__init__.cpython-39.pyc +0 -0
  48. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/jinja_mgr.cpython-39.pyc +0 -0
  49. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/__pycache__/report_html.cpython-39.pyc +0 -0
  50. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/behavex.css +0 -0
  51. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap-theme.css +0 -0
  52. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap-theme.min.css +0 -0
  53. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap-theme.min.css.map +0 -0
  54. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap.css +0 -0
  55. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap.min.css +0 -0
  56. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/bootstrap.min.css.map +0 -0
  57. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/css/pace-theme-loading-bar.min.css +0 -0
  58. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  59. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/fonts/glyphicons-halflings-regular.svg +0 -0
  60. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  61. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  62. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  63. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/bootstrap.js +0 -0
  64. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/bootstrap.min.js +0 -0
  65. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/chart.js +0 -0
  66. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/jquery-1.11.0.min.js +0 -0
  67. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/jquery-ui.min.js +0 -0
  68. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/jquery.tablesorter.min.js +0 -0
  69. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/jquery.tooltipster.min.js +0 -0
  70. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/bootstrap/js/pace.min.js +0 -0
  71. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/formatter_manager.py +0 -0
  72. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja/base.jinja2 +0 -0
  73. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja/main.jinja2 +0 -0
  74. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja/manifest.jinja2 +0 -0
  75. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja/xml.jinja2 +0 -0
  76. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja/xml_json.jinja2 +0 -0
  77. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/jinja_mgr.py +0 -0
  78. {behavex-4.2.1 → behavex-4.2.2}/behavex/outputs/report_html.py +0 -0
  79. {behavex-4.2.1 → behavex-4.2.2}/behavex/progress_bar.py +0 -0
  80. {behavex-4.2.1 → behavex-4.2.2}/behavex/steps/__init__.py +0 -0
  81. {behavex-4.2.1 → behavex-4.2.2}/behavex.egg-info/SOURCES.txt +0 -0
  82. {behavex-4.2.1 → behavex-4.2.2}/behavex.egg-info/dependency_links.txt +0 -0
  83. {behavex-4.2.1 → behavex-4.2.2}/behavex.egg-info/entry_points.txt +0 -0
  84. {behavex-4.2.1 → behavex-4.2.2}/behavex.egg-info/requires.txt +0 -0
  85. {behavex-4.2.1 → behavex-4.2.2}/behavex.egg-info/top_level.txt +0 -0
  86. {behavex-4.2.1 → behavex-4.2.2}/docs/conf.py +0 -0
  87. {behavex-4.2.1 → behavex-4.2.2}/setup.cfg +0 -0
  88. {behavex-4.2.1 → behavex-4.2.2}/tests/features/crashing_features/environment.py +0 -0
  89. {behavex-4.2.1 → behavex-4.2.2}/tests/features/crashing_features/steps/__init__.py +0 -0
  90. {behavex-4.2.1 → behavex-4.2.2}/tests/features/crashing_features/steps/crashing_steps.py +0 -0
  91. {behavex-4.2.1 → behavex-4.2.2}/tests/features/environment.py +0 -0
  92. {behavex-4.2.1 → behavex-4.2.2}/tests/features/secondary_features/__init__.py +0 -0
  93. {behavex-4.2.1 → behavex-4.2.2}/tests/features/secondary_features/environment.py +0 -0
  94. {behavex-4.2.1 → behavex-4.2.2}/tests/features/secondary_features/steps/__init__.py +0 -0
  95. {behavex-4.2.1 → behavex-4.2.2}/tests/features/secondary_features/steps/allure_formatter_secondary_steps.py +0 -0
  96. {behavex-4.2.1 → behavex-4.2.2}/tests/features/secondary_features/steps/generic_secondary_steps.py +0 -0
  97. {behavex-4.2.1 → behavex-4.2.2}/tests/features/steps/__init__.py +0 -0
  98. {behavex-4.2.1 → behavex-4.2.2}/tests/features/steps/allure_formatter_unit_steps.py +0 -0
  99. {behavex-4.2.1 → behavex-4.2.2}/tests/features/steps/execution_steps.py +0 -0
  100. {behavex-4.2.1 → behavex-4.2.2}/tests/features/steps/install_steps.py +0 -0
  101. {behavex-4.2.1 → behavex-4.2.2}/venv/bin/activate_this.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behavex
3
- Version: 4.2.1
3
+ Version: 4.2.2
4
4
  Summary: Agile testing framework on top of Behave (BDD).
5
5
  Home-page: https://github.com/hrcorval/behavex
6
6
  Author: Hernan Rey
@@ -87,20 +87,20 @@ BehaveX now provides seamless integration with Allure, the popular test reportin
87
87
 
88
88
  ## Introduction
89
89
 
90
- **BehaveX** is a BDD testing solution designed to enhance your Behave-based testing workflow by providing additional features and performance improvements. It's particularly beneficial in the following scenarios:
90
+ **BehaveX** is a BDD testing solution built on top of the Python Behave library, orchestrating parallel test sessions to enhance your testing workflow with additional features and performance improvements. It's particularly beneficial in the following scenarios:
91
91
 
92
92
  - **Accelerating test execution**: Significantly reduce test run times through parallel execution by feature or scenario.
93
93
  - **Enhancing test reporting**: Generate comprehensive and visually appealing HTML and JSON reports for in-depth analysis and integration with other tools.
94
94
  - **Improving test visibility**: Provide detailed evidence, such as screenshots and logs, essential for understanding test failures and successes.
95
95
  - **Optimizing test automation**: Utilize features like test retries, test muting, and performance metrics for efficient test maintenance and analysis.
96
- - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and reporting.
96
+ - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and comprehensive reporting through multiple formats and custom formatters.
97
97
 
98
98
  ## Features
99
99
 
100
100
  BehaveX provides the following features:
101
101
 
102
102
  - **Parallel Test Executions**: Execute tests using multiple processes, either by feature or by scenario.
103
- - **Enhanced Reporting**: Generate friendly HTML and JSON reports that can be exported and integrated with third-party tools.
103
+ - **Enhanced Reporting**: Generate comprehensive reports in multiple formats (HTML, JSON, JUnit) and utilize custom formatters like Allure for advanced reporting capabilities that can be exported and integrated with third-party tools.
104
104
  - **Evidence Collection**: Include images/screenshots and additional evidence in the HTML report.
105
105
  - **Test Logs**: Automatically compile logs generated during test execution into individual log reports for each scenario.
106
106
  - **Test Muting**: Add the `@MUTE` tag to test scenarios to execute them without including them in JUnit reports.
@@ -210,6 +210,9 @@ Execute BehaveX in the same way as Behave from the command line, using the `beha
210
210
  - **parallel-processes** (--parallel-processes): Specifies the number of parallel Behave processes.
211
211
  - **parallel-scheme** (--parallel-scheme): Performs parallel test execution by [scenario|feature].
212
212
  - **show-progress-bar** (--show-progress-bar): Displays a progress bar in the console during parallel test execution.
213
+ - **formatter** (--formatter): Specifies a custom formatter for test reports (e.g., Allure formatter).
214
+ - **formatter-outdir** (--formatter-outdir): Specifies the output directory for formatter results (default: output/allure-results for Allure).
215
+ - **no-formatter-attach-logs** (--no-formatter-attach-logs): Disables automatic attachment of scenario log files to formatter reports.
213
216
 
214
217
  ## Parallel Test Executions
215
218
 
@@ -39,20 +39,20 @@ BehaveX now provides seamless integration with Allure, the popular test reportin
39
39
 
40
40
  ## Introduction
41
41
 
42
- **BehaveX** is a BDD testing solution designed to enhance your Behave-based testing workflow by providing additional features and performance improvements. It's particularly beneficial in the following scenarios:
42
+ **BehaveX** is a BDD testing solution built on top of the Python Behave library, orchestrating parallel test sessions to enhance your testing workflow with additional features and performance improvements. It's particularly beneficial in the following scenarios:
43
43
 
44
44
  - **Accelerating test execution**: Significantly reduce test run times through parallel execution by feature or scenario.
45
45
  - **Enhancing test reporting**: Generate comprehensive and visually appealing HTML and JSON reports for in-depth analysis and integration with other tools.
46
46
  - **Improving test visibility**: Provide detailed evidence, such as screenshots and logs, essential for understanding test failures and successes.
47
47
  - **Optimizing test automation**: Utilize features like test retries, test muting, and performance metrics for efficient test maintenance and analysis.
48
- - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and reporting.
48
+ - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and comprehensive reporting through multiple formats and custom formatters.
49
49
 
50
50
  ## Features
51
51
 
52
52
  BehaveX provides the following features:
53
53
 
54
54
  - **Parallel Test Executions**: Execute tests using multiple processes, either by feature or by scenario.
55
- - **Enhanced Reporting**: Generate friendly HTML and JSON reports that can be exported and integrated with third-party tools.
55
+ - **Enhanced Reporting**: Generate comprehensive reports in multiple formats (HTML, JSON, JUnit) and utilize custom formatters like Allure for advanced reporting capabilities that can be exported and integrated with third-party tools.
56
56
  - **Evidence Collection**: Include images/screenshots and additional evidence in the HTML report.
57
57
  - **Test Logs**: Automatically compile logs generated during test execution into individual log reports for each scenario.
58
58
  - **Test Muting**: Add the `@MUTE` tag to test scenarios to execute them without including them in JUnit reports.
@@ -162,6 +162,9 @@ Execute BehaveX in the same way as Behave from the command line, using the `beha
162
162
  - **parallel-processes** (--parallel-processes): Specifies the number of parallel Behave processes.
163
163
  - **parallel-scheme** (--parallel-scheme): Performs parallel test execution by [scenario|feature].
164
164
  - **show-progress-bar** (--show-progress-bar): Displays a progress bar in the console during parallel test execution.
165
+ - **formatter** (--formatter): Specifies a custom formatter for test reports (e.g., Allure formatter).
166
+ - **formatter-outdir** (--formatter-outdir): Specifies the output directory for formatter results (default: output/allure-results for Allure).
167
+ - **no-formatter-attach-logs** (--no-formatter-attach-logs): Disables automatic attachment of scenario log files to formatter reports.
165
168
 
166
169
  ## Parallel Test Executions
167
170
 
@@ -28,10 +28,12 @@ Context.__getattribute__ = create_custom_log_when_called
28
28
 
29
29
  hooks_already_set = False
30
30
 
31
+
31
32
  def _get_current_timestamp_ms():
32
33
  """Get current time as Unix epoch milliseconds."""
33
34
  return int(datetime.now().timestamp() * 1000)
34
35
 
36
+
35
37
  def extend_behave_hooks():
36
38
  """
37
39
  Extend Behave hooks with BehaveX hooks code.
@@ -106,7 +108,7 @@ def extend_behave_hooks():
106
108
  def before_all(context):
107
109
  """Setup up initial tests configuration."""
108
110
  try:
109
- # Initialyzing log handler
111
+ # Initialize critical state variables to ensure consistent state
110
112
  context.bhx_log_handler = None
111
113
  context.bhx_inside_scenario = False
112
114
  # Store framework settings to make them accessible from steps
@@ -120,40 +122,68 @@ def before_feature(context, feature):
120
122
  try:
121
123
  context.bhx_execution_attempts = {}
122
124
  scenarios_instances = get_scenarios_instances(feature.scenarios)
125
+
123
126
  for scenario in scenarios_instances:
124
127
  scenario_tags = get_scenario_tags(scenario)
125
- if get_param('dry_run'):
126
- if 'MANUAL' not in scenario_tags:
127
- scenario.tags.append(u'BHX_MANUAL_DRY_RUN')
128
- scenario.tags.append(u'MANUAL')
128
+
129
+ # Handle dry run scenario tagging
130
+ if get_param('dry_run') and 'MANUAL' not in scenario_tags:
131
+ scenario.tags.extend(['BHX_MANUAL_DRY_RUN', 'MANUAL'])
132
+
133
+ # Configure scenario auto-retry
129
134
  configured_attempts = get_autoretry_attempts(scenario_tags)
130
135
  if configured_attempts > 0:
131
136
  patch_scenario_with_autoretry(scenario, configured_attempts)
132
137
  except Exception as exception:
138
+ # Keep generic exception as fallback for truly unexpected errors
133
139
  _log_exception_and_continue('before_feature (behavex)', exception)
134
140
 
135
141
 
136
142
  def before_scenario(context, scenario):
137
143
  """Initialize logs for current scenario."""
144
+ # Initialize critical variables first to ensure they're always set
145
+ context.bhx_inside_scenario = True
146
+ # Initialize log handler to None to ensure it's always defined
147
+ context.bhx_log_handler = None
148
+
138
149
  try:
139
- context.bhx_inside_scenario = True
150
+ # Handle execution attempts tracking
140
151
  if scenario.name not in context.bhx_execution_attempts:
141
152
  context.bhx_execution_attempts[scenario.name] = 0
142
153
  execution_attempt = context.bhx_execution_attempts[scenario.name]
143
154
  retrying_execution = True if execution_attempt > 0 else False
144
-
145
155
  # Calculate and store the scenario identifier hash
146
156
  scenario_identifier = f"{str(context.feature.filename)}-{str(scenario.line)}"
157
+ # Calculate the scenario identifier hash
147
158
  scenario.identifier_hash = get_string_hash(scenario_identifier)
148
-
149
- context.log_path = create_log_path(scenario_identifier, retrying_execution)
159
+ # Create log path
160
+ context.log_path = create_log_path(scenario.identifier_hash, retrying_execution)
161
+ # Add log handler
150
162
  context.bhx_log_handler = _add_log_handler(context.log_path)
163
+ # Handle retry scenario cleanup
151
164
  if retrying_execution:
152
- logging.info('Retrying scenario execution...\n'.format())
153
- shutil.rmtree(context.evidence_path)
165
+ try:
166
+ logging.info('Retrying scenario execution...')
167
+ if hasattr(context, 'evidence_path') and context.evidence_path:
168
+ try:
169
+ shutil.rmtree(context.evidence_path)
170
+ except FileNotFoundError:
171
+ # Directory already removed by another process - this is fine
172
+ pass
173
+ except Exception as cleanup_ex:
174
+ logging.warning(f"Failed to remove evidence directory {context.evidence_path}: {cleanup_ex}")
175
+ except Exception as retry_ex:
176
+ logging.warning(f"Failed to clean up evidence path during retry: {retry_ex}")
177
+ scenario.process_id = os.getpid()
178
+ if "config" in context and "worker_id" in context.config.userdata:
179
+ scenario.worker_id = context.config.userdata['worker_id']
180
+ else:
181
+ scenario.worker_id = str(os.getpid())
154
182
  except Exception as exception:
183
+ # Log the exception but ensure execution continues
155
184
  _log_exception_and_continue('before_scenario (behavex)', exception)
156
- scenario.start = _get_current_timestamp_ms()
185
+ finally:
186
+ scenario.start = _get_current_timestamp_ms()
157
187
 
158
188
 
159
189
  def before_step(context, step):
@@ -191,9 +221,12 @@ def after_scenario(context, scenario):
191
221
  else:
192
222
  global_vars.retried_scenarios[feature_name].append(scenario.name)
193
223
  context.bhx_execution_attempts[scenario.name] += 1
194
- _close_log_handler(context.bhx_log_handler)
195
224
  except Exception as exception:
196
225
  _log_exception_and_continue('after_scenario (behavex)', exception)
226
+ finally:
227
+ # Always reset the scenario state flag, regardless of success or failure
228
+ _close_log_handler(context.bhx_log_handler)
229
+ context.bhx_inside_scenario = False
197
230
 
198
231
 
199
232
  # noinspection PyUnusedLocal
@@ -217,24 +250,31 @@ def after_all(context):
217
250
 
218
251
  def _add_log_handler(log_path):
219
252
  """Adding a new log handler to logger"""
220
- log_filename = os.path.join(log_path, 'scenario.log')
221
- file_handler = logging.FileHandler(
222
- log_filename, mode='+a', encoding=LOGGING_CFG['file_handler']['encoding']
223
- )
224
- log_level = get_logging_level()
225
- logging.getLogger().setLevel(log_level)
226
- file_handler.addFilter(lambda record: setattr(record, 'msg', strip_ansi_codes(str(record.msg))) or True)
227
- file_handler.setFormatter(_get_log_formatter())
228
- logging.getLogger().addHandler(file_handler)
253
+ file_handler = None
254
+ try:
255
+ log_filename = os.path.join(log_path, 'scenario.log')
256
+ file_handler = logging.FileHandler(
257
+ log_filename, mode='+a', encoding=LOGGING_CFG['file_handler']['encoding']
258
+ )
259
+ log_level = get_logging_level()
260
+ logging.getLogger().setLevel(log_level)
261
+ file_handler.addFilter(lambda record: setattr(record, 'msg', strip_ansi_codes(str(record.msg))) or True)
262
+ file_handler.setFormatter(_get_log_formatter())
263
+ logging.getLogger().addHandler(file_handler)
264
+ except Exception as exception:
265
+ _log_exception_and_continue('_add_log_handler', exception)
229
266
  return file_handler
230
267
 
231
268
 
232
269
  def _close_log_handler(handler):
233
- """Closing current log handlers and removing them from logger"""
270
+ """Closing current log handlers and removing them from logger."""
234
271
  if handler:
235
- if hasattr(handler, 'stream') and hasattr(handler.stream, 'close'):
236
- handler.stream.close()
237
- logging.getLogger().removeHandler(handler)
272
+ try:
273
+ if hasattr(handler, 'stream') and hasattr(handler.stream, 'close'):
274
+ handler.stream.close()
275
+ logging.getLogger().removeHandler(handler)
276
+ except Exception as exception:
277
+ _log_exception_and_continue('_close_log_handler', exception)
238
278
 
239
279
 
240
280
  def _get_log_formatter():
@@ -433,6 +433,7 @@ class AllureBehaveXFormatter:
433
433
 
434
434
  # Process tags
435
435
  package_from_tag = None
436
+ test_thread_already_set = False
436
437
  for tag in scenario['tags']:
437
438
  if tag.startswith("epic="):
438
439
  test_case.labels.append({"name": "epic", "value": tag.split("=")[-1]})
@@ -465,9 +466,19 @@ class AllureBehaveXFormatter:
465
466
  test_case.labels = [label for label in test_case.labels if label.get("name") != "package"]
466
467
  # Add the package from tag
467
468
  test_case.labels.append({"name": "package", "value": package_from_tag})
469
+ elif tag.startswith("allure.label."):
470
+ label_key, label_value = tag.removeprefix("allure.label.").split(":")
471
+ if label_key == "thread":
472
+ test_thread_already_set = True
473
+ test_case.labels.append({
474
+ "name": label_key,
475
+ "value": label_value
476
+ })
468
477
  else:
469
478
  test_case.labels.append({"name": "tag", "value": tag})
470
-
479
+ # Add thread label if not already set and process_id is present
480
+ if "process_id" in scenario.keys() and not test_thread_already_set:
481
+ test_case.labels.append({"name": "thread", "value": scenario["process_id"]})
471
482
  # Add scenario timing
472
483
  test_case.start = scenario.get("start", now())
473
484
  test_case.stop = scenario.get("stop", now())
@@ -68,4 +68,20 @@ TEXTS = {
68
68
  },
69
69
  'folder': {'run_behave': u"Running folder: '{}' and feature '{}'."},
70
70
  'path': {'not_found': u'\nThe path "{}" was not found.\n'},
71
+ 'process': {
72
+ 'pool': {
73
+ 'broken': u'\nWarning: Process pool was terminated abruptly: {}',
74
+ 'broken.main': u'\nWarning: A process in the pool was terminated abruptly during main execution: {}'
75
+ }
76
+ },
77
+ 'execution': {
78
+ 'callback': {
79
+ 'error': u'\nError occurred during execution callback: {}'
80
+ }
81
+ },
82
+ 'parallel': {
83
+ 'process': {
84
+ 'error': u'\nError occurred during parallel process execution: {}'
85
+ }
86
+ }
71
87
  }
@@ -202,6 +202,8 @@ def _processing_scenarios(scenarios, scenario_list, id_feature):
202
202
  scenario_info['error_background'] = error_background
203
203
  scenario_info['id_hash'] = generate_uuid()
204
204
  scenario_info['identifier_hash'] = scenario.identifier_hash if hasattr(scenario, 'identifier_hash') else get_string_hash(f"{str(scenario.feature.filename)}-{str(scenario.line)}")
205
+ scenario_info['process_id'] = str(scenario.process_id) if hasattr(scenario, 'process_id') else str(os.getpid())
206
+ scenario_info['worker_id'] = str(scenario.worker_id) if hasattr(scenario, 'worker_id') else str(os.getpid())
205
207
  if scenario.feature.name in global_vars.retried_scenarios:
206
208
  if (
207
209
  scenario.name
@@ -15,11 +15,13 @@ from __future__ import absolute_import, print_function
15
15
 
16
16
  import codecs
17
17
  import hashlib
18
+ import logging
18
19
  import os
19
20
  import re
20
21
  import shutil
21
22
  import string
22
23
  import sys
24
+ import tempfile
23
25
  import traceback
24
26
  import unicodedata
25
27
  from operator import getitem
@@ -331,25 +333,65 @@ def get_save_function(path, content):
331
333
  return fun_save
332
334
 
333
335
 
334
- def create_log_path(name, execution_retry=False):
335
- name = get_string_hash(name)
336
- if sys.version_info < (3, 0):
337
- path = os.path.join(get_env('logs').decode('utf8'), str(name))
338
- else:
339
- path = os.path.join(get_env('logs'), str(name))
340
- initial_path = path
341
- scenario_outline_index = 1
342
- while os.path.exists(path):
343
- if execution_retry:
344
- return path
345
- path = initial_path + u'_' + str(scenario_outline_index)
346
- scenario_outline_index += 1
336
+ def create_log_path(scenario_identifier_hash, execution_retry=False):
337
+ """Create log path with fallback mechanisms for robust error handling.
338
+
339
+ Args:
340
+ scenario_identifier_hash: Hash representation of the scenario
341
+ execution_retry: Whether this is a retry execution
342
+
343
+ Returns:
344
+ str: The created log path
345
+ """
347
346
  try:
348
- os.makedirs(path)
349
- except FileExistsError:
350
- # path already exists
351
- pass
352
- return path
347
+ # Validate and sanitize the logs directory
348
+ logs_dir = _get_safe_logs_directory()
349
+
350
+ # Create the full path
351
+ path = os.path.join(logs_dir, str(scenario_identifier_hash))
352
+ initial_path = path
353
+ scenario_outline_index = 1
354
+
355
+ # Handle existing paths
356
+ while os.path.exists(path):
357
+ if execution_retry:
358
+ return path
359
+ path = initial_path + u'_' + str(scenario_outline_index)
360
+ scenario_outline_index += 1
361
+
362
+ # Create the directory with robust error handling
363
+ os.makedirs(path, exist_ok=True)
364
+ return path
365
+
366
+ except Exception as ex:
367
+ logging.warning(f"Primary log path creation failed: {ex}")
368
+ # Only use temp fallback for actual file system issues
369
+ temp_path = tempfile.mkdtemp(prefix=f"bhx_scenario_{scenario_identifier_hash or 'unknown'}_")
370
+ logging.error(f"Using temp directory as last resort: {temp_path}")
371
+ return temp_path
372
+
373
+
374
+ def _get_safe_logs_directory():
375
+ """Get a safe logs directory, creating it if necessary."""
376
+ # Try to get the configured logs directory
377
+ logs_dir = get_env('logs')
378
+
379
+ # If not configured, use a safe default
380
+ if not logs_dir:
381
+ output_dir = get_env('output', '.')
382
+ logs_dir = os.path.join(output_dir, 'outputs', 'logs')
383
+
384
+ # Ensure the logs directory exists
385
+ try:
386
+ os.makedirs(logs_dir, exist_ok=True)
387
+ except OSError as ex:
388
+ # If we can't create the logs directory, fall back to output directory
389
+ logging.warning(f"Cannot create logs directory {logs_dir}: {ex}")
390
+ output_dir = get_env('output', '.')
391
+ logs_dir = os.path.join(output_dir, 'logs')
392
+ os.makedirs(logs_dir, exist_ok=True)
393
+
394
+ return logs_dir
353
395
 
354
396
 
355
397
  def get_error_message(error_message):
@@ -399,7 +441,31 @@ def text(value, encoding=None, errors=None):
399
441
 
400
442
 
401
443
  def get_string_hash(string_to_hash):
402
- return str(hashlib.sha256(string_to_hash.encode('UTF-8')).hexdigest())
444
+ """Generate SHA256 hash for a given string input.
445
+
446
+ Args:
447
+ string_to_hash: String or bytes to hash
448
+
449
+ Returns:
450
+ str: Hexadecimal representation of the SHA256 hash
451
+
452
+ Raises:
453
+ TypeError: If input is not string or bytes
454
+ """
455
+ if string_to_hash is None:
456
+ raise ValueError("Input cannot be None")
457
+
458
+ if isinstance(string_to_hash, str):
459
+ # Convert string to bytes
460
+ bytes_to_hash = string_to_hash.encode('UTF-8')
461
+ elif isinstance(string_to_hash, bytes):
462
+ # Already bytes, use as-is
463
+ bytes_to_hash = string_to_hash
464
+ else:
465
+ # Convert other types to string first
466
+ bytes_to_hash = str(string_to_hash).encode('UTF-8')
467
+
468
+ return hashlib.sha256(bytes_to_hash).hexdigest()
403
469
 
404
470
 
405
471
  def normalize_filename(input_name):
@@ -21,6 +21,41 @@ from behavex.outputs.report_utils import (get_save_function,
21
21
  from behavex.utils import get_scenario_tags
22
22
 
23
23
 
24
+ def generate_junit_xml_filename(filename):
25
+ """Generate JUnit XML report filename from feature file path.
26
+
27
+ Args:
28
+ filename (str): Path to the feature file
29
+
30
+ Returns:
31
+ str: JUnit XML filename in format TESTS-<name>.xml
32
+ """
33
+ if not filename:
34
+ return 'TESTS-default.xml'
35
+
36
+ # Normalize path for the current OS and replace backslashes for consistency
37
+ normalized_path = os.path.normpath(filename).replace('\\', '/')
38
+ path_parts = normalized_path.split('/')
39
+
40
+ # Find 'features' directory
41
+ if 'features' in path_parts:
42
+ features_idx = path_parts.index('features')
43
+ # Use everything after 'features'
44
+ name_parts = path_parts[features_idx + 1:]
45
+ else:
46
+ # Fallback: use all parts but filter out empty ones and drive letters
47
+ name_parts = [part for part in path_parts if part and ':' not in part]
48
+
49
+ # Remove .feature extension from the last part
50
+ if name_parts and name_parts[-1].endswith('.feature'):
51
+ name_parts[-1] = name_parts[-1][:-8] # Remove '.feature'
52
+
53
+ # Join with dots, filter out empty parts
54
+ name = '.'.join(part for part in name_parts if part)
55
+
56
+ return f'TESTS-{name or "default"}.xml'
57
+
58
+
24
59
  def _export_feature_to_xml(feature, isobject=True):
25
60
  def get_scenarios(feature_):
26
61
  def flatter_scenarios(scenarios_list):
@@ -94,15 +129,19 @@ def _export_feature_to_xml(feature, isobject=True):
94
129
  parameters_template,
95
130
  )
96
131
  output_text = output_text.replace('status="Status.', 'status="')
97
- exp = re.compile('\\\\|/')
132
+
98
133
  filename = feature.filename if isobject else feature['filename']
99
134
  filename = text(filename)
100
- name = u'.'.join(exp.split(filename.partition('features')[2][1:-8]))
135
+
136
+ # Generate XML filename using the helper function
137
+ xml_filename = generate_junit_xml_filename(filename)
138
+
101
139
  junit_path = os.path.join(get_env('OUTPUT'), 'behave')
102
- path_output = os.path.join(junit_path, u'TESTS-' + name + u'.xml')
140
+
141
+ path_output = os.path.join(junit_path, xml_filename)
103
142
 
104
143
  retry_file_operation(
105
- path_output + '.xml', get_save_function(path_output, output_text)
144
+ path_output, get_save_function(path_output, output_text)
106
145
  )
107
146
 
108
147
 
@@ -12,6 +12,7 @@ including setup, execution, and reporting.
12
12
  from __future__ import absolute_import, print_function
13
13
 
14
14
  import codecs
15
+ import concurrent
15
16
  import copy
16
17
  import io
17
18
  import json
@@ -81,7 +82,23 @@ def main():
81
82
  Parses command-line arguments and initiates the test run.
82
83
  """
83
84
  args = sys.argv[1:]
84
- exit_code = run(args)
85
+ exit_code = 1 # Default to error
86
+ try:
87
+ exit_code = run(args)
88
+ except SystemExit as e:
89
+ # Handle cases where scenarios call exit() directly
90
+ if hasattr(e, 'code') and e.code is not None:
91
+ exit_code = e.code
92
+ else:
93
+ exit_code = 1
94
+ # Ensure we print the exit code even when scenarios crash
95
+ print('Exit code: {}'.format(exit_code))
96
+ except Exception as e:
97
+ # Handle unexpected exceptions
98
+ print(f'Unexpected error: {e}')
99
+ exit_code = 1
100
+ print('Exit code: {}'.format(exit_code))
101
+
85
102
  try:
86
103
  exit(exit_code)
87
104
  except:
@@ -483,7 +500,19 @@ def launch_by_feature(features,
483
500
  global_vars.progress_bar_instance,
484
501
  ))
485
502
  for parallel_process in parallel_processes:
486
- parallel_process.result()
503
+ try:
504
+ parallel_process.result()
505
+ except:
506
+ from concurrent.futures.process import BrokenProcessPool
507
+ e = sys.exc_info()[1]
508
+ if isinstance(e, BrokenProcessPool):
509
+ print_parallel('process.pool.broken.main', str(e))
510
+ # Mark as failed execution but continue processing remaining futures
511
+ execution_codes.append(1)
512
+ else:
513
+ print_parallel('parallel.process.error', str(e))
514
+ execution_codes.append(1)
515
+ parallel_processes.clear()
487
516
  return execution_codes, json_reports
488
517
 
489
518
 
@@ -511,6 +540,7 @@ def launch_by_scenario(features,
511
540
  duplicated_scenarios = {}
512
541
  total_scenarios_to_run = {}
513
542
  features_with_no_scen_desc = []
543
+ parallel_processes = []
514
544
  for features_path, scenarios in features.items():
515
545
  scenarios_instances = get_scenarios_instances(scenarios)
516
546
  for scenario in scenarios_instances:
@@ -569,7 +599,6 @@ def launch_by_scenario(features,
569
599
  global_vars.progress_bar_instance.update()
570
600
  if parallel_scenarios:
571
601
  print_parallel('scenario.running_parallels')
572
- parallel_processes = []
573
602
  for features_path in parallel_scenarios.keys():
574
603
  for scenario_information in parallel_scenarios[features_path]:
575
604
  scenarios_to_run_in_feature = total_scenarios_to_run[scenario_information["feature_filename"]]
@@ -594,7 +623,19 @@ def launch_by_scenario(features,
594
623
  global_vars.progress_bar_instance
595
624
  ))
596
625
  for parallel_process in parallel_processes:
597
- parallel_process.result()
626
+ try:
627
+ parallel_process.result()
628
+ except:
629
+ from concurrent.futures.process import BrokenProcessPool
630
+ e = sys.exc_info()[1]
631
+ if isinstance(e, BrokenProcessPool):
632
+ print_parallel('process.pool.broken.main', str(e))
633
+ # Mark as failed execution but continue processing remaining futures
634
+ execution_codes.append(1)
635
+ else:
636
+ print_parallel('parallel.process.error', str(e))
637
+ execution_codes.append(1)
638
+ parallel_processes.clear()
598
639
  return execution_codes, json_reports
599
640
 
600
641
 
@@ -55,6 +55,13 @@ def handle_execution_complete_callback(codes,
55
55
  try:
56
56
  tuple_values = future.result()
57
57
  except: # isort:skip
58
+ # Handle BrokenProcessPool and other exceptions gracefully
59
+ from concurrent.futures.process import BrokenProcessPool
60
+ e = sys.exc_info()[1]
61
+ if isinstance(e, BrokenProcessPool):
62
+ print_parallel('process.pool.broken', str(e))
63
+ else:
64
+ print_parallel('execution.callback.error', str(e))
58
65
  json_reports += []
59
66
  codes.append(1)
60
67
  if tuple_values:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behavex
3
- Version: 4.2.1
3
+ Version: 4.2.2
4
4
  Summary: Agile testing framework on top of Behave (BDD).
5
5
  Home-page: https://github.com/hrcorval/behavex
6
6
  Author: Hernan Rey
@@ -87,20 +87,20 @@ BehaveX now provides seamless integration with Allure, the popular test reportin
87
87
 
88
88
  ## Introduction
89
89
 
90
- **BehaveX** is a BDD testing solution designed to enhance your Behave-based testing workflow by providing additional features and performance improvements. It's particularly beneficial in the following scenarios:
90
+ **BehaveX** is a BDD testing solution built on top of the Python Behave library, orchestrating parallel test sessions to enhance your testing workflow with additional features and performance improvements. It's particularly beneficial in the following scenarios:
91
91
 
92
92
  - **Accelerating test execution**: Significantly reduce test run times through parallel execution by feature or scenario.
93
93
  - **Enhancing test reporting**: Generate comprehensive and visually appealing HTML and JSON reports for in-depth analysis and integration with other tools.
94
94
  - **Improving test visibility**: Provide detailed evidence, such as screenshots and logs, essential for understanding test failures and successes.
95
95
  - **Optimizing test automation**: Utilize features like test retries, test muting, and performance metrics for efficient test maintenance and analysis.
96
- - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and reporting.
96
+ - **Managing complex test suites**: Handle large test suites with advanced features for organization, execution, and comprehensive reporting through multiple formats and custom formatters.
97
97
 
98
98
  ## Features
99
99
 
100
100
  BehaveX provides the following features:
101
101
 
102
102
  - **Parallel Test Executions**: Execute tests using multiple processes, either by feature or by scenario.
103
- - **Enhanced Reporting**: Generate friendly HTML and JSON reports that can be exported and integrated with third-party tools.
103
+ - **Enhanced Reporting**: Generate comprehensive reports in multiple formats (HTML, JSON, JUnit) and utilize custom formatters like Allure for advanced reporting capabilities that can be exported and integrated with third-party tools.
104
104
  - **Evidence Collection**: Include images/screenshots and additional evidence in the HTML report.
105
105
  - **Test Logs**: Automatically compile logs generated during test execution into individual log reports for each scenario.
106
106
  - **Test Muting**: Add the `@MUTE` tag to test scenarios to execute them without including them in JUnit reports.
@@ -210,6 +210,9 @@ Execute BehaveX in the same way as Behave from the command line, using the `beha
210
210
  - **parallel-processes** (--parallel-processes): Specifies the number of parallel Behave processes.
211
211
  - **parallel-scheme** (--parallel-scheme): Performs parallel test execution by [scenario|feature].
212
212
  - **show-progress-bar** (--show-progress-bar): Displays a progress bar in the console during parallel test execution.
213
+ - **formatter** (--formatter): Specifies a custom formatter for test reports (e.g., Allure formatter).
214
+ - **formatter-outdir** (--formatter-outdir): Specifies the output directory for formatter results (default: output/allure-results for Allure).
215
+ - **no-formatter-attach-logs** (--no-formatter-attach-logs): Disables automatic attachment of scenario log files to formatter reports.
213
216
 
214
217
  ## Parallel Test Executions
215
218
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "behavex"
7
- version = "4.2.1"
7
+ version = "4.2.2"
8
8
  description = "Agile testing framework on top of Behave (BDD)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -6,7 +6,7 @@ with open('README.md', 'r') as fh:
6
6
 
7
7
  setup(
8
8
  name='behavex',
9
- version='4.2.1',
9
+ version='4.2.2',
10
10
  license="MIT",
11
11
  platforms=['any'],
12
12
  python_requires='>=3.5',
@@ -6,10 +6,8 @@ import tempfile
6
6
  from pathlib import Path
7
7
 
8
8
  from behave import given, then, when
9
-
10
- from tests.features.steps.execution_steps import (execute_command,
11
- get_random_number,
12
- root_project_path)
9
+ from execution_steps import (execute_command, get_random_number,
10
+ root_project_path)
13
11
 
14
12
  tests_features_path = os.path.join(root_project_path, 'tests', 'features')
15
13
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes