from __future__ import annotations

import os
import shutil
import tempfile
import unittest

import pytest

from mypy.find_sources import InvalidSourceList, SourceFinder, create_source_list
from mypy.fscache import FileSystemCache
from mypy.modulefinder import BuildSource
from mypy.options import Options

class FakeFSCache(FileSystemCache):
    def __init__(self, files: set[str]) -> None:
        self.files = {os.path.abspath(f) for f in files}

    def isfile(self, file: str) -> bool:
        return file in self.files

    def isdir(self, dir: str) -> bool:
        if not dir.endswith(os.sep):
            dir += os.sep
        return any(f.startswith(dir) for f in self.files)

    def listdir(self, dir: str) -> list[str]:
        if not dir.endswith(os.sep):
            dir += os.sep
        return list({f[len(dir) :].split(os.sep)[0] for f in self.files if f.startswith(dir)})

    def init_under_package_root(self, file: str) -> bool:
        return False

def normalise_path(path: str) -> str:
    path = os.path.splitdrive(path)[1]
    path = path.replace(os.sep, "/")
    return path

def normalise_build_source_list(sources: list[BuildSource]) -> list[tuple[str, str | None]]:
    return sorted(
        (s.module, (normalise_path(s.base_dir) if s.base_dir is not None else None))
        for s in sources
    )

def crawl(finder: SourceFinder, f: str) -> tuple[str, str]:
    module, base_dir = finder.crawl_up(f)
    return module, normalise_path(base_dir)

def find_sources_in_dir(finder: SourceFinder, f: str) -> list[tuple[str, str | None]]:
    return normalise_build_source_list(finder.find_sources_in_dir(os.path.abspath(f)))

def find_sources(
    paths: list[str], options: Options, fscache: FileSystemCache
) -> list[tuple[str, str | None]]:
    paths = [os.path.abspath(p) for p in paths]
    return normalise_build_source_list(create_source_list(paths, options, fscache))

