Skip to content

Commit

Permalink
Make testfinegrained use dmypy_server (python#4699)
Browse files Browse the repository at this point in the history
  • Loading branch information
msullivan authored and yedpodtrzitko committed Mar 13, 2018
1 parent 69c9e47 commit 525e142
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 127 deletions.
8 changes: 4 additions & 4 deletions mypy/dmypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ def do_restart(args: argparse.Namespace) -> None:
def start_server(args: argparse.Namespace) -> None:
"""Start the server from command arguments and wait for it."""
# Lazy import so this import doesn't slow down other commands.
from mypy.dmypy_server import daemonize, Server
if daemonize(Server(args.flags).serve, args.log_file) != 0:
from mypy.dmypy_server import daemonize, Server, process_start_options
if daemonize(Server(process_start_options(args.flags)).serve, args.log_file) != 0:
sys.exit(1)
wait_for_server()

Expand Down Expand Up @@ -283,8 +283,8 @@ def do_hang(args: argparse.Namespace) -> None:
def do_daemon(args: argparse.Namespace) -> None:
"""Serve requests in the foreground."""
# Lazy import so this import doesn't slow down other commands.
from mypy.dmypy_server import Server
Server(args.flags).serve()
from mypy.dmypy_server import Server, process_start_options
Server(process_start_options(args.flags)).serve()


@action(help_parser)
Expand Down
71 changes: 41 additions & 30 deletions mypy/dmypy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
import mypy.build
import mypy.errors
import mypy.main
import mypy.server.update
from mypy.server.update import FineGrainedBuildManager
from mypy.dmypy_util import STATUS_FILE, receive
from mypy.gclogger import GcLogger
from mypy.fscache import FileSystemCache
from mypy.fswatcher import FileSystemWatcher, FileData
from mypy.options import Options


def daemonize(func: Callable[[], None], log_file: Optional[str] = None) -> int:
Expand Down Expand Up @@ -78,33 +79,44 @@ def daemonize(func: Callable[[], None], log_file: Optional[str] = None) -> int:
SOCKET_NAME = 'dmypy.sock' # In current directory.


def process_start_options(flags: List[str]) -> Options:
import mypy.main
sources, options = mypy.main.process_options(['-i'] + flags,
require_targets=False,
server_options=True)
if sources:
sys.exit("dmypy: start/restart does not accept sources")
if options.report_dirs:
sys.exit("dmypy: start/restart cannot generate reports")
if options.junit_xml:
sys.exit("dmypy: start/restart does not support --junit-xml; "
"pass it to check/recheck instead")
if not options.incremental:
sys.exit("dmypy: start/restart should not disable incremental mode")
if options.quick_and_dirty:
sys.exit("dmypy: start/restart should not specify quick_and_dirty mode")
if options.use_fine_grained_cache and not options.fine_grained_incremental:
sys.exit("dmypy: fine-grained cache can only be used in experimental mode")
# Our file change tracking can't yet handle changes to files that aren't
# specified in the sources list.
if options.follow_imports not in ('skip', 'error'):
sys.exit("dmypy: follow-imports must be 'skip' or 'error'")
return options


class Server:

# NOTE: the instance is constructed in the parent process but
# serve() is called in the grandchild (by daemonize()).

def __init__(self, flags: List[str]) -> None:
def __init__(self, options: Options, alt_lib_path: Optional[str] = None) -> None:
"""Initialize the server with the desired mypy flags."""
self.saved_cache = {} # type: mypy.build.SavedCache
self.fine_grained_initialized = False
sources, options = mypy.main.process_options(['-i'] + flags,
require_targets=False,
server_options=True)
self.fine_grained = options.fine_grained_incremental
if sources:
sys.exit("dmypy: start/restart does not accept sources")
if options.report_dirs:
sys.exit("dmypy: start/restart cannot generate reports")
if options.junit_xml:
sys.exit("dmypy: start/restart does not support --junit-xml; "
"pass it to check/recheck instead")
if not options.incremental:
sys.exit("dmypy: start/restart should not disable incremental mode")
if options.quick_and_dirty:
sys.exit("dmypy: start/restart should not specify quick_and_dirty mode")
if options.use_fine_grained_cache and not options.fine_grained_incremental:
sys.exit("dmypy: fine-grained cache can only be used in experimental mode")
self.options = options
self.alt_lib_path = alt_lib_path
self.fine_grained_manager = None # type: Optional[FineGrainedBuildManager]

if os.path.isfile(STATUS_FILE):
os.unlink(STATUS_FILE)
if self.fine_grained:
Expand Down Expand Up @@ -214,15 +226,13 @@ def cmd_recheck(self) -> Dict[str, object]:
# Needed by tests.
last_manager = None # type: Optional[mypy.build.BuildManager]

def check(self, sources: List[mypy.build.BuildSource],
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
def check(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
if self.fine_grained:
return self.check_fine_grained(sources)
else:
return self.check_default(sources, alt_lib_path)
return self.check_default(sources)

def check_default(self, sources: List[mypy.build.BuildSource],
alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
def check_default(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
"""Check using the default (per-file) incremental mode."""
self.last_manager = None
blockers = False
Expand All @@ -231,7 +241,7 @@ def check_default(self, sources: List[mypy.build.BuildSource],
# saved_cache is mutated in place.
res = mypy.build.build(sources, self.options,
saved_cache=self.saved_cache,
alt_lib_path=alt_lib_path)
alt_lib_path=self.alt_lib_path)
msgs = res.errors
self.last_manager = res.manager # type: Optional[mypy.build.BuildManager]
except mypy.errors.CompileError as err:
Expand All @@ -254,7 +264,7 @@ def check_default(self, sources: List[mypy.build.BuildSource],

def check_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
"""Check using fine-grained incremental mode."""
if not self.fine_grained_initialized:
if not self.fine_grained_manager:
return self.initialize_fine_grained(sources)
else:
return self.fine_grained_increment(sources)
Expand All @@ -267,9 +277,9 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
# Stores the initial state of sources as a side effect.
self.fswatcher.find_changed()
try:
# TODO: alt_lib_path
result = mypy.build.build(sources=sources,
options=self.options)
options=self.options,
alt_lib_path=self.alt_lib_path)
except mypy.errors.CompileError as e:
output = ''.join(s + '\n' for s in e.messages)
if e.use_stdout:
Expand All @@ -280,8 +290,7 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
messages = result.errors
manager = result.manager
graph = result.graph
self.fine_grained_manager = mypy.server.update.FineGrainedBuildManager(manager, graph)
self.fine_grained_initialized = True
self.fine_grained_manager = FineGrainedBuildManager(manager, graph)
self.previous_sources = sources
self.fscache.flush()

Expand Down Expand Up @@ -310,6 +319,8 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
return {'out': ''.join(s + '\n' for s in messages), 'err': '', 'status': status}

def fine_grained_increment(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
assert self.fine_grained_manager is not None

t0 = time.time()
self.update_sources(sources)
changed = self.find_changed(sources)
Expand Down
20 changes: 20 additions & 0 deletions mypy/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import subprocess
import sys
import time
import shutil

from typing import List, Dict, Tuple, Callable, Any, Optional

Expand Down Expand Up @@ -356,3 +357,22 @@ def run_command(cmdline: List[str], *, env: Optional[Dict[str, str]] = None,
out = err = b''
process.kill()
return process.returncode, split_lines(out, err)


def copy_and_fudge_mtime(source_path: str, target_path: str) -> None:
# In some systems, mtime has a resolution of 1 second which can
# cause annoying-to-debug issues when a file has the same size
# after a change. We manually set the mtime to circumvent this.
# Note that we increment the old file's mtime, which guarentees a
# different value, rather than incrementing the mtime after the
# copy, which could leave the mtime unchanged if the old file had
# a similarly fudged mtime.
new_time = None
if os.path.isfile(target_path):
new_time = os.stat(target_path).st_mtime + 1

# Use retries to work around potential flakiness on Windows (AppVeyor).
retry_on_error(lambda: shutil.copy(source_path, target_path))

if new_time:
os.utime(target_path, times=(new_time, new_time))
12 changes: 3 additions & 9 deletions mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import (
assert_string_arrays_equal, normalize_error_messages,
retry_on_error, update_testcase_output, parse_options
retry_on_error, update_testcase_output, parse_options,
copy_and_fudge_mtime
)
from mypy.errors import CompileError
from mypy.options import Options
Expand Down Expand Up @@ -132,14 +133,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0)
if file.endswith('.' + str(incremental_step)):
full = os.path.join(dn, file)
target = full[:-2]
# Use retries to work around potential flakiness on Windows (AppVeyor).
retry_on_error(lambda: shutil.copy(full, target))

# In some systems, mtime has a resolution of 1 second which can cause
# annoying-to-debug issues when a file has the same size after a
# change. We manually set the mtime to circumvent this.
new_time = os.stat(target).st_mtime + 1
os.utime(target, times=(new_time, new_time))
copy_and_fudge_mtime(full, target)
# Delete files scheduled to be deleted in [delete <path>.num] sections.
for path in testcase.deleted_paths.get(incremental_step, set()):
# Use retries to work around potential flakiness on Windows (AppVeyor).
Expand Down
18 changes: 4 additions & 14 deletions mypy/test/testdmypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from mypy.test.helpers import (
assert_string_arrays_equal, normalize_error_messages,
retry_on_error, testcase_pyversion, update_testcase_output,
copy_and_fudge_mtime,
)
from mypy.options import Options

Expand Down Expand Up @@ -91,14 +92,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) ->
if file.endswith('.' + str(incremental_step)):
full = os.path.join(dn, file)
target = full[:-2]
# Use retries to work around potential flakiness on Windows (AppVeyor).
retry_on_error(lambda: shutil.copy(full, target))

# In some systems, mtime has a resolution of 1 second which can cause
# annoying-to-debug issues when a file has the same size after a
# change. We manually set the mtime to circumvent this.
new_time = os.stat(target).st_mtime + 1
os.utime(target, times=(new_time, new_time))
copy_and_fudge_mtime(full, target)
# Delete files scheduled to be deleted in [delete <path>.num] sections.
for path in testcase.deleted_paths.get(incremental_step, set()):
# Use retries to work around potential flakiness on Windows (AppVeyor).
Expand All @@ -117,20 +111,16 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) ->
# Parse options after moving files (in case mypy.ini is being moved).
options = self.parse_options(original_program_text, testcase, incremental_step)
if incremental_step == 1:
server_options = [] # type: List[str]
if 'fine-grained' in testcase.file:
server_options.append('--experimental')
options.fine_grained_incremental = True
options.local_partial_types = True
self.server = dmypy_server.Server(server_options) # TODO: Fix ugly API
self.server.options = options
self.server = dmypy_server.Server(options, alt_lib_path=test_temp_dir)

assert self.server is not None # Set in step 1 and survives into next steps
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(build.BuildSource(program_path, module_name, None))
response = self.server.check(sources, alt_lib_path=test_temp_dir)
response = self.server.check(sources)
a = (response['out'] or response['err']).splitlines()
a = normalize_error_messages(a)

Expand Down
Loading

0 comments on commit 525e142

Please sign in to comment.