-
Notifications
You must be signed in to change notification settings - Fork 3
Added a C Function Mutator #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a058f11
ca863fb
b6e3f91
81d7f38
39b0b82
6da6c87
53b15f5
fa1a8a2
766ba37
4d27904
2081740
ce005b9
6942d62
483715f
368d683
3a167da
1a8a5ee
8749301
c7fa6c1
5a2cbea
f4245b9
4323386
6ff5038
f500e42
606ce38
9419aad
9edddbe
816e09b
3dcf3d6
e9dd56d
1521e9c
287bcfb
e8e040b
cf21886
67709ba
a9c450a
7d26f5a
4032399
228409e
82f35b5
f43ad33
9193202
0a11299
917a09f
f405f51
45b361c
2faa568
6903bf9
eeac35d
bb98e0e
d61fe23
305525e
2286c06
fce5676
4065f5e
a9d7d16
58ad1bb
a23ddf1
b0685c6
9542042
164057a
b9744d1
466c2ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| """Tests for the CMutator mutation-testing utility.""" | ||
|
|
||
| from itertools import groupby | ||
|
|
||
| from pathlib import Path | ||
| from textwrap import dedent | ||
|
|
||
| from util import CFunction, CMutator, Mutant, MutationOperator | ||
| from util.c_function_graph import CFunctionGraph | ||
|
|
||
|
|
||
| def _mutant_replacements(mutants: list[Mutant]) -> set[tuple[str, str]]: | ||
| """Return the (original, replacement) pairs from a list of mutants.""" | ||
| return {(m.original_expr, m.replacement_expr) for m in mutants} | ||
|
|
||
|
|
||
| def _get_function(file: str, name: str) -> CFunction: | ||
| project = CFunctionGraph(Path(file)) | ||
| fn = project.get_function_or_none(name) | ||
| assert fn, f"Function '{name}' not found in '{file}'" | ||
| return fn | ||
|
|
||
|
|
||
| def _get_mutants_grouped_by_operator(mutator: CMutator) -> dict[MutationOperator, list[Mutant]]: | ||
| mutants = sorted(mutator.get_mutants(), key=lambda m: m.operator) | ||
| return {op: list(group) for op, group in groupby(mutants, lambda m: m.operator)} | ||
|
|
||
|
|
||
| def test_aor_finds_arithmetic_operator_in_partition() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| aor_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR] | ||
| operators_replaced = {m.original_expr for m in aor_mutants} | ||
| assert operators_replaced, "Expected at least one AOR mutation in partition" | ||
|
|
||
|
|
||
| def test_aor_mutant_source_differs_from_original() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]: | ||
| assert mutant.source_code != mutator.get_source_code() | ||
|
|
||
|
|
||
| def test_aor_replaces_plus_with_minus() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| aor_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR] | ||
| pairs = _mutant_replacements(aor_mutants) | ||
| assert ("+", "-") in pairs, "Expected a + → - AOR mutation in partition" | ||
|
|
||
|
|
||
| def test_aor_operator_label() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.AOR]: | ||
| assert mutant.operator == MutationOperator.AOR | ||
|
|
||
|
|
||
| def test_ror_finds_less_than_or_equal_in_factorial() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| ror_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR] | ||
| operators_replaced = {m.original_expr for m in ror_mutants} | ||
| assert "<=" in operators_replaced, "Expected a '<=' ROR mutation site in factorial_iter" | ||
|
|
||
|
|
||
| def test_ror_less_than_or_equal_generates_lt_replacement() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| ror_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR] | ||
| pairs = _mutant_replacements(ror_mutants) | ||
| assert ("<=", "<") in pairs | ||
|
|
||
|
|
||
| def test_ror_operator_label() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]: | ||
| assert mutant.operator == MutationOperator.ROR | ||
|
|
||
|
|
||
| def test_ror_mutant_source_contains_replacement_operator() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.ROR]: | ||
| assert mutant.replacement_expr in mutant.source_code | ||
|
|
||
|
|
||
| def test_lcr_on_function_without_logical_operators() -> None: | ||
| fn = _get_function("data/qsort.c", "quickSort") | ||
| mutator = CMutator(fn) | ||
| assert MutationOperator.LCR not in _get_mutants_grouped_by_operator(mutator), ( | ||
| "Unexpected LCR mutants generated for a function without logical operators" | ||
| ) | ||
|
|
||
|
|
||
| def test_lcr_replaces_and_with_or() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| src_with_and = dedent("""\ | ||
| int f(int a, int b, int c) { | ||
| if (a > 0 && b > 0) { | ||
| return a + b; | ||
| } | ||
| return c; | ||
| } | ||
| """) | ||
| fn.set_source_code(src_with_and) | ||
| mutator = CMutator(fn) | ||
| lcr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.LCR] | ||
| assert len(lcr_mutants) == 1 | ||
| assert lcr_mutants[0].original_expr == "&&" | ||
| assert lcr_mutants[0].replacement_expr == "||" | ||
| assert "||" in lcr_mutants[0].source_code | ||
|
|
||
|
|
||
| def test_lcr_operator_label() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| src = dedent("""\ | ||
| int f(int a, int b) { | ||
| return (a > 0 && b > 0) || (a < 0); | ||
| } | ||
| """) | ||
| fn.set_source_code(src) | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.LCR]: | ||
| assert mutant.operator == MutationOperator.LCR | ||
|
|
||
|
|
||
| def test_crp_finds_literal_in_factorial() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP] | ||
| assert crp_mutants, "Expected at least one CRP mutant in factorial_iter" | ||
|
|
||
|
|
||
| def test_crp_produces_zero_replacement_for_one() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP] | ||
| pairs = _mutant_replacements(crp_mutants) | ||
| assert ("1", "0") in pairs, "Expected literal '1' → '0' CRP mutation" | ||
|
|
||
|
|
||
| def test_crp_produces_incremented_value() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP] | ||
| pairs = _mutant_replacements(crp_mutants) | ||
| assert ("1", "2") in pairs, "Expected literal '1' → '2' CRP mutation" | ||
|
|
||
|
|
||
| def test_crp_zero_literal_only_generates_increment() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| src_with_zero = dedent("""\ | ||
| int f() { | ||
| int x = 0; | ||
| return x; | ||
| } | ||
| """) | ||
| fn.set_source_code(src_with_zero) | ||
| mutator = CMutator(fn) | ||
| crp_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP] | ||
| replacements_for_zero = [m.replacement_expr for m in crp_mutants if m.original_expr == "0"] | ||
| assert "1" in replacements_for_zero | ||
| assert "0" not in replacements_for_zero | ||
|
Comment on lines
+152
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align the zero-literal CRP test with the CRP contract. CRP currently generates both Suggested fix-def test_crp_zero_literal_only_generates_increment() -> None:
+def test_crp_zero_literal_generates_non_noop_neighbors() -> None:
@@
replacements_for_zero = [m.replacement_expr for m in crp_mutants if m.original_expr == "0"]
assert "1" in replacements_for_zero
+ assert "-1" in replacements_for_zero
assert "0" not in replacements_for_zero🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_crp_operator_label() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.CRP]: | ||
| assert mutant.operator == MutationOperator.CRP | ||
|
|
||
|
|
||
| def test_rvr_finds_return_in_factorial() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| rvr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR] | ||
| assert rvr_mutants, "Expected at least one RVR mutant in factorial_iter" | ||
|
|
||
|
|
||
| def test_rvr_replaces_return_with_zero() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| rvr_mutants = _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR] | ||
| for mutant in rvr_mutants: | ||
| assert mutant.replacement_expr == "0" | ||
|
|
||
|
|
||
| def test_rvr_skips_existing_zero_return() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| src_returns_zero = dedent("""\ | ||
| int f() { | ||
| return 0; | ||
| } | ||
| """) | ||
| fn.set_source_code(src_returns_zero) | ||
| mutator = CMutator(fn) | ||
| rvr_mutants = _get_mutants_grouped_by_operator(mutator).get(MutationOperator.RVR, []) | ||
| assert rvr_mutants == [], "Should not mutate a return that already returns 0" | ||
|
|
||
|
|
||
| def test_rvr_skips_void_function() -> None: | ||
| fn = _get_function("data/qsort.c", "swap") | ||
| mutator = CMutator(fn) | ||
| assert MutationOperator.RVR not in _get_mutants_grouped_by_operator(mutator), ( | ||
| "RVR should not generate mutants for a 'void' function." | ||
| ) | ||
|
|
||
|
|
||
| def test_rvr_operator_label() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| for mutant in _get_mutants_grouped_by_operator(mutator)[MutationOperator.RVR]: | ||
| assert mutant.operator == MutationOperator.RVR | ||
|
|
||
|
|
||
| def test_get_mutants_returns_nonempty_list() -> None: | ||
| fn = _get_function("data/factorial_iterative.c", "factorial_iter") | ||
| mutator = CMutator(fn) | ||
| mutants = mutator.get_mutants() | ||
| assert isinstance(mutants, list) | ||
| assert len(mutants) > 0 | ||
|
|
||
|
|
||
| def test_get_mutants_equals_sum_of_all_operators() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| by_operator = _get_mutants_grouped_by_operator(mutator) | ||
| total_by_op = sum(len(v) for v in by_operator.values()) | ||
| assert len(mutator.get_mutants()) == total_by_op | ||
|
|
||
|
|
||
| def test_each_mutant_has_nonempty_description() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| for mutant in mutator.get_mutants(): | ||
| assert mutant.description, "Each mutant should have a non-empty description" | ||
|
|
||
|
|
||
| def test_each_mutant_line_is_positive() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| for mutant in mutator.get_mutants(): | ||
| assert mutant.line >= 1 | ||
|
|
||
|
|
||
| def test_mutants_are_unique() -> None: | ||
| fn = _get_function("data/qsort.c", "partition") | ||
| mutator = CMutator(fn) | ||
| sources = [m.source_code for m in mutator.get_mutants()] | ||
| assert len(sources) == len(set(sources)), "Mutants should be unique" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from .mutant import Mutant | ||
| from .c_mutator import CMutator | ||
| from .mutation_operator import ( | ||
| MutationOperator, | ||
| ArithmeticOperatorReplacement, | ||
| RelationalOperatorReplacement, | ||
| LogicalConnectorReplacement, | ||
| ConstantReplacement, | ||
| ReturnValueReplacement, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "CMutator", | ||
| "Mutant", | ||
| "MutationOperator", | ||
| "ArithmeticOperatorReplacement", | ||
| "RelationalOperatorReplacement", | ||
| "LogicalConnectorReplacement", | ||
| "ConstantReplacement", | ||
| "ReturnValueReplacement", | ||
| ] | ||
|
jyoo980 marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annotate grouped mutant keys as
str, notMutationOperator.Mutant.operatoris astr, andMutationOperator.AOR/CRP/etc. are string constants, so this helper returnsdict[str, list[Mutant]]. The current annotation risks failingmake checks.Suggested fix
🤖 Prompt for AI Agents