class SourceFinderSuite(unittest.TestCase):
    def setUp(self) -> None:
        self.tempdir = tempfile.mkdtemp()
        self.oldcwd = os.getcwd()
        os.chdir(self.tempdir)

    def tearDown(self) -> None:
        os.chdir(self.oldcwd)
        shutil.rmtree(self.tempdir)

    def test_crawl_no_namespace(self) -> None:
        options = Options()
        options.namespace_packages = False

        finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
        assert crawl(finder, "/setup.py") == ("setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("setup", "/a")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")

        finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("a.setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")

        finder = SourceFinder(
            FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
        )
        assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")

    def test_crawl_namespace(self) -> None:
        options = Options()
        options.namespace_packages = True

        finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
        assert crawl(finder, "/setup.py") == ("setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("setup", "/a")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")

        finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("a.setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("a.b.setup", "/")

        finder = SourceFinder(
            FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
        )
        assert crawl(finder, "/a/b/c/setup.py") == ("a.b.c.setup", "/")

    def test_crawl_namespace_explicit_base(self) -> None:
        options = Options()
        options.namespace_packages = True
        options.explicit_package_bases = True

        finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
        assert crawl(finder, "/setup.py") == ("setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("setup", "/a")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")

        finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/setup.py") == ("a.setup", "/")

        finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")

        finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
        assert crawl(finder, "/a/b/setup.py") == ("a.b.setup", "/")

        finder = SourceFinder(
            FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
        )
        assert crawl(finder, "/a/b/c/setup.py") == ("a.b.c.setup", "/")

        # set mypy path, so we actually have some explicit base dirs
        options.mypy_path = ["/a/b"]

        finder = SourceFinder(FakeFSCache({"/a/b/c/setup.py"}), options)
        assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")

        finder = SourceFinder(
            FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
        )
        assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")

        options.mypy_path = ["/a/b", "/a/b/c"]
        finder = SourceFinder(FakeFSCache({"/a/b/c/setup.py"}), options)
        assert crawl(finder, "/a/b/c/setup.py") == ("setup", "/a/b/c")

    def test_crawl_namespace_multi_dir(self) -> None:
        options = Options()
        options.namespace_packages = True
        options.explicit_package_bases = True
        options.mypy_path = ["/a", "/b"]

        finder = SourceFinder(FakeFSCache({"/a/pkg/a.py", "/b/pkg/b.py"}), options)
        assert crawl(finder, "/a/pkg/a.py") == ("pkg.a", "/a")
        assert crawl(finder, "/b/pkg/b.py") == ("pkg.b", "/b")

    def test_find_sources_in_dir_no_namespace(self) -> None:
        options = Options()
        options.namespace_packages = False

        files = {
            "/pkg/a1/b/c/d/e.py",
            "/pkg/a1/b/f.py",
            "/pkg/a2/__init__.py",
            "/pkg/a2/b/c/d/e.py",
            "/pkg/a2/b/f.py",
        }
        finder = SourceFinder(FakeFSCache(files), options)
        assert find_sources_in_dir(finder, "/") == [
            ("a2", "/pkg"),
            ("e", "/pkg/a1/b/c/d"),
            ("e", "/pkg/a2/b/c/d"),
            ("f", "/pkg/a1/b"),
            ("f", "/pkg/a2/b"),
        ]

    def test_find_sources_in_dir_namespace(self) -> None:
        options = Options()
        options.namespace_packages = True

        files = {
            "/pkg/a1/b/c/d/e.py",
            "/pkg/a1/b/f.py",
            "/pkg/a2/__init__.py",
            "/pkg/a2/b/c/d/e.py",
            "/pkg/a2/b/f.py",
        }
        finder = SourceFinder(FakeFSCache(files), options)
        assert find_sources_in_dir(finder, "/") == [
            ("a2", "/pkg"),
            ("a2.b.c.d.e", "/pkg"),
            ("a2.b.f", "/pkg"),
            ("e", "/pkg/a1/b/c/d"),
            ("f", "/pkg/a1/b"),
        ]

    def test_find_sources_in_dir_namespace_explicit_base(self) -> None:
        options = Options()
        options.namespace_packages = True
        options.explicit_package_bases = True
        options.mypy_path = ["/"]

        files = {
            "/pkg/a1/b/c/d/e.py",
            "/pkg/a1/b/f.py",
            "/pkg/a2/__init__.py",
            "/pkg/a2/b/c/d/e.py",
            "/pkg/a2/b/f.py",
        }
        finder = SourceFinder(FakeFSCache(files), options)
        assert find_sources_in_dir(finder, "/") == [
            ("pkg.a1.b.c.d.e", "/"),
            ("pkg.a1.b.f", "/"),
            ("pkg.a2", "/"),
            ("pkg.a2.b.c.d.e", "/"),
            ("pkg.a2.b.f", "/"),
        ]

        options.mypy_path = ["/pkg"]
        finder = SourceFinder(FakeFSCache(files), options)
        assert find_sources_in_dir(finder, "/") == [
            ("a1.b.c.d.e", "/pkg"),
            ("a1.b.f", "/pkg"),
            ("a2", "/pkg"),
            ("a2.b.c.d.e", "/pkg"),
            ("a2.b.f", "/pkg"),
        ]

    def test_find_sources_in_dir_namespace_multi_dir(self) -> None:
        options = Options()
        options.namespace_packages = True
        options.explicit_package_bases = True
        options.mypy_path = ["/a", "/b"]

        finder = SourceFinder(FakeFSCache({"/a/pkg/a.py", "/b/pkg/b.py"}), options)
        assert find_sources_in_dir(finder, "/") == [("pkg.a", "/a"), ("pkg.b", "/b")]

    def test_find_sources_exclude(self) -> None:
        options = Options()
        options.namespace_packages = True

        # default
        for excluded_dir in ["site-packages", ".whatever", "node_modules", ".x/.z"]:
            fscache = FakeFSCache({"/dir/a.py", f"/dir/venv/{excluded_dir}/b.py"})
            assert find_sources(["/"], options, fscache) == [("a", "/dir")]
            with pytest.raises(InvalidSourceList):
                find_sources(["/dir/venv/"], options, fscache)
            assert find_sources([f"/dir/venv/{excluded_dir}"], options, fscache) == [
                ("b", f"/dir/venv/{excluded_dir}")
            ]
            assert find_sources([f"/dir/venv/{excluded_dir}/b.py"], options, fscache) == [
                ("b", f"/dir/venv/{excluded_dir}")
            ]

        files = {
            "/pkg/a1/b/c/d/e.py",
            "/pkg/a1/b/f.py",
            "/pkg/a2/__init__.py",
            "/pkg/a2/b/c/d/e.py",
            "/pkg/a2/b/f.py",
        }

        # file name
        options.exclude = [r"/f\.py$"]
        fscache = FakeFSCache(files)
        assert find_sources(["/"], options, fscache) == [
            ("a2", "/pkg"),
            ("a2.b.c.d.e", "/pkg"),
            ("e", "/pkg/a1/b/c/d"),
        ]
        assert find_sources(["/pkg/a1/b/f.py"], options, fscache) == [("f", "/pkg/a1/b")]
        assert find_sources(["/pkg/a2/b/f.py"], options, fscache) == [("a2.b.f", "/pkg")]

        # directory name
        options.exclude = ["/a1/"]
        fscache = FakeFSCache(files)
        assert find_sources(["/"], options, fscache) == [
            ("a2", "/pkg"),
            ("a2.b.c.d.e", "/pkg"),
            ("a2.b.f", "/pkg"),
        ]
        with pytest.raises(InvalidSourceList):
            find_sources(["/pkg/a1"], options, fscache)
        with pytest.raises(InvalidSourceList):
            find_sources(["/pkg/a1/"], options, fscache)
        with pytest.raises(InvalidSourceList):
            find_sources(["/pkg/a1/b"], options, fscache)

        options.exclude = ["/a1/$"]
        assert find_sources(["/pkg/a1"], options, fscache) == [
            ("e", "/pkg/a1/b/c/d"),
            ("f", "/pkg/a1/b"),
        ]

        # paths
        options.exclude = ["/pkg/a1/"]
        fscache = FakeFSCache(files)
        assert find_sources(["/"], options, fscache) == [
            ("a2", "/pkg"),
            ("a2.b.c.d.e", "/pkg"),
            ("a2.b.f", "/pkg"),
        ]
        with pytest.raises(InvalidSourceList):
            find_sources(["/pkg/a1"], options, fscache)

        # OR two patterns together
        for orred in [["/(a1|a3)/"], ["a1", "a3"], ["a3", "a1"]]:
            options.exclude = orred
            fscache = FakeFSCache(files)
            assert find_sources(["/"], options, fscache) == [
                ("a2", "/pkg"),
                ("a2.b.c.d.e", "/pkg"),
                ("a2.b.f", "/pkg"),
            ]

        options.exclude = ["b/c/"]
        fscache = FakeFSCache(files)
        assert find_sources(["/"], options, fscache) == [
            ("a2", "/pkg"),
            ("a2.b.f", "/pkg"),
            ("f", "/pkg/a1/b"),
        ]

        # nothing should be ignored as a result of this
        big_exclude1 = [
            "/pkg/a/",
            "/2",
            "/1",
            "/pk/",
            "/kg",
            "/g.py",
            "/bc",
            "/xxx/pkg/a2/b/f.py",
            "xxx/pkg/a2/b/f.py",
        ]
        big_exclude2 = ["|".join(big_exclude1)]
        for big_exclude in [big_exclude1, big_exclude2]:
            options.exclude = big_exclude
            fscache = FakeFSCache(files)
            assert len(find_sources(["/"], options, fscache)) == len(files)

            files = {
                "pkg/a1/b/c/d/e.py",
                "pkg/a1/b/f.py",
                "pkg/a2/__init__.py",
                "pkg/a2/b/c/d/e.py",
                "pkg/a2/b/f.py",
            }
            fscache = FakeFSCache(files)
            assert len(find_sources(["."], options, fscache)) == len(files)


"""Test exporting line-level reference information (undocumented feature)"""

from __future__ import annotations

import json
import os
import sys

from mypy import build
from mypy.modulefinder import BuildSource
from mypy.options import Options
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal

class RefInfoSuite(DataSuite):
    required_out_section = True
    files = ["ref-info.test"]

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        options = Options()
        options.use_builtins_fixtures = True
        options.show_traceback = True
        options.export_ref_info = True  # This is the flag we are testing

        src = "\n".join(testcase.input)
        result = build.build(
            sources=[BuildSource("main", None, src)], options=options, alt_lib_path=test_temp_dir
        )
        assert not result.errors

        major, minor = sys.version_info[:2]
        ref_path = os.path.join(options.cache_dir, f"{major}.{minor}", "__main__.refs.json")

        with open(ref_path) as refs_file:
            data = json.load(refs_file)

        a = []
        for item in data:
            a.append(f"{item['line']}:{item['column']}:{item['target']}")

        assert_string_arrays_equal(
            testcase.output, a, f"Invalid output ({testcase.file}, line {testcase.line})"
        )


from __future__ import annotations

import sys
from io import StringIO

import mypy.api
from mypy.test.helpers import Suite

class APISuite(Suite):
    def setUp(self) -> None:
        self.sys_stdout = sys.stdout
        self.sys_stderr = sys.stderr
        sys.stdout = self.stdout = StringIO()
        sys.stderr = self.stderr = StringIO()

    def tearDown(self) -> None:
        sys.stdout = self.sys_stdout
        sys.stderr = self.sys_stderr
        assert self.stdout.getvalue() == ""
        assert self.stderr.getvalue() == ""

    def test_capture_bad_opt(self) -> None:
        """stderr should be captured when a bad option is passed."""
        _, stderr, _ = mypy.api.run(["--some-bad-option"])
        assert isinstance(stderr, str)
        assert stderr != ""

    def test_capture_empty(self) -> None:
        """stderr should be captured when a bad option is passed."""
        _, stderr, _ = mypy.api.run([])
        assert isinstance(stderr, str)
        assert stderr != ""

    def test_capture_help(self) -> None:
        """stdout should be captured when --help is passed."""
        stdout, _, _ = mypy.api.run(["--help"])
        assert isinstance(stdout, str)
        assert stdout != ""

    def test_capture_version(self) -> None:
        """stdout should be captured when --version is passed."""
        stdout, _, _ = mypy.api.run(["--version"])
        assert isinstance(stdout, str)
        assert stdout != ""


"""Ensure the argparse parser and Options class are in sync.

In particular, verify that the argparse defaults are the same as the Options
defaults, and that argparse doesn't assign any new members to the Options
object it creates.
"""

from __future__ import annotations

import argparse
import sys

from mypy.main import infer_python_executable, process_options
from mypy.options import Options
from mypy.test.helpers import Suite, assert_equal

class ArgSuite(Suite):
    def test_coherence(self) -> None:
        options = Options()
        _, parsed_options = process_options([], require_targets=False)
        # FIX: test this too. Requires changing working dir to avoid finding 'setup.cfg'
        options.config_file = parsed_options.config_file
        assert_equal(options.snapshot(), parsed_options.snapshot())

    def test_executable_inference(self) -> None:
        """Test the --python-executable flag with --python-version"""
        sys_ver_str = "{ver.major}.{ver.minor}".format(ver=sys.version_info)

        base = ["file.py"]  # dummy file

        # test inference given one (infer the other)
        matching_version = base + [f"--python-version={sys_ver_str}"]
        _, options = process_options(matching_version)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable == sys.executable

        matching_version = base + [f"--python-executable={sys.executable}"]
        _, options = process_options(matching_version)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable == sys.executable

        # test inference given both
        matching_version = base + [
            f"--python-version={sys_ver_str}",
            f"--python-executable={sys.executable}",
        ]
        _, options = process_options(matching_version)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable == sys.executable

        # test that --no-site-packages will disable executable inference
        matching_version = base + [f"--python-version={sys_ver_str}", "--no-site-packages"]
        _, options = process_options(matching_version)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable is None

        # Test setting python_version/executable from config file
        special_opts = argparse.Namespace()
        special_opts.python_executable = None
        special_opts.python_version = None
        special_opts.no_executable = None

        # first test inferring executable from version
        options = Options()
        options.python_executable = None
        options.python_version = sys.version_info[:2]
        infer_python_executable(options, special_opts)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable == sys.executable

        # then test inferring version from executable
        options = Options()
        options.python_executable = sys.executable
        infer_python_executable(options, special_opts)
        assert options.python_version == sys.version_info[:2]
        assert options.python_executable == sys.executable


"""Type checker test cases"""

from __future__ import annotations

import os
import re
import sys

from mypy import build
from mypy.build import Graph
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths
from mypy.test.config import test_data_prefix, test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite, FileOperation, module_from_path
from mypy.test.helpers import (
    assert_module_equivalence,
    assert_string_arrays_equal,
    assert_target_equivalence,
    check_test_output_files,
    find_test_files,
    normalize_error_messages,
    parse_options,
    perform_file_operations,
)
from mypy.test.update_data import update_testcase_output

try:
    import lxml  # type: ignore[import-untyped]
except ImportError:
    lxml = None

import pytest

# List of files that contain test case descriptions.
# Includes all check-* files with the .test extension in the test-data/unit directory
typecheck_files = find_test_files(pattern="check-*.test")

# Tests that use Python version specific features:
if sys.version_info < (3, 9):
    typecheck_files.remove("check-python39.test")
if sys.version_info < (3, 10):
    typecheck_files.remove("check-python310.test")
if sys.version_info < (3, 11):
    typecheck_files.remove("check-python311.test")
if sys.version_info < (3, 12):
    typecheck_files.remove("check-python312.test")
if sys.version_info < (3, 13):
    typecheck_files.remove("check-python313.test")

# Special tests for platforms with case-insensitive filesystems.
if sys.platform not in ("darwin", "win32"):
    typecheck_files.remove("check-modules-case.test")

class TypeCheckSuite(DataSuite):
    files = typecheck_files

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        if lxml is None and os.path.basename(testcase.file) == "check-reports.test":
            pytest.skip("Cannot import lxml. Is it installed?")
        incremental = (
            "incremental" in testcase.name.lower()
            or "incremental" in testcase.file
            or "serialize" in testcase.file
        )
        if incremental:
            # Incremental tests are run once with a cold cache, once with a warm cache.
            # Expect success on first run, errors from testcase.output (if any) on second run.
            num_steps = max([2] + list(testcase.output2.keys()))
            # Check that there are no file changes beyond the last run (they would be ignored).
            for dn, dirs, files in os.walk(os.curdir):
                for file in files:
                    m = re.search(r"\.([2-9])$", file)
                    if m and int(m.group(1)) > num_steps:
                        raise ValueError(
                            "Output file {} exists though test case only has {} runs".format(
                                file, num_steps
                            )
                        )
            steps = testcase.find_steps()
            for step in range(1, num_steps + 1):
                idx = step - 2
                ops = steps[idx] if idx < len(steps) and idx >= 0 else []
                self.run_case_once(testcase, ops, step)
        else:
            self.run_case_once(testcase)

    def _sort_output_if_needed(self, testcase: DataDrivenTestCase, a: list[str]) -> None:
        idx = testcase.output_inline_start
        if not testcase.files or idx == len(testcase.output):
            return

        def _filename(_msg: str) -> str:
            return _msg.partition(":")[0]

        file_weights = {file: idx for idx, file in enumerate(_filename(msg) for msg in a)}
        testcase.output[idx:] = sorted(
            testcase.output[idx:], key=lambda msg: file_weights.get(_filename(msg), -1)
        )

    def run_case_once(
        self,
        testcase: DataDrivenTestCase,
        operations: list[FileOperation] | None = None,
        incremental_step: int = 0,
    ) -> None:
        if operations is None:
            operations = []
        original_program_text = "\n".join(testcase.input)
        module_data = self.parse_module(original_program_text, incremental_step)

        # Unload already loaded plugins, they may be updated.
        for file, _ in testcase.files:
            module = module_from_path(file)
            if module.endswith("_plugin") and module in sys.modules:
                del sys.modules[module]
        if incremental_step == 0 or incremental_step == 1:
            # In run 1, copy program text to program file.
            for module_name, program_path, program_text in module_data:
                if module_name == "__main__":
                    with open(program_path, "w", encoding="utf8") as f:
                        f.write(program_text)
                    break
        elif incremental_step > 1:
            # In runs 2+, copy *.[num] files to * files.
            perform_file_operations(operations)

        # Parse options after moving files (in case mypy.ini is being moved).
        options = parse_options(original_program_text, testcase, incremental_step)
        options.use_builtins_fixtures = True
        options.show_traceback = True

        # Enable some options automatically based on test file name.
        if "columns" in testcase.file:
            options.show_column_numbers = True
        if "errorcodes" in testcase.file:
            options.hide_error_codes = False
        if "abstract" not in testcase.file:
            options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
        if "lowercase" not in testcase.file:
            options.force_uppercase_builtins = True
        if "union-error" not in testcase.file:
            options.force_union_syntax = True

        if incremental_step and options.incremental:
            # Don't overwrite # flags: --no-incremental in incremental test cases
            options.incremental = True
        else:
            options.incremental = False
            # Don't waste time writing cache unless we are specifically looking for it
            if not testcase.writescache:
                options.cache_dir = os.devnull

        sources = []
        for module_name, program_path, program_text in module_data:
            # Always set to none so we're forced to reread the module in incremental mode
            sources.append(
                BuildSource(program_path, module_name, None if incremental_step else program_text)
            )

        plugin_dir = os.path.join(test_data_prefix, "plugins")
        sys.path.insert(0, plugin_dir)

        res = None
        try:
            res = build.build(sources=sources, options=options, alt_lib_path=test_temp_dir)
            a = res.errors
        except CompileError as e:
            a = e.messages
        finally:
            assert sys.path[0] == plugin_dir
            del sys.path[0]

        if testcase.normalize_output:
            a = normalize_error_messages(a)

        # Make sure error messages match
        if incremental_step < 2:
            if incremental_step == 1:
                msg = "Unexpected type checker output in incremental, run 1 ({}, line {})"
            else:
                assert incremental_step == 0
                msg = "Unexpected type checker output ({}, line {})"
            self._sort_output_if_needed(testcase, a)
            output = testcase.output
        else:
            msg = (
                f"Unexpected type checker output in incremental, run {incremental_step}"
                + " ({}, line {})"
            )
            output = testcase.output2.get(incremental_step, [])

        if output != a and testcase.config.getoption("--update-data", False):
            update_testcase_output(testcase, a, incremental_step=incremental_step)

        assert_string_arrays_equal(output, a, msg.format(testcase.file, testcase.line))

        if res:
            if options.cache_dir != os.devnull:
                self.verify_cache(module_data, res.errors, res.manager, res.graph)

            name = "targets"
            if incremental_step:
                name += str(incremental_step + 1)
            expected = testcase.expected_fine_grained_targets.get(incremental_step + 1)
            actual = [
                target
                for module, target in res.manager.processed_targets
                if module in testcase.test_modules
            ]
            if expected is not None:
                assert_target_equivalence(name, expected, actual)
            if incremental_step > 1:
                suffix = "" if incremental_step == 2 else str(incremental_step - 1)
                expected_rechecked = testcase.expected_rechecked_modules.get(incremental_step - 1)
                if expected_rechecked is not None:
                    assert_module_equivalence(
                        "rechecked" + suffix, expected_rechecked, res.manager.rechecked_modules
                    )
                expected_stale = testcase.expected_stale_modules.get(incremental_step - 1)
                if expected_stale is not None:
                    assert_module_equivalence(
                        "stale" + suffix, expected_stale, res.manager.stale_modules
                    )

        if testcase.output_files:
            check_test_output_files(testcase, incremental_step, strip_prefix="tmp/")

    def verify_cache(
        self,
        module_data: list[tuple[str, str, str]],
        a: list[str],
        manager: build.BuildManager,
        graph: Graph,
    ) -> None:
        # There should be valid cache metadata for each module except
        # for those that had an error in themselves or one of their
        # dependencies.
        error_paths = self.find_error_message_paths(a)
        busted_paths = {m.path for id, m in manager.modules.items() if graph[id].transitive_error}
        modules = self.find_module_files(manager)
        modules.update({module_name: path for module_name, path, text in module_data})
        missing_paths = self.find_missing_cache_files(modules, manager)
        # We would like to assert error_paths.issubset(busted_paths)
        # but this runs into trouble because while some 'notes' are
        # really errors that cause an error to be marked, many are
        # just notes attached to other errors.
        assert error_paths or not busted_paths, "Some modules reported error despite no errors"
        if not missing_paths == busted_paths:
            raise AssertionError(f"cache data discrepancy {missing_paths} != {busted_paths}")
        assert os.path.isfile(os.path.join(manager.options.cache_dir, ".gitignore"))
        cachedir_tag = os.path.join(manager.options.cache_dir, "CACHEDIR.TAG")
        assert os.path.isfile(cachedir_tag)
        with open(cachedir_tag) as f:
            assert f.read().startswith("Signature: 8a477f597d28d172789f06886806bc55")

    def find_error_message_paths(self, a: list[str]) -> set[str]:
        hits = set()
        for line in a:
            m = re.match(r"([^\s:]+):(\d+:)?(\d+:)? (error|warning|note):", line)
            if m:
                p = m.group(1)
                hits.add(p)
        return hits

    def find_module_files(self, manager: build.BuildManager) -> dict[str, str]:
        return {id: module.path for id, module in manager.modules.items()}

    def find_missing_cache_files(
        self, modules: dict[str, str], manager: build.BuildManager
    ) -> set[str]:
        ignore_errors = True
        missing = {}
        for id, path in modules.items():
            meta = build.find_cache_meta(id, path, manager)
            if not build.validate_meta(meta, id, path, ignore_errors, manager):
                missing[id] = path
        return set(missing.values())

    def parse_module(
        self, program_text: str, incremental_step: int = 0
    ) -> list[tuple[str, str, str]]:
        """Return the module and program names for a test case.

        Normally, the unit tests will parse the default ('__main__')
        module and follow all the imports listed there. You can override
        this behavior and instruct the tests to check multiple modules
        by using a comment like this in the test case input:

          # cmd: mypy -m foo.bar foo.baz

        You can also use `# cmdN:` to have a different cmd for incremental
        step N (2, 3, ...).

        Return a list of tuples (module name, file name, program text).
        """
        m = re.search("# cmd: mypy -m ([a-zA-Z0-9_. ]+)$", program_text, flags=re.MULTILINE)
        if incremental_step > 1:
            alt_regex = f"# cmd{incremental_step}: mypy -m ([a-zA-Z0-9_. ]+)$"
            alt_m = re.search(alt_regex, program_text, flags=re.MULTILINE)
            if alt_m is not None:
                # Optionally return a different command if in a later step
                # of incremental mode, otherwise default to reusing the
                # original cmd.
                m = alt_m

        if m:
            # The test case wants to use a non-default main
            # module. Look up the module and give it as the thing to
            # analyze.
            module_names = m.group(1)
            out = []
            search_paths = SearchPaths((test_temp_dir,), (), (), ())
            cache = FindModuleCache(search_paths, fscache=None, options=None)
            for module_name in module_names.split(" "):
                path = cache.find_module(module_name)
                assert isinstance(path, str), f"Can't find ad hoc case file: {module_name}"
                with open(path, encoding="utf8") as f:
                    program_text = f.read()
                out.append((module_name, path, program_text))
            return out
        else:
            return [("__main__", "main", program_text)]


"""Test cases for the command line.

To begin we test that "mypy <directory>[/]" always recurses down the
whole tree.
"""

from __future__ import annotations

import os
import re
import subprocess
import sys

from mypy.test.config import PREFIX, test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import (
    assert_string_arrays_equal,
    check_test_output_files,
    normalize_error_messages,
)

try:
    import lxml  # type: ignore[import-untyped]
except ImportError:
    lxml = None

import pytest

# Path to Python 3 interpreter
python3_path = sys.executable

# Files containing test case descriptions.
cmdline_files = ["cmdline.test", "cmdline.pyproject.test", "reports.test", "envvars.test"]

class PythonCmdlineSuite(DataSuite):
    files = cmdline_files
    native_sep = True

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        if lxml is None and os.path.basename(testcase.file) == "reports.test":
            pytest.skip("Cannot import lxml. Is it installed?")
        for step in [1] + sorted(testcase.output2):
            test_python_cmdline(testcase, step)

def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
    assert testcase.old_cwd is not None, "test was not properly set up"
    # Write the program to a file.
    program = "_program.py"
    program_path = os.path.join(test_temp_dir, program)
    with open(program_path, "w", encoding="utf8") as file:
        for s in testcase.input:
            file.write(f"{s}\n")
    args = parse_args(testcase.input[0])
    custom_cwd = parse_cwd(testcase.input[1]) if len(testcase.input) > 1 else None
    args.append("--show-traceback")
    if "--error-summary" not in args:
        args.append("--no-error-summary")
    if "--show-error-codes" not in args:
        args.append("--hide-error-codes")
    if "--disallow-empty-bodies" not in args:
        args.append("--allow-empty-bodies")
    if "--no-force-uppercase-builtins" not in args:
        args.append("--force-uppercase-builtins")
    if "--no-force-union-syntax" not in args:
        args.append("--force-union-syntax")
    # Type check the program.
    fixed = [python3_path, "-m", "mypy"]
    env = os.environ.copy()
    env.pop("COLUMNS", None)
    extra_path = os.path.join(os.path.abspath(test_temp_dir), "pypath")
    env["PYTHONPATH"] = PREFIX
    if os.path.isdir(extra_path):
        env["PYTHONPATH"] += os.pathsep + extra_path
    cwd = os.path.join(test_temp_dir, custom_cwd or "")
    args = [arg.replace("$CWD", os.path.abspath(cwd)) for arg in args]
    process = subprocess.Popen(
        fixed + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env
    )
    outb, errb = process.communicate()
    result = process.returncode
    # Split output into lines.
    out = [s.rstrip("\n\r") for s in str(outb, "utf8").splitlines()]
    err = [s.rstrip("\n\r") for s in str(errb, "utf8").splitlines()]

    if "PYCHARM_HOSTED" in os.environ:
        for pos, line in enumerate(err):
            if line.startswith("pydev debugger: "):
                # Delete the attaching debugger message itself, plus the extra newline added.
                del err[pos : pos + 2]
                break

    # Remove temp file.
    os.remove(program_path)
    # Compare actual output to expected.
    if testcase.output_files:
        # Ignore stdout, but we insist on empty stderr and zero status.
        if err or result:
            raise AssertionError(
                "Expected zero status and empty stderr%s, got %d and\n%s"
                % (" on step %d" % step if testcase.output2 else "", result, "\n".join(err + out))
            )
        check_test_output_files(testcase, step)
    else:
        if testcase.normalize_output:
            out = normalize_error_messages(err + out)
        obvious_result = 1 if out else 0
        if obvious_result != result:
            out.append(f"== Return code: {result}")
        expected_out = testcase.output if step == 1 else testcase.output2[step]
        # Strip "tmp/" out of the test so that # E: works...
        expected_out = [s.replace("tmp" + os.sep, "") for s in expected_out]
        assert_string_arrays_equal(
            expected_out,
            out,
            "Invalid output ({}, line {}){}".format(
                testcase.file, testcase.line, " on step %d" % step if testcase.output2 else ""
            ),
        )

def parse_args(line: str) -> list[str]:
    """Parse the first line of the program for the command line.

    This should have the form

      # cmd: mypy <options>

    For example:

      # cmd: mypy pkg/
    """
    m = re.match("# cmd: mypy (.*)$", line)
    if not m:
        return []  # No args; mypy will spit out an error.
    return m.group(1).split()

def parse_cwd(line: str) -> str | None:
    """Parse the second line of the program for the command line.

    This should have the form

      # cwd: <directory>

    For example:

      # cwd: main/subdir
    """
    m = re.match("# cwd: (.*)$", line)
    return m.group(1) if m else None


from __future__ import annotations

from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints
from mypy.test.helpers import Suite
from mypy.test.typefixture import TypeFixture
from mypy.types import Instance, TupleType, UnpackType

class ConstraintsSuite(Suite):
    def setUp(self) -> None:
        self.fx = TypeFixture()

    def test_no_type_variables(self) -> None:
        assert not infer_constraints(self.fx.o, self.fx.o, SUBTYPE_OF)

    def test_basic_type_variable(self) -> None:
        fx = self.fx
        for direction in [SUBTYPE_OF, SUPERTYPE_OF]:
            assert infer_constraints(fx.gt, fx.ga, direction) == [
                Constraint(type_var=fx.t, op=direction, target=fx.a)
            ]

    def test_basic_type_var_tuple_subtype(self) -> None:
        fx = self.fx
        assert infer_constraints(
            Instance(fx.gvi, [UnpackType(fx.ts)]), Instance(fx.gvi, [fx.a, fx.b]), SUBTYPE_OF
        ) == [
            Constraint(type_var=fx.ts, op=SUBTYPE_OF, target=TupleType([fx.a, fx.b], fx.std_tuple))
        ]

    def test_basic_type_var_tuple(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gvi, [UnpackType(fx.ts)]), Instance(fx.gvi, [fx.a, fx.b]), SUPERTYPE_OF
            )
        ) == {
            Constraint(
                type_var=fx.ts, op=SUPERTYPE_OF, target=TupleType([fx.a, fx.b], fx.std_tuple)
            ),
            Constraint(
                type_var=fx.ts, op=SUBTYPE_OF, target=TupleType([fx.a, fx.b], fx.std_tuple)
            ),
        }

    def test_type_var_tuple_with_prefix_and_suffix(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gv2i, [fx.t, UnpackType(fx.ts), fx.s]),
                Instance(fx.gv2i, [fx.a, fx.b, fx.c, fx.d]),
                SUPERTYPE_OF,
            )
        ) == {
            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.a),
            Constraint(
                type_var=fx.ts, op=SUPERTYPE_OF, target=TupleType([fx.b, fx.c], fx.std_tuple)
            ),
            Constraint(
                type_var=fx.ts, op=SUBTYPE_OF, target=TupleType([fx.b, fx.c], fx.std_tuple)
            ),
            Constraint(type_var=fx.s, op=SUPERTYPE_OF, target=fx.d),
        }

    def test_unpack_homogenous_tuple(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gvi, [UnpackType(Instance(fx.std_tuplei, [fx.t]))]),
                Instance(fx.gvi, [fx.a, fx.b]),
                SUPERTYPE_OF,
            )
        ) == {
            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.a),
            Constraint(type_var=fx.t, op=SUBTYPE_OF, target=fx.a),
            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.b),
            Constraint(type_var=fx.t, op=SUBTYPE_OF, target=fx.b),
        }

    def test_unpack_homogenous_tuple_with_prefix_and_suffix(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gv2i, [fx.t, UnpackType(Instance(fx.std_tuplei, [fx.s])), fx.u]),
                Instance(fx.gv2i, [fx.a, fx.b, fx.c, fx.d]),
                SUPERTYPE_OF,
            )
        ) == {
            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.a),
            Constraint(type_var=fx.s, op=SUPERTYPE_OF, target=fx.b),
            Constraint(type_var=fx.s, op=SUBTYPE_OF, target=fx.b),
            Constraint(type_var=fx.s, op=SUPERTYPE_OF, target=fx.c),
            Constraint(type_var=fx.s, op=SUBTYPE_OF, target=fx.c),
            Constraint(type_var=fx.u, op=SUPERTYPE_OF, target=fx.d),
        }

    def test_unpack_with_prefix_and_suffix(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gv2i, [fx.u, fx.t, fx.s, fx.u]),
                Instance(fx.gv2i, [fx.a, fx.b, fx.c, fx.d]),
                SUPERTYPE_OF,
            )
        ) == {
            Constraint(type_var=fx.u, op=SUPERTYPE_OF, target=fx.a),
            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.b),
            Constraint(type_var=fx.t, op=SUBTYPE_OF, target=fx.b),
            Constraint(type_var=fx.s, op=SUPERTYPE_OF, target=fx.c),
            Constraint(type_var=fx.s, op=SUBTYPE_OF, target=fx.c),
            Constraint(type_var=fx.u, op=SUPERTYPE_OF, target=fx.d),
        }

    def test_unpack_tuple_length_non_match(self) -> None:
        fx = self.fx
        assert set(
            infer_constraints(
                Instance(fx.gv2i, [fx.u, fx.t, fx.s, fx.u]),
                Instance(fx.gv2i, [fx.a, fx.b, fx.d]),
                SUPERTYPE_OF,
            )
            # We still get constraints on the prefix/suffix in this case.
        ) == {
            Constraint(type_var=fx.u, op=SUPERTYPE_OF, target=fx.a),
            Constraint(type_var=fx.u, op=SUPERTYPE_OF, target=fx.d),
        }

    def test_var_length_tuple_with_fixed_length_tuple(self) -> None:
        fx = self.fx
        assert not infer_constraints(
            TupleType([fx.t, fx.s], fallback=Instance(fx.std_tuplei, [fx.o])),
            Instance(fx.std_tuplei, [fx.a]),
            SUPERTYPE_OF,
        )


