pytest-html-plus 0.4.8__tar.gz → 0.5.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytest-html-plus
3
- Version: 0.4.8
3
+ Version: 0.5.0
4
4
  Summary: Generate Actionable, automatic screenshots, unified Mobile friendly Pytest HTML report in less than 3 seconds — no hooks, merge plugins, no config, xdist-ready.
5
5
  License: MIT
6
6
  Keywords: pytest,pytest-html-plus,pytest-plugin,html-test-report,beautiful-test-report,shareable-test-results,test-report,test-results,unit-test-report,functional-test-report,test-summary,reporting,python-testing,automated-testing,test-runner,report-generator,continuous-integration,ci-cd,github-actions,jenkins,pytest-html,pytest-report
@@ -20,7 +20,7 @@ Project-URL: Source, https://github.com/reporterplus/pytest-html-plus
20
20
  Project-URL: Tracker, https://github.com/reporterplus/pytest-html-plus/issues
21
21
  Description-Content-Type: text/markdown
22
22
 
23
- ⚡ **Plug. Play. Debug without delay.**
23
+ ⚡ **Test your code, not your reporting setup.**
24
24
  > _Get started with rich pytest reports in under 3 seconds. Just install — no setup required. The simplest, fastest reporter for pytest._
25
25
 
26
26
  ## Get a self-contained, actionable, easy-to-read single page HTML unified reports summarizing all your test results — no hassle, just clarity. Detect **flaky tests**, **attach screenshots** automatically without hooks and optionally send reports via email**. Works beautifully with or without `xdist`.
