Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 78 additions & 31 deletions mutmut/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,33 +312,35 @@ def __init__(self, *, path):
self.estimated_time_of_tests_by_mutant = {}
self.path = path
self.meta_path = Path('mutants') / (str(path) + '.meta')
self.meta = None
self.key_by_pid = {}
self.exit_code_by_key = {}
self.durations_by_key = {}
self.hash_by_function_name = {}
self.start_time_by_pid = {}
self.estimated_time_of_tests_by_pid = {}

def load(self):
try:
with open(self.meta_path) as f:
self.meta = json.load(f)
meta = json.load(f)
except FileNotFoundError:
return

self.exit_code_by_key = self.meta.pop('exit_code_by_key')
self.hash_by_function_name = self.meta.pop('hash_by_function_name')
assert not self.meta, self.meta # We should read all the data!
self.exit_code_by_key = meta.pop('exit_code_by_key')
self.hash_by_function_name = meta.pop('hash_by_function_name')
self.durations_by_key = meta.pop('durations_by_key')
self.estimated_time_of_tests_by_mutant = meta.pop('estimated_durations_by_key')
assert not meta, f'Meta file {self.meta_path} constains unexpected keys: {set(meta.keys())}'

def register_pid(self, *, pid, key, estimated_time_of_tests):
def register_pid(self, *, pid, key):
self.key_by_pid[pid] = key
with START_TIMES_BY_PID_LOCK:
self.start_time_by_pid[pid] = datetime.now()
self.estimated_time_of_tests_by_pid[pid] = estimated_time_of_tests

def register_result(self, *, pid, exit_code):
assert self.key_by_pid[pid] in self.exit_code_by_key
self.exit_code_by_key[self.key_by_pid[pid]] = exit_code
key = self.key_by_pid[pid]
self.exit_code_by_key[key] = exit_code
self.durations_by_key[key] = (datetime.now() - self.start_time_by_pid[pid]).total_seconds()
# TODO: maybe rate limit this? Saving on each result can slow down mutation testing a lot if the test run is fast.
del self.key_by_pid[pid]
with START_TIMES_BY_PID_LOCK:
Expand All @@ -354,6 +356,8 @@ def save(self):
json.dump(dict(
exit_code_by_key=self.exit_code_by_key,
hash_by_function_name=self.hash_by_function_name,
durations_by_key=self.durations_by_key,
estimated_durations_by_key=self.estimated_time_of_tests_by_mutant,
), f, indent=4)


Expand Down Expand Up @@ -1110,7 +1114,7 @@ def read_one_child_exit_status():
else:
# in the parent
source_file_mutation_data_by_pid[pid] = m
m.register_pid(pid=pid, key=mutant_name, estimated_time_of_tests=estimated_time_of_tests)
m.register_pid(pid=pid, key=mutant_name)
running_children += 1

if running_children >= max_children:
Expand Down Expand Up @@ -1297,8 +1301,6 @@ def apply_mutant(mutant_name):
f.write(new_module.code)


# TODO: junitxml, html commands

@cli.command()
@click.option("--show-killed", is_flag=True, default=False, help="Display killed mutants.")
def browse(show_killed):
Expand All @@ -1321,6 +1323,7 @@ class ResultBrowser(App):
("f", "retest_function()", "Retest function"),
("m", "retest_module()", "Retest module"),
("a", "apply_mutant()", "Apply mutant to disk"),
("t", "view_tests()", "View tests for mutant"),
]

columns = [
Expand All @@ -1331,13 +1334,14 @@ class ResultBrowser(App):
]

cursor_type = 'row'
source_file_mutation_data_and_stat_by_path = None
source_file_mutation_data_and_stat_by_path: dict[str, tuple[SourceFileMutationData, Stat]] = {}

def compose(self):
with Container(classes='container'):
yield DataTable(id='files')
yield DataTable(id='mutants')
with Widget(id="diff_view_widget"):
yield Static(id='description')
yield Static(id='diff_view')
yield Footer()