"""End-to-end test cases for the daemon (dmypy).

These are special because they run multiple shell commands.

This also includes some unit tests.
"""

from __future__ import annotations

import os
import subprocess
import sys
import tempfile
import unittest

from mypy.dmypy_server import filter_out_missing_top_level_packages
from mypy.fscache import FileSystemCache
from mypy.modulefinder import SearchPaths
from mypy.test.config import PREFIX, test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal, normalize_error_messages

# Files containing test cases descriptions.
daemon_files = ["daemon.test"]

class DaemonSuite(DataSuite):
    files = daemon_files

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        try:
            test_daemon(testcase)
        finally:
            # Kill the daemon if it's still running.
            run_cmd("dmypy kill")

def test_daemon(testcase: DataDrivenTestCase) -> None:
    assert testcase.old_cwd is not None, "test was not properly set up"
    for i, step in enumerate(parse_script(testcase.input)):
        cmd = step[0]
        expected_lines = step[1:]
        assert cmd.startswith("$")
        cmd = cmd[1:].strip()
        cmd = cmd.replace("{python}", sys.executable)
        sts, output = run_cmd(cmd)
        output_lines = output.splitlines()
        output_lines = normalize_error_messages(output_lines)
        if sts:
            output_lines.append("== Return code: %d" % sts)
        assert_string_arrays_equal(
            expected_lines,
            output_lines,
            "Command %d (%s) did not give expected output" % (i + 1, cmd),
        )

