ni pour indenter du code, ni pour structurer une arborescence comme tu le voudrais dans un report de tests.
-
-Si tu veux faire une indentation réelle de texte (genre un tableau, ou une hiérarchie), il faut utiliser :
-
-Des espaces (fonctionnent seulement dans des blocs ou de code)
-
-Ou des imbriqués (comme déjà vu)
-
-Ou des blockquotes imbriqués (pour un effet citation, pas arborescence)
-
-Tu veux un exemple visuel dans le contexte d’un report de test ou autre ? Dis-le-moi !
-
-
-
-
-
-
-
-
-You said:
-et ca permet d indenter non ? on peut pas faire ca pour les details ? des citations de citation de citation ... ?
-
-
-ChatGPT said:
-Tu peux imbriquer les blockquotes (> > > ...) en Markdown,
-et ça va décaler le texte à droite à chaque niveau avec une barre supplémentaire —
-MAIS :
-
-Ça ne s’applique qu’au texte, pas aux blocs .
-
-Tu ne peux pas “imbriquer” des à l’intérieur de citations Markdown (blockquote) :
-le HTML à l’intérieur d’un blockquote > ne sera pas interprété correctement sur la plupart des plateformes (notamment GitHub, VSCode, etc).
-
-Les blockquotes ne sont PAS repliables, contrairement à .
-
-Exemple d’imbrication de blockquotes :
-markdown
-Copy
-Edit
-> Premier niveau
->
-> > Deuxième niveau
-> >
-> > > Troisième niveau
-> > >
-> > > > Quatrième niveau
-Rendu Markdown :
-
-Premier niveau
-
-Deuxième niveau
-
-Troisième niveau
-
-Quatrième niveau
-
-Mais… pour
-On ne peut PAS écrire :
-
-markdown
-Copy
-Edit
->
-> Test
-> Contenu
->
-Ça ne sera pas interprété comme tu veux (ça peut s’afficher comme du texte brut, ou être cassé selon le moteur de rendu).
-
-Si tu imbriques dans
-Ça marche !
-
-Mais pas d’indentation visuelle, juste des blocs repliables imbriqués les uns dans les autres.
-
-Pour de la VRAIE indentation visuelle :
-Blockquote : ok pour du texte, PAS pour du HTML ni pour .
-
- imbriqué : ok pour l’interactivité, PAS pour l’indentation visible.
-
-Pas de solution native Markdown pour “indenter” un bloc avec une barre verticale.
-
-Conclusion rapide
-Oui, tu peux faire des citations imbriquées pour le texte ➔ effet indentation + barre grise
-
-Non, tu ne peux pas faire ça pour des ➔ aucune indentation visuelle avec blockquote
-
-Les deux sont incompatibles (blockquote ne “prend” pas les blocs HTML dans le markdown standard).
-
-Tu veux un exemple de ce que ça donne si on force dans un blockquote ?
-Ou tu veux un exemple “Markdown pur” avec seulement du texte indenté ?
-Dis-moi, je te le montre !
-
-
-
-
-
-
-
-
-You said:
-ok bin alors donne des titres a tous les grps de details comme ca ca decale le titre et apres on a tous les details tu vois ou pas ?
-
-
-ChatGPT said:
-Oui, je vois très bien :
-Tu veux que chaque niveau de ait un titre explicite dans son ,
-pour que l’arborescence logique (fichier → fonction → test, etc.)
-soit visible, même si ce n’est pas “décalé” visuellement.
-Ça permet de s’y retrouver facilement :
-Titre du groupe > titre du fichier > titre de la fonction > détail du test.
-
-Exemple Markdown avec titres explicites à chaque niveau
-markdown
-Copy
-Edit
-
-📋 STATUS: Passed (10)
-
-
- 📁 File: test_math_utils.py
-
-
- 🔧 Function: test_addition
-
-
- Test #1
- Résultat, logs, etc.
-
-
-
-
-
-Ce que ça donne :
-
-Tu cliques sur “STATUS: Passed (10)” pour ouvrir la liste de fichiers.
-
-Tu cliques sur un fichier (“File: test_math_utils.py”) pour voir ses fonctions.
-
-Tu cliques sur une fonction pour voir ses différents tests/détails.
-
-Ça reste très lisible :
-
-Même si tout n’est pas “décalé”,
-
-Tu as toujours le contexte : tu vois à chaque fois dans quel groupe/fichier/fonction tu es.
-
-Conseil de lisibilité
-Mets des emojis (📁, 🔧, 🧪, etc.) ou des tags [File], [Function], [Test] dans les .
-
-Mets bien chaque
à la ligne pour que la structure reste claire.
-
-Si tu veux, ajoute le numéro de test ou l’état (passed, failed) dans chaque summary.
-
-Exemple plus complexe, prêt à copier-coller :
-markdown
-Copy
-Edit
-
-✅ PASSED (12 tests)
-
-
- 📁 File: test_io_utils.py
-
-
- 🔧 Function: test_read_file
-
-
- Test #1
- - Status: ✅ passed
- - Duration: 0.001s
- - Détails, logs...
-
-
-
-
-
- 🔧 Function: test_write_file
-
- Test #1
- ...
-
-
-
-
-
-Pourquoi ça marche bien ?
-Tu peux t’y retrouver à chaque niveau (le titre indique le contexte)
-
-Tu évites de scroller inutilement ou d’ouvrir trop de détails en aveugle.
-
-Tu as l’effet “explorateur de fichiers” sans indentation, mais avec la logique de groupe/titre.
-
-Implémentation dans ton script
-Pour chaque niveau (status, file, function, test),
-mets un ...
... avec un titre explicite.
-
-Structure la génération comme tu fais déjà, mais pense à donner un vrai titre de groupe à chaque summary.
-
-Si tu veux que je te génère un template Python tout fait, ou un exemple plus grand, dis-le-moi !
-Mais tu as 100% la bonne intuition pour améliorer la lisibilité.
-
-
-
-
-
-
-
-
-You said:
-genre t appuie sur statue tu sors un titre que tu fous en citation et tu mets tous les details d apres a la suite du titre dans la citation et ainsi de suite non ?
-
-
-ChatGPT said:
-Oui, tu peux faire ça !
-Mais :
-
-Ça ne te donnera pas une indentation “graphique” (pas de vrai arbre décalé à droite),
-
-Mais ça mettra le titre de chaque groupe (status, file, fonction, etc.) en citation avec tous les détails dessous, ce qui rend chaque “bloc” bien visible.
-
-Exemple concret :
-Format Markdown :
-markdown
-Copy
-Edit
-
-✅ PASSED (10)
-
-> ### 📁 test_math_utils.py
->
-> - 🔧 test_addition
-> - Résultat : Passed
-> - Durée : 0.002s
->
-> - 🔧 test_subtraction
-> - Résultat : Passed
-> - Durée : 0.001s
->
-> ### 📁 test_io_utils.py
->
-> - 🔧 test_read_file
-> - Résultat : Passed
-> - Durée : 0.001s
->
-> - 🔧 test_write_file
-> - Résultat : Passed
-> - Durée : 0.002s
->
-
-Effet obtenu :
-Quand tu ouvres le détail “PASSED (10)”,
-tu vois chaque titre de fichier (ici en citation, donc décalé avec une barre verticale).
-
-Tous les tests de ce fichier sont listés juste dessous, dans la même citation.
-
-Tu peux faire la même chose pour les “FAILED”, “SKIPPED”, etc.
-
-À retenir :
-Oui, tu peux faire comme ça :
-
-Un Status...
-
-Dans le bloc, une ou plusieurs citations Markdown (avec >) pour grouper les tests du même fichier ou même fonction.
-
-Ça reste lisible, compact, et groupé par titre (même si pas une vraie indentation arborescente).
-
-Et c’est facile à automatiser côté Python !
-
-Astuce
-Mets un saut de ligne entre chaque citation pour aérer,
-
-Ajoute des emojis ou titres [File], [Function] pour bien distinguer chaque bloc.
-
-Template Python pour générer ça :
-python
-Copy
-Edit
-f.write("\n✅ PASSED ({})
\n\n".format(len(passed_tests)))
-for filename, tests in tests_grouped_by_file.items():
- f.write(f"> ### 📁 {filename}\n>\n")
- for test in tests:
- f.write(f"> - 🔧 {test['function_name']}\n")
- f.write(f"> - Résultat : {test['status']}\n")
- f.write(f"> - Durée : {test['duration']}\n>\n")
- f.write(">\n")
-f.write(" \n\n")
-En bref : oui, ce que tu proposes marche très bien, et c’est simple à faire.
-Ça donne une “impression” de groupe/indentation grâce à la citation Markdown.
-Si tu veux le code Python complet adapté à ta structure, demande !
-
-
-
-
-
-
-
-
-You said:
-from collections import defaultdict
-from datetime import datetime
-import json
-import argparse
-import os
-import re
-import pytest
-from pytest import ExitCode
-import traceback
-
-def stringify(obj):
- if obj is None or obj == "":
- return "None"
- if isinstance(obj, list):
- return ', '.join(stringify(e) for e in obj)
- if isinstance(obj, dict):
- return ', '.join(f"{k}: {stringify(v)}" for k, v in obj.items())
- return f"{str(obj)}"
-
-def normalize_nodeid(nodeid):
- """Convert pytest nodeid to Allure fullName format"""
- match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
- if match:
- file_part = match.group(1).replace("/", ".").replace("\\", ".")
- func_part = match.group(2)
- return f"{file_part}#{func_part}"
- return None
-
-def write_details(f, summary, body, level=0):
- margin = 18 * level # 18px de marge/indent par niveau
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- f.write(f'\n')
- f.write(f"\n{summary}
\n\n")
- f.write(body)
- f.write(" \n")
- f.write("
\n\n")
-
-def load_allure_metadata(allure_test_cases_dir):
- allure_data = {}
- if not os.path.exists(allure_test_cases_dir):
- print(f"❌ Allure directory not found: {allure_test_cases_dir}")
- return allure_data
-
- print(f"Loading Allure files from: {allure_test_cases_dir}")
- for file in os.listdir(allure_test_cases_dir):
- # Ignore temp/hidden/invalid files, just in case
- if not file.endswith(".json") or file.startswith("."):
- continue
- path = os.path.join(allure_test_cases_dir, file)
- # Si le fichier est vide ou absent (par exemple pas collecté), on skippe
- if not os.path.isfile(path) or os.path.getsize(path) == 0:
- print(f"Allure file: {file} skipped (not found or empty).")
- continue
- try:
- with open(path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- full_name = data.get("fullName")
- if not full_name:
- print(f"Allure file: {file} skipped (no fullName).")
- continue
- params = data.get("parameters", [])
- severity = data.get("extra", {}).get("severity", None)
- allure_data[full_name] = {
- "parameters": params,
- "severity": severity
- }
- print(f"Allure file: {file} loaded ✔️")
-
-
- except Exception as e:
- short_summary = traceback.format_exception_only(type(e), e)[-1].strip()
- print(f"Allure file: {file} failed ❌ (reason: {short_summary})")
-
- return allure_data
-
-
-def json_to_md_nested(json_path, md_path, allure_dir=None):
- with open(json_path) as f:
- data = json.load(f)
-
- allure_data = load_allure_metadata(allure_dir) if allure_dir else {}
-
- def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- body_test = f"- **Status:** {emoji} {status}\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** {duration:.6f} s\n" if duration else "- **Duration:** None\n"
-
- full_name = normalize_nodeid(nodeid)
- allure_info = allure_data.get(full_name)
- if allure_info:
- if allure_info["parameters"]:
- body_test += "- **Parameters (Allure):**\n"
- for param in allure_info["parameters"]:
- name = param.get("name")
- val = param.get("value")
- body_test += f" - {name} = {val}\n"
- body_test += "\n"
- if allure_info["severity"]:
- body_test += f"- **Severity:** {allure_info['severity']}\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "
-\n"
- details_body += stringify(value) + "\n"
- details_body += "
-\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1)
-
- def get_details_block(summary, body, level=0):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
- with open(md_path, 'w', encoding='utf-8') as f:
- f.write(f"# 🧪 Test Report\n")
- f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
-
- if 'summary' in data:
- f.write("## 📋 Summary\n")
- for key, value in data['summary'].items():
- f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
- duration = data.get("duration")
- f.write(f"- **Total Duration**: {duration:.3f}s\n" if duration else "- **Total Duration**: None\n")
- f.write("\n")
-
- # --------- Tests section ----------
- if "tests" in data and "summary" in data:
- test_counter = 1
- for test in data["tests"]:
- test['global_number'] = test_counter
- test_counter += 1
-
- f.write("## 🔎 Tests\n")
-
- summary_items = list(data["summary"].items())
- total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
- status_order = [k for k, _ in summary_items[:total_index]]
-
- tests_by_status = defaultdict(list)
- for test in data["tests"]:
- outcome = test.get("outcome", "unknown")
- tests_by_status[outcome].append(test)
-
- for status in status_order:
- if status not in tests_by_status:
- continue
-
- count = len(tests_by_status[status])
- emoji = "✅" if status == "passed" else "❌"
- status_label = status.capitalize().replace('_', ' ')
-
- body_status = ""
- grouped = defaultdict(lambda: defaultdict(list))
- for test in tests_by_status[status]:
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
-
- for filename, funcs in grouped.items():
- body_file = ""
- for funcname, tests in funcs.items():
- body_func = ""
- for test in sorted(tests, key=lambda x: x['global_number']):
- body_func += make_test_block(test, status, emoji, level=3)
- body_file += get_details_block(f"🔧 Function: {funcname}", body_func, level=2)
- body_status += get_details_block(f"📁 {filename}", body_file, level=1)
- f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
-
- # ---------- Collectors section -----------
- if "collectors" in data:
- f.write("## 📚 Collected files\n")
- grouped = defaultdict(list)
- for collector in data["collectors"]:
- nodeid = collector.get("nodeid", "unknown")
- path = nodeid.split("::")[0]
- main_folder = path.split("/")[0] if "/" in path else path
- grouped[main_folder].append(collector)
-
- for folder, collectors in grouped.items():
- has_fail = any(c.get("outcome") != "passed" for c in collectors)
- folder_emoji = "✅" if not has_fail else "❌"
-
- body_collectors = ""
- outputs = []
- for collector in collectors:
- outcome = collector.get("outcome", "unknown")
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
- results = collector.get("result", [])
- if outcome != "passed" and results:
- outputs.append(f"### ❌ {short_node}\n
-\n" + "\n".join(
- f"{k}: {v}" if isinstance(item, dict) else str(item)
- for item in results
- for k, v in item.items() if isinstance(item, dict)
- ) + "\n
-")
-
- if outputs:
- body_collectors += "### 🧾 Error or Result Summary\n\n"
- for out in outputs:
- body_collectors += out + "\n"
-
- collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
- folder_body = ""
- for collector in collectors_sorted:
- outcome = collector.get("outcome", "unknown")
- emoji = "✅" if outcome == "passed" else "❌"
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
-
- body_coll = f"- **Outcome:** {outcome}\n"
- other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
- if other_keys:
- body_coll += "- **Details:**\n"
- body_coll += "
-\n"
- for k, v in other_keys.items():
- body_coll += f"{k}:\n"
- if v is None:
- body_coll += " None\n"
- else:
- body_coll += stringify(v) + "\n"
- body_coll += "\n"
- body_coll += "
-\n"
- else:
- body_coll += "- **Details:** None\n"
- folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
- f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1))
-
- # ---------- Warnings section -----------
- if 'warnings' in data and data['warnings']:
- f.write("## ⚠️ Warnings\n\n")
- for i, warning in enumerate(data['warnings'], 1):
- warn_body = "
-\n"
- for k, v in warning.items():
- warn_body += f"{k}: {v}\n"
- warn_body += "
-\n"
- f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
-
-def run_pytest_and_generate_banner_with_logs(md_path, log_path):
- exit_code = pytest.main(["tests/"])
-
- if exit_code == 0 or exit_code == 1:
- return
-
- if exit_code == 2:
- banner = (
- "⚠️ **Test execution interrupted**\n\n"
- "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
- )
- elif exit_code == 3:
- banner = (
- "🛑 **Internal error during testing**\n\n"
- "> An internal error occurred while executing the tests.\n\n"
- )
- elif exit_code == 4:
- banner = (
- "❗ **Pytest command line usage error**\n\n"
- "> There was an error in how pytest was invoked.\n\n"
- )
- elif exit_code == 5:
- banner = (
- "❗ **No tests were collected**\n\n"
- "> Pytest did not find any tests to run.\n\n"
- )
- else:
- banner = (
- f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
- "> Unexpected result during test execution.\n\n"
- )
-
- try:
- with open(log_path, "r") as lf:
- log_lines = lf.readlines()
- except Exception as e:
- print(f"❌ Could not read log file: {e}")
- return
-
- short_summary_lines = []
- in_summary = False
- for line in log_lines:
- if "short test summary info" in line.lower():
- in_summary = True
- if in_summary:
- short_summary_lines.append(line)
- if re.match(r"=+.* in .*s =+", line):
- break
-
- try:
- with open(md_path, "r") as f:
- original_md = f.read()
- except Exception as e:
- print(f"❌ Could not read markdown report: {e}")
- return
-
- full_banner = (
- banner +
- "\n📋 Short test summary info
\n\n" +
- "
-\n" + "".join(short_summary_lines) + "
-\n \n\n" +
- "\n🪵 Full raw pytest log
\n\n" +
- "
-\n" + "".join(log_lines) + "
-\n \n\n" +
- "---\n\n"
- )
-
- try:
- with open(md_path, "w") as f:
- f.write(full_banner + original_md)
- print("✅ Banner and log summary added to markdown report.")
- except Exception as e:
- print(f"❌ Failed to update markdown report: {e}")
-
-def main():
- parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
- parser.add_argument("--input", required=True, help="Path to pytest JSON file")
- parser.add_argument("--output", required=True, help="Path to output Markdown file")
- parser.add_argument("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
- parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
- parser.add_argument("--json", required=False, help="Path to pytest-report.json")
-
- args = parser.parse_args()
-
- json_to_md_nested(args.input, args.output, args.allure_dir)
- run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
-
- print(f"✅ Report generated at {args.output}")
-
-if __name__ == "__main__":
- main()
-
-vsy changes ce code en ca
-
-
-ChatGPT said:
-Bien sûr !
-Je vais réorganiser ton code pour :
-
-Générer un report markdown où chaque bloc a un titre explicite,
-
-Regrouper par status > fichier > fonction > test, chaque niveau ayant un clair (avec emoji/titre explicite),
-
-Garder la structure HTML/CSS simple, mais bien lisible, sans “vraie” indentation Markdown mais une logique claire par niveaux,
-
-Corriger les petites incohérences, simplifier, et te donner un template ultra propre.
-
-🔥 Version propre & directe (à copier-coller) :
-python
-Copy
-Edit
-from collections import defaultdict
-from datetime import datetime
-import json
-import argparse
-import os
-import re
-import pytest
-import traceback
-
-def stringify(obj):
- if obj is None or obj == "":
- return "`None`"
- if isinstance(obj, list):
- return ', '.join(stringify(e) for e in obj)
- if isinstance(obj, dict):
- return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items())
- return f"`{str(obj)}`"
-
-def normalize_nodeid(nodeid):
- match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
- if match:
- file_part = match.group(1).replace("/", ".").replace("\\", ".")
- func_part = match.group(2)
- return f"{file_part}#{func_part}"
- return None
-
-def load_allure_metadata(allure_test_cases_dir):
- allure_data = {}
- if not allure_test_cases_dir or not os.path.exists(allure_test_cases_dir):
- print(f"❌ Allure directory not found: {allure_test_cases_dir}")
- return allure_data
- print(f"Loading Allure files from: {allure_test_cases_dir}")
- for file in os.listdir(allure_test_cases_dir):
- if not file.endswith(".json") or file.startswith("."):
- continue
- path = os.path.join(allure_test_cases_dir, file)
- if not os.path.isfile(path) or os.path.getsize(path) == 0:
- print(f"Allure file: {file} skipped (not found or empty).")
- continue
- try:
- with open(path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- full_name = data.get("fullName")
- if not full_name:
- print(f"Allure file: {file} skipped (no fullName).")
- continue
- allure_data[full_name] = {
- "parameters": data.get("parameters", []),
- "severity": data.get("extra", {}).get("severity", None)
- }
- print(f"Allure file: {file} loaded ✔️")
- except Exception as e:
- short_summary = traceback.format_exception_only(type(e), e)[-1].strip()
- print(f"Allure file: {file} failed ❌ (reason: {short_summary})")
- return allure_data
-
-def get_details_block(summary, body, level=0):
- # Level = profondeur (pour margin visuelle, mais ça reste vertical)
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
-def json_to_md_nested(json_path, md_path, allure_dir=None):
- with open(json_path) as f:
- data = json.load(f)
- allure_data = load_allure_metadata(allure_dir) if allure_dir else {}
-
- def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration**: `None`\n"
-
- full_name = normalize_nodeid(nodeid)
- allure_info = allure_data.get(full_name)
- if allure_info:
- if allure_info["parameters"]:
- body_test += "- **Parameters (Allure):**\n"
- for param in allure_info["parameters"]:
- name = param.get("name")
- val = param.get("value")
- body_test += f" - `{name}` = `{val}`\n"
- body_test += "\n"
- if allure_info["severity"]:
- body_test += f"- **Severity:** `{allure_info['severity']}`\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "```\n"
- details_body += stringify(value) + "\n"
- details_body += "```\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- return get_details_block(f"{emoji} Test #{test['global_number']}", body_test, level+1)
-
- with open(md_path, 'w', encoding='utf-8') as f:
- f.write(f"# 🧪 Test Report\n")
- f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
-
- if 'summary' in data:
- f.write("## 📋 Summary\n")
- for key, value in data['summary'].items():
- f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
- duration = data.get("duration")
- f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n")
- f.write("\n")
-
- # --------- Tests section ----------
- if "tests" in data and "summary" in data:
- test_counter = 1
- for test in data["tests"]:
- test['global_number'] = test_counter
- test_counter += 1
-
- f.write("## 🔎 Tests\n")
- summary_items = list(data["summary"].items())
- total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
- status_order = [k for k, _ in summary_items[:total_index]]
-
- tests_by_status = defaultdict(list)
- for test in data["tests"]:
- outcome = test.get("outcome", "unknown")
- tests_by_status[outcome].append(test)
-
- for status in status_order:
- if status not in tests_by_status:
- continue
- count = len(tests_by_status[status])
- emoji = "✅" if status == "passed" else "❌"
- status_label = status.capitalize().replace('_', ' ')
- body_status = ""
- grouped = defaultdict(lambda: defaultdict(list))
- for test in tests_by_status[status]:
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
- for filename, funcs in grouped.items():
- body_file = ""
- for funcname, tests in funcs.items():
- body_func = ""
- for test in sorted(tests, key=lambda x: x['global_number']):
- body_func += make_test_block(test, status, emoji, level=3)
- body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2)
- body_status += get_details_block(f"📁 File: `{filename}`", body_file, level=1)
- f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
-
- # ---------- Collectors section -----------
- if "collectors" in data:
- f.write("## 📚 Collected files\n")
- grouped = defaultdict(list)
- for collector in data["collectors"]:
- nodeid = collector.get("nodeid", "unknown")
- path = nodeid.split("::")[0]
- main_folder = path.split("/")[0] if "/" in path else path
- grouped[main_folder].append(collector)
- for folder, collectors in grouped.items():
- has_fail = any(c.get("outcome") != "passed" for c in collectors)
- folder_emoji = "✅" if not has_fail else "❌"
- folder_body = ""
- for collector in sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0]):
- outcome = collector.get("outcome", "unknown")
- emoji = "✅" if outcome == "passed" else "❌"
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
- body_coll = f"- **Outcome:** `{outcome}`\n"
- other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
- if other_keys:
- body_coll += "- **Details:**\n"
- body_coll += "```\n"
- for k, v in other_keys.items():
- body_coll += f"{k}:\n"
- if v is None:
- body_coll += " None\n"
- else:
- body_coll += stringify(v) + "\n"
- body_coll += "\n"
- body_coll += "```\n"
- else:
- body_coll += "- **Details:** `None`\n"
- folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
- f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body, level=1))
-
- # ---------- Warnings section -----------
- if 'warnings' in data and data['warnings']:
- f.write("## ⚠️ Warnings\n\n")
- for i, warning in enumerate(data['warnings'], 1):
- warn_body = "```\n"
- for k, v in warning.items():
- warn_body += f"{k}: {v}\n"
- warn_body += "```\n"
- f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
-
-def run_pytest_and_generate_banner_with_logs(md_path, log_path):
- exit_code = pytest.main(["tests/"])
- if exit_code == 0 or exit_code == 1:
- return
- if exit_code == 2:
- banner = (
- "⚠️ **Test execution interrupted**\n\n"
- "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
- )
- elif exit_code == 3:
- banner = (
- "🛑 **Internal error during testing**\n\n"
- "> An internal error occurred while executing the tests.\n\n"
- )
- elif exit_code == 4:
- banner = (
- "❗ **Pytest command line usage error**\n\n"
- "> There was an error in how pytest was invoked.\n\n"
- )
- elif exit_code == 5:
- banner = (
- "❗ **No tests were collected**\n\n"
- "> Pytest did not find any tests to run.\n\n"
- )
- else:
- banner = (
- f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
- "> Unexpected result during test execution.\n\n"
- )
- try:
- with open(log_path, "r") as lf:
- log_lines = lf.readlines()
- except Exception as e:
- print(f"❌ Could not read log file: {e}")
- return
- short_summary_lines = []
- in_summary = False
- for line in log_lines:
- if "short test summary info" in line.lower():
- in_summary = True
- if in_summary:
- short_summary_lines.append(line)
- if re.match(r"=+.* in .*s =+", line):
- break
- try:
- with open(md_path, "r") as f:
- original_md = f.read()
- except Exception as e:
- print(f"❌ Could not read markdown report: {e}")
- return
- full_banner = (
- banner +
- "\n📋 Short test summary info
\n\n" +
- "```\n" + "".join(short_summary_lines) + "```\n \n\n" +
- "\n🪵 Full raw pytest log
\n\n" +
- "```\n" + "".join(log_lines) + "```\n \n\n" +
- "---\n\n"
- )
- try:
- with open(md_path, "w") as f:
- f.write(full_banner + original_md)
- print("✅ Banner and log summary added to markdown report.")
- except Exception as e:
- print(f"❌ Failed to update markdown report: {e}")
-
-def main():
- parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
- parser.add_argument("--input", required=True, help="Path to pytest JSON file")
- parser.add_argument("--output", required=True, help="Path to output Markdown file")
- parser.add_argument("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
- parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
- parser.add_argument("--json", required=False, help="Path to pytest-report.json")
- args = parser.parse_args()
- json_to_md_nested(args.input, args.output, args.allure_dir)
- run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
- print(f"✅ Report generated at {args.output}")
-
-if __name__ == "__main__":
- main()
-Chaque niveau de groupement a un titre explicite (“PASSED”, “File: …”, “Function: …”, etc.).
-
-Indentation HTML légère avec margin-left, mais la structure repose surtout sur la logique de titres dans chaque .
-
-Lisible sur VSCode, GitHub, ou navigateur.
-
-Si tu veux une version plus “markdown only” (sans HTML), possible aussi.
-Mais celle-ci est la plus visuelle et maintient une bonne hiérarchie dans la structure .
-
-Dis-moi si tu veux d’autres ajustements visuels !
-
-
-
-
-
-
-
-
-You said:
-ok jveux plus utiliser allure, jveux prendre les parametres d ailleurs, jpeux trouver ca ou ?
-
-
-ChatGPT said:
-Excellente question !
-Si tu veux ne plus dépendre d’Allure et récupérer les paramètres des tests paramétrés (ceux qui sont lancés via @pytest.mark.parametrize),
-tu peux les trouver directement dans le JSON de pytest si tu utilises pytest-json-report (ou un plugin équivalent).
-
-Où trouver les paramètres d’un test paramétré sans Allure ?
-Dans le fichier pytest-report.json généré par pytest-json-report (ou option --json-report),
-chaque test paramétré aura une section "parameters" dans son bloc test.
-
-Exemple d’un test paramétré dans le JSON :
-
-json
-Copy
-Edit
-{
- "nodeid": "tests/test_math_utils.py::test_addition[2-3]",
- "outcome": "passed",
- "parameters": [
- {
- "name": "a",
- "value": 2
- },
- {
- "name": "b",
- "value": 3
- }
- ],
- ...
-}
-Dans ton code, tu dois donc juste prendre :
-python
-Copy
-Edit
-test.get("parameters", [])
-… au lieu d’aller chercher dans Allure !
-
-Implémentation rapide
-Dans ta boucle qui parcourt chaque test (for test in data["tests"]:),
-remplace la partie :
-
-python
-Copy
-Edit
-full_name = normalize_nodeid(nodeid)
-allure_info = allure_data.get(full_name)
-if allure_info:
- if allure_info["parameters"]:
- # ... affichage
-par
-
-python
-Copy
-Edit
-if test.get("parameters"):
- body_test += "- **Parameters:**\n"
- for param in test["parameters"]:
- name = param.get("name")
- val = param.get("value")
- body_test += f" - `{name}` = `{val}`\n"
- body_test += "\n"
-Récapitulatif
-Tu n’as plus besoin de charger Allure, ni de leur format.
-
-Les paramètres sont dans la clé parameters de chaque test dans le JSON généré par pytest-json-report.
-
-Ça marche pour tous les tests paramétrés (fixtures, parametrize, etc.) nativement supportés par pytest-json-report.
-
-Autre astuce
-Si tu veux aussi afficher les paramètres dans le log de test markdown,
-rajoute juste la boucle ci-dessus dans ta génération de blocs de test.
-
-Si tu veux un patch du code pour n’utiliser que le JSON pytest et les paramètres de là, demande-moi, je te file la fonction corrigée !
-
-Tu veux que je te modifie le script direct ?
-
-
-
-
-
-
-
-
-You said:
-tu peux les trouver directement dans le JSON de pytest si tu utilises pytest-json-report
-
-faux
-
-
-ChatGPT said:
-Bonne remarque : c’est partiellement vrai… et partiellement faux selon l’outil et la version utilisée.
-
-1. Avec pytest-json-report (plugin officiel)
-Normalement, si tu utilises :
-
-bash
-Copy
-Edit
-pytest --json-report --json-report-file=pytest-report.json
-Les tests paramétrés devraient inclure un champ "parameters" dans leur bloc JSON
-… MAIS
-
-Ce n’est pas garanti à 100% selon la version et le type de paramétrage.
-
-Parfois, seul le nodeid change (test_addition[2-3]) et le champ "parameters" peut être absent (vieux bug connu).
-
-Exemple si supporté (récent):
-
-json
-Copy
-Edit
-{
- "nodeid": "tests/test_math_utils.py::test_addition[2-3]",
- "outcome": "passed",
- "parameters": [
- {"name": "a", "value": 2},
- {"name": "b", "value": 3}
- ]
-}
-Mais sur certaines versions/configs, il n’y a pas de clé "parameters" !
-
-2. Ce qui est garanti
-Le nodeid contient TOUJOURS les valeurs paramétrées
-Ex: "tests/test_math_utils.py::test_addition[2-3]"
-⇒ Tu peux parser le nodeid pour retrouver les valeurs si "parameters" est absent.
-
-Aucun autre champ standard dans le JSON ne donne les paramètres, sauf plugin custom.
-
-3. Solution universelle
-A. Si parameters existe dans le JSON
-Tu l’utilises directement.
-
-B. Sinon, tu dois parser le nodeid
-Exemple pour :
-"tests/test_math_utils.py::test_addition[2-3]"
-
-Le nom de la fonction est test_addition
-
-Les paramètres [2-3] sont dans le nodeid, il faut donc savoir comment le test est paramétré dans le code source pour donner du sens à 2 et 3.
-
-Méthode pour parser
-python
-Copy
-Edit
-import re
-
-def parse_nodeid_params(nodeid):
- # Ex : "tests/test_math_utils.py::test_addition[2-3]"
- match = re.match(r".+::.+\[(.+)\]$", nodeid)
- if not match:
- return None
- raw_params = match.group(1)
- # Séparation simple par '-' ou autre selon la convention (attention aux cas complexes)
- return raw_params.split('-')
-
-# Exemple :
-nodeid = "tests/test_math_utils.py::test_addition[2-3]"
-params = parse_nodeid_params(nodeid) # ["2", "3"]
-⚠️ Ce n’est pas parfait pour tous les cas (si tu as des objets, des tuples, etc.),
-mais pour les paramétrages simples ça passe.
-
-Résumé
-Tu as raison : il n’y a PAS TOUJOURS "parameters" dans le JSON de pytest,
-même avec le plugin pytest-json-report !
-
-Mais tu peux toujours retrouver les valeurs paramétrées via le nodeid en dernier recours (en les parsant).
-
-Si tu veux une fonction robuste qui essaye d’extraire un maximum d’infos, je peux t’en écrire une (JSON parameters si dispo, parsing du nodeid sinon).
-
-Tu veux que je t’écrive la fonction “récupère les paramètres d’un test” universelle ?
-(qui prend en compte la présence/absence de "parameters" + parsing du nodeid)
-Dis-le-moi !
-
-
-
-
-
-
-
-
-You said:
-def test_addition_pass():
- # Test passes: correct addition
- assert addition(2, 2) == 4
-
-parametrise ce test avec 2?3 trucs en entrée
-
-
-ChatGPT said:
-Bien sûr !
-Voici comment paramétrer ce test avec 2 ou 3 jeux d’entrées,
-en utilisant @pytest.mark.parametrize :
-
-Version avec 3 jeux d’entrées
-python
-Copy
-Edit
-import pytest
-
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- (2, 2, 4),
- (1, 5, 6),
- (3, 4, 7),
- ]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-Tu peux mettre autant de tuples que tu veux dans la liste : chaque tuple est un test.
-
-Chaque test sera nommé : test_addition_pass[a-b-expected] (dans le nodeid).
-
-Si tu veux juste 2 jeux de tests :
-
-python
-Copy
-Edit
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- (2, 2, 4),
- (5, 5, 10)
- ]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-Voilà, tu peux utiliser ce template directement !
-Dis-moi si tu veux un autre exemple (ex : paramètres plus compliqués, fixtures, etc.)
-
-
-
-
-
-
-
-
-You said:
-{"nodeid": "tests/test_math_utils.py::test_addition_pass[2-2-4]", "lineno": 21, "outcome": "passed", "keywords": ["test_addition_pass[2-2-4]", "parametrize", "pytestmark", "2-2-4", "test_math_utils.py", "tests", "slic", ""], "setup": {"duration": 0.0016042559873312712, "outcome": "passed"}, "call": {"duration": 0.00022420205641537905, "outcome": "passed"}, "teardown": {"duration": 0.0009944828925654292, "outcome": "passed"}}, {"nodeid": "tests/test_math_utils.py::test_addition_pass[1-5-6]", "lineno": 21, "outcome": "passed", "keywords": ["test_addition_pass[1-5-6]", "parametrize", "pytestmark", "1-5-6", "test_math_utils.py", "tests", "slic", ""], "setup": {"duration": 0.0005949789192527533, "outcome": "passed"}, "call": {"duration": 0.00019372603856027126, "outcome": "passed"}, "teardown": {"duration": 0.0009457810083404183, "outcome": "passed"}}, {"nodeid": "tests/test_math_utils.py::test_addition_pass[3-4-7]", "lineno": 21, "outcome": "passed", "keywords": ["test_addition_pass[3-4-7]", "parametrize", "pytestmark", "3-4-7", "test_math_utils.py", "tests", "slic", ""], "setup": {"duration": 0.0005542120197787881, "outcome": "passed"}, "call": {"duration": 0.00019125291146337986, "outcome": "passed"}, "teardown": {"duration": 0.000869528972543776, "outcome": "passed"}},
-
-on a aps vraiment la section parameters tu veux, mais maybe si je rajoute une commande dans parametrize cv changer les choses
-
-
-ChatGPT said:
-Excellente intuition !
-
-Tu as raison :
-
-Le JSON généré ici ne contient pas la section "parameters".
-
-Il encode les valeurs uniquement dans le nodeid (ex : [2-2-4]).
-
-Tu ne peux pas demander à pytest d’ajouter "parameters" via une option standard.
-Ce n’est pas supporté nativement par pytest-json-report ni par pytest.
-
-🟢 Astuce pour ajouter les paramètres au JSON (sans Allure)
-1. Solution “simple” (ce que tu peux déjà faire) :
-Parser les paramètres à partir du nodeid, comme vu plus haut.
-
-Mais ça ne te donne pas les noms (a, b, expected), juste les valeurs (2, 2, 4).
-
-2. Solution “propre” (si tu veux vraiment une section "parameters")
-Créer un pytest hook pour ajouter les paramètres explicitement dans le test report JSON.
-
-Exemple de hook à ajouter dans conftest.py :
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- # Ajoute une clé "parameters" dans l'objet report qui sera dumpé dans le JSON
- if hasattr(item, 'callspec'):
- params = []
- for name, value in item.callspec.params.items():
- params.append({'name': name, 'value': value})
- # On ajoute dynamiquement sur l'objet
- setattr(item, 'parameters', params)
-Mais cela ne s’ajoute pas automatiquement dans le JSON de pytest-json-report…
-Il faut modifier comment tu génères le rapport JSON ou utiliser un plugin custom.
-
-3. Workaround universel
-Si tu veux un mapping entre les valeurs du nodeid et le nom des arguments,
-
-Stocke les noms dans une constante en dur,
-
-Ou, mieux :
-
-Récupère-les à partir du test source (via introspection, AST, etc.), mais c’est plus complexe.
-
-🚩 Conclusion simple pour ton cas
-Ce que tu vois est le comportement normal de pytest.
-
-Tu ne peux pas avoir parameters dans le JSON sans custom plugin ou hack.
-
-La solution robuste est de parser le nodeid (comme [2-2-4]) et, si tu connais l’ordre des paramètres, tu peux reconstituer le mapping :
-
-python
-Copy
-Edit
-def extract_params_from_nodeid(nodeid, param_names):
- import re
- match = re.search(r'\[(.*?)\]$', nodeid)
- if not match:
- return {}
- values = match.group(1).split('-')
- return dict(zip(param_names, values))
-
-# Exemple d'utilisation
-nodeid = "tests/test_math_utils.py::test_addition_pass[2-2-4]"
-param_names = ["a", "b", "expected"]
-params = extract_params_from_nodeid(nodeid, param_names)
-print(params) # {'a': '2', 'b': '2', 'expected': '4'}
-Astuce : stocke le mapping nodeid → noms de paramètres pour chaque fonction testée.
-
-⭐️ Pour avoir les vrais paramètres dans le rapport
-Soit tu restes avec le parsing du nodeid (le plus portable/simple)
-
-Soit tu bidouilles le plugin de rapport pour injecter les paramètres dans le JSON
-
-Mais, pour 99% des usages, parser le nodeid est suffisant (c’est ce que font même certains outils pro)
-
-Dis-moi si tu veux une fonction clé en main pour parser nodeid → params, ou si tu veux aller plus loin (ex : introspection AST pour matcher le nom de chaque test) !
-
-
-
-
-
-
-
-
-You said:
-il y a pas d autres types de reports qui peuvent me donner les parametres explicitement ?
-
-
-ChatGPT said:
-Très bonne question !
-Voici le point complet sur ce que permet pytest en natif, les plugins, et ce qui existe vraiment pour les rapports avec paramètres explicitement listés :
-
-1. Plugins pytest natifs (standards)
-A. pytest-json-report
-Ne donne pas explicitement les paramètres dans la clé "parameters" (sauf très rare version/patch ou custom).
-
-Seule info : les valeurs dans le nodeid ([2-2-4]), comme tu l’as déjà vu.
-
-B. pytest-html
-Permet de générer un joli rapport HTML.
-
-Mais n’affiche PAS non plus explicitement les paramètres par défaut (les valeurs sont visibles dans les nodeids/noms des tests, mais pas dans une section séparée).
-
-Peut s’étendre avec du code custom, mais ce n’est pas natif.
-
-C. pytest-xml / JUnit XML (--junitxml)
-Génère un fichier XML standard.
-
-N’inclut pas explicitement les paramètres non plus (les valeurs apparaissent dans le nom du test/cas, mais pas dans une balise dédiée).
-
-Utilisé pour l’intégration CI/CD, peu d’intérêt pour l’extraction des paramètres.
-
-2. Allure (tu veux éviter)
-Oui, Allure gère bien les paramètres (affichage dans l’interface et dans le JSON),
-mais c’est ce que tu veux éviter.
-
-3. Outils/Plugins alternatifs
-À ce jour, il n’existe PAS de plugin maintenu qui ajoute une section parameters explicite dans le rapport JSON ou XML (sauf Allure).
-
-Certains plugins “non maintenus” ou internes à des boîtes peuvent exister mais ne sont pas sur PyPI.
-
-Aucun plugin natif officiel autre qu’Allure ne gère ce niveau de détail.
-
-4. Solutions Custom (ce que font certains projets pro)
-Ajouter un hook dans conftest.py pour enrichir les reports (voir plus haut),
-
-Ou faire un petit post-traitement du rapport (JSON, XML, HTML) pour parser et reformater le tout,
-
-Utiliser les outils de parsing AST/code source si vraiment tu veux récupérer les noms des paramètres dynamiquement (compliqué pour un simple besoin de reporting).
-
-5. Résumé visuel des possibilités
-Plugin / Outil Paramètres explicitement disponibles ?
-pytest-json-report ❌ (seulement dans nodeid)
-pytest-html ❌ (affichés dans nodeid, pas séparés)
-pytest-xml / junitxml ❌ (affichés dans nodeid, pas séparés)
-Allure ✅ Oui, section dédiée
-Custom conftest.py hook ✅ (si tu le fais toi-même)
-
-6. Conclusion
-Non, il n’y a PAS d’autres types de reports pytest maintenus qui te donnent directement une section “parameters” explicite.
-
-La seule solution plug & play, c’est Allure.
-
-Sinon, il faut parser le nodeid ou ajouter un peu de code custom dans ton conftest.py pour enrichir le rapport.
-
-Si tu veux une solution custom (ex: enrichir le JSON pytest avec les paramètres), je peux t’écrire un hook à copier dans ton conftest.py qui fera exactement ça !
-Veux-tu ce genre de code ?
-(Si oui, dis-le et je te le prépare : il ajoutera une clé parameters à chaque entrée test dans le JSON final, même sans Allure)
-
-
-
-
-
-
-
-
-You said:
-il y a vraiment pas comment recup les parametres en interne ... ?
-
-
-ChatGPT said:
-Je comprends ton doute, mais en natif : il n’existe PAS de façon “clé en main” d’avoir les paramètres explicitement dans les rapports générés par pytest (hors Allure).
-MAIS... en interne, pytest connaît parfaitement les paramètres au moment d’exécuter chaque test paramétré !
-Il est donc possible d’y accéder et de les injecter dans le report, mais ça demande un petit hack ou un hook.
-
-🔥 La preuve : accès aux paramètres à l’exécution
-Dans chaque test paramétré, l’objet item a un attribut callspec qui donne les paramètres !
-
-Exemple dans un hook pytest_runtest_makereport :
-
-python
-Copy
-Edit
-# conftest.py
-def pytest_runtest_makereport(item, call):
- # Ceci est appelé pour chaque test
- if hasattr(item, 'callspec'):
- print("Test:", item.nodeid)
- print("PARAMS:", item.callspec.params) # <--- Ici tu as tout
-Tu verras les paramètres pour chaque test dans la sortie console.
-
-🚩 Pourquoi ce n’est pas dans le rapport ?
-Le plugin pytest-json-report ne copie pas automatiquement ce champ dans le JSON final.
-
-Il te faut donc soit :
-
-Un plugin custom
-
-Un hook qui écrit les paramètres ailleurs
-
-Un patch du JSON après coup (en te basant sur ce que tu vois dans la console, cf ci-dessus)
-
-✅ La seule solution “propre” (sans Allure) pour avoir ça dans le report
-Ajoute ce hook dans conftest.py :
-
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- # Ajoute un attribut custom à l'objet report
- if hasattr(item, "callspec"):
- # On l'ajoute à l'item pour usage custom (par ex. collecte ultérieure)
- item.user_properties.append(
- ("parameters", dict(item.callspec.params))
- )
-OU
-
-Patche le rapport JSON APRÈS le run, en recollant via nodeid les valeurs (en les extrayant via parsing comme montré précédemment).
-
-🚩 Récap
-pytest connaît les paramètres → via item.callspec.params.
-
-Aucun plugin natif ne les écrit dans le report JSON sauf Allure.
-
-Il FAUT ajouter un hook ou faire un post-traitement.
-
-Ce n’est pas accessible via le report JSON natif directement.
-
-Exemple complet à copier-coller
-1. Ajoute dans ton conftest.py :
-
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- if hasattr(item, "callspec"):
- # Ajoute les params pour pytest-html, pytest-json, etc. via user_properties
- item.user_properties.append(("parameters", dict(item.callspec.params)))
-Certains plugins (pytest-html, pytest-json-report récent) intègrent user_properties dans le rapport généré.
-
-2. Vérifie dans le JSON la présence d’une section user_properties.
-
-🌟 Conclusion
-C’est possible, mais il FAUT bricoler (hook ou plugin custom).
-
-Sinon, ce n’est pas disponible “en interne” dans les reports par défaut.
-
-Allure le fait, mais sinon il faut être un peu créatif.
-
-Dis-moi si tu veux une solution clé en main pour enrichir ton rapport avec les paramètres en natif (hook conftest + parsing, etc.) — je te code ça tout de suite !
-
-
-
-
-
-
-
-
-You said:
-Dans chaque test paramétré, l’objet item a un attribut callspec qui donne les paramètres !
-
-
-c est ou ca ?
-
-
-ChatGPT said:
-Excellente question !
-C’est dans le système interne de pytest, accessible pendant l’exécution du test, via les hooks pytest dans un fichier conftest.py.
-
-Explication concrète
-item : C’est l’objet qui représente chaque test individuel pour pytest, passé à certains hooks comme pytest_runtest_makereport, pytest_generate_tests, etc.
-
-item.callspec : Attribut qui n’existe que pour les tests paramétrés.
-Il contient tous les paramètres utilisés pour ce test.
-
-Où accède-t-on à item.callspec ?
-Dans le hook pytest_runtest_makereport
-
-Ce hook est appelé pour chaque test lors de la génération du rapport.
-
-Tu ajoutes ce code dans un fichier conftest.py à la racine de ton projet.
-
-Exemple :
-
-python
-Copy
-Edit
-# conftest.py
-
-def pytest_runtest_makereport(item, call):
- # Ce hook est exécuté pour chaque test lors du reporting
- if hasattr(item, 'callspec'):
- # Seulement pour les tests paramétrés !
- print("Test:", item.nodeid)
- print("PARAMS:", item.callspec.params)
-Que contient item.callspec.params ?
-Un dictionnaire :
-
-python
-Copy
-Edit
-{'a': 2, 'b': 3, 'expected': 5}
-Pour un test comme :
-
-python
-Copy
-Edit
-@pytest.mark.parametrize("a,b,expected", [(2,3,5), (1,2,3)])
-def test_add(a, b, expected): ...
-Quand est-ce que c’est accessible ?
-Pendant la session de test, jamais dans le JSON généré par défaut.
-
-Donc : uniquement dans des hooks pytest (fichier conftest.py ou plugin custom).
-
-Résumé
-item.callspec est disponible UNIQUEMENT pendant le run de pytest, pour les tests paramétrés.
-
-C’est là que tu peux récupérer les paramètres, les logger, ou les injecter dans un rapport custom.
-
-Tu veux un exemple complet qui écrit ces paramètres dans le JSON du rapport ?
-Je peux te faire :
-
-Un code hook qui enrichit le JSON pytest avec les params.
-
-Un exemple pour récupérer ces paramètres et les afficher/traiter après coup.
-
-Dis-moi juste si tu veux ça !
-
-
-
-
-
-
-
-
-You said:
-name: Run Pytest with Allure and Coverage Reports
-
-on:
- push:
- branches: [testing_json]
- pull_request:
-
-jobs:
- tests:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Show checked out files
- run: |
- echo "📂 Files in repo root:"
- ls -1 | head -n 20
-
- - name: Install Pixi & project dependencies
- run: |
- echo "🔧 Installing Pixi..."
- curl -fsSL https://pixi.sh/install.sh | bash > pixi.log 2>&1 || {
- echo "❌ Pixi download failed"
- cat pixi.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "$HOME/.pixi/bin" >> $GITHUB_PATH
- export PATH="$HOME/.pixi/bin:$PATH"
-
- pixi install > pixi-install.log 2>&1 || {
- echo "❌ Pixi project dependencies failed"
- cat pixi-install.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "✅ Pixi installed"
- pixi list
-
- - name: Install Python test/report tools
- run: |
- echo "🐍 Setting up Python environment..."
- python -m venv venv
- source venv/bin/activate
-
- pip install -U pip > pip.log 2>&1 || {
- echo "❌ pip upgrade failed"
- cat pip.log | grep -i 'error\|fatal'
- exit 1
- }
-
- pip install pytest pytest-cov coverage pytest-md-report > pip.log 2>&1 || {
- echo "❌ Python testing tools install failed"
- cat pip.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "✅ Python tools installed"
- pip list
-
- - name: Install Java
- run: |
- echo "☕ Installing Java..."
- sudo apt update > java.log 2>&1 && sudo apt install -y openjdk-11-jdk >> java.log 2>&1 || {
- echo "❌ Java installation failed"
- cat java.log | grep -i 'error\|fatal'
- exit 1
- }
- echo "✅ Java installed"
- java -version
-
- - name: Install Allure CLI
- run: |
- echo "📊 Installing Allure..."
- curl -sL https://github.com/allure-framework/allure2/releases/download/2.27.0/allure-2.27.0.tgz | tar -xz > allure.log 2>&1 || {
- echo "❌ Allure download failed"
- cat allure.log | grep -i 'error\|fatal'
- exit 1
- }
-
- sudo mv allure-2.27.0 /opt/allure
- sudo ln -s /opt/allure/bin/allure /usr/local/bin/allure
- echo "✅ Allure installed"
- allure --version
-
- - name: Run tests and generate reports
- run: |
- echo "🚀 Running tests and generating reports..."
- source venv/bin/activate
- mkdir -p ci-reports/{allure,coverage,markdown}
-
- echo "🧪 Running pytest with coverage and report generation..."
- pixi run pytest . \
- --continue-on-collection-errors \
- --cov=functions \
- --cov-report=xml:ci-reports/coverage/coverage.xml \
- --cov-report=html:ci-reports/coverage/ \
- --alluredir=allure-results \
- --json-report \
- --json-report-file=ci-reports/markdown/pytest-report.json \
- --capture=no > ci-reports/markdown/raw-test-output.log 2>&1 && {
- echo "✅ Pytest completed"
- echo ""
- echo "📄 Markdown: ci-reports/markdown/test-report.md"
- echo ""
- echo "📦 JSON: ci-reports/markdown/pytest.json"
- } || {
- echo ""
- echo "⚠️ Some tests failed"
- awk '/=+ short test summary info =+/,/=+.* in .*s =+/' ci-reports/markdown/raw-test-output.log || true
- #cat ci-reports/markdown/raw-test-output.log
- }
-
- echo ""
- echo "📊 Generating Allure report..."
- allure generate allure-results -o ci-reports/allure --clean > /dev/null 2>&1 && {
- echo "✅ Allure report generated"
- } || {
- echo "⚠️ Allure generation failed"
- }
-
- echo ""
- echo "📈 Generating coverage summary..."
- coverage report --format=markdown > ci-reports/markdown/coverage-summary.md && {
- echo "✅ Coverage summary generated"
- } || {
- echo "⚠️ Coverage generation failed"
- }
-
- {
- echo ""
- echo "📊 Generating tests markdown summary..."
- python json_to_md.py \
- --input ci-reports/markdown/pytest-report.json \
- --output ci-reports/markdown/TEST-REPORT.md \
- --allure-dir ci-reports/allure/data/test-cases \
- --log ci-reports/markdown/raw-test-output.log \
- --json ci-reports/markdown/pytest-report.json
-
- echo "✅ Tests markdown generated"
- } || {
- echo "⚠️ Tests markdown generation failed"
- }
-
- - name: Commit and push reports
- run: |
- echo "📤 Committing test & coverage reports to slic.git..."
- git config user.name "ci-bot"
- git config user.email "ci-bot@example.com"
- git add ci-reports/markdown/TEST-REPORT.md ci-reports/markdown/pytest-report.json ci-reports/markdown/coverage-summary.md
- git commit -m "CI: update test and coverage reports" || echo "⚠️ Nothing to commit"
-
- echo "🚀 Pushing to branch 'testing_json' on slic.git..."
- git push https://ci-token:${{ secrets.CI_TOKEN }}@gitea.psi.ch/tligui_y/slic.git HEAD:testing_json > push_main.log 2>&1 || {
- echo "❌ Push to main repo failed"
- cat push_main.log | grep -i 'error\|fatal\|refused'
- exit 1
- }
- echo "✅ Reports pushed to https://gitea.psi.ch/tligui_y/slic/ci-reports/"
- env:
- CI_TOKEN: ${{ secrets.CI_TOKEN }}
-
-rajoute avec alors pour check il y a quoi dans le file apres pytest_runtest_makereport, enregistre tout pas que les parametres pour l instant, jveux tout voir
-
-
-ChatGPT said:
-Bien vu !
-Pour “espionner” tout ce qu’il y a dans item après pytest_runtest_makereport, tu peux logguer toutes les propriétés importantes dans un fichier texte lors du run.
-Voici comment faire :
-
-1. Ajoute ce hook dans conftest.py à la racine du projet
-python
-Copy
-Edit
-# conftest.py
-
-import json
-
-def pytest_runtest_makereport(item, call):
- try:
- # Récupère tout ce qui est intéressant dans item
- info = {
- 'nodeid': item.nodeid,
- 'name': getattr(item, 'name', None),
- 'keywords': list(getattr(item, 'keywords', {}).keys()),
- 'callspec': dict(item.callspec.params) if hasattr(item, "callspec") else None,
- 'user_properties': getattr(item, 'user_properties', None),
- 'fixtureinfo': str(getattr(item, 'fixturenames', None)),
- # Ajoute d'autres attributs ici si tu veux
- }
- with open("ci-reports/markdown/pytest_item_dump.txt", "a") as f:
- f.write(json.dumps(info, indent=2, default=str) + "\n\n")
- except Exception as e:
- with open("ci-reports/markdown/pytest_item_dump.txt", "a") as f:
- f.write(f"ERROR in pytest_runtest_makereport for {getattr(item, 'nodeid', '??')}: {e}\n")
-2. Explications
-Après chaque test, ce hook dump dans ci-reports/markdown/pytest_item_dump.txt :
-
-nodeid
-
-le nom du test
-
-les keywords (utile pour les paramétrés)
-
-les paramètres (callspec.params s’il y en a)
-
-les propriétés utilisateur (utiles pour certains plugins)
-
-les fixtures associées (si tu veux voir)
-
-Tu peux ajouter/supprimer des champs selon tes besoins.
-
-3. Lecture du fichier
-Après l’exécution du workflow, tu pourras voir le contenu de ci-reports/markdown/pytest_item_dump.txt dans ton repo ou en output d’action.
-
-4. Tu veux voir tout l’objet item ?
-Ce n’est pas recommandé (trop verbeux/inutilisable, certains champs ne sont pas sérialisables).
-
-Le code ci-dessus te donne déjà toutes les infos utiles pour l’analyse.
-
-5. Pour aller plus loin
-Tu peux ensuite injecter ce que tu veux dans ton rapport markdown/JSON à partir de ce log, ou améliorer ce hook pour générer directement un rapport custom.
-
-Résumé
-Ajoute ce hook dans conftest.py.
-
-Il dump toutes les infos utiles sur chaque test dans un fichier texte après chaque test (y compris les paramètres si présents).
-
-Dis-moi si tu veux une variante (ex : tout dumper en un seul JSON, filtrer sur les paramétrés seulement, etc.) !
-
-
-
-
-
-
-
-
-You said:
-donc je en change pas test.yml ?
-
-
-ChatGPT said:
-Exactement :
-Tu ne changes PAS le test.yml pour cela.
-
-Le fichier conftest.py s’exécute automatiquement à chaque run pytest (il est détecté par pytest s’il est à la racine ou dans un dossier de tests).
-
-Tu ajoutes juste le code donné dans ce conftest.py.
-
-Pytest va automatiquement écrire le fichier ci-reports/markdown/pytest_item_dump.txt lors de l’exécution des tests, y compris sur GitHub Actions (car tu as bien la ligne source venv/bin/activate qui active l’environnement Python).
-
-Résumé
-AUCUNE modif à apporter au workflow GitHub (test.yml).
-
-Juste ajouter/committer le conftest.py avec le hook proposé.
-
-C’est prêt, tu verras le fichier généré automatiquement à chaque CI run.
-
-Dispo si tu veux une amélioration du hook ou un log dans un format particulier (JSON, YAML, CSV, etc.) !
-
-
-
-
-
-
-
-
-You said:
-ca a rien rajouté sans ci-reports markdown
-
-
-ChatGPT said:
-Oui, c’est normal :
-
-Si le dossier ci-reports/markdown n’existe pas, le code va planter en silence (fichier non créé, pas d’erreur visible, car le dossier n’existe pas).
-
-Il faut s’assurer que le dossier existe avant d’ouvrir le fichier pour écrire dedans.
-
-Solution : Création auto du dossier
-Ajoute ces lignes au début du hook pour être sûr que le dossier existe :
-
-python
-Copy
-Edit
-import os
-import json
-
-def pytest_runtest_makereport(item, call):
- try:
- # Création du dossier s'il n'existe pas
- os.makedirs("ci-reports/markdown", exist_ok=True)
- info = {
- 'nodeid': item.nodeid,
- 'name': getattr(item, 'name', None),
- 'keywords': list(getattr(item, 'keywords', {}).keys()),
- 'callspec': dict(item.callspec.params) if hasattr(item, "callspec") else None,
- 'user_properties': getattr(item, 'user_properties', None),
- 'fixtureinfo': str(getattr(item, 'fixturenames', None)),
- }
- with open("ci-reports/markdown/pytest_item_dump.txt", "a") as f:
- f.write(json.dumps(info, indent=2, default=str) + "\n\n")
- except Exception as e:
- with open("ci-reports/markdown/pytest_item_dump.txt", "a") as f:
- f.write(f"ERROR in pytest_runtest_makereport for {getattr(item, 'nodeid', '??')}: {e}\n")
-Avec ce code, le dossier est créé automatiquement si besoin, et le fichier sera écrit à chaque run.
-
-Vérifie aussi que conftest.py est bien à la racine ou dans le dossier où pytest le trouve.
-
-Ça va marcher, même sur GitHub Actions ou en local !
-Si jamais tu veux que ça log dans un autre chemin, change juste "ci-reports/markdown/pytest_item_dump.txt".
-
-Test et redis-moi si c’est bon 👍
-
-
-
-
-
-
-
-
-You said:
-le dossier existe deja
-
-
-ChatGPT said:
-OK, dans ce cas, si le dossier existe déjà mais que tu ne vois aucun fichier pytest_item_dump.txt généré, il faut vérifier :
-
-1. Le hook est-il bien exécuté ?
-Ton conftest.py est-il bien à la racine OU dans un dossier de tests ?
-(pytest ne charge que les conftest.py dans les dossiers parents de là où il commence à chercher les tests.)
-
-2. Test minimal pour vérifier si le hook est appelé
-Ajoute une ligne de print :
-
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- print(f"HOOK CALLED for {item.nodeid}") # ← Ça doit s'afficher dans tes logs pytest !
- ...
-Lance un test en local ou sur CI :
-
-Si tu ne vois rien : le hook n’est pas trouvé.
-
-Si tu vois la ligne : le hook est exécuté → le souci est ailleurs.
-
-3. Vérifie les permissions
-Parfois, sur GitHub Actions, il y a des permissions d’écriture ou de droits selon l’utilisateur/dossier.
-
-En local, ça ne devrait pas bloquer, mais vérifie quand même.
-
-4. Est-ce qu’un autre bug stoppe l’écriture ?
-Ajoute un except Exception as e: print(e) avant d’écrire dans le fichier d’erreur (en plus de l’écriture dans le txt).
-
-5. Le chemin du fichier
-Essaye avec un chemin très simple pour tester :
-with open("pytest_item_dump.txt", "a") as f: ...
-
-Vérifie si le fichier est créé à la racine.
-
-6. Pytest l’ignore-t-il ?
-Certains frameworks CI ou configs suppriment les fichiers non explicitement listés/artifacts à la fin du job.
-
-Check-list rapide :
- conftest.py bien détecté ?
-
- Le print du hook s’affiche-t-il ?
-
- Permission OK ?
-
- Essaye d’écrire dans un chemin plus simple
-
- Vérifie en local AVANT de pousser sur CI
-
-Dis-moi où tu en es (print hook, fichier, etc.), et je t’aide à débug étape par étape si besoin !
-
-
-
-
-
-
-
-
-You said:
-conftest.py
-
-il est a la racine
-
-
-ChatGPT said:
-Parfait, c’est le bon emplacement.
-
-Pour débug très vite :
-
-Ajoute ce print dans pytest_runtest_makereport pour être sûr qu’il est appelé :
-
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- print(f"HOOK CALLED for {item.nodeid}")
- # ... (reste du code)
-Lance pytest en local (pas dans un notebook, mais bien dans un terminal) :
-
-bash
-Copy
-Edit
-pytest
-Observe le terminal :
-
-Est-ce que tu vois HOOK CALLED for ... pour chaque test ?
-
-Oui → Le hook marche, le problème est l’écriture fichier.
-
-Non → Pytest n’a pas trouvé/chargé ton conftest.py.
-
-Si tu vois bien les prints, mais toujours pas de fichier créé :
-Change le chemin du fichier en un truc tout simple pour tester, genre :
-
-python
-Copy
-Edit
-with open("pytest_item_dump.txt", "a") as f:
- f.write("Test log!\n")
-Relance, regarde si un fichier pytest_item_dump.txt apparaît à la racine.
-
-Sinon, si tu ne vois AUCUN print :
-Vérifie que :
-
-Tu lances pytest depuis la racine où il y a ton conftest.py :
-(par ex. : tu dois avoir le même niveau que le dossier tests/ et conftest.py)
-
-Tu n’as pas plusieurs conftest.py qui se masquent ;
-
-Tu n’as pas une configuration exotique dans pytest.ini ou autre qui limite les hooks chargés.
-
-Checklist finale
- Print affiché : le hook est bien appelé.
-
- Fichier créé : permissions/dossier/nom ok.
-
- Encore rien : dis-moi ce que tu observes (print ou pas), je t’aide à débug !
-
-Teste et dis-moi où ça coince !
-
-
-
-
-
-
-
-
-You said:
-jveux pas utiliser cette mathode pour avoir les parametres
-
-
-ChatGPT said:
-C’est compris, et c’est honnêtement un choix logique :
-La méthode du hook conftest.py est un “hack” pour contourner la limitation des rapports pytest natifs… mais elle ne convient pas si tu veux rester 100% plug & play.
-
-Bilan clair : Les possibilités pour récupérer les paramètres d’un test paramétré
-Solution Automatique ? Dans le JSON de base ? Remarques
-Parsing nodeid Oui Oui Noms et valeurs mélangés
-Hook conftest.py Non Non (code custom) “Hack”, pas natif
-Allure Oui Oui (section dédiée) Seule solution clean
-Autres plugins Non Non Aucun autre maintenu
-
-La seule alternative “pure pytest” : Parser le nodeid
-Le seul moyen plug & play sans code custom ni Allure est de parser le nodeid :
-
-Il contient toujours les valeurs (pas les noms), sous la forme :
-tests/test_math_utils.py::test_addition_pass[2-2-4]
-
-Mais :
-
-Tu n’auras jamais automatiquement les noms des paramètres (ex: a=2, b=2, expected=4), seulement leur ordre.
-
-Exemple de parsing (plug & play, pas parfait mais universel) :
-python
-Copy
-Edit
-import re
-
-def extract_param_values(nodeid):
- match = re.search(r"\[(.*)\]", nodeid)
- if match:
- return match.group(1).split('-')
- return []
-
-print(extract_param_values("tests/test_math_utils.py::test_addition_pass[2-2-4]"))
-# Output: ['2', '2', '4']
-Pour les noms, il faut connaître la signature du test à l’avance.
-
-Conclusion
-Il n’existe aucune méthode native “miracle” (hors Allure) pour avoir noms + valeurs des paramètres dans le rapport.
-
-Pas de solution “interne” en pytest natif, à part parser le nodeid comme ci-dessus.
-
-En résumé
-Tu peux :
-
-Parser le nodeid (simple, mais pas les noms)
-
-Utiliser Allure (complet)
-
-Rien d’autre sans hack/plugin custom.
-
-Tous les grands projets CI font pareil si Allure n’est pas utilisé : ils parsèrent le nodeid.
-
-Dis-moi si tu veux :
-
-Une fonction de parsing des nodeids pour tous tes tests.
-
-Ou une aide pour rendre le rapport markdown plus lisible à partir de ça !
-
-(Et tu fais bien de vouloir rester “pur pytest” 👍)
-
-
-
-
-
-
-
-
-You said:
-Output
-test_combinations.py::test_df_against_angle[df_data_set1.csv-0] PASSED
-test_combinations.py::test_df_against_angle[df_data_set1.csv-30] PASSED
-test_combinations.py::test_df_against_angle[df_data_set1.csv-60] PASSED
-test_combinations.py::test_df_against_angle[df_data_set2.csv-90] PASSED
-test_combinations.py::test_df_against_angle[df_data_set2.csv-120] PASSED
-test_combinations.py::test_df_against_angle[df_data_set2.csv-150] PASSED
-test_combinations.py::test_df_against_angle[df_data_set3.csv-180] PASSED
-test_combinations.py::test_df_against_angle[df_data_set3.csv-210] PASSED
-test_combinations.py::test_df_against_angle[df_data_set3.csv-240] PASSED
-Notes
-The generate_test_data function only load the df once and it is shared among the angles. Take care not to alter the df between tests.
-We load the df within the generate_test_data function, freeing the test from doing the setup part.
-Update
-If you want to control the names of the tests, use pytest.param. Replace yield df, angle with
-
-yield pytest.param(df, angle, id=f"df={filename}, angle={angle:>3}")
-and you get the following output (note the names of the tests):
-
-test_combinations.py::test_df_against_angle[df=data_set1.csv, angle= 0] PASSED
-test_combinations.py::test_df_against_angle[df=data_set1.csv, angle= 30] PASSED
-test_combinations.py::test_df_against_angle[df=data_set1.csv, angle= 60] PASSED
-test_combinations.py::test_df_against_angle[df=data_set2.csv, angle= 90] PASSED
-test_combinations.py::test_df_against_angle[df=data_set2.csv, angle=120] PASSED
-test_combinations.py::test_df_against_angle[df=data_set2.csv, angle=150] PASSED
-test_combinations.py::test_df_against_angle[df=data_set3.csv, angle=180] PASSED
-test_combinations.py::test_df_against_angle[df=data_set3.csv, angle=210] PASSED
-test_combinations.py::test_df_against_angle[df=data_set3.csv, angle=240] PASSED
-
-c est quoi ca ?
-
-
-ChatGPT said:
-Excellente question, ce que tu vois là c’est une fonctionnalité avancée de la paramétrisation dans pytest utilisant pytest.param et l’argument id pour donner des noms humains aux tests paramétrés.
-
-💡 Décryptage rapide
-Par défaut, pytest paramètre tes tests avec les différentes combinaisons d’arguments, et il affiche les valeurs dans le nodeid, ex:
-
-cpp
-Copy
-Edit
-test_combinations.py::test_df_against_angle[df_data_set1.csv-0]
-Mais si tu veux un nom plus lisible (par exemple avec des virgules, des espaces, ou des labels explicites), tu peux utiliser pytest.param avec un argument id=... :
-
-python
-Copy
-Edit
-pytest.param(df, angle, id=f"df={filename}, angle={angle:>3}")
-→ Cela va personnaliser le nom du test dans la sortie.
-
-📦 À quoi ça sert ?
-Lisibilité : Tu sais directement quels jeux de données et quels paramètres sont testés, sans te demander “c’est quoi ce 0 ou ce 30 ?”.
-
-Débogage rapide : En cas d’erreur, le rapport de pytest t’indique tout de suite quels paramètres étaient utilisés.
-
-🧑💻 Concrètement, comment ça marche ?
-Dans ton générateur de données de test (fixture ou générateur paramétrique), au lieu de simplement faire :
-
-python
-Copy
-Edit
-yield df, angle
-Tu écris :
-
-python
-Copy
-Edit
-yield pytest.param(df, angle, id=f"df={filename}, angle={angle:>3}")
-df : ton DataFrame à tester
-
-angle : l’angle de test
-
-id=f"df={filename}, angle={angle:>3}" : personnalise le nom du test affiché dans les logs/rapports
-
-📑 Exemple complet :
-python
-Copy
-Edit
-import pytest
-
-@pytest.mark.parametrize(
- "df,angle",
- [
- pytest.param("df_data_set1.csv", 0, id="df=data_set1.csv, angle= 0"),
- pytest.param("df_data_set1.csv", 30, id="df=data_set1.csv, angle= 30"),
- # ... etc ...
- ]
-)
-def test_df_against_angle(df, angle):
- # test logic here
- pass
-Résultat dans la console :
-
-arduino
-Copy
-Edit
-test_combinations.py::test_df_against_angle[df=data_set1.csv, angle= 0] PASSED
-🚀 Conclusion
-pytest.param(..., id=...) → permet de donner un nom lisible aux tests paramétrés dans les logs pytest.
-
-C’est très utilisé pour les gros jeux de tests pour identifier précisément ce qui échoue.
-
-Cela n’a rien à voir avec la façon dont les paramètres sont stockés en interne, c’est juste cosmétique et informatif.
-
-Si tu veux un exemple de code ou une adaptation pour ton propre paramétrage, dis-moi !
-
-
-
-
-
-
-
-
-You said:
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- (2, 2, 4),
- (1, 5, 6),
- (3, 4, 7),
- ]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-
-a la place de ca jdois mettre quoi pour les noms ?
-
-
-ChatGPT said:
-Tu dois remplacer chaque tuple (a, b, expected) par un appel à pytest.param et préciser l’argument id= pour chaque cas.
-
-Exemple :
-python
-Copy
-Edit
-import pytest
-
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- pytest.param(2, 2, 4, id="a=2, b=2, expected=4"),
- pytest.param(1, 5, 6, id="a=1, b=5, expected=6"),
- pytest.param(3, 4, 7, id="a=3, b=4, expected=7"),
- ]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-Résultat dans les logs :
-
-less
-Copy
-Edit
-test_math_utils.py::test_addition_pass[a=2, b=2, expected=4] PASSED
-test_math_utils.py::test_addition_pass[a=1, b=5, expected=6] PASSED
-test_math_utils.py::test_addition_pass[a=3, b=4, expected=7] PASSED
-Tu peux aussi rendre les ids plus compacts si tu veux, exemple :
-
-python
-Copy
-Edit
-id="2+2=4"
-Résultat :
-
-cpp
-Copy
-Edit
-test_math_utils.py::test_addition_pass[2+2=4] PASSED
-Dis-moi si tu veux une version auto-générée pour beaucoup de cas, ou une adaptation custom !
-
-
-
-
-
-
-
-
-You said:
-dans le id on met pas juste le nom ?
-
-
-ChatGPT said:
-Non, tu mets ce que tu veux dans le id !
-
-Le champ id sert à afficher le nom du cas de test dans les logs de pytest.
-
-Tu peux mettre un nom explicite, un résumé des valeurs, un label court, ce que tu veux.
-
-Exemples :
-1. Juste un nom personnalisé
-python
-Copy
-Edit
-pytest.param(2, 2, 4, id="cas_basique")
-Dans les logs :
-
-css
-Copy
-Edit
-test_addition_pass[cas_basique] PASSED
-2. Un label plus explicite
-python
-Copy
-Edit
-pytest.param(2, 2, 4, id="somme_deux_petits_nombres")
-css
-Copy
-Edit
-test_addition_pass[somme_deux_petits_nombres] PASSED
-3. Avec valeurs affichées
-python
-Copy
-Edit
-pytest.param(2, 2, 4, id="a=2, b=2, expected=4")
-css
-Copy
-Edit
-test_addition_pass[a=2, b=2, expected=4] PASSED
-4. Très court
-python
-Copy
-Edit
-pytest.param(2, 2, 4, id="cas1")
-css
-Copy
-Edit
-test_addition_pass[cas1] PASSED
-Résumé:
-
-Le champ id dans pytest.param c’est juste pour la lisibilité humaine dans les logs.
-
-Tu mets ce que tu veux dedans : un nom, une valeur, une explication, etc.
-
-Si tu ne mets rien, pytest affiche juste la liste des valeurs par défaut.
-Si tu veux un nom plus explicite, c’est le bon endroit !
-
-
-
-
-
-
-
-
-You said:
-et a part le truc id, tu m avais donné aitre chose pour ca la derniere fois
-
-
-ChatGPT said:
-Oui, bonne mémoire ! Il existe deux façons principales pour personnaliser le nom affiché des tests paramétrés dans pytest :
-
-1. Avec pytest.param(..., id=...)
-C’est la méthode la plus claire et la plus utilisée :
-Tu mets un id pour chaque cas.
-Exemple :
-
-python
-Copy
-Edit
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- pytest.param(2, 2, 4, id="cas_add_2_2"),
- pytest.param(1, 5, 6, id="cas_add_1_5"),
- pytest.param(3, 4, 7, id="cas_add_3_4"),
- ]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-Log :
-test_addition_pass[cas_add_2_2] PASSED
-
-2. Avec l’argument ids= de @pytest.mark.parametrize
-Tu peux aussi passer une liste d’ids à l’argument ids=... du décorateur :
-
-python
-Copy
-Edit
-@pytest.mark.parametrize(
- "a, b, expected",
- [
- (2, 2, 4),
- (1, 5, 6),
- (3, 4, 7),
- ],
- ids=["cas_add_2_2", "cas_add_1_5", "cas_add_3_4"]
-)
-def test_addition_pass(a, b, expected):
- assert addition(a, b) == expected
-Log :
-test_addition_pass[cas_add_2_2] PASSED
-
-Résumé
-pytest.param(..., id="nom") → Spécifique à chaque ligne.
-
-ids=["nom1", "nom2", ...] → Une seule fois pour toute la paramétrisation.
-
-Les deux méthodes sont équivalentes :
-
-pytest.param(..., id=...) te permet de customiser au cas par cas (utile si tu as des cas très différents ou des ids dynamiques).
-
-ids=[...] est plus pratique pour une simple liste.
-
-Dis-moi si tu veux un exemple dynamique, ou une astuce pour générer automatiquement ces ids !
-
-
-
-
-
-
-
-
-You said:
-ok bon tu sais quoi, mtn mon code c est ca :
-
-from collections import defaultdict
-from datetime import datetime
-import json
-import argparse
-import os
-import re
-import pytest
-from pytest import ExitCode
-import traceback
-
-def stringify(obj):
- if obj is None or obj == "":
- return "None"
- if isinstance(obj, list):
- return ', '.join(stringify(e) for e in obj)
- if isinstance(obj, dict):
- return ', '.join(f"{k}: {stringify(v)}" for k, v in obj.items())
- return f"{str(obj)}"
-
-def normalize_nodeid(nodeid):
- """Convert pytest nodeid to Allure fullName format"""
- match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
- if match:
- file_part = match.group(1).replace("/", ".").replace("\\", ".")
- func_part = match.group(2)
- return f"{file_part}#{func_part}"
- return None
-
-def write_details(f, summary, body, level=0):
- margin = 18 * level # 18px de marge/indent par niveau
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- f.write(f'\n')
- f.write(f"\n{summary}
\n\n")
- f.write(body)
- f.write(" \n")
- f.write("
\n\n")
-
-def load_allure_metadata(allure_test_cases_dir):
- allure_data = {}
- if not os.path.exists(allure_test_cases_dir):
- print(f"❌ Allure directory not found: {allure_test_cases_dir}")
- return allure_data
-
- print(f"Loading Allure files from: {allure_test_cases_dir}")
- for file in os.listdir(allure_test_cases_dir):
- # Ignore temp/hidden/invalid files, just in case
- if not file.endswith(".json") or file.startswith("."):
- continue
- path = os.path.join(allure_test_cases_dir, file)
- # Si le fichier est vide ou absent (par exemple pas collecté), on skippe
- if not os.path.isfile(path) or os.path.getsize(path) == 0:
- print(f"Allure file: {file} skipped (not found or empty).")
- continue
- try:
- with open(path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- full_name = data.get("fullName")
- if not full_name:
- print(f"Allure file: {file} skipped (no fullName).")
- continue
- params = data.get("parameters", [])
- severity = data.get("extra", {}).get("severity", None)
- allure_data[full_name] = {
- "parameters": params,
- "severity": severity
- }
- print(f"Allure file: {file} loaded ✔️")
-
-
- except Exception as e:
- short_summary = traceback.format_exception_only(type(e), e)[-1].strip()
- print(f"Allure file: {file} failed ❌ (reason: {short_summary})")
-
- return allure_data
-
-
-def json_to_md_nested(json_path, md_path, allure_dir=None):
- with open(json_path) as f:
- data = json.load(f)
-
- allure_data = load_allure_metadata(allure_dir) if allure_dir else {}
-
- def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- body_test = f"- **Status:** {emoji} {status}\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** {duration:.6f} s\n" if duration else "- **Duration:** None\n"
-
- full_name = normalize_nodeid(nodeid)
- allure_info = allure_data.get(full_name)
- if allure_info:
- if allure_info["parameters"]:
- body_test += "- **Parameters (Allure):**\n"
- for param in allure_info["parameters"]:
- name = param.get("name")
- val = param.get("value")
- body_test += f" - {name} = {val}\n"
- body_test += "\n"
- if allure_info["severity"]:
- body_test += f"- **Severity:** {allure_info['severity']}\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "
-\n"
- details_body += stringify(value) + "\n"
- details_body += "
-\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1)
-
- def get_details_block(summary, body, level=0):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
- with open(md_path, 'w', encoding='utf-8') as f:
- f.write(f"# 🧪 Test Report\n")
- f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
-
- if 'summary' in data:
- f.write("## 📋 Summary\n")
- for key, value in data['summary'].items():
- f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
- duration = data.get("duration")
- f.write(f"- **Total Duration**: {duration:.3f}s\n" if duration else "- **Total Duration**: None\n")
- f.write("\n")
-
- # --------- Tests section ----------
- if "tests" in data and "summary" in data:
- test_counter = 1
- for test in data["tests"]:
- test['global_number'] = test_counter
- test_counter += 1
-
- f.write("## 🔎 Tests\n")
-
- summary_items = list(data["summary"].items())
- total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
- status_order = [k for k, _ in summary_items[:total_index]]
-
- tests_by_status = defaultdict(list)
- for test in data["tests"]:
- outcome = test.get("outcome", "unknown")
- tests_by_status[outcome].append(test)
-
- for status in status_order:
- if status not in tests_by_status:
- continue
-
- count = len(tests_by_status[status])
- emoji = "✅" if status == "passed" else "❌"
- status_label = status.capitalize().replace('_', ' ')
-
- body_status = ""
- grouped = defaultdict(lambda: defaultdict(list))
- for test in tests_by_status[status]:
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
-
- for filename, funcs in grouped.items():
- body_file = ""
- for funcname, tests in funcs.items():
- body_func = ""
- for test in sorted(tests, key=lambda x: x['global_number']):
- body_func += make_test_block(test, status, emoji, level=3)
- body_file += get_details_block(f"🔧 Function: {funcname}", body_func, level=2)
- body_status += get_details_block(f"📁 {filename}", body_file, level=1)
- f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
-
- # ---------- Collectors section -----------
- if "collectors" in data:
- f.write("## 📚 Collected files\n")
- grouped = defaultdict(list)
- for collector in data["collectors"]:
- nodeid = collector.get("nodeid", "unknown")
- path = nodeid.split("::")[0]
- main_folder = path.split("/")[0] if "/" in path else path
- grouped[main_folder].append(collector)
-
- for folder, collectors in grouped.items():
- has_fail = any(c.get("outcome") != "passed" for c in collectors)
- folder_emoji = "✅" if not has_fail else "❌"
-
- body_collectors = ""
- outputs = []
- for collector in collectors:
- outcome = collector.get("outcome", "unknown")
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
- results = collector.get("result", [])
- if outcome != "passed" and results:
- outputs.append(f"### ❌ {short_node}\n
-\n" + "\n".join(
- f"{k}: {v}" if isinstance(item, dict) else str(item)
- for item in results
- for k, v in item.items() if isinstance(item, dict)
- ) + "\n
-")
-
- if outputs:
- body_collectors += "### 🧾 Error or Result Summary\n\n"
- for out in outputs:
- body_collectors += out + "\n"
-
- collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
- folder_body = ""
- for collector in collectors_sorted:
- outcome = collector.get("outcome", "unknown")
- emoji = "✅" if outcome == "passed" else "❌"
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
-
- body_coll = f"- **Outcome:** {outcome}\n"
- other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
- if other_keys:
- body_coll += "- **Details:**\n"
- body_coll += "
-\n"
- for k, v in other_keys.items():
- body_coll += f"{k}:\n"
- if v is None:
- body_coll += " None\n"
- else:
- body_coll += stringify(v) + "\n"
- body_coll += "\n"
- body_coll += "
-\n"
- else:
- body_coll += "- **Details:** None\n"
- folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
- f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1))
-
- # ---------- Warnings section -----------
- if 'warnings' in data and data['warnings']:
- f.write("## ⚠️ Warnings\n\n")
- for i, warning in enumerate(data['warnings'], 1):
- warn_body = "
-\n"
- for k, v in warning.items():
- warn_body += f"{k}: {v}\n"
- warn_body += "
-\n"
- f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
-
-def run_pytest_and_generate_banner_with_logs(md_path, log_path):
- exit_code = pytest.main(["tests/"])
-
- if exit_code == 0 or exit_code == 1:
- return
-
- if exit_code == 2:
- banner = (
- "⚠️ **Test execution interrupted**\n\n"
- "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
- )
- elif exit_code == 3:
- banner = (
- "🛑 **Internal error during testing**\n\n"
- "> An internal error occurred while executing the tests.\n\n"
- )
- elif exit_code == 4:
- banner = (
- "❗ **Pytest command line usage error**\n\n"
- "> There was an error in how pytest was invoked.\n\n"
- )
- elif exit_code == 5:
- banner = (
- "❗ **No tests were collected**\n\n"
- "> Pytest did not find any tests to run.\n\n"
- )
- else:
- banner = (
- f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
- "> Unexpected result during test execution.\n\n"
- )
-
- try:
- with open(log_path, "r") as lf:
- log_lines = lf.readlines()
- except Exception as e:
- print(f"❌ Could not read log file: {e}")
- return
-
- short_summary_lines = []
- in_summary = False
- for line in log_lines:
- if "short test summary info" in line.lower():
- in_summary = True
- if in_summary:
- short_summary_lines.append(line)
- if re.match(r"=+.* in .*s =+", line):
- break
-
- try:
- with open(md_path, "r") as f:
- original_md = f.read()
- except Exception as e:
- print(f"❌ Could not read markdown report: {e}")
- return
-
- full_banner = (
- banner +
- "\n📋 Short test summary info
\n\n" +
- "
-\n" + "".join(short_summary_lines) + "
-\n \n\n" +
- "\n🪵 Full raw pytest log
\n\n" +
- "
-\n" + "".join(log_lines) + "
-\n \n\n" +
- "---\n\n"
- )
-
- try:
- with open(md_path, "w") as f:
- f.write(full_banner + original_md)
- print("✅ Banner and log summary added to markdown report.")
- except Exception as e:
- print(f"❌ Failed to update markdown report: {e}")
-
-def main():
- parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
- parser.add_argument("--input", required=True, help="Path to pytest JSON file")
- parser.add_argument("--output", required=True, help="Path to output Markdown file")
- parser.add_argument("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
- parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
- parser.add_argument("--json", required=False, help="Path to pytest-report.json")
-
- args = parser.parse_args()
-
- json_to_md_nested(args.input, args.output, args.allure_dir)
- run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
-
- print(f"✅ Report generated at {args.output}")
-
-if __name__ == "__main__":
- main()
-
-tu tej le truc allure ... et prends les parametres direct du json file fais en sorte que ca prenne du oremier crochet au dernier crochet du nom, pcq il peut y avoir des crochets au milieu si les arg c est des listes ... et mets ca a cote de l id et dans la section parameters
-
-
-ChatGPT said:
-Bien vu pour le problème des crochets dans les paramètres pytest (par exemple, avec des listes ou tuples) !
-On va donc extraire les paramètres du nodeid :
-
-Tu veux récupérer tout ce qui est entre le premier [ et le dernier ] (même s’il y a d’autres crochets à l’intérieur).
-
-Puis, dans la section de chaque test, afficher cette string en tant que “Parameters” à côté de l’id du test.
-
-Je te propose une fonction robuste et un patch pour ton script :
-
-1. Fonction pour extraire les paramètres du nodeid
-python
-Copy
-Edit
-def extract_param_str_from_nodeid(nodeid):
- """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z
- Handles nested or inner brackets.
- """
- first = nodeid.find('[')
- last = nodeid.rfind(']')
- if first != -1 and last != -1 and last > first:
- return nodeid[first+1:last]
- return None
-2. Mets à jour make_test_block pour afficher les paramètres à côté de l’id
-Remplace la partie où tu veux afficher les paramètres (juste après le titre du test, ou dans la section “Parameters”).
-Voici un exemple d’intégration :
-
-python
-Copy
-Edit
-def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- param_str = extract_param_str_from_nodeid(nodeid)
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration**: `None`\n"
-
- # Ajoute la ligne Parameters si on a extrait un paramètre
- if param_str:
- body_test += f"- **Parameters:** `{param_str}`\n"
-
- # (Laisse le reste de la fonction inchangé)
- # ...
-3. Supprime tout ce qui concerne Allure
-Enlève complètement les morceaux qui appellent allure_data, load_allure_metadata et les parties allure_info, etc.
-
-Tout ce qui concerne les paramètres s’affichera directement via le parsing du nodeid du JSON.
-
-4. Exemple de rendu pour un test paramétré
-Dans le Markdown, tu auras un bloc comme ça :
-
-markdown
-Copy
-Edit
-
-✅ #2
-
-- **Status:** ✅ `passed`
-- **Duration:** `0.0012` s
-- **Parameters:** `2-2-4`
-
-...
-
-5. Patch complet pour make_test_block
-Voici juste le patch de la fonction (enlève tout ce qui est allure) :
-
-python
-Copy
-Edit
-def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- param_str = extract_param_str_from_nodeid(nodeid)
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n"
-
- # Ajout de la ligne parameters si présents
- if param_str:
- body_test += f"- **Parameters:** `{param_str}`\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "```\n"
- details_body += stringify(value) + "\n"
- details_body += "```\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1)
-6. N’oublie pas d’ajouter la fonction tout en haut du fichier :
-python
-Copy
-Edit
-def extract_param_str_from_nodeid(nodeid):
- """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z"""
- first = nodeid.find('[')
- last = nodeid.rfind(']')
- if first != -1 and last != -1 and last > first:
- return nodeid[first+1:last]
- return None
-Voilà, tu n’as plus qu’à supprimer tout ce qui est allure et utiliser ce parsing
-=> Les paramètres de chaque test seront bien extraits et affichés, même si les args sont des listes/tuples compliqués !
-
-💡 Si tu veux que le paramètre soit affiché aussi dans la colonne de summary ou dans un titre, tu peux l’ajouter partout où tu veux.
-
-Si tu veux le code complet "nettoyé" je peux te le faire aussi, dis-moi juste !
-
-
-
-
-
-
-
-
-You said:
-non mais repasse ton mon file avec les modifs qu on veut
-
-
-ChatGPT said:
-Bien sûr, voici ton fichier complet mis à jour :
-
-Suppression de tout ce qui est Allure.
-
-Extraction automatique des paramètres via nodeid avec une fonction robuste (même si les args sont des listes, tuples ou dicts).
-
-Les paramètres sont affichés pour chaque test dans la section appropriée, sous "Parameters".
-
-Aucune référence à Allure n'est gardée (tout fonctionne juste avec le JSON généré par pytest-json-report).
-
-python
-Copy
-Edit
-from collections import defaultdict
-from datetime import datetime
-import json
-import argparse
-import os
-import re
-import pytest
-from pytest import ExitCode
-import traceback
-
-def stringify(obj):
- if obj is None or obj == "":
- return "`None`"
- if isinstance(obj, list):
- return ', '.join(stringify(e) for e in obj)
- if isinstance(obj, dict):
- return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items())
- return f"`{str(obj)}`"
-
-def normalize_nodeid(nodeid):
- """Convert pytest nodeid to a compact id if needed (for function name grouping)"""
- match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
- if match:
- file_part = match.group(1).replace("/", ".").replace("\\", ".")
- func_part = match.group(2)
- return f"{file_part}#{func_part}"
- return None
-
-def extract_param_str_from_nodeid(nodeid):
- """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z.
- Handles nested or inner brackets."""
- first = nodeid.find('[')
- last = nodeid.rfind(']')
- if first != -1 and last != -1 and last > first:
- return nodeid[first+1:last]
- return None
-
-def write_details(f, summary, body, level=0):
- margin = 18 * level # 18px de marge/indent par niveau
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- f.write(f'\n')
- f.write(f"\n{summary}
\n\n")
- f.write(body)
- f.write(" \n")
- f.write("
\n\n")
-
-def json_to_md_nested(json_path, md_path):
- with open(json_path) as f:
- data = json.load(f)
-
- def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- param_str = extract_param_str_from_nodeid(nodeid)
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n"
-
- # Ajout de la ligne parameters si présents
- if param_str:
- body_test += f"- **Parameters:** `{param_str}`\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "```\n"
- details_body += stringify(value) + "\n"
- details_body += "```\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1)
-
- def get_details_block(summary, body, level=0):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
- with open(md_path, 'w', encoding='utf-8') as f:
- f.write(f"# 🧪 Test Report\n")
- f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
-
- if 'summary' in data:
- f.write("## 📋 Summary\n")
- for key, value in data['summary'].items():
- f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
- duration = data.get("duration")
- f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n")
- f.write("\n")
-
- # --------- Tests section ----------
- if "tests" in data and "summary" in data:
- test_counter = 1
- for test in data["tests"]:
- test['global_number'] = test_counter
- test_counter += 1
-
- f.write("## 🔎 Tests\n")
-
- summary_items = list(data["summary"].items())
- total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
- status_order = [k for k, _ in summary_items[:total_index]]
-
- tests_by_status = defaultdict(list)
- for test in data["tests"]:
- outcome = test.get("outcome", "unknown")
- tests_by_status[outcome].append(test)
-
- for status in status_order:
- if status not in tests_by_status:
- continue
-
- count = len(tests_by_status[status])
- emoji = "✅" if status == "passed" else "❌"
- status_label = status.capitalize().replace('_', ' ')
-
- body_status = ""
- grouped = defaultdict(lambda: defaultdict(list))
- for test in tests_by_status[status]:
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
-
- for filename, funcs in grouped.items():
- body_file = ""
- for funcname, tests in funcs.items():
- body_func = ""
- for test in sorted(tests, key=lambda x: x['global_number']):
- body_func += make_test_block(test, status, emoji, level=3)
- body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2)
- body_status += get_details_block(f"📁 {filename}", body_file, level=1)
- f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
-
- # ---------- Collectors section -----------
- if "collectors" in data:
- f.write("## 📚 Collected files\n")
- grouped = defaultdict(list)
- for collector in data["collectors"]:
- nodeid = collector.get("nodeid", "unknown")
- path = nodeid.split("::")[0]
- main_folder = path.split("/")[0] if "/" in path else path
- grouped[main_folder].append(collector)
-
- for folder, collectors in grouped.items():
- has_fail = any(c.get("outcome") != "passed" for c in collectors)
- folder_emoji = "✅" if not has_fail else "❌"
-
- body_collectors = ""
- outputs = []
- for collector in collectors:
- outcome = collector.get("outcome", "unknown")
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
- results = collector.get("result", [])
- if outcome != "passed" and results:
- outputs.append(f"### ❌ {short_node}\n```\n" + "\n".join(
- f"{k}: {v}" if isinstance(item, dict) else str(item)
- for item in results
- for k, v in item.items() if isinstance(item, dict)
- ) + "\n```")
-
- if outputs:
- body_collectors += "### 🧾 Error or Result Summary\n\n"
- for out in outputs:
- body_collectors += out + "\n"
-
- collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
- folder_body = ""
- for collector in collectors_sorted:
- outcome = collector.get("outcome", "unknown")
- emoji = "✅" if outcome == "passed" else "❌"
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
-
- body_coll = f"- **Outcome:** `{outcome}`\n"
- other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
- if other_keys:
- body_coll += "- **Details:**\n"
- body_coll += "```\n"
- for k, v in other_keys.items():
- body_coll += f"{k}:\n"
- if v is None:
- body_coll += " None\n"
- else:
- body_coll += stringify(v) + "\n"
- body_coll += "\n"
- body_coll += "```\n"
- else:
- body_coll += "- **Details:** `None`\n"
- folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
- f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1))
-
- # ---------- Warnings section -----------
- if 'warnings' in data and data['warnings']:
- f.write("## ⚠️ Warnings\n\n")
- for i, warning in enumerate(data['warnings'], 1):
- warn_body = "```\n"
- for k, v in warning.items():
- warn_body += f"{k}: {v}\n"
- warn_body += "```\n"
- f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
-
-def run_pytest_and_generate_banner_with_logs(md_path, log_path):
- exit_code = pytest.main(["tests/"])
-
- if exit_code == 0 or exit_code == 1:
- return
-
- if exit_code == 2:
- banner = (
- "⚠️ **Test execution interrupted**\n\n"
- "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
- )
- elif exit_code == 3:
- banner = (
- "🛑 **Internal error during testing**\n\n"
- "> An internal error occurred while executing the tests.\n\n"
- )
- elif exit_code == 4:
- banner = (
- "❗ **Pytest command line usage error**\n\n"
- "> There was an error in how pytest was invoked.\n\n"
- )
- elif exit_code == 5:
- banner = (
- "❗ **No tests were collected**\n\n"
- "> Pytest did not find any tests to run.\n\n"
- )
- else:
- banner = (
- f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
- "> Unexpected result during test execution.\n\n"
- )
-
- try:
- with open(log_path, "r") as lf:
- log_lines = lf.readlines()
- except Exception as e:
- print(f"❌ Could not read log file: {e}")
- return
-
- short_summary_lines = []
- in_summary = False
- for line in log_lines:
- if "short test summary info" in line.lower():
- in_summary = True
- if in_summary:
- short_summary_lines.append(line)
- if re.match(r"=+.* in .*s =+", line):
- break
-
- try:
- with open(md_path, "r") as f:
- original_md = f.read()
- except Exception as e:
- print(f"❌ Could not read markdown report: {e}")
- return
-
- full_banner = (
- banner +
- "\n📋 Short test summary info
\n\n" +
- "```\n" + "".join(short_summary_lines) + "```\n \n\n" +
- "\n🪵 Full raw pytest log
\n\n" +
- "```\n" + "".join(log_lines) + "```\n \n\n" +
- "---\n\n"
- )
-
- try:
- with open(md_path, "w") as f:
- f.write(full_banner + original_md)
- print("✅ Banner and log summary added to markdown report.")
- except Exception as e:
- print(f"❌ Failed to update markdown report: {e}")
-
-def main():
- parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
- parser.add_argument("--input", required=True, help="Path to pytest JSON file")
- parser.add_argument("--output", required=True, help="Path to output Markdown file")
- parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
- parser.add_argument("--json", required=False, help="Path to pytest-report.json")
-
- args = parser.parse_args()
-
- json_to_md_nested(args.input, args.output)
- run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
-
- print(f"✅ Report generated at {args.output}")
-
-if __name__ == "__main__":
- main()
-Tout est autonome (plus besoin d’Allure).
-
-Les paramètres des tests apparaîtront dès qu’ils sont paramétrés, même pour des listes/tuples/nombres/str.
-
-Le parsing des paramètres est robuste : il prend tout ce qu’il y a entre le premier [ et le dernier ] du nodeid.
-
-Dis-moi si tu veux un autre format ou des améliorations de style !
-
-
-
-
-
-
-
-
-You said:
-Les paramètres sont affichés pour chaque test dans la section appropriée, sous "Parameters".
-
-
-mais aussi a coté de l id
-
-
-ChatGPT said:
-Bien vu !
-Voici comment adapter :
-
-Les paramètres apparaissent à la fois dans le bloc “Parameters:” dans le détail, et directement à côté de l’id dans le résumé , pour un aperçu rapide sans tout ouvrir.
-
-Voici le code modifié (remplace ta fonction get_details_block et make_test_block)
-python
-Copy
-Edit
-def extract_param_str_from_nodeid(nodeid):
- """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z.
- Handles nested or inner brackets."""
- first = nodeid.find('[')
- last = nodeid.rfind(']')
- if first != -1 and last != -1 and last > first:
- return nodeid[first+1:last]
- return None
-
-def get_details_block(summary, body, level=0, params_str=None):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- if params_str:
- # Ajoute les paramètres dans le résumé
- summary = f"{summary} [{params_str}]"
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
-def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- param_str = extract_param_str_from_nodeid(nodeid)
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration:** `None`\n"
-
- if param_str:
- body_test += f"- **Parameters:** `{param_str}`\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "```\n"
- details_body += stringify(value) + "\n"
- details_body += "```\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- # Ajoute param_str au résumé si dispo
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1, params_str=param_str)
-Ce qu’il faut faire
-Remplace la fonction get_details_block et make_test_block dans ton code par ce qui est ci-dessus.
-
-Pas besoin de toucher au reste.
-
-Effet attendu :
-
-Dans le markdown, à côté de chaque id de test (ex: ❌ #2 [2-2-4]), les paramètres [2-2-4] sont visibles d’un coup d’œil dans le .
-
-Les paramètres sont aussi visibles en ouvrant le détail, rubrique “Parameters”.
-
-Dis-moi si tu veux que je t’envoie tout le fichier corrigé avec ça inclus !
-
-Error in message stream
-
-
-Retry
-
-
-
-
-
-You said:
-ok repasse tout le code et fais gaffe le test-report il est plus genere donc jsp cque tu m as foutu dans le code mais fais gaffe
-
-
-ChatGPT said:
-Voici tout le fichier complet, corrigé pour :
-
-Extraire et afficher les paramètres dans le ET dans le bloc détails,
-
-Ne plus dépendre d’Allure,
-
-Garder la génération du TEST-REPORT.md comme avant.
-
-Il ne te manque rien pour générer le markdown, avec les paramètres bien affichés.
-Tu peux remplacer tout ton fichier par ça.
-
-python
-Copy
-Edit
-from collections import defaultdict
-from datetime import datetime
-import json
-import argparse
-import os
-import re
-import pytest
-from pytest import ExitCode
-import traceback
-
-def stringify(obj):
- if obj is None or obj == "":
- return "`None`"
- if isinstance(obj, list):
- return ', '.join(stringify(e) for e in obj)
- if isinstance(obj, dict):
- return ', '.join(f"`{k}: {stringify(v)}`" for k, v in obj.items())
- return f"`{str(obj)}`"
-
-def normalize_nodeid(nodeid):
- """Convert pytest nodeid to Allure fullName format"""
- match = re.match(r"(tests[/\\].+?)\.py::(.+?)(?:\[.*)?$", nodeid)
- if match:
- file_part = match.group(1).replace("/", ".").replace("\\", ".")
- func_part = match.group(2)
- return f"{file_part}#{func_part}"
- return None
-
-def extract_param_str_from_nodeid(nodeid):
- """Extract parameter string from nodeid, e.g. test[x-y-z] → x-y-z.
- Handles nested or inner brackets."""
- first = nodeid.find('[')
- last = nodeid.rfind(']')
- if first != -1 and last != -1 and last > first:
- return nodeid[first+1:last]
- return None
-
-def get_details_block(summary, body, level=0, params_str=None):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- # Affiche les paramètres dans le résumé si présent
- if params_str:
- summary = f"{summary} [{params_str}]"
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-
-def make_test_block(test, status, emoji, level):
- nodeid = test.get("nodeid", "")
- param_str = extract_param_str_from_nodeid(nodeid)
- body_test = f"- **Status:** {emoji} `{status}`\n"
- duration = test.get("call", {}).get("duration")
- body_test += f"- **Duration:** `{duration:.6f}` s\n" if duration else "- **Duration**: `None`\n"
-
- if param_str:
- body_test += f"- **Parameters:** `{param_str}`\n"
-
- for phase in ['setup', 'call', 'teardown']:
- if phase in test:
- phase_body = ""
- for field, value in test[phase].items():
- if value is None:
- phase_body += f"- **{field.capitalize()}:** None\n"
- else:
- details_body = "```\n"
- details_body += stringify(value) + "\n"
- details_body += "```\n"
- phase_body += get_details_block(f"📌 {field.capitalize()}", details_body, level+2)
- if phase_body:
- body_test += f"\n### 🔧 {phase.capitalize()} Phase\n\n" + phase_body
-
- # Ajoute param_str au résumé si dispo
- return get_details_block(f"{emoji} #{test['global_number']}", body_test, level+1, params_str=param_str)
-
-def json_to_md_nested(json_path, md_path, allure_dir=None):
- with open(json_path) as f:
- data = json.load(f)
-
- with open(md_path, 'w', encoding='utf-8') as f:
- f.write(f"# 🧪 Test Report\n")
- f.write(f"*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
-
- if 'summary' in data:
- f.write("## 📋 Summary\n")
- for key, value in data['summary'].items():
- f.write(f"- **{key.capitalize()}**: {stringify(value)}\n")
- duration = data.get("duration")
- f.write(f"- **Total Duration**: `{duration:.3f}`s\n" if duration else "- **Total Duration**: `None`\n")
- f.write("\n")
-
- # --------- Tests section ----------
- if "tests" in data and "summary" in data:
- test_counter = 1
- for test in data["tests"]:
- test['global_number'] = test_counter
- test_counter += 1
-
- f.write("## 🔎 Tests\n")
-
- summary_items = list(data["summary"].items())
- total_index = next((i for i, (k, _) in enumerate(summary_items) if k == "total"), len(summary_items))
- status_order = [k for k, _ in summary_items[:total_index]]
-
- tests_by_status = defaultdict(list)
- for test in data["tests"]:
- outcome = test.get("outcome", "unknown")
- tests_by_status[outcome].append(test)
-
- for status in status_order:
- if status not in tests_by_status:
- continue
-
- count = len(tests_by_status[status])
- emoji = "✅" if status == "passed" else "❌"
- status_label = status.capitalize().replace('_', ' ')
-
- body_status = ""
- grouped = defaultdict(lambda: defaultdict(list))
- for test in tests_by_status[status]:
- nodeid = test.get("nodeid", "")
- parts = nodeid.split("::")
- filename = parts[0].replace("tests\\", "").replace("tests/", "")
- funcname = parts[1].split("[")[0]
- grouped[filename][funcname].append(test)
-
- for filename, funcs in grouped.items():
- body_file = ""
- for funcname, tests in funcs.items():
- body_func = ""
- for test in sorted(tests, key=lambda x: x['global_number']):
- body_func += make_test_block(test, status, emoji, level=3)
- body_file += get_details_block(f"🔧 Function: `{funcname}`", body_func, level=2)
- body_status += get_details_block(f"📁 {filename}", body_file, level=1)
- f.write(get_details_block(f"{emoji} {status_label} ({count})", body_status, level=0))
-
- # ---------- Collectors section -----------
- if "collectors" in data:
- f.write("## 📚 Collected files\n")
- grouped = defaultdict(list)
- for collector in data["collectors"]:
- nodeid = collector.get("nodeid", "unknown")
- path = nodeid.split("::")[0]
- main_folder = path.split("/")[0] if "/" in path else path
- grouped[main_folder].append(collector)
-
- for folder, collectors in grouped.items():
- has_fail = any(c.get("outcome") != "passed" for c in collectors)
- folder_emoji = "✅" if not has_fail else "❌"
-
- body_collectors = ""
- outputs = []
- for collector in collectors:
- outcome = collector.get("outcome", "unknown")
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
- results = collector.get("result", [])
- if outcome != "passed" and results:
- outputs.append(f"### ❌ {short_node}\n```\n" + "\n".join(
- f"{k}: {v}" if isinstance(item, dict) else str(item)
- for item in results
- for k, v in item.items() if isinstance(item, dict)
- ) + "\n```")
-
- if outputs:
- body_collectors += "### 🧾 Error or Result Summary\n\n"
- for out in outputs:
- body_collectors += out + "\n"
-
- collectors_sorted = sorted(collectors, key=lambda c: c.get("nodeid", "").split("[")[0])
- folder_body = ""
- for collector in collectors_sorted:
- outcome = collector.get("outcome", "unknown")
- emoji = "✅" if outcome == "passed" else "❌"
- nodeid = collector.get("nodeid", "unknown")
- short_node = nodeid.split("[")[0]
-
- body_coll = f"- **Outcome:** `{outcome}`\n"
- other_keys = {k: v for k, v in collector.items() if k not in {"nodeid", "outcome"}}
- if other_keys:
- body_coll += "- **Details:**\n"
- body_coll += "```\n"
- for k, v in other_keys.items():
- body_coll += f"{k}:\n"
- if v is None:
- body_coll += " None\n"
- else:
- body_coll += stringify(v) + "\n"
- body_coll += "\n"
- body_coll += "```\n"
- else:
- body_coll += "- **Details:** `None`\n"
- folder_body += get_details_block(f"{emoji} {short_node}", body_coll, level=2)
- f.write(get_details_block(f"{folder_emoji} {folder} ({len(collectors)} tests)", folder_body + body_collectors, level=1))
-
- # ---------- Warnings section -----------
- if 'warnings' in data and data['warnings']:
- f.write("## ⚠️ Warnings\n\n")
- for i, warning in enumerate(data['warnings'], 1):
- warn_body = "```\n"
- for k, v in warning.items():
- warn_body += f"{k}: {v}\n"
- warn_body += "```\n"
- f.write(get_details_block(f"Warning #{i}", warn_body, level=1))
-
-def run_pytest_and_generate_banner_with_logs(md_path, log_path):
- exit_code = pytest.main(["tests/"])
-
- if exit_code == 0 or exit_code == 1:
- return
-
- if exit_code == 2:
- banner = (
- "⚠️ **Test execution interrupted**\n\n"
- "> The test run was interrupted by the user (reasons : KeyboardInterrupt or ...).\n\n"
- )
- elif exit_code == 3:
- banner = (
- "🛑 **Internal error during testing**\n\n"
- "> An internal error occurred while executing the tests.\n\n"
- )
- elif exit_code == 4:
- banner = (
- "❗ **Pytest command line usage error**\n\n"
- "> There was an error in how pytest was invoked.\n\n"
- )
- elif exit_code == 5:
- banner = (
- "❗ **No tests were collected**\n\n"
- "> Pytest did not find any tests to run.\n\n"
- )
- else:
- banner = (
- f"❓ **Unknown pytest exit code: {exit_code}**\n\n"
- "> Unexpected result during test execution.\n\n"
- )
-
- try:
- with open(log_path, "r") as lf:
- log_lines = lf.readlines()
- except Exception as e:
- print(f"❌ Could not read log file: {e}")
- return
-
- short_summary_lines = []
- in_summary = False
- for line in log_lines:
- if "short test summary info" in line.lower():
- in_summary = True
- if in_summary:
- short_summary_lines.append(line)
- if re.match(r"=+.* in .*s =+", line):
- break
-
- try:
- with open(md_path, "r") as f:
- original_md = f.read()
- except Exception as e:
- print(f"❌ Could not read markdown report: {e}")
- return
-
- full_banner = (
- banner +
- "\n📋 Short test summary info
\n\n" +
- "```\n" + "".join(short_summary_lines) + "```\n \n\n" +
- "\n🪵 Full raw pytest log
\n\n" +
- "```\n" + "".join(log_lines) + "```\n \n\n" +
- "---\n\n"
- )
-
- try:
- with open(md_path, "w") as f:
- f.write(full_banner + original_md)
- print("✅ Banner and log summary added to markdown report.")
- except Exception as e:
- print(f"❌ Failed to update markdown report: {e}")
-
-def main():
- parser = argparse.ArgumentParser(description="Convert JSON test results to Markdown.")
- parser.add_argument("--input", required=True, help="Path to pytest JSON file")
- parser.add_argument("--output", required=True, help="Path to output Markdown file")
- parser.add_argument("--allure-dir", required=False, help="Directory of Allure test-cases (optional)")
- parser.add_argument("--log", required=False, help="Path to raw pytest output log (optional)")
- parser.add_argument("--json", required=False, help="Path to pytest-report.json")
-
- args = parser.parse_args()
-
- json_to_md_nested(args.input, args.output, args.allure_dir)
- run_pytest_and_generate_banner_with_logs(md_path=args.output, log_path=args.log)
-
- print(f"✅ Report generated at {args.output}")
-
-if __name__ == "__main__":
- main()
-Résumé
-Les paramètres sont extraits du nodeid (ex : [2-2-4]), affichés dans le résumé ET en détail.
-
-Allure n’est plus utilisé nulle part.
-
-Le fichier markdown est bien généré à chaque fois.
-
-Section tests + collectors + warnings sont toujours là.
-
-Tu peux copy/paste ce code, il est prêt à l’emploi.
-Dis-moi si tu veux une démo d’utilisation ou d’autres modifs !
-
-
-
-
-
-
-
-
-You said:
-
-Mets parameters : ... pas juste direct ...
-
-ChatGPT said:
-Oui, tu veux que ce soit affiché comme :
-
-✅ #13 Parameters: [2-2-4]
-
-au lieu de :
-
-✅ #13 [2-2-4]
-
-Voici ce qu’il faut changer dans la fonction get_details_block :
-
-Remplace :
-
-python
-Copy
-Edit
-if params_str:
- summary = f"{summary} [{params_str}]"
-par :
-
-python
-Copy
-Edit
-if params_str:
- summary = f"{summary} Parameters: [{params_str}]"
-Extrait modifié :
-
-python
-Copy
-Edit
-def get_details_block(summary, body, level=0, params_str=None):
- margin = 18 * level
- border = f"border-left: 2px solid #eee;" if level > 0 else ""
- # Ajoute "Parameters: ..." dans le résumé
- if params_str:
- summary = f"{summary} Parameters: [{params_str}]"
- return (f'\n'
- f"\n{summary}
\n\n"
- f"{body}\n"
- f" \n"
- f"
\n\n")
-Résultat :
-Les paramètres s’affichent :
-
-✅ #13 Parameters: [2-2-4]
-
-Rappelle-moi si tu veux encore adapter la couleur, la police, etc.
-Dis-moi si tu veux le code complet recopié !
-
-
-
-
-
-
-
-
-You said:
-ok et dcp t es sur que impo je trouve un moyen d avoir les noms des parametres ? c est pas possible si ?
-
-
-ChatGPT said:
-Excellente question : avec juste le JSON natif de pytest-json-report, tu n’as pas les noms des paramètres (genre a=2, b=2, expected=4), tu n’as que la valeur, c’est-à-dire [2-2-4] (et parfois c’est même stringifié !).
-
-Est-ce possible d’avoir les noms (a, b, expected) ?
-Avec le JSON report par défaut :
-→ NON, tu n’as pas les noms, car pytest construit le nodeid en concaténant juste les valeurs des paramètres.
-
-Avec Allure (si tu utilisais Allure) :
-→ OUI, parce que Allure sauve les noms et valeurs dans "parameters".
-
-Avec un plugin custom ou conftest (via hooks) :
-→ OUI, mais ça demande d’ajouter du code dans conftest.py pour les extraire pendant l’exécution et les sauvegarder dans un JSON custom.
-
-Exemple nodeid actuel (JSON report) :
-json
-Copy
-Edit
-{
- "nodeid": "tests/test_math_utils.py::test_addition_pass[2-2-4]",
- ...
-}
-Tu peux parser [2-2-4] mais tu n’as pas les noms.
-
-Si tu veux les NOMS des paramètres
-Tu dois :
-
-Soit passer par Allure (qui les fournit),
-
-Soit écrire un petit plugin (dans conftest.py) pour stocker item.callspec.params (pytest fournit ça à chaque test paramétré).
-
-Exemple ultra-light d’extraction dans un hook :
-
-python
-Copy
-Edit
-def pytest_runtest_makereport(item, call):
- if hasattr(item, "callspec"):
- print("PARAMS for", item.nodeid, item.callspec.params)
-Mais ça n’ira PAS dans le report JSON, à moins que tu ne customises le reporting.
-
-Résumé
-Juste avec le JSON natif ? → Pas de noms, seulement les valeurs.
-
-Noms ET valeurs ? → Soit Allure, soit un hook custom conftest.py qui sauvegarde toi-même ces infos.
-
-Pytest n’écrit pas les noms dans le JSON de base.
-
-Besoin d’un code qui dump tous les paramètres nommés dans un fichier après chaque test ?
-Demande-moi, je te le fais direct !
-
-
-
-
-
-
-
-
-You said:
-name: Run Pytest with Allure and Coverage Reports
-
-on:
- push:
- branches: [testing_json]
- pull_request:
-
-jobs:
- tests:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Show checked out files
- run: |
- echo "📂 Files in repo root:"
- ls -1 | head -n 20
-
- - name: Install Pixi & project dependencies
- run: |
- echo "🔧 Installing Pixi..."
- curl -fsSL https://pixi.sh/install.sh | bash > pixi.log 2>&1 || {
- echo "❌ Pixi download failed"
- cat pixi.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "$HOME/.pixi/bin" >> $GITHUB_PATH
- export PATH="$HOME/.pixi/bin:$PATH"
-
- pixi install > pixi-install.log 2>&1 || {
- echo "❌ Pixi project dependencies failed"
- cat pixi-install.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "✅ Pixi installed"
- pixi list
-
- - name: Install Python test/report tools
- run: |
- echo "🐍 Setting up Python environment..."
- python -m venv venv
- source venv/bin/activate
-
- pip install -U pip > pip.log 2>&1 || {
- echo "❌ pip upgrade failed"
- cat pip.log | grep -i 'error\|fatal'
- exit 1
- }
-
- pip install pytest pytest-cov coverage pytest-md-report > pip.log 2>&1 || {
- echo "❌ Python testing tools install failed"
- cat pip.log | grep -i 'error\|fatal'
- exit 1
- }
-
- echo "✅ Python tools installed"
- pip list
-
- - name: Install Java
- run: |
- echo "☕ Installing Java..."
- sudo apt update > java.log 2>&1 && sudo apt install -y openjdk-11-jdk >> java.log 2>&1 || {
- echo "❌ Java installation failed"
- cat java.log | grep -i 'error\|fatal'
- exit 1
- }
- echo "✅ Java installed"
- java -version
-
- - name: Install Allure CLI
- run: |
- echo "📊 Installing Allure..."
- curl -sL https://github.com/allure-framework/allure2/releases/download/2.27.0/allure-2.27.0.tgz | tar -xz > allure.log 2>&1 || {
- echo "❌ Allure download failed"
- cat allure.log | grep -i 'error\|fatal'
- exit 1
- }
-
- sudo mv allure-2.27.0 /opt/allure
- sudo ln -s /opt/allure/bin/allure /usr/local/bin/allure
- echo "✅ Allure installed"
- allure --version
-
- - name: Run tests and generate reports
- run: |
- echo "🚀 Running tests and generating reports..."
- source venv/bin/activate
- mkdir -p ci-reports/{allure,coverage,markdown}
-
- echo "🧪 Running pytest with coverage and report generation..."
- pixi run pytest . \
- --continue-on-collection-errors \
- --cov=functions \
- --cov-report=xml:ci-reports/coverage/coverage.xml \
- --cov-report=html:ci-reports/coverage/ \
- --alluredir=allure-results \
- --json-report \
- --json-report-file=ci-reports/markdown/pytest-report.json \
- --capture=no > ci-reports/markdown/raw-test-output.log 2>&1 && {
- echo "✅ Pytest completed"
- echo ""
- echo "📄 Markdown: ci-reports/markdown/test-report.md"
- echo ""
- echo "📦 JSON: ci-reports/markdown/pytest.json"
- } || {
- echo ""
- echo "⚠️ Some tests failed"
- awk '/=+ short test summary info =+/,/=+.* in .*s =+/' ci-reports/markdown/raw-test-output.log || true
- #cat ci-reports/markdown/raw-test-output.log
- }
-
- echo ""
- echo "📊 Generating Allure report..."
- allure generate allure-results -o ci-reports/allure --clean > /dev/null 2>&1 && {
- echo "✅ Allure report generated"
- } || {
- echo "⚠️ Allure generation failed"
- }
-
- echo ""
- echo "📈 Generating coverage summary..."
- coverage report --format=markdown > ci-reports/markdown/coverage-summary.md && {
- echo "✅ Coverage summary generated"
- } || {
- echo "⚠️ Coverage generation failed"
- }
-
- {
- echo ""
- echo "📊 Generating tests markdown summary..."
- python json_to_md.py \
- --input ci-reports/markdown/pytest-report.json \
- --output ci-reports/markdown/TEST-REPORT.md \
- --allure-dir ci-reports/allure/data/test-cases \
- --log ci-reports/markdown/raw-test-output.log \
- --json ci-reports/markdown/pytest-report.json
-
- echo "✅ Tests markdown generated"
- } || {
- echo "⚠️ Tests markdown generation failed"
- }
-
- - name: Commit and push reports
- run: |
- echo "📤 Committing test & coverage reports to slic.git..."
- git config user.name "ci-bot"
- git config user.email "ci-bot@example.com"
- git add ci-reports/markdown/TEST-REPORT.md ci-reports/markdown/pytest-report.json ci-reports/markdown/coverage-summary.md
- git commit -m "CI: update test and coverage reports" || echo "⚠️ Nothing to commit"
-
- echo "🚀 Pushing to branch 'testing_json' on slic.git..."
- git push https://ci-token:${{ secrets.CI_TOKEN }}@gitea.psi.ch/tligui_y/slic.git HEAD:testing_json > push_main.log 2>&1 || {
- echo "❌ Push to main repo failed"
- cat push_main.log | grep -i 'error\|fatal\|refused'
- exit 1
- }
- echo "✅ Reports pushed to https://gitea.psi.ch/tligui_y/slic/ci-reports/"
- env:
- CI_TOKEN: ${{ secrets.CI_TOKEN }}
-
-rajoute dans le ci-report markdowm le rapport rn html, en xml ... et tous les autres que je check si qlq part il y a les noms
-
-
-ChatGPT said:
-Bonne idée ! Voici comment ajouter dans ton dossier ci-reports/markdown/ tous les rapports que pytest/coverage peut générer (HTML, XML, etc.) afin que tu puisses fouiller s’il y a les noms des paramètres dans un des formats (spoiler : le XML est parfois plus verbeux).
-
-1. Ajoute toutes les variantes dans ta commande pytest
-Modifie la partie run tests and generate reports ainsi :
-
-yaml
-Copy
-Edit
- name: Run tests and generate reports
run: |
echo "🚀 Running tests and generating reports..."
@@ -10877,7 +97,7 @@ Edit
--json-report \
--json-report-file=ci-reports/markdown/pytest-report.json \
--html=ci-reports/markdown/pytest-report.html \
- --self-contained-html \
+ --alluredir=allure-results \
--capture=no > ci-reports/markdown/raw-test-output.log 2>&1 && {
echo "✅ Pytest completed"
echo ""
@@ -10887,14 +107,13 @@ Edit
echo "📝 Raw Log: ci-reports/markdown/pytest-result.log"
echo "🌐 HTML Coverage: ci-reports/coverage/index.html"
echo "📄 XML Coverage: ci-reports/coverage/coverage.xml"
- echo "🌐 HTML Test Report: ci-reports/markdown/pytest-report.html"
+ echo "🌐 Pytest HTML: ci-reports/markdown/pytest-report.html"
} || {
echo ""
echo "⚠️ Some tests failed"
awk '/=+ short test summary info =+/,/=+.* in .*s =+/' ci-reports/markdown/raw-test-output.log || true
}
-
echo ""
echo "📊 Generating Allure report..."
allure generate allure-results -o ci-reports/allure --clean > /dev/null 2>&1 && {
@@ -10928,12 +147,11 @@ Edit
- name: Commit and push reports
run: |
- echo "📤 Committing test & coverage reports to slic.git..."
+ echo "📤 Committing ALL generated reports to slic.git..."
git config user.name "ci-bot"
git config user.email "ci-bot@example.com"
- git add ci-reports/markdown/TEST-REPORT.md ci-reports/markdown/pytest-report.json ci-reports/markdown/coverage-summary.md ci-reports/markdown/pytest-junit.xml ci-reports/markdown/pytest-result.log ci-reports/markdown/pytest-report.html
- git commit -m "CI: update test and coverage reports" || echo "⚠️ Nothing to commit"
-
+ git add ci-reports/
+ git commit -m "CI: update all test and coverage reports" || echo "⚠️ Nothing to commit"
echo "🚀 Pushing to branch 'testing_json' on slic.git..."
git push https://ci-token:${{ secrets.CI_TOKEN }}@gitea.psi.ch/tligui_y/slic.git HEAD:testing_json > push_main.log 2>&1 || {
echo "❌ Push to main repo failed"
@@ -10942,4 +160,4 @@ Edit
}
echo "✅ Reports pushed to https://gitea.psi.ch/tligui_y/slic/ci-reports/"
env:
- CI_TOKEN: ${{ secrets.CI_TOKEN }}
\ No newline at end of file
+ CI_TOKEN: ${{ secrets.CI_TOKEN }}