"""Tests for the API. """ import ast import sys import pytest from mutatest.api import Genome, GenomeGroup, MutationException from mutatest.transformers import LocIndex #################################################################################################### # GENOME AND MUTANT #################################################################################################### def test_genome_ast(binop_file, binop_expected_locs): """Test that the AST builds expected targets.""" genome = Genome(source_file=binop_file) assert len(genome.targets) == 4 assert genome.targets == binop_expected_locs def test_create_mutant_with_cache(binop_file, stdoutIO): """Change ast.Add to ast.Mult in a mutation including pycache changes.""" genome = Genome(source_file=binop_file) # this target is the add_five() function, changing add to mult end_lineno = None if sys.version_info < (3, 8) else 10 end_col_offset = None if sys.version_info < (3, 8) else 16 target_idx = LocIndex( ast_class="BinOp", lineno=10, col_offset=11, op_type=ast.Add, end_lineno=end_lineno, end_col_offset=end_col_offset, ) mutation_op = ast.Mult mutant = genome.mutate(target_idx, mutation_op, write_cache=True) # uses the redirection for stdout to capture the value from the final output of binop_file with stdoutIO() as s: exec(mutant.mutant_code) assert int(s.getvalue()) == 25 tag = sys.implementation.cache_tag expected_cfile = binop_file.parent / "__pycache__" / ".".join([binop_file.stem, tag, "pyc"]) assert mutant.src_file == binop_file assert mutant.cfile == expected_cfile assert mutant.src_idx == target_idx def test_filter_codes_ValueError(): """Setting invalid filter codes on the Genome raises a ValueError.""" with pytest.raises(ValueError): genome = Genome() genome.filter_codes = ("asdf",) def test_targets_TypeError(): """Targets with a NoneType source_file raises a TypeError.""" with pytest.raises(TypeError): genome = Genome() _ = genome.targets def test_covered_targets_source_file_TypeError(): """Targets with a NoneType source_file raises a TypeError.""" with pytest.raises(TypeError): genome = Genome() _ = genome.covered_targets def test_covered_targets_coverage_file_TypeError(binop_file): """Targets with a NoneType coverage_file but valid source_file raises a TypeError.""" with pytest.raises(TypeError): genome = Genome(binop_file) genome.coverage_file = None _ = genome.covered_targets def test_mutate_MutationException(binop_file, mock_LocIdx): """Mutate with an invalid operation raises a mutation exception.""" genome = Genome(binop_file) with pytest.raises(MutationException): _ = genome.mutate(target_idx=mock_LocIdx, mutation_op="badoperation", write_cache=False) def test_mutate_TypeError_source_file(mock_LocIdx): """Mutate with a NoneType source_file property raises a TypeError.""" genome = Genome() with pytest.raises(TypeError): _ = genome.mutate(target_idx=mock_LocIdx, mutation_op=ast.Div, write_cache=False) def test_mutate_ValueError_target(binop_file, mock_LocIdx): """Mutate with a target_idx not in the targets raises a ValueError.""" genome = Genome(binop_file) with pytest.raises(ValueError): _ = genome.mutate(target_idx=mock_LocIdx, mutation_op=ast.Div, write_cache=False) @pytest.mark.coverage @pytest.mark.parametrize("filter_codes", [set(), ("bn",)], ids=["Filter Empty Set", "Filter BinOp"]) def test_covered_targets(filter_codes, binop_file, mock_binop_coverage_file): """Mock coverage file sets lines 6 and 10 (not 15) to be covered.""" genome = Genome(binop_file, coverage_file=mock_binop_coverage_file) genome.filter_codes = filter_codes assert len(genome.targets) == 4 assert len(genome.covered_targets) == 3 for ct in genome.covered_targets: assert ct.lineno in [6, 10] diff = list(genome.targets - genome.covered_targets) assert diff[0].lineno == 15 #################################################################################################### # GENOME GROUP # These tests will naturally cover some Genome functionality #################################################################################################### def test_init_GenomeGroup_from_flat_folder(tmp_path): """Test the only .py files are grabbed with GenomeGroup default initialization. This tests Genome as well. """ test_files = [ "first.py", "second.py", "third.py", "test_first.py", "test_second.py", "third_test.py", "fourth_test.py", "first.pyc", "first.pyo", "first.pyi", ] expected = ["first.py", "second.py", "third.py"] for tf in test_files: with open(tmp_path / tf, "w") as temp_py: temp_py.write("import this") ggrp = GenomeGroup(tmp_path) assert sorted([g.name for g in ggrp.keys()]) == sorted(expected) for k, v in ggrp.items(): assert v.source_file.name in expected def test_init_GenomeGroup_from_recursive_folder(tmp_path): """Ensure recursive glob search works for finding py files. This tests Genome as well.""" f = tmp_path / "folder" f.mkdir() test_files = [ tmp_path / "first.py", tmp_path / "second.py", tmp_path / "test_first.py", tmp_path / "test_second.py", tmp_path / "third_test.py", f / "third.py", f / "test_third.py", ] expected = ["first.py", "second.py", "third.py"] for tf in test_files: with open(tf, "w") as temp_py: temp_py.write("import this") ggrp = GenomeGroup(tmp_path) assert sorted([g.name for g in ggrp.keys()]) == sorted(expected) for k, v in ggrp.items(): assert v.source_file.name in expected def test_init_GenomeGroup_from_single_file(binop_file): """Initialize the GenomgGroup from a single file. This tests Genome as well.""" ggrp = GenomeGroup(binop_file) assert len(ggrp.keys()) == 1 assert list(ggrp.keys())[0].resolve() == binop_file.resolve() def test_init_GenomeGroup_raise_TypeError(): """Initialization with an non-file non-dir raises a TypeError.""" with pytest.raises(TypeError): _ = GenomeGroup("somethingrandom") def test_GenomeGroup_folder_exception(): """Invalid folders raise a type error.""" with pytest.raises(TypeError): ggrp = GenomeGroup() ggrp.add_folder("somethingrandom") @pytest.mark.parametrize("key", [1, "a", 2.2, True], ids=["int", "str", "float", "bool"]) def test_GenomeGroup_key_TypeError(key, binop_file): """Values that are not Path type keys raise a type error.""" with pytest.raises(TypeError): ggrp = GenomeGroup() ggrp[key] = Genome(binop_file) @pytest.mark.parametrize("value", [1, "a", 2.2, True], ids=["int", "str", "float", "bool"]) def test_GenomeGroup_value_TypeError(value, binop_file): """Non-Genome values raise a type error.""" with pytest.raises(TypeError): ggrp = GenomeGroup() ggrp[binop_file] = value def test_GenomeGroup_add_folder_with_exclusions(tmp_path): """Ensure excluded files are not used in the GenomeGroup add folder method.""" f = tmp_path / "folder" f.mkdir() test_files = [ tmp_path / "first.py", tmp_path / "second.py", tmp_path / "test_first.py", tmp_path / "test_second.py", tmp_path / "third_test.py", f / "third.py", f / "test_third.py", ] exclude = [(tmp_path / "second.py").resolve(), (f / "third.py").resolve()] expected = "first.py" # need at least on valid location operation to return a value for trees/targets for tf in test_files: with open(tf, "w") as temp_py: temp_py.write("x: int = 1 + 2") ggrp = GenomeGroup() ggrp.add_folder(tmp_path, exclude_files=exclude) assert len(ggrp) == 1 assert list(ggrp.keys())[0].name == expected @pytest.mark.coverage @pytest.mark.parametrize("filter_codes", [set(), ("bn",)], ids=["Filter Empty Set", "Filter BinOp"]) def test_GenomeGroup_covered_targets(filter_codes, binop_file, mock_binop_coverage_file): """Mock coverage file sets lines 6 and 10 (not 15) to be covered.""" ggrp = GenomeGroup(binop_file) ggrp.set_coverage(mock_binop_coverage_file) ggrp.set_filter(filter_codes) assert len(ggrp.targets) == 4 assert len(ggrp.covered_targets) == 3 for ct in ggrp.covered_targets: assert ct.source_path == binop_file assert ct.loc_idx.lineno in [6, 10] diff = list(ggrp.targets - ggrp.covered_targets) assert diff[0].loc_idx.lineno == 15 def test_GenomeGroup_TypeError_source_file(): """GenomeGroup raises a TypeError adding a Genome without a set source_file.""" ggrp = GenomeGroup() with pytest.raises(TypeError): ggrp.add_genome(Genome()) def test_GenomeGroup_basic_properties(binop_file, boolop_file, compare_file): """Basic class property tests and dictionary manipulation.""" ggrp = GenomeGroup(binop_file) ggrp.add_file(boolop_file) ggrp.add_file(compare_file) # test ValuesView is iterable view. for v in ggrp.values(): assert isinstance(v, Genome) # test basic __iter__ property keys = [k for k in ggrp] assert len(keys) == 3 # __repr__ is a string representation of the store assert isinstance(ggrp.__repr__(), str) # test basic .items() method, uses .pop() to activate __del__ key_values = [(k, v) for k, v in ggrp.items()] for k, v in key_values: v2 = ggrp.pop(k) assert v2 == v