Algorithme d'extraction des première et dernière lignes du fichier de sortie sectionné

J'essaie d'analyser la section FAILURES de la sortie du terminal d'une session Pytest, ligne par ligne, en identifiant le nom du test et le nom du fichier de test pour chaque test, que je souhaite ensuite ajouter pour former un "nom de test complet" (FQTN), par ex tests/test_1.py::test_3_fails. Je souhaite également obtenir et enregistrer les informations de traçabilité (qui se trouvent entre le nom du test et le nom du fichier de test).

La partie d'analyse est simple et j'ai déjà des regex qui correspondent au nom du test et au nom du fichier de test, et je peux extraire les informations de trace en fonction de cela. Mon problème avec les FQTN est algorithmique - je n'arrive pas à comprendre la logique globale pour identifier un nom de test, puis le nom de fichier du test, qui se produit sur une ligne ultérieure. Je dois tenir compte non seulement des tests qui se trouvent au milieu de la section FAILURES, mais également du premier test et du dernier test de la section FAILURES.

Voici un exemple. Il s'agit de la section de sortie pour tous les échecs lors d'un test, ainsi que d'une partie de la sortie du terminal qui vient juste avant FAILURES et juste après.

.
.
.
============================================== ERRORS ===============================================
__________________________________ ERROR at setup of test_c_error ___________________________________
@pytest.fixture
def error_fixture():
> assert 0
E assert 0
tests/test_2.py:19: AssertionError
============================================= FAILURES ==============================================
___________________________________________ test_3_fails ____________________________________________
log_testname = None
def test_3_fails(log_testname):
> assert 0
E assert 0
tests/test_1.py:98: AssertionError
--------------------------------------- Captured stdout setup ---------------------------------------
Running test tests.test_1...
Running test tests.test_1...
Setting test up...
Setting test up...
Executing test...
Executing test...
Tearing test down...
Tearing test down...
---------------------------------------- Captured log setup -----------------------------------------
INFO root:test_1.py:68 Running test tests.test_1...
INFO root:test_1.py:69 Setting test up...
INFO root:test_1.py:70 Executing test...
INFO root:test_1.py:72 Tearing test down...
______________________________________ test_8_causes_a_warning ______________________________________
log_testname = None
def test_8_causes_a_warning(log_testname):
> assert api_v1() == 1
E TypeError: api_v1() missing 1 required positional argument: 'log_testname'
tests/test_1.py:127: TypeError
--------------------------------------- Captured stdout setup ---------------------------------------
Running test tests.test_1...
Running test tests.test_1...
Setting test up...
Setting test up...
Executing test...
Executing test...
Tearing test down...
Tearing test down...
---------------------------------------- Captured log setup -----------------------------------------
INFO root:test_1.py:68 Running test tests.test_1...
INFO root:test_1.py:69 Setting test up...
INFO root:test_1.py:70 Executing test...
INFO root:test_1.py:72 Tearing test down...
___________________________ test_16_fail_compare_dicts_for_pytest_icdiff ____________________________
def test_16_fail_compare_dicts_for_pytest_icdiff():
listofStrings = ["Hello", "hi", "there", "at", "this"]
listofInts = [7, 10, 45, 23, 77]
assert len(listofStrings) == len(listofInts)
> assert listofStrings == listofInts
E AssertionError: assert ['Hello', 'hi... 'at', 'this'] == [7, 10, 45, 23, 77]
E At index 0 diff: 'Hello'!= 7
E Full diff:
E - [7, 10, 45, 23, 77]
E + ['Hello', 'hi', 'there', 'at', 'this']
tests/test_1.py:210: AssertionError
____________________________________________ test_b_fail ____________________________________________
def test_b_fail():
> assert 0
E assert 0
tests/test_2.py:27: AssertionError
============================================== PASSES ===============================================
___________________________________________ test_4_passes ___________________________________________
--------------------------------------- Captured stdout setup ---------------------------------------
Running test tests.test_1...
Running test tests.test_1...
Setting test up...
Setting test up...
Executing test...
Executing test...
Tearing test down...
Tearing test down...
.
.
.

Est-ce que quelqu'un ici est bon avec les algorithmes, peut-être un pseudo-code qui montre une manière globale d'obtenir chaque nom de test et son nom de fichier de test associé?


Solution du problème

Voici ma proposition pour obtenir le résumé rendu pour un rapport de cas de test. Utilisez ce stub comme une idée approximative - vous voudrez peut-être parcourir les rapports et vider d'abord les résumés rendus, puis faire la magie des malédictions pour afficher les données collectées.

Quelques tests pour jouer avec:

import pytest
def test_1():
assert False
def test_2():
raise RuntimeError('call error')
@pytest.fixture
def f():
raise RuntimeError('setup error')
def test_3(f):
assert True
@pytest.fixture
def g():
yield
raise RuntimeError('teardown error')
def test_4(g):
assert True

Exemple de plugin factice qui rend le résumé pour le test_3cas. Mettez l'extrait dans conftest.py:

def pytest_unconfigure(config):
# example: get rendered output for test case `test_spam.py::test_3`
# get the reporter
reporter = config.pluginmanager.getplugin('terminalreporter')
# create a buffer to dump reporter output to
import io
buf = io.StringIO()
# fake tty or pytest will not colorize the output
buf.isatty = lambda: True
# replace writer in reporter to dump the output in buffer instead of stdout
from _pytest.config import create_terminal_writer
# I want to use the reporter again later to dump the rendered output,
# so I store the original writer here (you probably don't need it)
original_writer = reporter._tw
writer = create_terminal_writer(config, file=buf)
# replace the writer
reporter._tw = writer
# find the report for `test_spam.py::test_3` (we already know it will be an error report)
errors = reporter.stats['error']
test_3_report = next(
report for report in errors if report.nodeid == 'test_spam.py::test_3'
)
# dump the summary along with the stack trace for the report of `test_spam.py::test_3`
reporter._outrep_summary(test_3_report)
# print dumped contents
# you probably don't need this - this is just for demo purposes
# restore the original writer to write to stdout again
reporter._tw = original_writer
reporter.section('My own section', sep='>')
reporter.write(buf.getvalue())
reporter.write_sep('<')

Une pytestcourse donne maintenant une section supplémentaire

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> My own section >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
@pytest.fixture
def f():
> raise RuntimeError('setup error')
E RuntimeError: setup error
test_spam.py:14: RuntimeError
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

avec la trace de la pile rendue de la même manière pytestdans la ERRORSsection récapitulative. Vous pouvez jouer avec les résultats de différents cas de test si vous le souhaitez - remplacez la reporter.statssection si nécessaire ( errorsou failed, ou même passed- même si le résumé doit être vide pour les tests réussis) et modifiez le cas de test nodeid.

Commentaires

Posts les plus consultés de ce blog

Erreur Symfony : "Une exception a été levée lors du rendu d'un modèle"

Détecter les appuis sur les touches fléchées en JavaScript

Une chaîne vide donne "Des erreurs ont été détectées dans les arguments de la ligne de commande, veuillez vous assurer que tous les arguments sont correctement définis"