def parse_script(input: list[str]) -> list[list[str]]:
    """Parse testcase.input into steps.

    Each command starts with a line starting with '$'.
    The first line (less '$') is sent to the shell.
    The remaining lines are expected output.
    """
    steps = []
    step: list[str] = []
    for line in input:
        if line.startswith("$"):
            if step:
                assert step[0].startswith("$")
                steps.append(step)
                step = []
        step.append(line)
    if step:
        steps.append(step)
    return steps

def run_cmd(input: str) -> tuple[int, str]:
    if input[1:].startswith("mypy run --") and "--show-error-codes" not in input:
        input += " --hide-error-codes"
    if input.startswith("dmypy "):
        input = sys.executable + " -m mypy." + input
    if input.startswith("mypy "):
        input = sys.executable + " -m" + input
    env = os.environ.copy()
    env["PYTHONPATH"] = PREFIX
    try:
        output = subprocess.check_output(
            input, shell=True, stderr=subprocess.STDOUT, text=True, cwd=test_temp_dir, env=env
        )
        return 0, output
    except subprocess.CalledProcessError as err:
        return err.returncode, err.output

class DaemonUtilitySuite(unittest.TestCase):
    """Unit tests for helpers"""

    def test_filter_out_missing_top_level_packages(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            self.make_file(td, "base/a/")
            self.make_file(td, "base/b.py")
            self.make_file(td, "base/c.pyi")
            self.make_file(td, "base/missing.txt")
            self.make_file(td, "typeshed/d.pyi")
            self.make_file(td, "typeshed/@python2/e")  # outdated
            self.make_file(td, "pkg1/f-stubs")
            self.make_file(td, "pkg2/g-python2-stubs")  # outdated
            self.make_file(td, "mpath/sub/long_name/")

            def makepath(p: str) -> str:
                return os.path.join(td, p)

            search = SearchPaths(
                python_path=(makepath("base"),),
                mypy_path=(makepath("mpath/sub"),),
                package_path=(makepath("pkg1"), makepath("pkg2")),
                typeshed_path=(makepath("typeshed"),),
            )
            fscache = FileSystemCache()
            res = filter_out_missing_top_level_packages(
                {"a", "b", "c", "d", "e", "f", "g", "long_name", "ff", "missing"}, search, fscache
            )
            assert res == {"a", "b", "c", "d", "f", "long_name"}

    def make_file(self, base: str, path: str) -> None:
        fullpath = os.path.join(base, path)
        os.makedirs(os.path.dirname(fullpath), exist_ok=True)
        if not path.endswith("/"):
            with open(fullpath, "w") as f:
                f.write("# test file")


"""Test cases for generating node-level dependencies (for fine-grained incremental checking)"""

from __future__ import annotations

import os
import sys
from collections import defaultdict

import pytest

from mypy import build
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.nodes import Expression, MypyFile
from mypy.options import Options
from mypy.server.deps import get_dependencies
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal, find_test_files, parse_options
from mypy.types import Type
from mypy.typestate import type_state

# Only dependencies in these modules are dumped
dumped_modules = ["__main__", "pkg", "pkg.mod"]

class GetDependenciesSuite(DataSuite):
    files = find_test_files(pattern="deps*.test")

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        src = "\n".join(testcase.input)
        dump_all = "# __dump_all__" in src
        options = parse_options(src, testcase, incremental_step=1)
        if options.python_version > sys.version_info:
            pytest.skip("Test case requires a newer Python version")
        options.use_builtins_fixtures = True
        options.show_traceback = True
        options.cache_dir = os.devnull
        options.export_types = True
        options.preserve_asts = True
        options.allow_empty_bodies = True
        messages, files, type_map = self.build(src, options)
        a = messages
        if files is None or type_map is None:
            if not a:
                a = ["Unknown compile error (likely syntax error in test case or fixture)"]
        else:
            deps: defaultdict[str, set[str]] = defaultdict(set)
            for module, file in files.items():
                if (module in dumped_modules or dump_all) and (module in testcase.test_modules):
                    new_deps = get_dependencies(file, type_map, options.python_version, options)
                    for source in new_deps:
                        deps[source].update(new_deps[source])

            type_state.add_all_protocol_deps(deps)

            for source, targets in sorted(deps.items()):
                if source.startswith(("<enum", "<typing", "<mypy", "<_typeshed.")):
                    # Remove noise.
                    continue
                line = f"{source} -> {', '.join(sorted(targets))}"
                # Clean up output a bit
                line = line.replace("__main__", "m")
                a.append(line)

        assert_string_arrays_equal(
            testcase.output, a, f"Invalid output ({testcase.file}, line {testcase.line})"
        )

    def build(
        self, source: str, options: Options
    ) -> tuple[list[str], dict[str, MypyFile] | None, dict[Expression, Type] | None]:
        try:
            result = build.build(
                sources=[BuildSource("main", None, source)],
                options=options,
                alt_lib_path=test_temp_dir,
            )
        except CompileError as e:
            # TODO: Should perhaps not return None here.
            return e.messages, None, None
        return result.errors, result.files, result.types


"""Test cases for AST diff (used for fine-grained incremental checking)"""

from __future__ import annotations

import os
import sys

import pytest

from mypy import build
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.nodes import MypyFile
from mypy.options import Options
from mypy.server.astdiff import compare_symbol_table_snapshots, snapshot_symbol_table
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal, parse_options

class ASTDiffSuite(DataSuite):
    files = ["diff.test"]

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        first_src = "\n".join(testcase.input)
        files_dict = dict(testcase.files)
        second_src = files_dict["tmp/next.py"]
        options = parse_options(first_src, testcase, 1)
        if options.python_version > sys.version_info:
            pytest.skip("Test case requires a newer Python version")

        messages1, files1 = self.build(first_src, options)
        messages2, files2 = self.build(second_src, options)

        a = []
        if messages1:
            a.extend(messages1)
        if messages2:
            a.append("== next ==")
            a.extend(messages2)

        assert (
            files1 is not None and files2 is not None
        ), "cases where CompileError occurred should not be run"
        prefix = "__main__"
        snapshot1 = snapshot_symbol_table(prefix, files1["__main__"].names)
        snapshot2 = snapshot_symbol_table(prefix, files2["__main__"].names)
        diff = compare_symbol_table_snapshots(prefix, snapshot1, snapshot2)
        for trigger in sorted(diff):
            a.append(trigger)

        assert_string_arrays_equal(
            testcase.output, a, f"Invalid output ({testcase.file}, line {testcase.line})"
        )

    def build(self, source: str, options: Options) -> tuple[list[str], dict[str, MypyFile] | None]:
        options.use_builtins_fixtures = True
        options.show_traceback = True
        options.cache_dir = os.devnull
        options.allow_empty_bodies = True
        try:
            result = build.build(
                sources=[BuildSource("main", None, source)],
                options=options,
                alt_lib_path=test_temp_dir,
            )
        except CompileError as e:
            # TODO: Is it okay to return None?
            return e.messages, None
        return result.errors, result.files


"""Tests for mypy incremental error output."""

from __future__ import annotations

from mypy import build
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.options import Options
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal

class ErrorStreamSuite(DataSuite):
    required_out_section = True
    base_path = "."
    files = ["errorstream.test"]

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        test_error_stream(testcase)

def test_error_stream(testcase: DataDrivenTestCase) -> None:
    """Perform a single error streaming test case.

    The argument contains the description of the test case.
    """
    options = Options()
    options.show_traceback = True
    options.hide_error_codes = True

    logged_messages: list[str] = []

    def flush_errors(filename: str | None, msgs: list[str], serious: bool) -> None:
        if msgs:
            logged_messages.append("==== Errors flushed ====")
            logged_messages.extend(msgs)

    sources = [BuildSource("main", "__main__", "\n".join(testcase.input))]
    try:
        build.build(sources=sources, options=options, flush_errors=flush_errors)
    except CompileError as e:
        assert e.messages == []

    assert_string_arrays_equal(
        testcase.output, logged_messages, f"Invalid output ({testcase.file}, line {testcase.line})"
    )


"""Test cases for fine-grained incremental checking.

Each test cases runs a batch build followed by one or more fine-grained
incremental steps. We verify that each step produces the expected output.

See the comment at the top of test-data/unit/fine-grained.test for more
information.

N.B.: Unlike most of the other test suites, testfinegrained does not
rely on an alt_lib_path for finding source files. This means that they
can test interactions with the lib_path that is built implicitly based
on specified sources.
"""

from __future__ import annotations

import os
import re
import sys
import unittest
from typing import Any

import pytest

from mypy import build
from mypy.config_parser import parse_config_file
from mypy.dmypy_server import Server
from mypy.dmypy_util import DEFAULT_STATUS_FILE
from mypy.errors import CompileError
from mypy.find_sources import create_source_list
from mypy.modulefinder import BuildSource
from mypy.options import Options
from mypy.server.mergecheck import check_consistency
from mypy.server.update import sort_messages_preserving_file_order
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite, DeleteFile, UpdateFile
from mypy.test.helpers import (
    assert_module_equivalence,
    assert_string_arrays_equal,
    assert_target_equivalence,
    find_test_files,
    parse_options,
    perform_file_operations,
)

# Set to True to perform (somewhat expensive) checks for duplicate AST nodes after merge
CHECK_CONSISTENCY = False

class FineGrainedSuite(DataSuite):
    files = find_test_files(
        pattern="fine-grained*.test", exclude=["fine-grained-cache-incremental.test"]
    )

    # Whether to use the fine-grained cache in the testing. This is overridden
    # by a trivial subclass to produce a suite that uses the cache.
    use_cache = False

    def should_skip(self, testcase: DataDrivenTestCase) -> bool:
        # Decide whether to skip the test. This could have been structured
        # as a filter() classmethod also, but we want the tests reported
        # as skipped, not just elided.
        if self.use_cache:
            if testcase.only_when == "-only_when_nocache":
                return True
            # TODO: In caching mode we currently don't well support
            # starting from cached states with errors in them.
            if testcase.output and testcase.output[0] != "==":
                return True
        else:
            if testcase.only_when == "-only_when_cache":
                return True
        return False

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        if self.should_skip(testcase):
            pytest.skip()
            return

        main_src = "\n".join(testcase.input)
        main_path = os.path.join(test_temp_dir, "main")
        with open(main_path, "w", encoding="utf8") as f:
            f.write(main_src)

        options = self.get_options(main_src, testcase, build_cache=False)
        if options.python_version > sys.version_info:
            pytest.skip("Test case requires a newer Python version")

        build_options = self.get_options(main_src, testcase, build_cache=True)
        server = Server(options, DEFAULT_STATUS_FILE)

        num_regular_incremental_steps = self.get_build_steps(main_src)
        step = 1
        sources = self.parse_sources(main_src, step, options)
        if step <= num_regular_incremental_steps:
            messages = self.build(build_options, sources)
        else:
            messages = self.run_check(server, sources)

        a = []
        if messages:
            a.extend(normalize_messages(messages))

        assert testcase.tmpdir
        a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name))
        a.extend(self.maybe_inspect(step, server, main_src))

        if server.fine_grained_manager:
            if CHECK_CONSISTENCY:
                check_consistency(server.fine_grained_manager)

        steps = testcase.find_steps()
        all_triggered = []

        for operations in steps:
            step += 1
            output, triggered = self.perform_step(
                operations,
                server,
                options,
                build_options,
                testcase,
                main_src,
                step,
                num_regular_incremental_steps,
            )
            a.append("==")
            a.extend(output)
            all_triggered.extend(triggered)

        # Normalize paths in test output (for Windows).
        a = [line.replace("\\", "/") for line in a]

        assert_string_arrays_equal(
            testcase.output, a, f"Invalid output ({testcase.file}, line {testcase.line})"
        )

        if testcase.triggered:
            assert_string_arrays_equal(
                testcase.triggered,
                self.format_triggered(all_triggered),
                f"Invalid active triggers ({testcase.file}, line {testcase.line})",
            )

    def get_options(self, source: str, testcase: DataDrivenTestCase, build_cache: bool) -> Options:
        # This handles things like '# flags: --foo'.
        options = parse_options(source, testcase, incremental_step=1)
        options.incremental = True
        options.use_builtins_fixtures = True
        options.show_traceback = True
        options.error_summary = False
        options.fine_grained_incremental = not build_cache
        options.use_fine_grained_cache = self.use_cache and not build_cache
        options.cache_fine_grained = self.use_cache
        options.local_partial_types = True
        options.export_types = "inspect" in testcase.file
        # Treat empty bodies safely for these test cases.
        options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
        if re.search("flags:.*--follow-imports", source) is None:
            # Override the default for follow_imports
            options.follow_imports = "error"

        for name, _ in testcase.files:
            if "mypy.ini" in name or "pyproject.toml" in name:
                parse_config_file(options, lambda: None, name)
                break

        return options

    def run_check(self, server: Server, sources: list[BuildSource]) -> list[str]:
        response = server.check(sources, export_types=False, is_tty=False, terminal_width=-1)
        out = response["out"] or response["err"]
        assert isinstance(out, str)
        return out.splitlines()

    def build(self, options: Options, sources: list[BuildSource]) -> list[str]:
        try:
            result = build.build(sources=sources, options=options)
        except CompileError as e:
            return e.messages
        return result.errors

    def format_triggered(self, triggered: list[list[str]]) -> list[str]:
        result = []
        for n, triggers in enumerate(triggered):
            filtered = [trigger for trigger in triggers if not trigger.endswith("__>")]
            filtered = sorted(filtered)
            result.append(("%d: %s" % (n + 2, ", ".join(filtered))).strip())
        return result

    def get_build_steps(self, program_text: str) -> int:
        """Get the number of regular incremental steps to run, from the test source"""
        if not self.use_cache:
            return 0
        m = re.search("# num_build_steps: ([0-9]+)$", program_text, flags=re.MULTILINE)
        if m is not None:
            return int(m.group(1))
        return 1

    def perform_step(
        self,
        operations: list[UpdateFile | DeleteFile],
        server: Server,
        options: Options,
        build_options: Options,
        testcase: DataDrivenTestCase,
        main_src: str,
        step: int,
        num_regular_incremental_steps: int,
    ) -> tuple[list[str], list[list[str]]]:
        """Perform one fine-grained incremental build step (after some file updates/deletions).

        Return (mypy output, triggered targets).
        """
        perform_file_operations(operations)
        sources = self.parse_sources(main_src, step, options)

        if step <= num_regular_incremental_steps:
            new_messages = self.build(build_options, sources)
        else:
            new_messages = self.run_check(server, sources)

        updated: list[str] = []
        changed: list[str] = []
        targets: list[str] = []
        triggered = []
        if server.fine_grained_manager:
            if CHECK_CONSISTENCY:
                check_consistency(server.fine_grained_manager)
            triggered.append(server.fine_grained_manager.triggered)

            updated = server.fine_grained_manager.updated_modules
            changed = [mod for mod, file in server.fine_grained_manager.changed_modules]
            targets = server.fine_grained_manager.processed_targets

        expected_stale = testcase.expected_stale_modules.get(step - 1)
        if expected_stale is not None:
            assert_module_equivalence("stale" + str(step - 1), expected_stale, changed)

        expected_rechecked = testcase.expected_rechecked_modules.get(step - 1)
        if expected_rechecked is not None:
            assert_module_equivalence("rechecked" + str(step - 1), expected_rechecked, updated)

        expected = testcase.expected_fine_grained_targets.get(step)
        if expected:
            assert_target_equivalence("targets" + str(step), expected, targets)

        new_messages = normalize_messages(new_messages)

        a = new_messages
        assert testcase.tmpdir
        a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name))
        a.extend(self.maybe_inspect(step, server, main_src))

        return a, triggered

    def parse_sources(
        self, program_text: str, incremental_step: int, options: Options
    ) -> list[BuildSource]:
        """Return target BuildSources for a test case.

        Normally, the unit tests will check all files included in the test
        case. This differs from how testcheck works by default, as dmypy
        doesn't currently support following imports.

        You can override this behavior and instruct the tests to check
        multiple modules by using a comment like this in the test case
        input:

          # cmd: main a.py

        You can also use `# cmdN:` to have a different cmd for incremental
        step N (2, 3, ...).

        """
        m = re.search("# cmd: mypy ([a-zA-Z0-9_./ ]+)$", program_text, flags=re.MULTILINE)
        regex = f"# cmd{incremental_step}: mypy ([a-zA-Z0-9_./ ]+)$"
        alt_m = re.search(regex, program_text, flags=re.MULTILINE)
        if alt_m is not None:
            # Optionally return a different command if in a later step
            # of incremental mode, otherwise default to reusing the
            # original cmd.
            m = alt_m

        if m:
            # The test case wants to use a non-default set of files.
            paths = [os.path.join(test_temp_dir, path) for path in m.group(1).strip().split()]
            return create_source_list(paths, options)
        else:
            base = BuildSource(os.path.join(test_temp_dir, "main"), "__main__", None)
            # Use expand_dir instead of create_source_list to avoid complaints
            # when there aren't any .py files in an increment
            return [base] + create_source_list([test_temp_dir], options, allow_empty_dir=True)

    def maybe_suggest(self, step: int, server: Server, src: str, tmp_dir: str) -> list[str]:
        output: list[str] = []
        targets = self.get_suggest(src, step)
        for flags, target in targets:
            json = "--json" in flags
            callsites = "--callsites" in flags
            no_any = "--no-any" in flags
            no_errors = "--no-errors" in flags
            m = re.match("--flex-any=([0-9.]+)", flags)
            flex_any = float(m.group(1)) if m else None
            m = re.match(r"--use-fixme=(\w+)", flags)
            use_fixme = m.group(1) if m else None
            m = re.match("--max-guesses=([0-9]+)", flags)
            max_guesses = int(m.group(1)) if m else None
            res: dict[str, Any] = server.cmd_suggest(
                target.strip(),
                json=json,
                no_any=no_any,
                no_errors=no_errors,
                flex_any=flex_any,
                use_fixme=use_fixme,
                callsites=callsites,
                max_guesses=max_guesses,
            )
            val = res["error"] if "error" in res else res["out"] + res["err"]
            if json:
                # JSON contains already escaped \ on Windows, so requires a bit of care.
                val = val.replace("\\\\", "\\")
                val = val.replace(os.path.realpath(tmp_dir) + os.path.sep, "")
                val = val.replace(os.path.abspath(tmp_dir) + os.path.sep, "")
            output.extend(val.strip().split("\n"))
        return normalize_messages(output)

    def maybe_inspect(self, step: int, server: Server, src: str) -> list[str]:
        output: list[str] = []
        targets = self.get_inspect(src, step)
        for flags, location in targets:
            m = re.match(r"--show=(\w+)", flags)
            show = m.group(1) if m else "type"
            verbosity = 0
            if "-v" in flags:
                verbosity = 1
            if "-vv" in flags:
                verbosity = 2
            m = re.match(r"--limit=([0-9]+)", flags)
            limit = int(m.group(1)) if m else 0
            include_span = "--include-span" in flags
            include_kind = "--include-kind" in flags
            include_object_attrs = "--include-object-attrs" in flags
            union_attrs = "--union-attrs" in flags
            force_reload = "--force-reload" in flags
            res: dict[str, Any] = server.cmd_inspect(
                show,
                location,
                verbosity=verbosity,
                limit=limit,
                include_span=include_span,
                include_kind=include_kind,
                include_object_attrs=include_object_attrs,
                union_attrs=union_attrs,
                force_reload=force_reload,
            )
            val = res["error"] if "error" in res else res["out"] + res["err"]
            output.extend(val.strip().split("\n"))
        return output

    def get_suggest(self, program_text: str, incremental_step: int) -> list[tuple[str, str]]:
        step_bit = "1?" if incremental_step == 1 else str(incremental_step)
        regex = f"# suggest{step_bit}: (--[a-zA-Z0-9_\\-./=?^ ]+ )*([a-zA-Z0-9_.:/?^ ]+)$"
        m = re.findall(regex, program_text, flags=re.MULTILINE)
        return m

    def get_inspect(self, program_text: str, incremental_step: int) -> list[tuple[str, str]]:
        step_bit = "1?" if incremental_step == 1 else str(incremental_step)
        regex = f"# inspect{step_bit}: (--[a-zA-Z0-9_\\-=?^ ]+ )*([a-zA-Z0-9_.:/?^ ]+)$"
        m = re.findall(regex, program_text, flags=re.MULTILINE)
        return m

