testpythoneval.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Test cases for running mypy programs using a Python interpreter.
  2. Each test case type checks a program then runs it using Python. The
  3. output (stdout) of the program is compared to expected output. Type checking
  4. uses full builtins and other stubs.
  5. Note: Currently Python interpreter paths are hard coded.
  6. Note: These test cases are *not* included in the main test suite, as including
  7. this suite would slow down the main suite too much.
  8. """
  9. from __future__ import annotations
  10. import os
  11. import os.path
  12. import re
  13. import subprocess
  14. import sys
  15. from tempfile import TemporaryDirectory
  16. from mypy import api
  17. from mypy.defaults import PYTHON3_VERSION
  18. from mypy.test.config import test_temp_dir
  19. from mypy.test.data import DataDrivenTestCase, DataSuite
  20. from mypy.test.helpers import assert_string_arrays_equal, split_lines
  21. # Path to Python 3 interpreter
  22. python3_path = sys.executable
  23. program_re = re.compile(r"\b_program.py\b")
  24. class PythonEvaluationSuite(DataSuite):
  25. files = ["pythoneval.test", "pythoneval-asyncio.test"]
  26. cache_dir = TemporaryDirectory()
  27. def run_case(self, testcase: DataDrivenTestCase) -> None:
  28. test_python_evaluation(testcase, os.path.join(self.cache_dir.name, ".mypy_cache"))
  29. def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None:
  30. """Runs Mypy in a subprocess.
  31. If this passes without errors, executes the script again with a given Python
  32. version.
  33. """
  34. assert testcase.old_cwd is not None, "test was not properly set up"
  35. # We must enable site packages to get access to installed stubs.
  36. mypy_cmdline = [
  37. "--show-traceback",
  38. "--no-silence-site-packages",
  39. "--no-error-summary",
  40. "--hide-error-codes",
  41. "--allow-empty-bodies",
  42. "--force-uppercase-builtins",
  43. ]
  44. interpreter = python3_path
  45. mypy_cmdline.append(f"--python-version={'.'.join(map(str, PYTHON3_VERSION))}")
  46. m = re.search("# flags: (.*)$", "\n".join(testcase.input), re.MULTILINE)
  47. if m:
  48. additional_flags = m.group(1).split()
  49. for flag in additional_flags:
  50. if flag.startswith("--python-version="):
  51. targetted_python_version = flag.split("=")[1]
  52. targetted_major, targetted_minor = targetted_python_version.split(".")
  53. if (int(targetted_major), int(targetted_minor)) > (
  54. sys.version_info.major,
  55. sys.version_info.minor,
  56. ):
  57. return
  58. mypy_cmdline.extend(additional_flags)
  59. # Write the program to a file.
  60. program = "_" + testcase.name + ".py"
  61. program_path = os.path.join(test_temp_dir, program)
  62. mypy_cmdline.append(program_path)
  63. with open(program_path, "w", encoding="utf8") as file:
  64. for s in testcase.input:
  65. file.write(f"{s}\n")
  66. mypy_cmdline.append(f"--cache-dir={cache_dir}")
  67. output = []
  68. # Type check the program.
  69. out, err, returncode = api.run(mypy_cmdline)
  70. # split lines, remove newlines, and remove directory of test case
  71. for line in (out + err).splitlines():
  72. if line.startswith(test_temp_dir + os.sep):
  73. output.append(line[len(test_temp_dir + os.sep) :].rstrip("\r\n"))
  74. else:
  75. # Normalize paths so that the output is the same on Windows and Linux/macOS.
  76. line = line.replace(test_temp_dir + os.sep, test_temp_dir + "/")
  77. output.append(line.rstrip("\r\n"))
  78. if returncode > 1 and not testcase.output:
  79. # Either api.run() doesn't work well in case of a crash, or pytest interferes with it.
  80. # Tweak output to prevent tests with empty expected output to pass in case of a crash.
  81. output.append("!!! Mypy crashed !!!")
  82. if returncode == 0 and not output:
  83. # Execute the program.
  84. proc = subprocess.run(
  85. [interpreter, "-Wignore", program], cwd=test_temp_dir, capture_output=True
  86. )
  87. output.extend(split_lines(proc.stdout, proc.stderr))
  88. # Remove temp file.
  89. os.remove(program_path)
  90. for i, line in enumerate(output):
  91. if os.path.sep + "typeshed" + os.path.sep in line:
  92. output[i] = line.split(os.path.sep)[-1]
  93. assert_string_arrays_equal(
  94. adapt_output(testcase), output, f"Invalid output ({testcase.file}, line {testcase.line})"
  95. )
  96. def adapt_output(testcase: DataDrivenTestCase) -> list[str]:
  97. """Translates the generic _program.py into the actual filename."""
  98. program = "_" + testcase.name + ".py"
  99. return [program_re.sub(program, line) for line in testcase.output]