@@ -641,3 +641,160 @@ def pytest_configure():
641641 assert "INTERNALERROR" not in result .stderr .str ()
642642 warning = recwarn .pop ()
643643 assert str (warning .message ) == "from pytest_configure"
644+
645+
646+ class TestStackLevel :
647+ @pytest .fixture
648+ def capwarn (self , testdir ):
649+ class CapturedWarnings :
650+ captured = []
651+
652+ @classmethod
653+ def pytest_warning_captured (cls , warning_message , when , item , location ):
654+ cls .captured .append ((warning_message , location ))
655+
656+ testdir .plugins = [CapturedWarnings ()]
657+
658+ return CapturedWarnings
659+
660+ def test_issue4445_rewrite (self , testdir , capwarn ):
661+ """#4445: Make sure the warning points to a reasonable location
662+ See origin of _issue_warning_captured at: _pytest.assertion.rewrite.py:241
663+ """
664+ testdir .makepyfile (some_mod = "" )
665+ conftest = testdir .makeconftest (
666+ """
667+ import some_mod
668+ import pytest
669+
670+ pytest.register_assert_rewrite("some_mod")
671+ """
672+ )
673+ testdir .parseconfig ()
674+
675+ # with stacklevel=5 the warning originates from register_assert_rewrite
676+ # function in the created conftest.py
677+ assert len (capwarn .captured ) == 1
678+ warning , location = capwarn .captured .pop ()
679+ file , lineno , func = location
680+
681+ assert "Module already imported" in str (warning .message )
682+ assert file == str (conftest )
683+ assert func == "<module>" # the above conftest.py
684+ assert lineno == 4
685+
686+ def test_issue4445_preparse (self , testdir , capwarn ):
687+ """#4445: Make sure the warning points to a reasonable location
688+ See origin of _issue_warning_captured at: _pytest.config.__init__.py:910
689+ """
690+ testdir .makeconftest (
691+ """
692+ import nothing
693+ """
694+ )
695+ testdir .parseconfig ("--help" )
696+
697+ # with stacklevel=2 the warning should originate from config._preparse and is
698+ # thrown by an errorneous conftest.py
699+ assert len (capwarn .captured ) == 1
700+ warning , location = capwarn .captured .pop ()
701+ file , _ , func = location
702+
703+ assert "could not load initial conftests" in str (warning .message )
704+ assert "config{sep}__init__.py" .format (sep = os .sep ) in file
705+ assert func == "_preparse"
706+
707+ def test_issue4445_import_plugin (self , testdir , capwarn ):
708+ """#4445: Make sure the warning points to a reasonable location
709+ See origin of _issue_warning_captured at: _pytest.config.__init__.py:585
710+ """
711+ testdir .makepyfile (
712+ some_plugin = """
713+ import pytest
714+ pytest.skip("thing", allow_module_level=True)
715+ """
716+ )
717+ testdir .syspathinsert ()
718+ testdir .parseconfig ("-p" , "some_plugin" )
719+
720+ # with stacklevel=2 the warning should originate from
721+ # config.PytestPluginManager.import_plugin is thrown by a skipped plugin
722+
723+ # During config parsing the the pluginargs are checked in a while loop
724+ # that as a result of the argument count runs import_plugin twice, hence
725+ # two identical warnings are captured (is this intentional?).
726+ assert len (capwarn .captured ) == 2
727+ warning , location = capwarn .captured .pop ()
728+ file , _ , func = location
729+
730+ assert "skipped plugin 'some_plugin': thing" in str (warning .message )
731+ assert "config{sep}__init__.py" .format (sep = os .sep ) in file
732+ assert func == "import_plugin"
733+
734+ def test_issue4445_resultlog (self , testdir , capwarn ):
735+ """#4445: Make sure the warning points to a reasonable location
736+ See origin of _issue_warning_captured at: _pytest.resultlog.py:35
737+ """
738+ testdir .makepyfile (
739+ """
740+ def test_dummy():
741+ pass
742+ """
743+ )
744+ # Use parseconfigure() because the warning in resultlog.py is triggered in
745+ # the pytest_configure hook
746+ testdir .parseconfigure (
747+ "--result-log={dir}" .format (dir = testdir .tmpdir .join ("result.log" ))
748+ )
749+
750+ # with stacklevel=2 the warning originates from resultlog.pytest_configure
751+ # and is thrown when --result-log is used
752+ warning , location = capwarn .captured .pop ()
753+ file , _ , func = location
754+
755+ assert "--result-log is deprecated" in str (warning .message )
756+ assert "resultlog.py" in file
757+ assert func == "pytest_configure"
758+
759+ def test_issue4445_cacheprovider_set (self , testdir , capwarn ):
760+ """#4445: Make sure the warning points to a reasonable location
761+ See origin of _issue_warning_captured at: _pytest.cacheprovider.py:59
762+ """
763+ testdir .tmpdir .join (".pytest_cache" ).write ("something wrong" )
764+ testdir .runpytest (plugins = [capwarn ()])
765+
766+ # with stacklevel=3 the warning originates from one stacklevel above
767+ # _issue_warning_captured in cacheprovider.Cache.set and is thrown
768+ # when there are errors during cache folder creation
769+
770+ # set is called twice (in module stepwise and in cacheprovider) so emits
771+ # two warnings when there are errors during cache folder creation. (is this intentional?)
772+ assert len (capwarn .captured ) == 2
773+ warning , location = capwarn .captured .pop ()
774+ file , lineno , func = location
775+
776+ assert "could not create cache path" in str (warning .message )
777+ assert "cacheprovider.py" in file
778+ assert func == "set"
779+
780+ def test_issue4445_issue5928_mark_generator (self , testdir ):
781+ """#4445 and #5928: Make sure the warning from an unknown mark points to
782+ the test file where this mark is used.
783+ """
784+ testfile = testdir .makepyfile (
785+ """
786+ import pytest
787+
788+ @pytest.mark.unknown
789+ def test_it():
790+ pass
791+ """
792+ )
793+ result = testdir .runpytest_subprocess ()
794+ # with stacklevel=2 the warning should originate from the above created test file
795+ result .stdout .fnmatch_lines_random (
796+ [
797+ "*{testfile}:3*" .format (testfile = str (testfile )),
798+ "*Unknown pytest.mark.unknown*" ,
799+ ]
800+ )
0 commit comments