@@ -46,6 +46,12 @@ If you don’t want the burden of installing pytest-html-plus manually and your
46
46
  [![🚀 Checkout on GitHub Marketplace](https://img.shields.io/badge/Marketplace-Pytest%20HTML%20Plus-blue?logo=github)](https://github.com/marketplace/actions/pytest-html-plus-action)
47
47
  [![Documentation](https://img.shields.io/badge/docs-readthedocs.io-brightgreen)](https://pytest-html-plus.readthedocs.io/en/main/marketplace/usage.html)
48
48
 
49
+ ## Pytest HTML Plus VSCode
50
+
51
+ [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/reporterplus.pytest-html-plus-vscode)]
52
+ [![Installs](https://img.shields.io/visual-studio-marketplace/i/reporterplus.pytest-html-plus-vscode)]
53
+ [![Docs](https://img.shields.io/badge/docs-online-blue)](https://pytest-html-plus.readthedocs.io/en/main/extensions/vscode/usage.html)
54
+
49
55
  ## ✨ Features
50
56
 
51
57
  #### 🧩 Seamless Combined XML Export to your favourite test management tools — No Plugins Needed
@@ -1,4 +1,4 @@
1
- ⚡ **Plug. Play. Debug without delay.**
1
+ ⚡ **Test your code, not your reporting setup.**
2
2
  > _Get started with rich pytest reports in under 3 seconds. Just install — no setup required. The simplest, fastest reporter for pytest._
3
3
 
4
4
  ## Get a self-contained, actionable, easy-to-read single page HTML unified reports summarizing all your test results — no hassle, just clarity. Detect **flaky tests**, **attach screenshots** automatically without hooks and optionally send reports via email**. Works beautifully with or without `xdist`.
@@ -24,6 +24,12 @@ If you don’t want the burden of installing pytest-html-plus manually and your
24
24
  [![🚀 Checkout on GitHub Marketplace](https://img.shields.io/badge/Marketplace-Pytest%20HTML%20Plus-blue?logo=github)](https://github.com/marketplace/actions/pytest-html-plus-action)
25
25
  [![Documentation](https://img.shields.io/badge/docs-readthedocs.io-brightgreen)](https://pytest-html-plus.readthedocs.io/en/main/marketplace/usage.html)
26
26
 
27
+ ## Pytest HTML Plus VSCode
28
+
29
+ [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/reporterplus.pytest-html-plus-vscode)]
30
+ [![Installs](https://img.shields.io/visual-studio-marketplace/i/reporterplus.pytest-html-plus-vscode)]
31
+ [![Docs](https://img.shields.io/badge/docs-online-blue)](https://pytest-html-plus.readthedocs.io/en/main/extensions/vscode/usage.html)
32
+
27
33
  ## ✨ Features
28
34
 
29
35
  #### 🧩 Seamless Combined XML Export to your favourite test management tools — No Plugins Needed
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pytest-html-plus"
3
- version = "0.4.8"
3
+ version = "0.5.0"
4
4
  description = "Generate Actionable, automatic screenshots, unified Mobile friendly Pytest HTML report in less than 3 seconds — no hooks, merge plugins, no config, xdist-ready."
5
5
  readme = "README.md"
6
6
  authors = ["reporterplus"]
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  from datetime import datetime
3
4
 
4
5
  from pytest_html_plus.utils import is_main_worker, get_env_marker, get_report_title, \
@@ -8,16 +9,18 @@ from pytest_html_plus.utils import is_main_worker, get_env_marker, get_report_ti
8
9
  def write_plus_metadata_if_main_worker(config, report_path, output_path="plus_metadata.json", **kwargs):
9
10
  if not is_main_worker():
10
11
  return
12
+ metadata_path = os.path.join(report_path, output_path)
11
13
  branch = kwargs.get("git_branch", "Pass --git-branch to populate git metadata")
12
14
  commit = kwargs.get("git_commit", "Pass --git-commit to populate git metadata")
13
15
  metadata = {
14
16
  "report_title": get_report_title(output_path=report_path),
15
- "environment": get_env_marker(config),
17
+ "environment": kwargs.get("rp_env") or get_env_marker(config),
16
18
  "branch": branch,
17
19
  "commit": commit,
18
20
  "python_version": get_python_version(),
19
21
  "generated_at": datetime.now().isoformat()
20
22
  }
21
- with open(output_path, "w") as f:
22
- print(metadata)
23
+ os.makedirs(report_path, exist_ok=True)
24
+
25
+ with open(metadata_path, "w") as f:
23
26
  json.dump(metadata, f, indent=2)
@@ -60,6 +60,7 @@ class JSONReporter:
60
60
  else:
61
61
  self.metadata = {}
62
62
 
63
+
63
64
  def log_result(
64
65
  self,
65
66
  test_name,
@@ -148,6 +149,7 @@ class JSONReporter:
148
149
  return os.path.join("screenshots", file)
149
150
  return None
150
151
 
152
+
151
153
  def generate_copy_button(self, content, label):
152
154
  if isinstance(content, list):
153
155
  # Convert list to string (for logs)
@@ -184,16 +186,35 @@ class JSONReporter:
184
186
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
185
187
  <style>
186
188
 
187
- body {{ font-family: Arial, sans-serif; padding: 1rem; background: #f2f4f7; }}
188
- .test {{ border: 1px solid #ddd; margin-bottom: 0.5rem; border-radius: 5px; background: #ffffff; }}
189
+ body {{
190
+ font-family: Arial, sans-serif;
191
+ padding: 1rem;
192
+ background: #FAF7F2;
193
+ }}
194
+ .test {{
195
+ border: 1px solid #D9C3A5;
196
+ margin-bottom: 0.5rem;
197
+ border-radius: 5px;
198
+ background: #FDFBF7;
199
+ }}
189
200
  .header {{ padding: 0.5rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; }}
190
- .header.passed {{ background: #e6f4ea; color: #2f7a33; }}
191
- .header.failed {{ background: #fdecea; color: #a83232; }}
192
- .header.skipped {{ background: #fff8e1; color: #b36b00; }}
201
+ .header.passed {{
202
+ background: #E1F3E8;
203
+ color: #1E6B3A;
204
+ border-left: 4px solid #2E7D32;
205
+ }}
206
+ .header.failed {{
207
+ background: #FBE4E4;
208
+ color: #8B1E1E;
209
+ border-left: 4px solid #C62828;
210
+ }}
211
+ .header.skipped {{
212
+ background: #fff8e1;
213
+ color: #b36b00;
214
+ }}
193
215
  .header.error {{
194
- background: #fdecea; /* light red / pink */
195
- color: #b71c1c; /* deep red */
196
- border-left: 4px solid #d32f2f;
216
+ background: #fdecea;
217
+ color: #b71c1c;
197
218
  }}
198
219
  .details {{ padding: 0.5rem 1rem; display: none; border-top: 1px solid #ddd; }}
199
220
  .toggle::before {{ content: "▶"; display: inline-block; margin-right: 0.5rem; transition: transform 0.3s ease; }}
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import pytest
7
7
  import json
8
+ import sys
8
9
 
9
10
  from pytest_html_plus.compute_report_metadata import write_plus_metadata_if_main_worker
10
11
  from pytest_html_plus.extract_link import extract_links_from_item
@@ -36,6 +37,19 @@ def pytest_runtest_setup(item):
36
37
  if "caplog" not in item.fixturenames:
37
38
  item.fixturenames.append("caplog")
38
39
 
40
+ import warnings
41
+
42
+ def _warn_python_39_deprecation():
43
+ if sys.version_info[:2] == (3, 9):
44
+ warnings.warn(
45
+ "pytest-html-plus is not actively tested in Python 3.9 and support will be dropped in v0.5.1. "
46
+ "Please upgrade to Python 3.10+.",
47
+ DeprecationWarning,
48
+ stacklevel=2,
49
+ )
50
+
51
+ _warn_python_39_deprecation()
52
+
39
53
 
40
54
  @pytest.hookimpl(hookwrapper=True)
41
55
  def pytest_runtest_makereport(item, call):
@@ -113,78 +127,123 @@ import subprocess
113
127
 
114
128
 
115
129
  def pytest_sessionfinish(session, exitstatus):
116
- reporter = session.config._json_reporter
117
-
118
- json_path = session.config.getoption("--json-report") or "final_report.json"
119
- html_output = session.config.getoption("--html-output") or "report_output"
120
- screenshots_path = session.config.getoption("--screenshots") or "screenshots"
121
- xml_path = session.config.getoption("--xml-report") or "final_xml.xml"
122
-
123
- is_worker = os.getenv("PYTEST_XDIST_WORKER") is not None
124
- try:
125
- is_xdist = bool(session.config.getoption("-n"))
126
- except ValueError:
127
- is_xdist = False
128
-
129
- if is_worker:
130
- reporter.write_report()
131
- print(f"Worker {os.getenv('PYTEST_XDIST_WORKER')} finished – skipping merge.")
132
- return
133
-
134
- if is_xdist:
135
- merge_json_reports(directory=".pytest_worker_jsons", output_path=json_path)
136
- else:
137
- reporter.results = mark_flaky_tests(reporter.results)
138
- reporter.write_report()
139
-
140
- script_path = os.path.join(os.path.dirname(__file__), "generate_html_report.py")
141
-
142
- if not os.path.exists(script_path):
143
- logger.warning(f"Report generation script not found at {script_path}. Skipping HTML report generation.")
144
- return
145
-
146
- try:
147
- subprocess.run([
148
- sys.executable,
149
- script_path,
150
- "--report", json_path,
151
- "--screenshots", screenshots_path,
152
- "--output", html_output
153
- ], check=True)
154
- except Exception as e:
155
- raise RuntimeError(f"Exception during HTML report generation: {e}") from e
130
+ reporter = session.config._json_reporter
156
131
 
157
- if session.config.getoption("--plus-email"):
158
- print("📬 --plus-email enabled. Sending report...")
159
- try:
160
- config = load_email_env()
161
- config["report_path"] = f"{html_output}"
162
- sender = EmailSender(config, report_path=config["report_path"])
163
- sender.send()
164
- except Exception as e:
165
- raise RuntimeError(f"Failed to send email: {e}") from e
132
+ raw_json_report = session.config.getoption("--json-report")
133
+ html_output = session.config.getoption("--html-output") or "report_output"
134
+ screenshots_path = session.config.getoption("--screenshots") or "screenshots"
135
+ raw_xml_report = session.config.getoption("--xml-report")
136
+
137
+ # ---- XML filename validation ----
138
+ if raw_xml_report:
139
+ if os.path.basename(raw_xml_report) != raw_xml_report:
140
+ raise pytest.UsageError("--xml-report must be a filename, not a path")
141
+ xml_filename = raw_xml_report
142
+ else:
143
+ xml_filename = "final_xml.xml"
144
+
145
+ xml_path = os.path.join(html_output, xml_filename)
146
+
147
+ os.makedirs(html_output, exist_ok=True)
148
+
149
+ # ---- JSON filename validation ----
150
+ if raw_json_report:
151
+ if os.path.basename(raw_json_report) != raw_json_report:
152
+ raise pytest.UsageError("--json-report must be a filename, not a path")
153
+ json_filename = raw_json_report
154
+ else:
155
+ json_filename = "final_report.json"
156
+
157
+ json_path = os.path.join(html_output, json_filename)
158
+ reporter.report_path = json_path
159
+
160
+ is_worker = os.getenv("PYTEST_XDIST_WORKER") is not None
161
+ try:
162
+ is_xdist = bool(session.config.getoption("-n"))
163
+ except ValueError:
164
+ is_xdist = False
165
+
166
+ # ---- Worker behavior ----
167
+ if is_worker:
168
+ worker_id = os.getenv("PYTEST_XDIST_WORKER")
169
+ worker_dir = ".pytest_worker_jsons"
170
+ os.makedirs(worker_dir, exist_ok=True)
171
+
172
+ reporter.report_path = os.path.join(
173
+ worker_dir,
174
+ f"{worker_id}.json"
175
+ )
166
176
 
167
- open_html_report(report_path=f"{html_output}/report.html",json_path=json_path, config=session.config)
177
+ reporter.write_report()
178
+ return
168
179
 
169
- if session.config.getoption("--generate-xml"):
170
- try:
171
- json_path = reporter.report_path
172
- convert_json_to_junit_xml(json_path, xml_path)
173
- print(f"XML report generated: {xml_path}")
174
- except Exception as e:
175
- raise RuntimeError(f"Failed to generate XML report: {e}") from e
180
+ # ---- Controller behavior ----
181
+ if is_xdist:
182
+ merge_json_reports(directory=".pytest_worker_jsons", output_path=json_path)
183
+ else:
184
+ reporter.results = mark_flaky_tests(reporter.results)
185
+ reporter.write_report()
176
186
 
187
+ script_path = os.path.join(os.path.dirname(__file__), "generate_html_report.py")
188
+ if not os.path.exists(script_path):
189
+ logger.warning(
190
+ f"Report generation script not found at {script_path}. Skipping HTML report generation."
191
+ )
192
+ return
193
+
194
+ try:
195
+ subprocess.run([
196
+ sys.executable,
197
+ script_path,
198
+ "--report", json_path,
199
+ "--screenshots", screenshots_path,
200
+ "--output", html_output
201
+ ], check=True)
202
+ except Exception as e:
203
+ raise RuntimeError(f"Exception during HTML report generation: {e}") from e
204
+
205
+ # ---- Generate XML ----
206
+ if session.config.getoption("--generate-xml"):
207
+ try:
208
+ convert_json_to_junit_xml(json_path, xml_path)
209
+ print(f"XML report generated: {xml_path}")
210
+ except Exception as e:
211
+ raise RuntimeError(f"Failed to generate XML report: {e}") from e
212
+
213
+ if not os.getenv("PYTEST_XDIST_WORKER"):
214
+ if os.path.exists(screenshots_path):
215
+ try:
216
+ shutil.rmtree(screenshots_path)
217
+ except Exception:
218
+ logger.warning("Could not clean up screenshots directory")
219
+
220
+ if session.config.getoption("--plus-email"):
221
+ try:
222
+ config = load_email_env()
223
+ config["report_path"] = html_output
224
+ sender = EmailSender(config, report_path=html_output)
225
+ sender.send()
226
+ except Exception as e:
227
+ raise RuntimeError(f"Failed to send email: {e}") from e
228
+
229
+ # ---- Open report (controller only) ----
230
+ open_html_report(
231
+ report_path=os.path.join(html_output, "report.html"),
232
+ json_path=json_path,
233
+ config=session.config
234
+ )
177
235
 
178
236
  def pytest_sessionstart(session):
179
237
  html_output = session.config.getoption("--html-output") or "report_output"
180
238
  git_branch = session.config.getoption("--git-branch") or "Pass --git-branch to populate git metadata"
181
239
  git_commit = session.config.getoption("--git-commit") or "Pass --git-commit to populate git metadata"
240
+ rp_env = session.config.getoption("--rp-env") or "Pass --rp-env <name> to populate environment"
182
241
  configure_logging()
183
242
  session.config.addinivalue_line(
184
243
  "markers", "link(url): Add a link to external test case or documentation."
185
244
  )
186
245
  write_plus_metadata_if_main_worker(session.config, report_path=html_output,
187
- git_branch=git_branch, git_commit=git_commit)
246
+ git_branch=git_branch, git_commit=git_commit, rp_env=rp_env)
188
247
 
189
248
 
190
249
  def pytest_load_initial_conftests(args):
@@ -193,75 +252,75 @@ def pytest_load_initial_conftests(args):
193
252
 
194
253
 
195
254
  def pytest_addoption(parser):
196
- parser.addoption(
197
- "--json-report",
198
- action="store",
199
- default="final_report.json",
200
- help="Directory to save individual JSON test reports"
201
- )
202
- parser.addoption(
255
+ group = parser.getgroup(
256
+ "pytest-html-plus",
257
+ "pytest-html-plus reporting options"
258
+ )
259
+
260
+
261
+ group.addoption(
262
+ "--json-report",
263
+ action="store",
264
+ default="final_report.json",
265
+ help="Name of the JSON report file generated alongside the HTML report"
266
+ )
267
+ group.addoption(
203
268
  "--capture-screenshots",
204
269
  action="store",
205
270
  default="failed",
206
271
  choices=["failed", "all", "none"],
207
272
  help="Capture screenshots: failed (default), all, or none"
208
273
  )
209
- parser.addoption("--html-output", default="report_output")
210
- parser.addoption("--screenshots", default="screenshots")
211
- parser.addoption(
274
+ group.addoption("--html-output", default="report_output")
275
+ group.addoption("--screenshots", default="screenshots")
276
+ group.addoption(
212
277
  "--plus-email",
213
278
  action="store_true",
214
279
  default=False,
215
280
  help="Send HTML test report via email after test run"
216
281
  )
217
- parser.addoption(
282
+ group.addoption(
218
283
  "--detect-flake",
219
284
  action="store",
220
285
  default=False,
221
286
  help="Helps capture flaky tests in the last n number of builds"
222
287
  )
223
- parser.addoption(
288
+ group.addoption(
224
289
  "--should-open-report",
225
290
  action="store",
226
291
  default="failed",
227
292
  choices=["always", "failed", "never"],
228
293
  help="When to open the HTML report: always, failed, or never (default: failed)",
229
294
  )
230
- parser.addoption(
295
+ group.addoption(
231
296
  "--generate-xml",
232
297
  action="store_true",
233
298
  default=False,
234
299
  help="Generate JUnit-style XML from the final JSON report"
235
300
  )
236
- parser.addoption(
301
+ group.addoption(
237
302
  "--xml-report",
238
303
  action="store",
239
304
  default=None,
240
- help="Path to output the XML report (used with --generatexml)"
305
+ help="Name of the XML report file generated alongside the HTML report (used with --generate-xml)"
241
306
  )
242
- parser.addoption(
307
+ group.addoption(
243
308
  "--git-branch",
244
309
  action="store",
245
310
  default="Pass --git-branch to populate git metadata",
246
311
  help="Helps show branch information on the report"
247
312
  )
248
- parser.addoption(
313
+ group.addoption(
249
314
  "--git-commit",
250
315
  action="store",
251
316
  default="Pass --git-commit to populate git metadata",
252
317
  help="Helps show commitId information on the report"
253
318
  )
254
- parser.addoption(
255
- "--env",
256
- action="store",
257
- default="Pass --env or --environment <name> to populate environment",
258
- help="Helps show commitId information on the report"
259
- )
260
- parser.addoption(
261
- "--environment",
319
+ group.addoption(
320
+ "--rp-env",
262
321
  action="store",
263
- default="Pass --env or --environment <name> to populate environment",
264
- help="Helps show commitId information on the report"
322
+ default="Pass --rp-env to populate environment",
323
+ help="Helps show env information on the report"
265
324
  )
266
325
 
267
326
  import logging
@@ -361,6 +420,7 @@ def open_html_report(report_path: str, json_path: str, config) -> None:
361
420
 
362
421
  results = report_data.get("results", [])
363
422
 
423
+
364
424
  has_failures = any(
365
425
  t.get("status") == "failed" or t.get("error")
366
426
  for t in results
@@ -7,7 +7,7 @@ def get_env_marker(config):
7
7
  for arg in ("--env", "--environment"):
8
8
  if config.getoption(arg.lstrip("-").replace("-", "_"), default=None):
9
9
  return config.getoption(arg.lstrip("-").replace("-", "_"))
10
- return "Pass --env <name> to populate environment"
10
+ return "Pass --rp-env to populate environment"
11
11
 
12
12
  def get_report_title(output_path):
13
13
  report_path = output_path