summaryrefslogtreecommitdiff
path: root/tests/test_execfile.py
blob: 90885794286879aefc3dcb1e79c8643de5a77656 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""Tests for coverage.execfile"""

from __future__ import annotations

import compileall
import json
import os
import os.path
import pathlib
import py_compile
import re
import sys

from typing import Any, Iterator

import pytest

from coverage.exceptions import NoCode, NoSource, _ExceptionDuringRun
from coverage.execfile import run_python_file, run_python_module
from coverage.files import python_reported_file

from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin

TRY_EXECFILE = os.path.join(TESTS_DIR, "modules/process_test/try_execfile.py")


class RunFileTest(CoverageTest):
    """Test cases for `run_python_file`."""

    @pytest.fixture(autouse=True)
    def clean_up(self) -> Iterator[None]:
        """These tests all run in-process. Clean up global changes."""
        yield
        sys.excepthook = sys.__excepthook__

    def test_run_python_file(self) -> None:
        run_python_file([TRY_EXECFILE, "arg1", "arg2"])
        mod_globs = json.loads(self.stdout())

        # The file should think it is __main__
        assert mod_globs['__name__'] == "__main__"

        # It should seem to come from a file named try_execfile.py
        dunder_file = os.path.basename(mod_globs['__file__'])
        assert dunder_file == "try_execfile.py"

        # It should have its correct module data.
        assert mod_globs['__doc__'].splitlines()[0] == "Test file for run_python_file."
        assert mod_globs['DATA'] == "xyzzy"
        assert mod_globs['FN_VAL'] == "my_fn('fooey')"

        # It must be self-importable as __main__.
        assert mod_globs['__main__.DATA'] == "xyzzy"

        # Argv should have the proper values.
        assert mod_globs['argv0'] == TRY_EXECFILE
        assert mod_globs['argv1-n'] == ["arg1", "arg2"]

        # __builtins__ should have the right values, like open().
        assert mod_globs['__builtins__.has_open'] is True

    def test_no_extra_file(self) -> None:
        # Make sure that running a file doesn't create an extra compiled file.
        self.make_file("xxx", """\
            desc = "a non-.py file!"
            """)

        assert os.listdir(".") == ["xxx"]
        run_python_file(["xxx"])
        assert os.listdir(".") == ["xxx"]

    def test_universal_newlines(self) -> None:
        # Make sure we can read any sort of line ending.
        pylines = """# try newlines|print('Hello, world!')|""".split('|')
        for nl in ('\n', '\r\n', '\r'):
            with open('nl.py', 'wb') as fpy:
                fpy.write(nl.join(pylines).encode('utf-8'))
            run_python_file(['nl.py'])
        assert self.stdout() == "Hello, world!\n"*3

    def test_missing_final_newline(self) -> None:
        # Make sure we can deal with a Python file with no final newline.
        self.make_file("abrupt.py", """\
            if 1:
                a = 1
                print(f"a is {a!r}")
                #""")
        with open("abrupt.py") as f:
            abrupt = f.read()
        assert abrupt[-1] == '#'
        run_python_file(["abrupt.py"])
        assert self.stdout() == "a is 1\n"

    def test_no_such_file(self) -> None:
        path = python_reported_file('xyzzy.py')
        msg = re.escape(f"No file to run: '{path}'")
        with pytest.raises(NoSource, match=msg):
            run_python_file(["xyzzy.py"])

    def test_directory_with_main(self) -> None:
        self.make_file("with_main/__main__.py", """\
            print("I am __main__")
            """)
        run_python_file(["with_main"])
        assert self.stdout() == "I am __main__\n"

    def test_directory_without_main(self) -> None:
        self.make_file("without_main/__init__.py", "")
        with pytest.raises(NoSource, match="Can't find '__main__' module in 'without_main'"):
            run_python_file(["without_main"])

    def test_code_throws(self) -> None:
        self.make_file("throw.py", """\
            class MyException(Exception):
                pass

            def f1():
                print("about to raise..")
                raise MyException("hey!")

            def f2():
                f1()

            f2()
            """)

        with pytest.raises(SystemExit) as exc_info:
            run_python_file(["throw.py"])
        assert exc_info.value.args == (1,)
        assert self.stdout() == "about to raise..\n"
        assert self.stderr() == ""

    def test_code_exits(self) -> None:
        self.make_file("exit.py", """\
            import sys
            def f1():
                print("about to exit..")
                sys.exit(17)

            def f2():
                f1()

            f2()
            """)

        with pytest.raises(SystemExit) as exc_info:
            run_python_file(["exit.py"])
        assert exc_info.value.args == (17,)
        assert self.stdout() == "about to exit..\n"
        assert self.stderr() == ""

    def test_excepthook_exit(self) -> None:
        self.make_file("excepthook_exit.py", """\
            import sys

            def excepthook(*args):
                print('in excepthook')
                sys.exit(0)

            sys.excepthook = excepthook

            raise RuntimeError('Error Outside')
            """)
        with pytest.raises(SystemExit):
            run_python_file(["excepthook_exit.py"])
        cov_out = self.stdout()
        assert cov_out == "in excepthook\n"

    def test_excepthook_throw(self) -> None:
        self.make_file("excepthook_throw.py", """\
            import sys

            def excepthook(*args):
                # Write this message to stderr so that we don't have to deal
                # with interleaved stdout/stderr comparisons in the assertions
                # in the test.
                sys.stderr.write('in excepthook\\n')
                raise RuntimeError('Error Inside')

            sys.excepthook = excepthook

            raise RuntimeError('Error Outside')
            """)
        with pytest.raises(_ExceptionDuringRun) as exc_info:
            run_python_file(["excepthook_throw.py"])
        # The _ExceptionDuringRun exception has the RuntimeError as its argument.
        assert exc_info.value.args[1].args[0] == "Error Outside"
        stderr = self.stderr()
        assert "in excepthook\n" in stderr
        assert "Error in sys.excepthook:\n" in stderr
        assert "RuntimeError: Error Inside" in stderr


