1"""End-to-end test cases for the daemon (dmypy).
2
3These are special because they run multiple shell commands.
4
5This also includes some unit tests.
6"""
7
8import os
9import subprocess
10import sys
11import tempfile
12import unittest
13from typing import List, Tuple
14
15from mypy.modulefinder import SearchPaths
16from mypy.fscache import FileSystemCache
17from mypy.dmypy_server import filter_out_missing_top_level_packages
18
19from mypy.test.config import test_temp_dir, PREFIX
20from mypy.test.data import DataDrivenTestCase, DataSuite
21from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages
22
23# Files containing test cases descriptions.
24daemon_files = [
25    'daemon.test',
26]
27
28
29class DaemonSuite(DataSuite):
30    files = daemon_files
31
32    def run_case(self, testcase: DataDrivenTestCase) -> None:
33        try:
34            test_daemon(testcase)
35        finally:
36            # Kill the daemon if it's still running.
37            run_cmd('dmypy kill')
38
39
40def test_daemon(testcase: DataDrivenTestCase) -> None:
41    assert testcase.old_cwd is not None, "test was not properly set up"
42    for i, step in enumerate(parse_script(testcase.input)):
43        cmd = step[0]
44        expected_lines = step[1:]
45        assert cmd.startswith('$')
46        cmd = cmd[1:].strip()
47        cmd = cmd.replace('{python}', sys.executable)
48        sts, output = run_cmd(cmd)
49        output_lines = output.splitlines()
50        output_lines = normalize_error_messages(output_lines)
51        if sts:
52            output_lines.append('== Return code: %d' % sts)
53        assert_string_arrays_equal(expected_lines,
54                                   output_lines,
55                                   "Command %d (%s) did not give expected output" %
56                                   (i + 1, cmd))
57
58
59def parse_script(input: List[str]) -> List[List[str]]:
60    """Parse testcase.input into steps.
61
62    Each command starts with a line starting with '$'.
63    The first line (less '$') is sent to the shell.
64    The remaining lines are expected output.
65    """
66    steps = []
67    step = []  # type: List[str]
68    for line in input:
69        if line.startswith('$'):
70            if step:
71                assert step[0].startswith('$')
72                steps.append(step)
73                step = []
74        step.append(line)
75    if step:
76        steps.append(step)
77    return steps
78
79
80def run_cmd(input: str) -> Tuple[int, str]:
81    if input.startswith('dmypy '):
82        input = sys.executable + ' -m mypy.' + input
83    if input.startswith('mypy '):
84        input = sys.executable + ' -m' + input
85    env = os.environ.copy()
86    env['PYTHONPATH'] = PREFIX
87    try:
88        output = subprocess.check_output(input,
89                                         shell=True,
90                                         stderr=subprocess.STDOUT,
91                                         universal_newlines=True,
92                                         cwd=test_temp_dir,
93                                         env=env)
94        return 0, output
95    except subprocess.CalledProcessError as err:
96        return err.returncode, err.output
97
98
99class DaemonUtilitySuite(unittest.TestCase):
100    """Unit tests for helpers"""
101
102    def test_filter_out_missing_top_level_packages(self) -> None:
103        with tempfile.TemporaryDirectory() as td:
104            self.make_file(td, 'base/a/')
105            self.make_file(td, 'base/b.py')
106            self.make_file(td, 'base/c.pyi')
107            self.make_file(td, 'base/missing.txt')
108            self.make_file(td, 'typeshed/d.pyi')
109            self.make_file(td, 'typeshed/@python2/e')
110            self.make_file(td, 'pkg1/f-stubs')
111            self.make_file(td, 'pkg2/g-python2-stubs')
112            self.make_file(td, 'mpath/sub/long_name/')
113
114            def makepath(p: str) -> str:
115                return os.path.join(td, p)
116
117            search = SearchPaths(python_path=(makepath('base'),),
118                                 mypy_path=(makepath('mpath/sub'),),
119                                 package_path=(makepath('pkg1'), makepath('pkg2')),
120                                 typeshed_path=(makepath('typeshed'),))
121            fscache = FileSystemCache()
122            res = filter_out_missing_top_level_packages(
123                {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'long_name', 'ff', 'missing'},
124                search,
125                fscache)
126            assert res == {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'long_name'}
127
128    def make_file(self, base: str, path: str) -> None:
129        fullpath = os.path.join(base, path)
130        os.makedirs(os.path.dirname(fullpath), exist_ok=True)
131        if not path.endswith('/'):
132            with open(fullpath, 'w') as f:
133                f.write('# test file')
134