def normalize_messages(messages: list[str]) -> list[str]:
    return [re.sub("^tmp" + re.escape(os.sep), "", message) for message in messages]

class TestMessageSorting(unittest.TestCase):
    def test_simple_sorting(self) -> None:
        msgs = ['x.py:1: error: "int" not callable', 'foo/y.py:123: note: "X" not defined']
        old_msgs = ['foo/y.py:12: note: "Y" not defined', 'x.py:8: error: "str" not callable']
        assert sort_messages_preserving_file_order(msgs, old_msgs) == list(reversed(msgs))
        assert sort_messages_preserving_file_order(list(reversed(msgs)), old_msgs) == list(
            reversed(msgs)
        )

    def test_long_form_sorting(self) -> None:
        # Multi-line errors should be sorted together and not split.
        msg1 = [
            'x.py:1: error: "int" not callable',
            "and message continues (x: y)",
            "    1()",
            "    ^~~",
        ]
        msg2 = [
            'foo/y.py: In function "f":',
            'foo/y.py:123: note: "X" not defined',
            "and again message continues",
        ]
        old_msgs = ['foo/y.py:12: note: "Y" not defined', 'x.py:8: error: "str" not callable']
        assert sort_messages_preserving_file_order(msg1 + msg2, old_msgs) == msg2 + msg1
        assert sort_messages_preserving_file_order(msg2 + msg1, old_msgs) == msg2 + msg1

    def test_mypy_error_prefix(self) -> None:
        # Some errors don't have a file and start with "mypy: ". These
        # shouldn't be sorted together with file-specific errors.
        msg1 = 'x.py:1: error: "int" not callable'
        msg2 = 'foo/y:123: note: "X" not defined'
        msg3 = "mypy: Error not associated with a file"
        old_msgs = [
            "mypy: Something wrong",
            'foo/y:12: note: "Y" not defined',
            'x.py:8: error: "str" not callable',
        ]
        assert sort_messages_preserving_file_order([msg1, msg2, msg3], old_msgs) == [
            msg2,
            msg1,
            msg3,
        ]
        assert sort_messages_preserving_file_order([msg3, msg2, msg1], old_msgs) == [
            msg2,
            msg1,
            msg3,
        ]

    def test_new_file_at_the_end(self) -> None:
        msg1 = 'x.py:1: error: "int" not callable'
        msg2 = 'foo/y.py:123: note: "X" not defined'
        new1 = "ab.py:3: error: Problem: error"
        new2 = "aaa:3: error: Bad"
        old_msgs = ['foo/y.py:12: note: "Y" not defined', 'x.py:8: error: "str" not callable']
        assert sort_messages_preserving_file_order([msg1, msg2, new1], old_msgs) == [
            msg2,
            msg1,
            new1,
        ]
        assert sort_messages_preserving_file_order([new1, msg1, msg2, new2], old_msgs) == [
            msg2,
            msg1,
            new1,
            new2,
        ]