class RunPycFileTest(CoverageTest):
    """Test cases for `run_python_file`."""

    def make_pyc(self, **kwargs: Any) -> str:
        """Create a .pyc file, and return the path to it."""
        self.make_file("compiled.py", """\
            def doit():
                print("I am here!")

            doit()
            """)
        compileall.compile_dir(".", quiet=True, **kwargs)
        os.remove("compiled.py")

        # Find the .pyc file!
        return str(next(pathlib.Path(".").rglob("compiled*.pyc")))

    def test_running_pyc(self) -> None:
        pycfile = self.make_pyc()
        run_python_file([pycfile])
        assert self.stdout() == "I am here!\n"

    def test_running_pyo(self) -> None:
        pycfile = self.make_pyc()
        pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile)
        assert pycfile != pyofile
        os.rename(pycfile, pyofile)
        run_python_file([pyofile])
        assert self.stdout() == "I am here!\n"

    def test_running_pyc_from_wrong_python(self) -> None:
        pycfile = self.make_pyc()

        # Jam Python 2.1 magic number into the .pyc file.
        with open(pycfile, "r+b") as fpyc:
            fpyc.seek(0)
            fpyc.write(bytes([0x2a, 0xeb, 0x0d, 0x0a]))

        with pytest.raises(NoCode, match="Bad magic number in .pyc file"):
            run_python_file([pycfile])

        # In some environments, the pycfile persists and pollutes another test.
        os.remove(pycfile)

    def test_running_hashed_pyc(self) -> None:
        pycfile = self.make_pyc(invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH)
        run_python_file([pycfile])
        assert self.stdout() == "I am here!\n"

    def test_no_such_pyc_file(self) -> None:
        path = python_reported_file('xyzzy.pyc')
        msg = re.escape(f"No file to run: '{path}'")
        with pytest.raises(NoCode, match=msg):
            run_python_file(["xyzzy.pyc"])

    def test_running_py_from_binary(self) -> None:
        # Use make_file to get the bookkeeping. Ideally, it would
        # be able to write binary files.
        bf = self.make_file("binary")
        with open(bf, "wb") as f:
            f.write(b'\x7fELF\x02\x01\x01\x00\x00\x00')

        path = python_reported_file('binary')
        msg = (
            re.escape(f"Couldn't run '{path}' as Python code: ") +
            r"(ValueError|SyntaxError): source code string cannot contain null bytes"
        )
        with pytest.raises(Exception, match=msg):
            run_python_file([bf])


class RunModuleTest(UsingModulesMixin, CoverageTest):
    """Test run_python_module."""

    run_in_temp_dir = False

    def test_runmod1(self) -> None:
        run_python_module(["runmod1", "hello"])
        out, err = self.stdouterr()
        assert out == "runmod1: passed hello\n"
        assert err == ""

    def test_runmod2(self) -> None:
        run_python_module(["pkg1.runmod2", "hello"])
        out, err = self.stdouterr()
        assert out == "pkg1.__init__: pkg1\nrunmod2: passed hello\n"
        assert err == ""

    def test_runmod3(self) -> None:
        run_python_module(["pkg1.sub.runmod3", "hello"])
        out, err = self.stdouterr()
        assert out == "pkg1.__init__: pkg1\nrunmod3: passed hello\n"
        assert err == ""

    def test_pkg1_main(self) -> None:
        run_python_module(["pkg1", "hello"])
        out, err = self.stdouterr()
        assert out == "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n"
        assert err == ""

    def test_pkg1_sub_main(self) -> None:
        run_python_module(["pkg1.sub", "hello"])
        out, err = self.stdouterr()
        assert out == "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n"
        assert err == ""

    def test_pkg1_init(self) -> None:
        run_python_module(["pkg1.__init__", "wut?"])
        out, err = self.stdouterr()
        assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n"
        assert err == ""

    def test_no_such_module(self) -> None:
        with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"):
            run_python_module(["i_dont_exist"])
        with pytest.raises(NoSource, match="No module named '?i'?"):
            run_python_module(["i.dont_exist"])
        with pytest.raises(NoSource, match="No module named '?i'?"):
            run_python_module(["i.dont.exist"])

    def test_no_main(self) -> None:
        with pytest.raises(NoSource):
            run_python_module(["pkg2", "hi"])