Expand Down Expand Up @@ -1405,31 +1409,71 @@ def on_data_table_row_highlighted(self, event):
else:
assert event.data_table.id == 'mutants'
# noinspection PyTypeChecker
description_view: Static = self.query_one('#description')
mutant_name = event.row_key.value
self.loading_id = mutant_name
path = self.path_by_name.get(mutant_name)
source_file_mutation_data, stat = self.source_file_mutation_data_and_stat_by_path[str(path)]

exit_code = source_file_mutation_data.exit_code_by_key[mutant_name]
status = status_by_exit_code[exit_code]
estimated_duration = source_file_mutation_data.estimated_time_of_tests_by_mutant.get(mutant_name, '?')
duration = source_file_mutation_data.durations_by_key.get(mutant_name, '?')

view_tests_description = f'(press t to view tests executed for this mutant)'

match status:
case 'killed':
description = f'Killed ({exit_code=}): Mutant caused a test to fail 🎉'
case 'survived':
description = f'Survived ({exit_code=}): No test detected this mutant. {view_tests_description}'
case 'skipped':
description = f'Skipped ({exit_code=})'
case 'check was interrupted by user':
description = f'User interrupted ({exit_code=})'
case 'timeout':
description = (f'Timeout ({exit_code=}): Timed out because tests did not finish within {duration:.3f} seconds. '
f'Tests without mutation took {estimated_duration:.3f} seconds. {view_tests_description}')
case 'no tests':
description = f'Untested ({exit_code=}): Skipped because selected tests do not execute this code.'
case 'segfault':
description = f'Segfault ({exit_code=}): Running pytest with this mutant segfaulted.'
case 'suspicious':
description = f'Unknown ({exit_code=}): Running pytest with this mutant resulted in an unknown exit code.'
case 'not checked':
description = 'Not checked in the last mutmut run.'
case _:
description = f'Unknown status ({exit_code=}, {status=})'
description_view.update(f'\n {description}\n')

diff_view: Static = self.query_one('#diff_view')
if event.row_key.value is None:
diff_view.update('')
else:
diff_view.update('<loading...>')
self.loading_id = event.row_key.value
path = self.path_by_name.get(event.row_key.value)

def load_thread():
ensure_config_loaded()
try:
d = get_diff_for_mutant(event.row_key.value, path=path)
if event.row_key.value == self.loading_id:
diff_view.update(Syntax(d, "diff"))
except Exception as e:
diff_view.update(f"<{type(e)} {e}>")
diff_view.update('<loading code diff...>')

def load_thread():
ensure_config_loaded()
try:
d = get_diff_for_mutant(event.row_key.value, path=path)
if event.row_key.value == self.loading_id:
diff_view.update(Syntax(d, "diff"))
except Exception as e:
diff_view.update(f"<{type(e)} {e}>")

t = Thread(target=load_thread)
t.start()
t = Thread(target=load_thread)
t.start()

def retest(self, pattern):
self._run_subprocess_command('run', [pattern])

def view_tests(self, mutant_name: str):
self._run_subprocess_command('tests-for-mutant', [mutant_name])

def _run_subprocess_command(self, command: str, args: list[str]):
with self.suspend():
browse_index = sys.argv.index('browse')
initial_args = sys.argv[:browse_index]
subprocess.run([sys.executable, *initial_args, 'run', pattern])
subprocess_args = [sys.executable, *initial_args, command, *args]
print('>', *subprocess_args)
subprocess.run(subprocess_args)
input('press enter to return to browser')

self.read_data()
Expand Down Expand Up @@ -1460,6 +1504,9 @@ def action_apply_mutant(self):
return
apply_mutant(mutants_table.get_row_at(mutants_table.cursor_row)[0])

def action_view_tests(self):
self.view_tests(self.get_mutant_name_from_selection())

ResultBrowser().run()


Expand Down