"""Tests for fine-grained incremental checking using the cache.

All of the real code for this lives in testfinegrained.py.
"""

# We can't "import FineGrainedSuite from ..." because that will cause pytest
# to collect the non-caching tests when running this file.
from __future__ import annotations

import mypy.test.testfinegrained

class FineGrainedCacheSuite(mypy.test.testfinegrained.FineGrainedSuite):
    use_cache = True
    test_name_suffix = "_cached"
    files = mypy.test.testfinegrained.FineGrainedSuite.files + [
        "fine-grained-cache-incremental.test"
    ]


from __future__ import annotations

from unittest import TestCase, main

from mypy.util import split_words, trim_source_line

class FancyErrorFormattingTestCases(TestCase):
    def test_trim_source(self) -> None:
        assert trim_source_line("0123456789abcdef", max_len=16, col=5, min_width=2) == (
            "0123456789abcdef",
            0,
        )

        # Locations near start.
        assert trim_source_line("0123456789abcdef", max_len=7, col=0, min_width=2) == (
            "0123456...",
            0,
        )
        assert trim_source_line("0123456789abcdef", max_len=7, col=4, min_width=2) == (
            "0123456...",
            0,
        )

        # Middle locations.
        assert trim_source_line("0123456789abcdef", max_len=7, col=5, min_width=2) == (
            "...1234567...",
            -2,
        )
        assert trim_source_line("0123456789abcdef", max_len=7, col=6, min_width=2) == (
            "...2345678...",
            -1,
        )
        assert trim_source_line("0123456789abcdef", max_len=7, col=8, min_width=2) == (
            "...456789a...",
            1,
        )

        # Locations near the end.
        assert trim_source_line("0123456789abcdef", max_len=7, col=11, min_width=2) == (
            "...789abcd...",
            4,
        )
        assert trim_source_line("0123456789abcdef", max_len=7, col=13, min_width=2) == (
            "...9abcdef",
            6,
        )
        assert trim_source_line("0123456789abcdef", max_len=7, col=15, min_width=2) == (
            "...9abcdef",
            6,
        )

    def test_split_words(self) -> None:
        assert split_words("Simple message") == ["Simple", "message"]
        assert split_words('Message with "Some[Long, Types]" in it') == [
            "Message",
            "with",
            '"Some[Long, Types]"',
            "in",
            "it",
        ]
        assert split_words('Message with "Some[Long, Types]" and [error-code]') == [
            "Message",
            "with",
            '"Some[Long, Types]"',
            "and",
            "[error-code]",
        ]
        assert split_words('"Type[Stands, First]" then words') == [
            '"Type[Stands, First]"',
            "then",
            "words",
        ]
        assert split_words('First words "Then[Stands, Type]"') == [
            "First",
            "words",
            '"Then[Stands, Type]"',
        ]
        assert split_words('"Type[Only, Here]"') == ['"Type[Only, Here]"']
        assert split_words("OneWord") == ["OneWord"]
        assert split_words(" ") == ["", ""]

if __name__ == "__main__":
    main()


"""Unit tests for file system cache."""

from __future__ import annotations

import os
import shutil
import sys
import tempfile
import unittest

from mypy.fscache import FileSystemCache

class TestFileSystemCache(unittest.TestCase):
    def setUp(self) -> None:
        self.tempdir = tempfile.mkdtemp()
        self.oldcwd = os.getcwd()
        os.chdir(self.tempdir)
        self.fscache = FileSystemCache()

    def tearDown(self) -> None:
        os.chdir(self.oldcwd)
        shutil.rmtree(self.tempdir)

    def test_isfile_case_1(self) -> None:
        self.make_file("bar.py")
        self.make_file("pkg/sub_package/__init__.py")
        self.make_file("pkg/sub_package/foo.py")
        # Run twice to test both cached and non-cached code paths.
        for i in range(2):
            assert self.isfile_case("bar.py")
            assert self.isfile_case("pkg/sub_package/__init__.py")
            assert self.isfile_case("pkg/sub_package/foo.py")
            assert not self.isfile_case("non_existent.py")
            assert not self.isfile_case("pkg/non_existent.py")
            assert not self.isfile_case("pkg/")
            assert not self.isfile_case("bar.py/")
        for i in range(2):
            assert not self.isfile_case("Bar.py")
            assert not self.isfile_case("pkg/sub_package/__init__.PY")
            assert not self.isfile_case("pkg/Sub_Package/foo.py")
            assert not self.isfile_case("Pkg/sub_package/foo.py")

    def test_isfile_case_2(self) -> None:
        self.make_file("bar.py")
        self.make_file("pkg/sub_package/__init__.py")
        self.make_file("pkg/sub_package/foo.py")
        # Run twice to test both cached and non-cached code paths.
        # This reverses the order of checks from test_isfile_case_1.
        for i in range(2):
            assert not self.isfile_case("Bar.py")
            assert not self.isfile_case("pkg/sub_package/__init__.PY")
            assert not self.isfile_case("pkg/Sub_Package/foo.py")
            assert not self.isfile_case("Pkg/sub_package/foo.py")
        for i in range(2):
            assert self.isfile_case("bar.py")
            assert self.isfile_case("pkg/sub_package/__init__.py")
            assert self.isfile_case("pkg/sub_package/foo.py")
            assert not self.isfile_case("non_existent.py")
            assert not self.isfile_case("pkg/non_existent.py")

    def test_isfile_case_3(self) -> None:
        self.make_file("bar.py")
        self.make_file("pkg/sub_package/__init__.py")
        self.make_file("pkg/sub_package/foo.py")
        # Run twice to test both cached and non-cached code paths.
        for i in range(2):
            assert self.isfile_case("bar.py")
            assert not self.isfile_case("non_existent.py")
            assert not self.isfile_case("pkg/non_existent.py")
            assert not self.isfile_case("Bar.py")
            assert not self.isfile_case("pkg/sub_package/__init__.PY")
            assert not self.isfile_case("pkg/Sub_Package/foo.py")
            assert not self.isfile_case("Pkg/sub_package/foo.py")
            assert self.isfile_case("pkg/sub_package/__init__.py")
            assert self.isfile_case("pkg/sub_package/foo.py")

    def test_isfile_case_other_directory(self) -> None:
        self.make_file("bar.py")
        with tempfile.TemporaryDirectory() as other:
            self.make_file("other_dir.py", base=other)
            self.make_file("pkg/other_dir.py", base=other)
            assert self.isfile_case(os.path.join(other, "other_dir.py"))
            assert not self.isfile_case(os.path.join(other, "Other_Dir.py"))
            assert not self.isfile_case(os.path.join(other, "bar.py"))
            if sys.platform in ("win32", "darwin"):
                # We only check case for directories under our prefix, and since
                # this path is not under the prefix, case difference is fine.
                assert self.isfile_case(os.path.join(other, "PKG/other_dir.py"))

    def make_file(self, path: str, base: str | None = None) -> None:
        if base is None:
            base = self.tempdir
        fullpath = os.path.join(base, path)
        os.makedirs(os.path.dirname(fullpath), exist_ok=True)
        if not path.endswith("/"):
            with open(fullpath, "w") as f:
                f.write("# test file")

    def isfile_case(self, path: str) -> bool:
        return self.fscache.isfile_case(os.path.join(self.tempdir, path), self.tempdir)


"""Test cases for graph processing code in build.py."""

from __future__ import annotations

import sys
from typing import AbstractSet

from mypy.build import BuildManager, BuildSourceSet, State, order_ascc, sorted_components
from mypy.errors import Errors
from mypy.fscache import FileSystemCache
from mypy.graph_utils import strongly_connected_components, topsort
from mypy.modulefinder import SearchPaths
from mypy.options import Options
from mypy.plugin import Plugin
from mypy.report import Reports
from mypy.test.helpers import Suite, assert_equal
from mypy.version import __version__

class GraphSuite(Suite):
    def test_topsort(self) -> None:
        a = frozenset({"A"})
        b = frozenset({"B"})
        c = frozenset({"C"})
        d = frozenset({"D"})
        data: dict[AbstractSet[str], set[AbstractSet[str]]] = {a: {b, c}, b: {d}, c: {d}}
        res = list(topsort(data))
        assert_equal(res, [{d}, {b, c}, {a}])

    def test_scc(self) -> None:
        vertices = {"A", "B", "C", "D"}
        edges: dict[str, list[str]] = {"A": ["B", "C"], "B": ["C"], "C": ["B", "D"], "D": []}
        sccs = {frozenset(x) for x in strongly_connected_components(vertices, edges)}
        assert_equal(sccs, {frozenset({"A"}), frozenset({"B", "C"}), frozenset({"D"})})

    def _make_manager(self) -> BuildManager:
        options = Options()
        options.use_builtins_fixtures = True
        errors = Errors(options)
        fscache = FileSystemCache()
        search_paths = SearchPaths((), (), (), ())
        manager = BuildManager(
            data_dir="",
            search_paths=search_paths,
            ignore_prefix="",
            source_set=BuildSourceSet([]),
            reports=Reports("", {}),
            options=options,
            version_id=__version__,
            plugin=Plugin(options),
            plugins_snapshot={},
            errors=errors,
            flush_errors=lambda filename, msgs, serious: None,
            fscache=fscache,
            stdout=sys.stdout,
            stderr=sys.stderr,
        )
        return manager

    def test_sorted_components(self) -> None:
        manager = self._make_manager()
        graph = {
            "a": State("a", None, "import b, c", manager),
            "d": State("d", None, "pass", manager),
            "b": State("b", None, "import c", manager),
            "c": State("c", None, "import b, d", manager),
        }
        res = sorted_components(graph)
        assert_equal(res, [frozenset({"d"}), frozenset({"c", "b"}), frozenset({"a"})])

    def test_order_ascc(self) -> None:
        manager = self._make_manager()
        graph = {
            "a": State("a", None, "import b, c", manager),
            "d": State("d", None, "def f(): import a", manager),
            "b": State("b", None, "import c", manager),
            "c": State("c", None, "import b, d", manager),
        }
        res = sorted_components(graph)
        assert_equal(res, [frozenset({"a", "d", "c", "b"})])
        ascc = res[0]
        scc = order_ascc(graph, ascc)
        assert_equal(scc, ["d", "c", "b", "a"])


"""Test cases for type inference helper functions."""

from __future__ import annotations

from mypy.argmap import map_actuals_to_formals
from mypy.checker import DisjointDict, group_comparison_operands
from mypy.literals import Key
from mypy.nodes import ARG_NAMED, ARG_OPT, ARG_POS, ARG_STAR, ARG_STAR2, ArgKind, NameExpr
from mypy.test.helpers import Suite, assert_equal
from mypy.test.typefixture import TypeFixture
from mypy.types import AnyType, TupleType, Type, TypeOfAny

class MapActualsToFormalsSuite(Suite):
    """Test cases for argmap.map_actuals_to_formals."""

    def test_basic(self) -> None:
        self.assert_map([], [], [])

    def test_positional_only(self) -> None:
        self.assert_map([ARG_POS], [ARG_POS], [[0]])
        self.assert_map([ARG_POS, ARG_POS], [ARG_POS, ARG_POS], [[0], [1]])

    def test_optional(self) -> None:
        self.assert_map([], [ARG_OPT], [[]])
        self.assert_map([ARG_POS], [ARG_OPT], [[0]])
        self.assert_map([ARG_POS], [ARG_OPT, ARG_OPT], [[0], []])

    def test_callee_star(self) -> None:
        self.assert_map([], [ARG_STAR], [[]])
        self.assert_map([ARG_POS], [ARG_STAR], [[0]])
        self.assert_map([ARG_POS, ARG_POS], [ARG_STAR], [[0, 1]])

    def test_caller_star(self) -> None:
        self.assert_map([ARG_STAR], [ARG_STAR], [[0]])
        self.assert_map([ARG_POS, ARG_STAR], [ARG_STAR], [[0, 1]])
        self.assert_map([ARG_STAR], [ARG_POS, ARG_STAR], [[0], [0]])
        self.assert_map([ARG_STAR], [ARG_OPT, ARG_STAR], [[0], [0]])

    def test_too_many_caller_args(self) -> None:
        self.assert_map([ARG_POS], [], [])
        self.assert_map([ARG_STAR], [], [])
        self.assert_map([ARG_STAR], [ARG_POS], [[0]])

    def test_tuple_star(self) -> None:
        any_type = AnyType(TypeOfAny.special_form)
        self.assert_vararg_map([ARG_STAR], [ARG_POS], [[0]], self.make_tuple(any_type))
        self.assert_vararg_map(
            [ARG_STAR], [ARG_POS, ARG_POS], [[0], [0]], self.make_tuple(any_type, any_type)
        )
        self.assert_vararg_map(
            [ARG_STAR],
            [ARG_POS, ARG_OPT, ARG_OPT],
            [[0], [0], []],
            self.make_tuple(any_type, any_type),
        )

    def make_tuple(self, *args: Type) -> TupleType:
        return TupleType(list(args), TypeFixture().std_tuple)

    def test_named_args(self) -> None:
        self.assert_map(["x"], [(ARG_POS, "x")], [[0]])
        self.assert_map(["y", "x"], [(ARG_POS, "x"), (ARG_POS, "y")], [[1], [0]])

    def test_some_named_args(self) -> None:
        self.assert_map(["y"], [(ARG_OPT, "x"), (ARG_OPT, "y"), (ARG_OPT, "z")], [[], [0], []])

    def test_missing_named_arg(self) -> None:
        self.assert_map(["y"], [(ARG_OPT, "x")], [[]])

    def test_duplicate_named_arg(self) -> None:
        self.assert_map(["x", "x"], [(ARG_OPT, "x")], [[0, 1]])

    def test_varargs_and_bare_asterisk(self) -> None:
        self.assert_map([ARG_STAR], [ARG_STAR, (ARG_NAMED, "x")], [[0], []])
        self.assert_map([ARG_STAR, "x"], [ARG_STAR, (ARG_NAMED, "x")], [[0], [1]])

    def test_keyword_varargs(self) -> None:
        self.assert_map(["x"], [ARG_STAR2], [[0]])
        self.assert_map(["x", ARG_STAR2], [ARG_STAR2], [[0, 1]])
        self.assert_map(["x", ARG_STAR2], [(ARG_POS, "x"), ARG_STAR2], [[0], [1]])
        self.assert_map([ARG_POS, ARG_STAR2], [(ARG_POS, "x"), ARG_STAR2], [[0], [1]])

    def test_both_kinds_of_varargs(self) -> None:
        self.assert_map([ARG_STAR, ARG_STAR2], [(ARG_POS, "x"), (ARG_POS, "y")], [[0, 1], [0, 1]])

    def test_special_cases(self) -> None:
        self.assert_map([ARG_STAR], [ARG_STAR, ARG_STAR2], [[0], []])
        self.assert_map([ARG_STAR, ARG_STAR2], [ARG_STAR, ARG_STAR2], [[0], [1]])
        self.assert_map([ARG_STAR2], [(ARG_POS, "x"), ARG_STAR2], [[0], [0]])
        self.assert_map([ARG_STAR2], [ARG_STAR2], [[0]])

    def assert_map(
        self,
        caller_kinds_: list[ArgKind | str],
        callee_kinds_: list[ArgKind | tuple[ArgKind, str]],
        expected: list[list[int]],
    ) -> None:
        caller_kinds, caller_names = expand_caller_kinds(caller_kinds_)
        callee_kinds, callee_names = expand_callee_kinds(callee_kinds_)
        result = map_actuals_to_formals(
            caller_kinds,
            caller_names,
            callee_kinds,
            callee_names,
            lambda i: AnyType(TypeOfAny.special_form),
        )
        assert_equal(result, expected)

    def assert_vararg_map(
        self,
        caller_kinds: list[ArgKind],
        callee_kinds: list[ArgKind],
        expected: list[list[int]],
        vararg_type: Type,
    ) -> None:
        result = map_actuals_to_formals(caller_kinds, [], callee_kinds, [], lambda i: vararg_type)
        assert_equal(result, expected)

def expand_caller_kinds(
    kinds_or_names: list[ArgKind | str],
) -> tuple[list[ArgKind], list[str | None]]:
    kinds = []
    names: list[str | None] = []
    for k in kinds_or_names:
        if isinstance(k, str):
            kinds.append(ARG_NAMED)
            names.append(k)
        else:
            kinds.append(k)
            names.append(None)
    return kinds, names

def expand_callee_kinds(
    kinds_and_names: list[ArgKind | tuple[ArgKind, str]]
) -> tuple[list[ArgKind], list[str | None]]:
    kinds = []
    names: list[str | None] = []
    for v in kinds_and_names:
        if isinstance(v, tuple):
            kinds.append(v[0])
            names.append(v[1])
        else:
            kinds.append(v)
            names.append(None)
    return kinds, names

class OperandDisjointDictSuite(Suite):
    """Test cases for checker.DisjointDict, which is used for type inference with operands."""

    def new(self) -> DisjointDict[int, str]:
        return DisjointDict()

    def test_independent_maps(self) -> None:
        d = self.new()
        d.add_mapping({0, 1}, {"group1"})
        d.add_mapping({2, 3, 4}, {"group2"})
        d.add_mapping({5, 6, 7}, {"group3"})

        self.assertEqual(
            d.items(), [({0, 1}, {"group1"}), ({2, 3, 4}, {"group2"}), ({5, 6, 7}, {"group3"})]
        )

    def test_partial_merging(self) -> None:
        d = self.new()
        d.add_mapping({0, 1}, {"group1"})
        d.add_mapping({1, 2}, {"group2"})
        d.add_mapping({3, 4}, {"group3"})
        d.add_mapping({5, 0}, {"group4"})
        d.add_mapping({5, 6}, {"group5"})
        d.add_mapping({4, 7}, {"group6"})

        self.assertEqual(
            d.items(),
            [
                ({0, 1, 2, 5, 6}, {"group1", "group2", "group4", "group5"}),
                ({3, 4, 7}, {"group3", "group6"}),
            ],
        )

    def test_full_merging(self) -> None:
        d = self.new()
        d.add_mapping({0, 1, 2}, {"a"})
        d.add_mapping({3, 4, 2}, {"b"})
        d.add_mapping({10, 11, 12}, {"c"})
        d.add_mapping({13, 14, 15}, {"d"})
        d.add_mapping({14, 10, 16}, {"e"})
        d.add_mapping({0, 10}, {"f"})

        self.assertEqual(
            d.items(),
            [({0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 15, 16}, {"a", "b", "c", "d", "e", "f"})],
        )

    def test_merge_with_multiple_overlaps(self) -> None:
        d = self.new()
        d.add_mapping({0, 1, 2}, {"a"})
        d.add_mapping({3, 4, 5}, {"b"})
        d.add_mapping({1, 2, 4, 5}, {"c"})
        d.add_mapping({6, 1, 2, 4, 5}, {"d"})
        d.add_mapping({6, 1, 2, 4, 5}, {"e"})

        self.assertEqual(d.items(), [({0, 1, 2, 3, 4, 5, 6}, {"a", "b", "c", "d", "e"})])

class OperandComparisonGroupingSuite(Suite):
    """Test cases for checker.group_comparison_operands."""

    def literal_keymap(self, assignable_operands: dict[int, NameExpr]) -> dict[int, Key]:
        output: dict[int, Key] = {}
        for index, expr in assignable_operands.items():
            output[index] = ("FakeExpr", expr.name)
        return output

    def test_basic_cases(self) -> None:
        # Note: the grouping function doesn't actually inspect the input exprs, so we
        # just default to using NameExprs for simplicity.
        x0 = NameExpr("x0")
        x1 = NameExpr("x1")
        x2 = NameExpr("x2")
        x3 = NameExpr("x3")
        x4 = NameExpr("x4")

        basic_input = [("==", x0, x1), ("==", x1, x2), ("<", x2, x3), ("==", x3, x4)]

        none_assignable = self.literal_keymap({})
        all_assignable = self.literal_keymap({0: x0, 1: x1, 2: x2, 3: x3, 4: x4})

        for assignable in [none_assignable, all_assignable]:
            self.assertEqual(
                group_comparison_operands(basic_input, assignable, set()),
                [("==", [0, 1]), ("==", [1, 2]), ("<", [2, 3]), ("==", [3, 4])],
            )
            self.assertEqual(
                group_comparison_operands(basic_input, assignable, {"=="}),
                [("==", [0, 1, 2]), ("<", [2, 3]), ("==", [3, 4])],
            )
            self.assertEqual(
                group_comparison_operands(basic_input, assignable, {"<"}),
                [("==", [0, 1]), ("==", [1, 2]), ("<", [2, 3]), ("==", [3, 4])],
            )
            self.assertEqual(
                group_comparison_operands(basic_input, assignable, {"==", "<"}),
                [("==", [0, 1, 2]), ("<", [2, 3]), ("==", [3, 4])],
            )

    def test_multiple_groups(self) -> None:
        x0 = NameExpr("x0")
        x1 = NameExpr("x1")
        x2 = NameExpr("x2")
        x3 = NameExpr("x3")
        x4 = NameExpr("x4")
        x5 = NameExpr("x5")

        self.assertEqual(
            group_comparison_operands(
                [("==", x0, x1), ("==", x1, x2), ("is", x2, x3), ("is", x3, x4)],
                self.literal_keymap({}),
                {"==", "is"},
            ),
            [("==", [0, 1, 2]), ("is", [2, 3, 4])],
        )
        self.assertEqual(
            group_comparison_operands(
                [("==", x0, x1), ("==", x1, x2), ("==", x2, x3), ("==", x3, x4)],
                self.literal_keymap({}),
                {"==", "is"},
            ),
            [("==", [0, 1, 2, 3, 4])],
        )
        self.assertEqual(
            group_comparison_operands(
                [("is", x0, x1), ("==", x1, x2), ("==", x2, x3), ("==", x3, x4)],
                self.literal_keymap({}),
                {"==", "is"},
            ),
            [("is", [0, 1]), ("==", [1, 2, 3, 4])],
        )
        self.assertEqual(
            group_comparison_operands(
                [("is", x0, x1), ("is", x1, x2), ("<", x2, x3), ("==", x3, x4), ("==", x4, x5)],
                self.literal_keymap({}),
                {"==", "is"},
            ),
            [("is", [0, 1, 2]), ("<", [2, 3]), ("==", [3, 4, 5])],
        )

    def test_multiple_groups_coalescing(self) -> None:
        x0 = NameExpr("x0")
        x1 = NameExpr("x1")
        x2 = NameExpr("x2")
        x3 = NameExpr("x3")
        x4 = NameExpr("x4")

        nothing_combined = [("==", [0, 1, 2]), ("<", [2, 3]), ("==", [3, 4, 5])]
        everything_combined = [("==", [0, 1, 2, 3, 4, 5]), ("<", [2, 3])]

        # Note: We do 'x4 == x0' at the very end!
        two_groups = [
            ("==", x0, x1),
            ("==", x1, x2),
            ("<", x2, x3),
            ("==", x3, x4),
            ("==", x4, x0),
        ]
        self.assertEqual(
            group_comparison_operands(
                two_groups, self.literal_keymap({0: x0, 1: x1, 2: x2, 3: x3, 4: x4, 5: x0}), {"=="}
            ),
            everything_combined,
            "All vars are assignable, everything is combined",
        )
        self.assertEqual(
            group_comparison_operands(
                two_groups, self.literal_keymap({1: x1, 2: x2, 3: x3, 4: x4}), {"=="}
            ),
            nothing_combined,
            "x0 is unassignable, so no combining",
        )
        self.assertEqual(
            group_comparison_operands(
                two_groups, self.literal_keymap({0: x0, 1: x1, 3: x3, 5: x0}), {"=="}
            ),
            everything_combined,
            "Some vars are unassignable but x0 is, so we combine",
        )
        self.assertEqual(
            group_comparison_operands(two_groups, self.literal_keymap({0: x0, 5: x0}), {"=="}),
            everything_combined,
            "All vars are unassignable but x0 is, so we combine",
        )

    def test_multiple_groups_different_operators(self) -> None:
        x0 = NameExpr("x0")
        x1 = NameExpr("x1")
        x2 = NameExpr("x2")
        x3 = NameExpr("x3")

        groups = [("==", x0, x1), ("==", x1, x2), ("is", x2, x3), ("is", x3, x0)]
        keymap = self.literal_keymap({0: x0, 1: x1, 2: x2, 3: x3, 4: x0})
        self.assertEqual(
            group_comparison_operands(groups, keymap, {"==", "is"}),
            [("==", [0, 1, 2]), ("is", [2, 3, 4])],
            "Different operators can never be combined",
        )

    def test_single_pair(self) -> None:
        x0 = NameExpr("x0")
        x1 = NameExpr("x1")

        single_comparison = [("==", x0, x1)]
        expected_output = [("==", [0, 1])]

        assignable_combinations: list[dict[int, NameExpr]] = [{}, {0: x0}, {1: x1}, {0: x0, 1: x1}]
        to_group_by: list[set[str]] = [set(), {"=="}, {"is"}]

        for combo in assignable_combinations:
            for operators in to_group_by:
                keymap = self.literal_keymap(combo)
                self.assertEqual(
                    group_comparison_operands(single_comparison, keymap, operators),
                    expected_output,
                )

    def test_empty_pair_list(self) -> None:
        # This case should never occur in practice -- ComparisionExprs
        # always contain at least one comparison. But in case it does...

        self.assertEqual(group_comparison_operands([], {}, set()), [])
        self.assertEqual(group_comparison_operands([], {}, {"=="}), [])