├── src └── mutahunter │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── entities │ │ ├── __init__.py │ │ └── config.py │ ├── queries │ │ ├── __init__.py │ │ ├── tree-sitter-c-tags.scm │ │ ├── tree-sitter-php-tags.scm │ │ ├── tree-sitter-cpp-tags.scm │ │ ├── tree-sitter-python-tags.scm │ │ ├── tree-sitter-java-tags.scm │ │ ├── tree-sitter-c_sharp-tags.scm │ │ ├── tree-sitter-go-tags.scm │ │ ├── tree-sitter-typescript-tags.scm │ │ ├── tree-sitter-ruby-tags.scm │ │ ├── tree-sitter-rust-tags.scm │ │ └── tree-sitter-javascript-tags.scm │ ├── exceptions.py │ ├── templates │ │ ├── utils │ │ │ └── yaml_fixer_user.txt │ │ └── mutant_generation │ │ │ ├── mutator_system.txt │ │ │ └── mutator_user.txt │ ├── parsers.py │ ├── logger.py │ ├── utils.py │ ├── prompt_factory.py │ ├── runner.py │ ├── report.py │ ├── io.py │ ├── llm_mutation_engine.py │ ├── router.py │ ├── controller.py │ └── analyzer.py │ └── main.py ├── MANIFEST.in ├── examples └── java_maven │ ├── readme.md │ ├── pom.xml │ ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── BankAccount.java │ └── test │ │ └── java │ │ └── BankAccountTest.java │ └── jacoco.xml ├── .github ├── dependabot.yml └── workflows │ └── test.yaml ├── pyproject.toml ├── README.md ├── .gitignore ├── tests └── test_analyzer.py └── LICENSE /src/mutahunter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mutahunter/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mutahunter/core/entities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/mutahunter/core/queries/*.scm 2 | recursive-include src/mutahunter/core/templates *.txt -------------------------------------------------------------------------------- /examples/java_maven/readme.md: -------------------------------------------------------------------------------- 1 | ## How to run 2 | 3 | ```bash 4 | export OPENAI_API_KEY=your-key-goes-here 5 | mutahunter run --test-command "mvn test" --model "gpt-4.1-mini" --source-path "src/main/java/com/example/BankAccount.java" --test-path "src/test/java/BankAccountTest.java" 6 | ``` 7 | -------------------------------------------------------------------------------- /src/mutahunter/core/entities/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class MutationTestControllerConfig: 7 | source_path: str 8 | test_path: str 9 | model: str 10 | api_base: str 11 | test_command: str 12 | exclude_files: List[str] 13 | -------------------------------------------------------------------------------- /src/mutahunter/core/exceptions.py: -------------------------------------------------------------------------------- 1 | class MutantSurvivedError(Exception): 2 | pass 3 | 4 | 5 | class MutantKilledError(Exception): 6 | pass 7 | 8 | 9 | 10 | class MutationTestingError(Exception): 11 | pass 12 | 13 | 14 | class ReportGenerationError(Exception): 15 | pass 16 | 17 | 18 | class UnexpectedTestResultError(Exception): 19 | pass 20 | -------------------------------------------------------------------------------- /src/mutahunter/core/templates/utils/yaml_fixer_user.txt: -------------------------------------------------------------------------------- 1 | Based on the error message, the YAML content provided is not in the correct format. Please ensure the YAML content is in the correct format and try again. 2 | 3 | YAML content: 4 | ```yaml 5 | {{ content }} 6 | ``` 7 | 8 | Error: 9 | {{error}} 10 | 11 | Provide only the YAML output. Do not include any additional explanations or comments. 12 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-c-tags.scm: -------------------------------------------------------------------------------- 1 | (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class 2 | 3 | (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class 4 | 5 | (function_declarator declarator: (identifier) @name.definition.function) @definition.function 6 | 7 | (type_definition declarator: (type_identifier) @name.definition.type) @definition.type 8 | 9 | (enum_specifier name: (type_identifier) @name.definition.type) @definition.type 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.12" 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -e '.[testing]' 26 | 27 | - name: Run tests 28 | run: | 29 | pytest tests/ 30 | -------------------------------------------------------------------------------- /src/mutahunter/core/parsers.py: -------------------------------------------------------------------------------- 1 | import os 2 | PARSERS = { 3 | ".py": "python", 4 | ".js": "javascript", 5 | ".mjs": "javascript", 6 | ".go": "go", 7 | ".c": "c", 8 | ".cc": "cpp", 9 | ".cs": "c_sharp", 10 | ".cpp": "cpp", 11 | ".gomod": "gomod", 12 | ".java": "java", 13 | ".kt": "kotlin", 14 | ".php": "php", 15 | ".r": "r", 16 | ".R": "r", 17 | ".rb": "ruby", 18 | ".rs": "rust", 19 | ".tsx": "typescript", 20 | ".ts": "typescript", 21 | } 22 | 23 | def filename_to_lang(filename: str) -> str: 24 | basename = os.path.basename(filename) 25 | if basename in PARSERS: 26 | return PARSERS[basename] 27 | file_extension = os.path.splitext(filename)[1] 28 | if file_extension in PARSERS: 29 | return PARSERS[file_extension] 30 | return None 31 | 32 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-php-tags.scm: -------------------------------------------------------------------------------- 1 | (class_declaration 2 | name: (name) @name.definition.class) @definition.class 3 | 4 | (function_definition 5 | name: (name) @name.definition.function) @definition.function 6 | 7 | (method_declaration 8 | name: (name) @name.definition.function) @definition.function 9 | 10 | (object_creation_expression 11 | [ 12 | (qualified_name (name) @name.reference.class) 13 | (variable_name (name) @name.reference.class) 14 | ]) @reference.class 15 | 16 | (function_call_expression 17 | function: [ 18 | (qualified_name (name) @name.reference.call) 19 | (variable_name (name)) @name.reference.call 20 | ]) @reference.call 21 | 22 | (scoped_call_expression 23 | name: (name) @name.reference.call) @reference.call 24 | 25 | (member_call_expression 26 | name: (name) @name.reference.call) @reference.call 27 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-cpp-tags.scm: -------------------------------------------------------------------------------- 1 | (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class 2 | 3 | (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class 4 | 5 | (function_declarator declarator: (identifier) @name.definition.function) @definition.function 6 | 7 | (function_declarator declarator: (field_identifier) @name.definition.function) @definition.function 8 | 9 | (function_declarator declarator: (qualified_identifier scope: (namespace_identifier) @scope name: (identifier) @name.definition.method)) @definition.method 10 | 11 | (type_definition declarator: (type_identifier) @name.definition.type) @definition.type 12 | 13 | (enum_specifier name: (type_identifier) @name.definition.type) @definition.type 14 | 15 | (class_specifier name: (type_identifier) @name.definition.class) @definition.class 16 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-python-tags.scm: -------------------------------------------------------------------------------- 1 | (class_definition 2 | name: (identifier) @name.definition.class) @definition.class 3 | 4 | (function_definition 5 | name: (identifier) @name.definition.function) @definition.function 6 | 7 | (call 8 | function: [ 9 | (identifier) @name.reference.call 10 | (attribute 11 | attribute: (identifier) @name.reference.call) 12 | ]) @reference.call 13 | 14 | ;; For extreme mutation testing 15 | 16 | ;; Match if statements 17 | (if_statement 18 | condition: (_) @condition 19 | consequence: (block) @consequence 20 | alternative: (block)? @alternative) @if_statement 21 | 22 | ;; Match for loops 23 | (for_statement 24 | body: (block) @loop_body) @loop 25 | 26 | ;; Match while loops 27 | (while_statement 28 | body: (block) @loop_body) @loop 29 | 30 | ;; Match return statements 31 | (return_statement 32 | "return" 33 | (_)? @return_value) @return -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-java-tags.scm: -------------------------------------------------------------------------------- 1 | (class_declaration 2 | name: (identifier) @name.definition.class) @definition.class 3 | 4 | (method_declaration 5 | name: (identifier) @name.definition.method) @definition.method 6 | 7 | (method_invocation 8 | name: (identifier) @name.reference.call 9 | arguments: (argument_list) @reference.call) 10 | 11 | (interface_declaration 12 | name: (identifier) @name.definition.interface) @definition.interface 13 | 14 | (type_list 15 | (type_identifier) @name.reference.implementation) @reference.implementation 16 | 17 | (object_creation_expression 18 | type: (type_identifier) @name.reference.class) @reference.class 19 | 20 | (superclass (type_identifier) @name.reference.class) @reference.class 21 | 22 | ; Query to find method declarations annotated with @Test 23 | (method_declaration 24 | (modifiers 25 | (marker_annotation 26 | name: (identifier) @annotation.test 27 | (#eq? @annotation.test "Test")))) @test.method 28 | 29 | (method_declaration 30 | (modifiers 31 | (marker_annotation 32 | name: (identifier) @annotation.test 33 | (#eq? @annotation.test "ParameterizedTest")))) @test.method 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = 'mutahunter' 7 | description = "LLM Mutation Testing for any programming language" 8 | requires-python = ">= 3.11" 9 | version = "1.3.2" 10 | dependencies = [ 11 | "tree-sitter==0.21.3", 12 | 'tree_sitter_languages==1.10.2', 13 | "tqdm", 14 | 'jinja2', 15 | 'litellm' 16 | ] 17 | keywords = ["mutahunter", 'test', "testing", "LLM", 'mutant'] 18 | readme = "README.md" 19 | license = { file = "LICENSE" } 20 | authors = [{ name = "Steven Jung" }] 21 | maintainers = [{ name = "Steven Jung" }] 22 | 23 | [project.urls] 24 | Repository = "https://github.com/codeintegrity-ai/mutahunter" 25 | 26 | [project.optional-dependencies] 27 | dev = ['isort', 'black'] 28 | testing = ['pytest'] 29 | 30 | [project.scripts] 31 | mutahunter = "mutahunter.main:run" 32 | 33 | [tool.setuptools.package-data] 34 | mutahunter = [ 35 | 'src/mutahunter/core/queries/*.scm', 36 | 'src/mutahunter/core/templates/**/*.txt', 37 | ] 38 | 39 | [tool.isort] 40 | profile = "black" 41 | line_length = 88 42 | multi_line_output = 3 43 | include_trailing_comma = true 44 | force_grid_wrap = 0 45 | use_parentheses = true 46 | ensure_newline_before_comments = true 47 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-c_sharp-tags.scm: -------------------------------------------------------------------------------- 1 | (class_declaration 2 | name: (identifier) @name.definition.class 3 | ) @definition.class 4 | 5 | (class_declaration 6 | bases: (base_list (_) @name.reference.class) 7 | ) @reference.class 8 | 9 | (interface_declaration 10 | name: (identifier) @name.definition.interface 11 | ) @definition.interface 12 | 13 | (interface_declaration 14 | bases: (base_list (_) @name.reference.interface) 15 | ) @reference.interface 16 | 17 | (method_declaration 18 | name: (identifier) @name.definition.method 19 | ) @definition.method 20 | 21 | (object_creation_expression 22 | type: (identifier) @name.reference.class 23 | ) @reference.class 24 | 25 | (type_parameter_constraints_clause 26 | target: (identifier) @name.reference.class 27 | ) @reference.class 28 | 29 | (type_constraint 30 | type: (identifier) @name.reference.class 31 | ) @reference.class 32 | 33 | (variable_declaration 34 | type: (identifier) @name.reference.class 35 | ) @reference.class 36 | 37 | (invocation_expression 38 | function: 39 | (member_access_expression 40 | name: (identifier) @name.reference.send 41 | ) 42 | ) @reference.send 43 | 44 | (namespace_declaration 45 | name: (identifier) @name.definition.module 46 | ) @definition.module 47 | -------------------------------------------------------------------------------- /src/mutahunter/core/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import warnings 4 | 5 | # Suppress specific FutureWarnings from tree_sitter 6 | warnings.filterwarnings("ignore", category=FutureWarning, module="tree_sitter") 7 | 8 | 9 | def setup_logger(name: str) -> logging.Logger: 10 | 11 | os.makedirs("logs/_latest", exist_ok=True) 12 | os.makedirs("logs/_latest/llm", exist_ok=True) 13 | os.makedirs("logs/_latest/mutants", exist_ok=True) 14 | # Create a custom format for your logs 15 | log_format = "%(asctime)s %(levelname)s: %(message)s" 16 | 17 | # Create a log handler for file output 18 | file_handler = logging.FileHandler( 19 | filename=os.path.join("logs", "_latest", "debug.log"), 20 | mode="w", 21 | encoding="utf-8", 22 | ) 23 | stream_handler = logging.StreamHandler() 24 | 25 | # Apply the custom format to the handler 26 | formatter = logging.Formatter(log_format) 27 | file_handler.setFormatter(formatter) 28 | stream_handler.setFormatter(formatter) 29 | 30 | # Create a logger and add the handler 31 | logger = logging.getLogger(name) 32 | logger.addHandler(file_handler) 33 | logger.addHandler(stream_handler) 34 | logger.setLevel(logging.INFO) 35 | 36 | return logger 37 | 38 | 39 | logger = setup_logger("mutahunter") 40 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-go-tags.scm: -------------------------------------------------------------------------------- 1 | ( 2 | (comment)* @doc 3 | . 4 | (function_declaration 5 | name: (identifier) @name.definition.function) @definition.function 6 | (#strip! @doc "^//\\s*") 7 | (#set-adjacent! @doc @definition.function) 8 | ) 9 | 10 | ( 11 | (comment)* @doc 12 | . 13 | (method_declaration 14 | name: (field_identifier) @name.definition.method) @definition.method 15 | (#strip! @doc "^//\\s*") 16 | (#set-adjacent! @doc @definition.method) 17 | ) 18 | 19 | (call_expression 20 | function: [ 21 | (identifier) @name.reference.call 22 | (parenthesized_expression (identifier) @name.reference.call) 23 | (selector_expression field: (field_identifier) @name.reference.call) 24 | (parenthesized_expression (selector_expression field: (field_identifier) @name.reference.call)) 25 | ]) @reference.call 26 | 27 | (type_spec 28 | name: (type_identifier) @name.definition.type) @definition.type 29 | 30 | (type_identifier) @name.reference.type @reference.type 31 | 32 | ;; For extreme mutation testing 33 | 34 | ;; Match if statements 35 | (if_statement 36 | condition: (_expression) @condition 37 | consequence: (block) @consequence 38 | alternative: (block)? @alternative) @if_statement 39 | 40 | ;; Match for loops 41 | (for_statement 42 | body: (block) @loop_body) @loop 43 | 44 | ;; Match return statements 45 | (return_statement 46 | "return" 47 | (_)? @return_value) @return -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-typescript-tags.scm: -------------------------------------------------------------------------------- 1 | (function_signature 2 | name: (identifier) @name.definition.function) @definition.function 3 | 4 | (method_signature 5 | name: (property_identifier) @name.definition.method) @definition.method 6 | 7 | (abstract_method_signature 8 | name: (property_identifier) @name.definition.method) @definition.method 9 | 10 | (abstract_class_declaration 11 | name: (type_identifier) @name.definition.class) @definition.class 12 | 13 | (module 14 | name: (identifier) @name.definition.module) @definition.module 15 | 16 | (interface_declaration 17 | name: (type_identifier) @name.definition.interface) @definition.interface 18 | 19 | (type_annotation 20 | (type_identifier) @name.reference.type) @reference.type 21 | 22 | (new_expression 23 | constructor: (identifier) @name.reference.class) @reference.class 24 | 25 | (function_declaration 26 | name: (identifier) @name.definition.function) @definition.function 27 | 28 | (method_definition 29 | name: (property_identifier) @name.definition.method) @definition.method 30 | 31 | (class_declaration 32 | name: (type_identifier) @name.definition.class) @definition.class 33 | 34 | (interface_declaration 35 | name: (type_identifier) @name.definition.class) @definition.class 36 | 37 | (type_alias_declaration 38 | name: (type_identifier) @name.definition.type) @definition.type 39 | 40 | (enum_declaration 41 | name: (identifier) @name.definition.enum) @definition.enum 42 | -------------------------------------------------------------------------------- /src/mutahunter/core/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from mutahunter.core.logger import logger 5 | 6 | 7 | class FileUtils: 8 | @staticmethod 9 | def read_file(path: str) -> str: 10 | try: 11 | with open(path, "r") as file: 12 | return file.read() 13 | except FileNotFoundError: 14 | logger.info(f"File not found: {path}") 15 | except Exception as e: 16 | logger.info(f"Error reading file {path}: {e}") 17 | raise 18 | 19 | @staticmethod 20 | def number_lines(code: str) -> str: 21 | return "\n".join(f"{i + 1} {line}" for i, line in enumerate(code.splitlines())) 22 | 23 | @staticmethod 24 | def backup_code(file_path: str) -> None: 25 | backup_path = f"{file_path}.bak" 26 | try: 27 | shutil.copyfile(file_path, backup_path) 28 | except Exception as e: 29 | logger.info(f"Failed to create backup file for {file_path}: {e}") 30 | raise 31 | 32 | @staticmethod 33 | def revert(file_path: str) -> None: 34 | backup_path = f"{file_path}.bak" 35 | try: 36 | if os.path.exists(backup_path): 37 | shutil.copyfile(backup_path, file_path) 38 | else: 39 | logger.info(f"No backup file found for {file_path}") 40 | raise FileNotFoundError(f"No backup file found for {file_path}") 41 | except Exception as e: 42 | logger.info(f"Failed to revert file {file_path}: {e}") 43 | raise 44 | -------------------------------------------------------------------------------- /src/mutahunter/core/prompt_factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for generating prompts based on the programming language. 3 | """ 4 | 5 | from importlib import resources 6 | 7 | from jinja2 import Environment, FileSystemLoader 8 | 9 | 10 | class MutationTestingPromptFactory: 11 | """Factory class to generate prompts based on the programming language.""" 12 | 13 | @staticmethod 14 | def get_prompt(): 15 | return MutationTestingPrompt() 16 | 17 | 18 | class MutationTestingPrompt: 19 | """Base prompt class with system and user prompts.""" 20 | 21 | def __init__(self): 22 | env = Environment( 23 | loader=FileSystemLoader( 24 | resources.files(__package__).joinpath( 25 | "templates", 26 | ) 27 | ) 28 | ) 29 | self.mutator_system_prompt = env.get_template( 30 | "mutant_generation/mutator_system.txt" 31 | ) 32 | self.mutator_user_prompt = env.get_template( 33 | "mutant_generation/mutator_user.txt" 34 | ) 35 | 36 | 37 | class YAMLFixerPromptFactory: 38 | @staticmethod 39 | def get_prompt(): 40 | return YAMLFixerPrompt() 41 | 42 | 43 | class YAMLFixerPrompt: 44 | def __init__(self): 45 | env = Environment( 46 | loader=FileSystemLoader( 47 | resources.files(__package__).joinpath( 48 | "templates", 49 | ) 50 | ) 51 | ) 52 | self.yaml_fixer_user_prompt = env.get_template("utils/yaml_fixer_user.txt") 53 | -------------------------------------------------------------------------------- /src/mutahunter/core/templates/mutant_generation/mutator_system.txt: -------------------------------------------------------------------------------- 1 | You are an AI mutation testing agent. Your task: mutate {{ language }} code to test robustness. Use the provided Abstract Syntax Tree (AST) for context. Read the AST before mutating. 2 | 3 | Mutation Guidelines: 4 | 1. Logic Modification: 5 | - Alter conditions: e.g., 'if (a < b)' to 'if (a <= b)' 6 | - Change loop boundaries 7 | - Introduce calculation errors 8 | Avoid: Infinite loops, excessive logic changes 9 | 10 | 2. Output Alteration: 11 | - Change return types 12 | - Modify response structures 13 | - Return corrupted data 14 | 15 | 3. Method Call Changes: 16 | - Tamper with parameters 17 | - Replace or remove critical functions 18 | 19 | 4. Failure Simulation: 20 | - Inject exceptions 21 | - Simulate resource failures 22 | 23 | 5. Data Handling Errors: 24 | - Introduce parsing errors 25 | - Bypass data validation 26 | - Alter object states incorrectly 27 | 28 | 6. Boundary Testing: 29 | - Use out-of-bounds indices 30 | - Test with extreme parameter values 31 | 32 | 7. Concurrency Issues: 33 | - Create race conditions 34 | - Introduce potential deadlocks 35 | - Simulate timeouts 36 | 37 | 8. Security Vulnerabilities: 38 | - Replicate common CVE bugs (e.g., buffer overflow, SQL injection, XSS) 39 | - Introduce authentication bypasses 40 | 41 | Apply mutations strategically. Focus on subtle changes that test code resilience without breaking core functionality. Aim for realistic scenarios that could occur due to programming errors or edge cases. -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-ruby-tags.scm: -------------------------------------------------------------------------------- 1 | ; Method definitions 2 | 3 | ( 4 | (comment)* @doc 5 | . 6 | [ 7 | (method 8 | name: (_) @name.definition.method) @definition.method 9 | (singleton_method 10 | name: (_) @name.definition.method) @definition.method 11 | ] 12 | (#strip! @doc "^#\\s*") 13 | (#select-adjacent! @doc @definition.method) 14 | ) 15 | 16 | (alias 17 | name: (_) @name.definition.method) @definition.method 18 | 19 | (setter 20 | (identifier) @ignore) 21 | 22 | ; Class definitions 23 | 24 | ( 25 | (comment)* @doc 26 | . 27 | [ 28 | (class 29 | name: [ 30 | (constant) @name.definition.class 31 | (scope_resolution 32 | name: (_) @name.definition.class) 33 | ]) @definition.class 34 | (singleton_class 35 | value: [ 36 | (constant) @name.definition.class 37 | (scope_resolution 38 | name: (_) @name.definition.class) 39 | ]) @definition.class 40 | ] 41 | (#strip! @doc "^#\\s*") 42 | (#select-adjacent! @doc @definition.class) 43 | ) 44 | 45 | ; Module definitions 46 | 47 | ( 48 | (module 49 | name: [ 50 | (constant) @name.definition.module 51 | (scope_resolution 52 | name: (_) @name.definition.module) 53 | ]) @definition.module 54 | ) 55 | 56 | ; Calls 57 | 58 | (call method: (identifier) @name.reference.call) @reference.call 59 | 60 | ( 61 | [(identifier) (constant)] @name.reference.call @reference.call 62 | (#is-not? local) 63 | (#not-match? @name.reference.call "^(lambda|load|require|require_relative|__FILE__|__LINE__)$") 64 | ) 65 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-rust-tags.scm: -------------------------------------------------------------------------------- 1 | ; ADT definitions 2 | 3 | (struct_item 4 | name: (type_identifier) @name.definition.class) @definition.class 5 | 6 | (enum_item 7 | name: (type_identifier) @name.definition.class) @definition.class 8 | 9 | (union_item 10 | name: (type_identifier) @name.definition.class) @definition.class 11 | 12 | ; type aliases 13 | 14 | (type_item 15 | name: (type_identifier) @name.definition.class) @definition.class 16 | 17 | ; method definitions 18 | 19 | (declaration_list 20 | (function_item 21 | name: (identifier) @name.definition.method)) @definition.method 22 | 23 | ; function definitions 24 | 25 | (function_item 26 | name: (identifier) @name.definition.function) @definition.function 27 | 28 | ; trait definitions 29 | (trait_item 30 | name: (type_identifier) @name.definition.interface) @definition.interface 31 | 32 | ; module definitions 33 | (mod_item 34 | name: (identifier) @name.definition.module) @definition.module 35 | 36 | ; macro definitions 37 | 38 | (macro_definition 39 | name: (identifier) @name.definition.macro) @definition.macro 40 | 41 | ; references 42 | 43 | (call_expression 44 | function: (identifier) @name.reference.call) @reference.call 45 | 46 | (call_expression 47 | function: (field_expression 48 | field: (field_identifier) @name.reference.call)) @reference.call 49 | 50 | (macro_invocation 51 | macro: (identifier) @name.reference.call) @reference.call 52 | 53 | ; implementations 54 | 55 | (impl_item 56 | trait: (type_identifier) @name.reference.implementation) @reference.implementation 57 | 58 | (impl_item 59 | type: (type_identifier) @name.reference.implementation 60 | !trait) @reference.implementation 61 | -------------------------------------------------------------------------------- /examples/java_maven/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 17 13 | 17 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | org.junit.jupiter 22 | junit-jupiter 23 | 5.7.0 24 | test 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-surefire-plugin 33 | 2.22.2 34 | 35 | 36 | **/*Test.java 37 | 38 | 39 | 40 | 41 | org.jacoco 42 | jacoco-maven-plugin 43 | 0.8.7 44 | 45 | 46 | 47 | prepare-agent 48 | 49 | 50 | 51 | report 52 | test 53 | 54 | report 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/mutahunter/core/templates/mutant_generation/mutator_user.txt: -------------------------------------------------------------------------------- 1 | {% if ast %} 2 | ## Abstract Syntax Tree (AST) for context 3 | ```ast 4 | {{ast}} 5 | ``` 6 | {% endif %} 7 | 8 | ## Output Format 9 | Provide a YAML object matching the $Mutants schema: 10 | ```python 11 | class SingleMutant(BaseModel): 12 | function_name: str = Field(..., description="The name of the function where the mutation was applied.") 13 | type: str = Field(..., description="The type of the mutation operator used.") 14 | description: str = Field(..., description="A brief description detailing the mutation applied.") 15 | line_number: int = Field(..., description="Line number where the mutation was applied.") 16 | original_code: str = Field(..., description="The original line of code before mutation. Ensure proper formatting for YAML literal block scalar. 17 | mutated_code: Ones Field(..., description="The mutated line of code. Please annotate with a {{language}} syntax comment explaining the mutation. Ensure proper formatting for YAML literal block scalar.") 18 | 19 | class Mutants(BaseModel): 20 | source_file: str = Field(..., description="The name of the source file where mutations were applied.") 21 | mutants: List[SingleMutant] = Field(..., description="A list of SingleMutant instances each representing a specific mutation change.") 22 | ``` 23 | 24 | ## Source Code to Mutate: {{src_code_file}} 25 | ```{{language}} 26 | {{numbered_src_code}} 27 | ``` 28 | 29 | ## Task 30 | 1. Analyze the source code line by line. 31 | 2. Focus on function blocks and critical areas. 32 | 3. Ensure mutations provide insights into code quality. 33 | 4. Organize output by ascending line numbers. 34 | 5. Do not include manually added line numbers in your response. 35 | 6. Generate single-line mutations only. 36 | 37 | ## Example Output 38 | ```yaml 39 | source_file: {{src_code_file}} 40 | mutants: 41 | - function_name: 42 | type: 43 | description: 44 | line_number: 45 | original_code: | 46 | 47 | mutated_code: | 48 | 49 | ``` 50 | Produce mutants that challenge the robustness of the code without breaking core functionality. Provide only the YAML output. Do not include any additional explanations or comments. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Mutahunter

4 | 5 |

Open-Source Language Agnostic LLM-based Mutation Testing

6 | 7 |

8 | GitHub license 9 | Unit Tests 10 | Last Commit 11 |

12 |
13 | 14 | ## Getting Started with Mutation Testing 15 | 16 | ```bash 17 | # Install Mutahunter package via GitHub. Python 3.11+ is required. 18 | $ pip install https://github.com/codeintegrity-ai/mutahunter 19 | 20 | # Work with GPT-4o on your repo 21 | $ export OPENAI_API_KEY=your-key-goes-here 22 | 23 | # Run Mutahunter on a specific file. 24 | $ mutahunter run --test-command "mvn clean test" --model "gpt-4o-mini" --source-path "src/main/java/com/example/BankAccount.java" --test-path "src/test/java/BankAccountTest.java" 25 | 26 | 27 | 2025-03-05 18:56:42,528 INFO: 'mvn clean test' - '/Users/taikorind/Desktop/mutahunter/examples/java_maven/logs/_latest/mutants/34a5d8a5_BankAccount.java' 28 | 2025-03-05 18:56:44,935 INFO: 🛡️ Mutant survived 🛡️ 29 | 30 | 2025-03-05 18:56:44,936 INFO: 'mvn clean test' - '/Users/taikorind/Desktop/mutahunter/examples/java_maven/logs/_latest/mutants/183e6826_BankAccount.java' 31 | 2025-03-05 18:56:47,308 INFO: 🗡️ Mutant killed 🗡️ 32 | 33 | . . . .-. .-. . . . . . . .-. .-. .-. 34 | |\/| | | | |-| |-| | | |\| | |-| | 35 | ' ` `-' ' ` ` ' ' ` `-' ' ` `-' ' ' 36 | 37 | 2024-07-29 12:31:22,045 INFO: 38 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 39 | 40 | 📊 Overall Mutation Coverage 📊 41 | 🎯 Mutation Coverage: 57.14% 🎯 42 | 🦠 Total Mutants: 7 🦠 43 | 🛡️ Survived Mutants: 3 🛡️ 44 | 🗡️ Killed Mutants: 4 🗡️ 45 | 🕒 Timeout Mutants: 0 🕒 46 | 🔥 Compile Error Mutants: 1 🔥 47 | 💰 Total Cost: $0.00060 USD 💰 48 | 49 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 50 | 2025-03-05 18:56:54,689 INFO: Mutation Testing Ended. Took 29s 51 | ``` 52 | 53 | ### Examples 54 | 55 | Go to the examples directory to see how to run Mutahunter on different programming languages: 56 | 57 | Check [Java Example](/examples/java_maven/) to see some interesting LLM-based mutation testing examples. 58 | 59 | - [Java Example](/examples/java_maven/) 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/java_maven/src/main/java/com/example/BankAccount.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class BankAccount { 7 | 8 | private double balance; 9 | private double overdraftLimit; 10 | private List transactionHistory; 11 | 12 | public BankAccount(double initialBalance, double overdraftLimit) { 13 | if (initialBalance < 0) { 14 | throw new IllegalArgumentException("Initial balance must be non-negative"); 15 | } 16 | this.balance = initialBalance; 17 | this.overdraftLimit = overdraftLimit; 18 | this.transactionHistory = new ArrayList<>(); 19 | transactionHistory.add("Account created with balance: " + initialBalance); 20 | } 21 | 22 | public double getBalance() { 23 | return balance; 24 | } 25 | 26 | public List getTransactionHistory() { 27 | return transactionHistory; 28 | } 29 | 30 | public void deposit(double amount) { 31 | if (amount <= 0) { 32 | throw new IllegalArgumentException("Deposit amount must be positive"); 33 | } 34 | balance += amount; 35 | transactionHistory.add("Deposited: " + amount); 36 | } 37 | 38 | public void withdraw(double amount) { 39 | if (amount <= 0) { 40 | throw new IllegalArgumentException("Withdrawal amount must be positive"); 41 | } 42 | if (amount > balance + overdraftLimit) { 43 | throw new IllegalArgumentException("Insufficient funds, including overdraft limit"); 44 | } 45 | balance -= amount; 46 | transactionHistory.add("Withdrew: " + amount); 47 | } 48 | 49 | public void applyAnnualInterest(double interestRate) { 50 | if (interestRate <= 0) { 51 | throw new IllegalArgumentException("Interest rate must be positive"); 52 | } 53 | double interest = balance * (interestRate / 100); 54 | balance += interest; 55 | transactionHistory.add("Interest applied: " + interest); 56 | } 57 | 58 | public void executeBatchTransactions(double[] deposits, double[] withdrawals) { 59 | for (double deposit : deposits) { 60 | deposit(deposit); 61 | } 62 | for (double withdrawal : withdrawals) { 63 | withdraw(withdrawal); 64 | } 65 | transactionHistory.add("Batch transactions executed"); 66 | } 67 | 68 | public void scheduleTransaction(String type, double amount, int daysFromNow) { 69 | if (daysFromNow < 0) { 70 | throw new IllegalArgumentException("Days from now must be non-negative"); 71 | } 72 | // This is a simplification; in a real system, you would have a scheduler. 73 | // We'll just log the scheduled transaction for demonstration purposes. 74 | transactionHistory.add("Scheduled " + type + " of " + amount + " in " + daysFromNow + " days"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/mutahunter/core/runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | from shlex import split 5 | import platform 6 | 7 | 8 | class MutantTestRunner: 9 | def __init__(self, test_command: str) -> None: 10 | self.test_command = test_command 11 | 12 | def dry_run(self) -> None: 13 | """ 14 | Performs a dry run of the tests to ensure they pass before mutation testing. 15 | 16 | Raises: 17 | Exception: If any tests fail during the dry run. 18 | """ 19 | result = self._run_test_command(self.test_command) 20 | if result.returncode != 0: 21 | raise Exception( 22 | "Tests failed. Please ensure all tests pass before running mutation testing." 23 | ) 24 | 25 | def _run_test_command(self, test_command: str) -> subprocess.CompletedProcess: 26 | """ 27 | Runs a given test command in a subprocess. 28 | 29 | Args: 30 | test_command (str): The command to run. 31 | 32 | Returns: 33 | subprocess.CompletedProcess: The result of the command execution. 34 | """ 35 | # On Windows, we pass the command as is 36 | # On non-Windows, we can use shell=True but don't split the command 37 | return subprocess.run( 38 | test_command, 39 | cwd=os.getcwd(), 40 | shell=True 41 | ) 42 | 43 | def run_test(self, params: dict) -> subprocess.CompletedProcess: 44 | module_path = params["module_path"] 45 | replacement_module_path = params["replacement_module_path"] 46 | test_command = params["test_command"] 47 | backup_path = f"{module_path}.bak" 48 | try: 49 | self.replace_file(module_path, replacement_module_path, backup_path) 50 | result = subprocess.run( 51 | test_command, 52 | text=True, 53 | capture_output=True, 54 | cwd=os.getcwd(), 55 | timeout=30, 56 | shell=True, 57 | ) 58 | except subprocess.TimeoutExpired: 59 | # Mutant Killed 60 | result = subprocess.CompletedProcess( 61 | test_command, 2, stdout="", stderr="TimeoutExpired" 62 | ) 63 | except subprocess.CalledProcessError: 64 | # Handle any command execution errors 65 | result = subprocess.CompletedProcess( 66 | test_command, 1, stdout="", stderr="Command execution failed" 67 | ) 68 | finally: 69 | self.revert_file(module_path, backup_path) 70 | return result 71 | 72 | def replace_file(self, original: str, replacement: str, backup: str) -> None: 73 | """Backup original file and replace it with the replacement file.""" 74 | if not os.path.exists(backup): 75 | shutil.copy2(original, backup) 76 | shutil.copy2(replacement, original) 77 | 78 | def revert_file(self, original: str, backup: str) -> None: 79 | """Revert the file to the original using the backup.""" 80 | if os.path.exists(backup): 81 | shutil.copy2(backup, original) 82 | os.remove(backup) 83 | -------------------------------------------------------------------------------- /src/mutahunter/core/report.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for generating mutation testing reports. 3 | """ 4 | 5 | from typing import Any, Dict, List, Union 6 | 7 | from mutahunter.core.logger import logger 8 | 9 | MUTAHUNTER_ASCII = r""" 10 | . . . . .-. .-. . . . . . . .-. .-. .-. 11 | |\/| | | | |-| |-| | | |\| | |- |( 12 | ' ` `-' ' ` ' ' ` `-' ' ` ' `-' ' ' 13 | """ 14 | 15 | 16 | class MutantReport: 17 | """Class for generating mutation testing reports.""" 18 | 19 | def __init__(self) -> None: 20 | pass 21 | 22 | def generate_report( 23 | self, 24 | total_cost: float, 25 | mutation_coverage: float, 26 | killed_mutants: int, 27 | survived_mutants: int, 28 | compile_error_mutants: int, 29 | timeout_mutants: int, 30 | ) -> None: 31 | """ 32 | Generates a comprehensive mutation testing report. 33 | 34 | Args: 35 | total_cost (float): The total cost of mutation testing. 36 | mutation_coverage (float): The mutation coverage rate. 37 | killed_mutants (int): The number of killed mutants. 38 | survived_mutants (int): The number of survived mutants. 39 | compile_error_mutants (int): The number of compile error mutants. 40 | timeout_mutants (int): The number of timeout mutants. 41 | """ 42 | print(MUTAHUNTER_ASCII) 43 | summary_text = self._format_summary( 44 | mutation_coverage, 45 | killed_mutants, 46 | survived_mutants, 47 | compile_error_mutants, 48 | timeout_mutants, 49 | total_cost, 50 | ) 51 | print(summary_text) 52 | 53 | def _get_source_code(self, file_name: str) -> str: 54 | with open(file_name, "r") as f: 55 | return f.read() 56 | 57 | def _format_summary( 58 | self, 59 | mutation_coverage: float, 60 | killed_mutants: int, 61 | survived_mutants: int, 62 | compile_error_mutants: int, 63 | timeout_mutants: int, 64 | total_cost: float, 65 | ) -> str: 66 | """ 67 | Formats the summary data into a string. 68 | 69 | Args: 70 | data (Dict[str, Any]): Summary data including counts of different mutant statuses. 71 | total_cost (float): The total cost of mutation testing. 72 | line_rate (float): The line coverage rate. 73 | 74 | Returns: 75 | str: Formatted summary report. 76 | """ 77 | mutation_coverage = f"{mutation_coverage*100:.2f}%" 78 | details = [ 79 | f"\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n", 80 | "📊 Overall Mutation Coverage 📊", 81 | f"🎯 Mutation Coverage: {mutation_coverage} 🎯", 82 | f"🦠 Total Mutants: {survived_mutants + killed_mutants} 🦠", 83 | f"🛡️ Survived Mutants: {survived_mutants} 🛡️", 84 | f"🗡️ Killed Mutants: {killed_mutants} 🗡️", 85 | f"🕒 Timeout Mutants: {timeout_mutants} 🕒", 86 | f"🔥 Compile Error Mutants: {compile_error_mutants} 🔥", 87 | f"💰 Total Cost: ${total_cost:.5f} USD 💰", 88 | f"\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n", 89 | ] 90 | return "\n".join(details) 91 | -------------------------------------------------------------------------------- /src/mutahunter/core/queries/tree-sitter-javascript-tags.scm: -------------------------------------------------------------------------------- 1 | ( 2 | (comment)* @doc 3 | . 4 | (method_definition 5 | name: (property_identifier) @name.definition.method) @definition.method 6 | (#not-eq? @name.definition.method "constructor") 7 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 8 | (#select-adjacent! @doc @definition.method) 9 | ) 10 | 11 | ( 12 | (comment)* @doc 13 | . 14 | [ 15 | (class 16 | name: (_) @name.definition.class) 17 | (class_declaration 18 | name: (_) @name.definition.class) 19 | ] @definition.class 20 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 21 | (#select-adjacent! @doc @definition.class) 22 | ) 23 | 24 | ( 25 | (comment)* @doc 26 | . 27 | [ 28 | (function 29 | name: (identifier) @name.definition.function) 30 | (function_declaration 31 | name: (identifier) @name.definition.function) 32 | (generator_function 33 | name: (identifier) @name.definition.function) 34 | (generator_function_declaration 35 | name: (identifier) @name.definition.function) 36 | ] @definition.function 37 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 38 | (#select-adjacent! @doc @definition.function) 39 | ) 40 | 41 | ( 42 | (comment)* @doc 43 | . 44 | (lexical_declaration 45 | (variable_declarator 46 | name: (identifier) @name.definition.function 47 | value: [(arrow_function) (function)]) @definition.function) 48 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 49 | (#select-adjacent! @doc @definition.function) 50 | ) 51 | 52 | ( 53 | (comment)* @doc 54 | . 55 | (variable_declaration 56 | (variable_declarator 57 | name: (identifier) @name.definition.function 58 | value: [(arrow_function) (function)]) @definition.function) 59 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 60 | (#select-adjacent! @doc @definition.function) 61 | ) 62 | 63 | (assignment_expression 64 | left: [ 65 | (identifier) @name.definition.function 66 | (member_expression 67 | property: (property_identifier) @name.definition.function) 68 | ] 69 | right: [(arrow_function) (function)] 70 | ) @definition.function 71 | 72 | (pair 73 | key: (property_identifier) @name.definition.function 74 | value: [(arrow_function) (function)]) @definition.function 75 | 76 | ( 77 | (call_expression 78 | function: (identifier) @name.reference.call) @reference.call 79 | (#not-match? @name.reference.call "^(require)$") 80 | ) 81 | 82 | (call_expression 83 | function: (member_expression 84 | property: (property_identifier) @name.reference.call) 85 | arguments: (_) @reference.call) 86 | 87 | (new_expression 88 | constructor: (_) @name.reference.class) @reference.class 89 | 90 | 91 | ;; For Extreme Mutation Testing 92 | 93 | ;; Match if statements 94 | (if_statement 95 | condition: (_) @condition 96 | consequence: (statement_block) @consequence 97 | alternative: (statement_block)? @alternative) @if_statement 98 | 99 | ;; Match for loops 100 | (for_statement 101 | body: (statement_block) @loop_body) @loop 102 | 103 | ;; Match while loops 104 | (while_statement 105 | body: (statement_block) @loop_body) @loop 106 | 107 | ;; Match do-while loops 108 | (do_statement 109 | body: (statement_block) @loop_body) @loop 110 | 111 | ;; Match return 112 | (return_statement 113 | "return" 114 | (_)? @return_value) @return -------------------------------------------------------------------------------- /src/mutahunter/core/io.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict, List, Optional 3 | from uuid import uuid4 4 | 5 | from mutahunter.core.parsers import filename_to_lang 6 | from tree_sitter_languages import get_parser 7 | 8 | TEST_FILE_PATTERNS = [ 9 | "test_", 10 | "_test", 11 | ".test", 12 | ".spec", 13 | ".tests", 14 | ".Test", 15 | "tests/", 16 | "test/", 17 | ] 18 | 19 | 20 | class FileOperationHandler: 21 | @staticmethod 22 | def read_file(file_path: str) -> str: 23 | with open(file_path, "r") as f: 24 | return f.read() 25 | 26 | @staticmethod 27 | def write_file(file_path: str, content: str) -> None: 28 | with open(file_path, "w") as f: 29 | f.write(content) 30 | 31 | @staticmethod 32 | def get_mutant_path(source_file_path: str, mutant_id: str) -> str: 33 | mutant_file_name = f"{mutant_id}_{os.path.basename(source_file_path)}" 34 | return os.path.join(os.getcwd(), f"logs/_latest/mutants/{mutant_file_name}") 35 | 36 | @staticmethod 37 | def prepare_mutant_file( 38 | mutant_data: Dict[str, Any], source_file_path: str 39 | ) -> Optional[str]: 40 | mutant_id = str(uuid4())[:8] 41 | mutant_path = FileOperationHandler.get_mutant_path(source_file_path, mutant_id) 42 | source_code = FileOperationHandler.read_file(source_file_path) 43 | applied_mutant = FileOperationHandler.apply_mutation(source_code, mutant_data) 44 | if not FileOperationHandler.check_syntax(source_file_path, applied_mutant): 45 | raise SyntaxError("Mutant syntax is incorrect.") 46 | FileOperationHandler.write_file(mutant_path, applied_mutant) 47 | return mutant_path 48 | 49 | @staticmethod 50 | def should_skip_file( 51 | filename: str, exclude_files: List[str], only_mutate_file_paths: List[str] 52 | ) -> bool: 53 | if only_mutate_file_paths: 54 | for file_path in only_mutate_file_paths: 55 | if not os.path.exists(file_path): 56 | raise FileNotFoundError(f"File {file_path} does not exist.") 57 | return all(file_path != filename for file_path in only_mutate_file_paths) 58 | if filename in exclude_files: 59 | return True 60 | 61 | @staticmethod 62 | def check_syntax(source_file_path: str, source_code: str) -> bool: 63 | """ 64 | Checks the syntax of the provided source code. 65 | 66 | Args: 67 | source_code (str): The source code to check. 68 | 69 | Returns: 70 | bool: True if the syntax is correct, False otherwise. 71 | """ 72 | lang = filename_to_lang(source_file_path) 73 | parser = get_parser(lang) 74 | tree = parser.parse(bytes(source_code, "utf8")) 75 | return not tree.root_node.has_error 76 | 77 | @staticmethod 78 | def apply_mutation(source_code: str, mutant_data: Dict[str, Any]) -> str: 79 | src_code_lines = source_code.splitlines(keepends=True) 80 | mutated_line = mutant_data["mutated_code"].strip() 81 | line_number = mutant_data["line_number"] 82 | 83 | indentation = len(src_code_lines[line_number - 1]) - len( 84 | src_code_lines[line_number - 1].lstrip() 85 | ) 86 | src_code_lines[line_number - 1] = " " * indentation + mutated_line + "\n" 87 | 88 | return "".join(src_code_lines) 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | # For a library or package, you might want to ignore these files since the code is 86 | # intended to run in multiple environments; otherwise, check them in: 87 | # .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # poetry 97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 98 | # This is especially recommended for binary packages to ensure reproducibility, and is more 99 | # commonly ignored for libraries. 100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 101 | #poetry.lock 102 | 103 | # pdm 104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 105 | #pdm.lock 106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 107 | # in version control. 108 | # https://pdm.fming.dev/#use-with-ide 109 | .pdm.toml 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | 148 | # pytype static type analyzer 149 | .pytype/ 150 | 151 | # Cython debug symbols 152 | cython_debug/ 153 | 154 | # PyCharm 155 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 156 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 157 | # and can be added to the global gitignore or merged into this file. For a more nuclear 158 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 159 | #.idea/ 160 | .DS_Store 161 | 162 | logs 163 | vendor 164 | *.lcov 165 | dist* 166 | 167 | *.log 168 | logs/* 169 | *.db -------------------------------------------------------------------------------- /src/mutahunter/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from mutahunter.core.analyzer import Analyzer 5 | from mutahunter.core.controller import MutationTestController 6 | from mutahunter.core.entities.config import ( 7 | MutationTestControllerConfig, 8 | ) 9 | from mutahunter.core.io import FileOperationHandler 10 | from mutahunter.core.llm_mutation_engine import LLMMutationEngine 11 | from mutahunter.core.prompt_factory import ( 12 | MutationTestingPromptFactory, 13 | ) 14 | from mutahunter.core.report import MutantReport 15 | from mutahunter.core.router import LLMRouter 16 | from mutahunter.core.runner import MutantTestRunner 17 | 18 | 19 | def add_mutation_testing_subparser(subparsers): 20 | parser = subparsers.add_parser("run", help="Run the mutation testing process.") 21 | parser.add_argument( 22 | "--model", 23 | type=str, 24 | default="gpt-4o-mini", 25 | help="The LLM model to use for mutation generation. Default is 'gpt-4o-mini'.", 26 | ) 27 | parser.add_argument( 28 | "--source-path", 29 | type=str, 30 | default="", 31 | help="The path to the source code to mutate.", 32 | ) 33 | parser.add_argument( 34 | "--test-path", 35 | type=str, 36 | default="", 37 | help="The path to the test code to run.", 38 | ) 39 | parser.add_argument( 40 | "--api-base", 41 | type=str, 42 | default="", 43 | help="The base URL for the API if using a self-hosted LLM model.", 44 | ) 45 | parser.add_argument( 46 | "--test-command", 47 | type=str, 48 | default=None, 49 | required=True, 50 | help="The command to run the tests (e.g., 'pytest'). This argument is required.", 51 | ) 52 | parser.add_argument( 53 | "--exclude-files", 54 | type=str, 55 | nargs="+", 56 | default=[], 57 | required=False, 58 | help="A list of files to exclude from mutation testing. Optional.", 59 | ) 60 | 61 | 62 | def parse_arguments(): 63 | """ 64 | Parses command-line arguments for the Mutahunter CLI. 65 | 66 | Returns: 67 | argparse.Namespace: Parsed command-line arguments. 68 | """ 69 | parser = argparse.ArgumentParser( 70 | description="Mutahunter CLI for performing mutation testing." 71 | ) 72 | subparsers = parser.add_subparsers(title="commands", dest="command") 73 | add_mutation_testing_subparser(subparsers) 74 | 75 | return parser.parse_args() 76 | 77 | 78 | def create_run_mutation_testing_controller( 79 | args: argparse.Namespace, 80 | ) -> MutationTestController: 81 | config = MutationTestControllerConfig( 82 | model=args.model, 83 | api_base=args.api_base, 84 | test_command=args.test_command, 85 | exclude_files=args.exclude_files, 86 | source_path=args.source_path, 87 | test_path=args.test_path, 88 | ) 89 | 90 | analyzer = Analyzer() 91 | test_runner = MutantTestRunner(test_command=config.test_command) 92 | prompt = MutationTestingPromptFactory.get_prompt() 93 | router = LLMRouter(model=config.model, api_base=config.api_base) 94 | engine = LLMMutationEngine(model=config.model, router=router, prompt=prompt) 95 | mutant_report = MutantReport() 96 | file_handler = FileOperationHandler() 97 | 98 | return MutationTestController( 99 | config=config, 100 | analyzer=analyzer, 101 | test_runner=test_runner, 102 | router=router, 103 | engine=engine, 104 | mutant_report=mutant_report, 105 | file_handler=file_handler, 106 | prompt=prompt, 107 | ) 108 | 109 | 110 | def run(): 111 | args = parse_arguments() 112 | if args.command == "run": 113 | controller = create_run_mutation_testing_controller(args) 114 | controller.run() 115 | pass 116 | else: 117 | print("Invalid command.") 118 | sys.exit(1) 119 | 120 | 121 | if __name__ == "__main__": 122 | run() 123 | -------------------------------------------------------------------------------- /examples/java_maven/src/test/java/BankAccountTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test; 2 | 3 | import com.example.BankAccount; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class BankAccountTest { 8 | 9 | @Test 10 | void testInitialBalance() { 11 | BankAccount account = new BankAccount(1000, 500); 12 | assertEquals(1000, account.getBalance()); 13 | } 14 | 15 | @Test 16 | void testInitialBalanceNegative() { 17 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 18 | new BankAccount(-1000, 500); 19 | }); 20 | assertEquals("Initial balance must be non-negative", exception.getMessage()); 21 | } 22 | 23 | @Test 24 | void testDeposit() { 25 | BankAccount account = new BankAccount(1000, 500); 26 | account.deposit(500); 27 | assertEquals(1500, account.getBalance()); 28 | } 29 | 30 | @Test 31 | void testWithdraw() { 32 | BankAccount account = new BankAccount(1000, 500); 33 | account.withdraw(500); 34 | assertEquals(500, account.getBalance()); 35 | } 36 | 37 | @Test 38 | void testApplyAnnualInterest() { 39 | BankAccount account = new BankAccount(1000, 500); 40 | account.applyAnnualInterest(5); 41 | assertEquals(1050, account.getBalance()); 42 | } 43 | 44 | @Test 45 | void testExecuteBatchTransactions() { 46 | BankAccount account = new BankAccount(1000, 500); 47 | double[] deposits = { 100, 200 }; 48 | double[] withdrawals = { 50, 150 }; 49 | account.executeBatchTransactions(deposits, withdrawals); 50 | assertEquals(1100, account.getBalance()); 51 | } 52 | 53 | @Test 54 | void testScheduleTransaction() { 55 | BankAccount account = new BankAccount(1000, 500); 56 | account.scheduleTransaction("Deposit", 500, 5); 57 | assertTrue(account.getTransactionHistory().contains("Scheduled Deposit of 500.0 in 5 days")); 58 | } 59 | 60 | @Test 61 | void testScheduleTransactionNegativeDays() { 62 | BankAccount account = new BankAccount(1000, 500); 63 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 64 | account.scheduleTransaction("Deposit", 500, -5); 65 | }); 66 | assertEquals("Days from now must be non-negative", exception.getMessage()); 67 | } 68 | 69 | @Test 70 | void testDepositNegativeAmount() { 71 | BankAccount account = new BankAccount(1000, 500); 72 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 73 | account.deposit(-500); 74 | }); 75 | assertEquals("Deposit amount must be positive", exception.getMessage()); 76 | } 77 | 78 | @Test 79 | void testWithdrawNegativeAmount() { 80 | BankAccount account = new BankAccount(1000, 500); 81 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 82 | account.withdraw(-500); 83 | }); 84 | assertEquals("Withdrawal amount must be positive", exception.getMessage()); 85 | } 86 | 87 | @Test 88 | void testWithdrawExceedingBalance() { 89 | BankAccount account = new BankAccount(1000, 500); 90 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 91 | account.withdraw(1600); 92 | }); 93 | assertEquals("Insufficient funds, including overdraft limit", exception.getMessage()); 94 | } 95 | 96 | @Test 97 | void testApplyZeroInterestRate() { 98 | BankAccount account = new BankAccount(1000, 500); 99 | Exception exception = assertThrows(IllegalArgumentException.class, () -> { 100 | account.applyAnnualInterest(0); 101 | }); 102 | assertEquals("Interest rate must be positive", exception.getMessage()); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/mutahunter/core/llm_mutation_engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict, List, Optional 3 | 4 | import yaml 5 | from mutahunter.core.parsers import filename_to_lang 6 | from jinja2 import Template 7 | 8 | from mutahunter.core.logger import logger 9 | from mutahunter.core.prompt_factory import MutationTestingPrompt 10 | from mutahunter.core.router import LLMRouter 11 | 12 | SYSTEM_YAML_FIX = """ 13 | Based on the error message, the YAML content provided is not in the correct format. Please ensure the YAML content is in the correct format and try again. 14 | """ 15 | 16 | USER_YAML_FIX = """ 17 | YAML content: 18 | ```yaml 19 | {{yaml_content}} 20 | ``` 21 | 22 | Error: 23 | {{error}} 24 | 25 | Output must be wrapped in triple backticks and in YAML format: 26 | ```yaml 27 | ...fix the yaml content here... 28 | ``` 29 | """ 30 | 31 | 32 | class LLMMutationEngine: 33 | MAX_RETRIES = 2 34 | 35 | def __init__( 36 | self, 37 | model: str, 38 | router: LLMRouter, 39 | prompt: MutationTestingPrompt, 40 | ) -> None: 41 | self.model = model 42 | self.router = router 43 | self.prompt = prompt 44 | self.num = 0 45 | 46 | def get_source_code(self, source_file_path: str) -> str: 47 | with open(source_file_path, "r") as f: 48 | return f.read() 49 | 50 | def add_line_numbers(self, src_code: str) -> str: 51 | lines = src_code.split("\n") 52 | numbered_lines = [f"{i+1}: {line}" for i, line in enumerate(lines)] 53 | return "\n".join(numbered_lines) 54 | 55 | def generate_mutant( 56 | self, 57 | source_file_path: str, 58 | ) -> str: 59 | language = filename_to_lang(source_file_path) 60 | src_code = self.get_source_code(source_file_path) 61 | 62 | numbered_src_code = self.add_line_numbers(src_code) 63 | 64 | system_template = self.prompt.mutator_system_prompt.render( 65 | { 66 | "language": language, 67 | } 68 | ) 69 | user_template = self.prompt.mutator_user_prompt.render( 70 | { 71 | "language": language, 72 | "numbered_src_code": numbered_src_code, 73 | "maximum_num_of_mutants_per_function_block": 2, 74 | } 75 | ) 76 | prompt = {"system": system_template, "user": user_template} 77 | model_response, _, _ = self.router.generate_response( 78 | prompt=prompt, streaming=True 79 | ) 80 | return model_response 81 | 82 | def generate(self, source_file_path: str) -> Dict[str, Any]: 83 | response = self.generate_mutant(source_file_path) 84 | extracted_response = self.extract_response(response) 85 | self._save_yaml(extracted_response) 86 | return extracted_response 87 | 88 | def extract_response(self, response: str) -> Dict[str, Any]: 89 | for attempt in range(self.MAX_RETRIES): 90 | try: 91 | cleaned_response = self._clean_response(response) 92 | data = yaml.safe_load(cleaned_response) 93 | return data 94 | except Exception as e: 95 | logger.error(f"Error extracting YAML content: {e}") 96 | if attempt < self.MAX_RETRIES - 1: 97 | logger.info(f"Retrying to extract YAML with retry {attempt + 1}...") 98 | response = self.fix_format(e, response) 99 | else: 100 | logger.error( 101 | f"Error extracting YAML content after {self.MAX_RETRIES} attempts: {e}" 102 | ) 103 | return {"mutants": []} 104 | 105 | def fix_format(self, error: Exception, content: str) -> str: 106 | system_template = Template(SYSTEM_YAML_FIX).render() 107 | user_template = Template(USER_YAML_FIX).render( 108 | yaml_content=content, error=error 109 | ) 110 | prompt = {"system": system_template, "user": user_template} 111 | model_response, _, _ = self.router.generate_response( 112 | prompt=prompt, streaming=True 113 | ) 114 | return model_response 115 | 116 | 117 | def _clean_response(self, response: str) -> str: 118 | return response.strip().removeprefix("```yaml").rstrip("`") 119 | 120 | def _save_yaml(self, data: Dict[str, Any]) -> None: 121 | output = f"output_{self.num}.yaml" 122 | with open(os.path.join("logs/_latest/llm", output), "w") as f: 123 | yaml.dump(data, f, default_flow_style=False, indent=2) 124 | self.num += 1 125 | logger.info(f"YAML output saved to {output}") 126 | -------------------------------------------------------------------------------- /src/mutahunter/core/router.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import yaml 4 | from litellm import completion, litellm 5 | 6 | from mutahunter.core.prompt_factory import YAMLFixerPromptFactory 7 | 8 | 9 | class LLMRouter: 10 | def __init__(self, model: str, api_base: str = "") -> None: 11 | """ 12 | Initialize the LLMRouter with a model and optional API base URL. 13 | """ 14 | self.model = model 15 | self.api_base = api_base 16 | self.total_cost = 0 17 | litellm.success_callback = [self.track_cost_callback] 18 | self.yaml_prompt = YAMLFixerPromptFactory().get_prompt() 19 | 20 | def track_cost_callback( 21 | self, 22 | kwargs, # kwargs to completion 23 | completion_response, # response from completion 24 | start_time, 25 | end_time, # start/end time 26 | ): 27 | try: 28 | response_cost = kwargs.get("response_cost", 0) 29 | self.total_cost += response_cost 30 | except: 31 | pass 32 | 33 | def generate_response( 34 | self, prompt: dict, max_tokens: int = 4096, streaming: bool = False 35 | ) -> tuple: 36 | """ 37 | Call the LLM model with the provided prompt and return the generated response. 38 | 39 | Args: 40 | prompt (dict): A dictionary containing 'system' and 'user' keys. 41 | max_tokens (int): Maximum number of tokens for the response. 42 | streaming (bool): Flag to enable or disable streaming response. 43 | 44 | Returns: 45 | tuple: Generated response, prompt tokens used, and completion tokens used. 46 | """ 47 | self._validate_prompt(prompt) 48 | messages = self._build_messages(prompt) 49 | completion_params = self._build_completion_params( 50 | messages, max_tokens, streaming 51 | ) 52 | 53 | try: 54 | if streaming: 55 | response_chunks = self._stream_response(completion_params) 56 | return self._process_response(response_chunks, messages) 57 | else: 58 | return self._non_stream_response(completion_params) 59 | except Exception as e: 60 | print(f"Error during response generation: {e}") 61 | return "", 0, 0 62 | 63 | def _validate_prompt(self, prompt: dict) -> None: 64 | """ 65 | Validate that the prompt contains the required keys. 66 | """ 67 | if "system" not in prompt or "user" not in prompt: 68 | raise Exception( 69 | "The prompt dictionary must contain 'system' and 'user' keys." 70 | ) 71 | 72 | def _build_messages(self, prompt: dict) -> list: 73 | """ 74 | Build the messages list from the prompt. 75 | """ 76 | return [ 77 | {"role": "system", "content": prompt["system"]}, 78 | {"role": "user", "content": prompt["user"]}, 79 | ] 80 | 81 | def _build_completion_params( 82 | self, messages: list, max_tokens: int, streaming: bool 83 | ) -> dict: 84 | """ 85 | Build the parameters for the LLM completion call. 86 | """ 87 | completion_params = { 88 | "model": self.model, 89 | "messages": messages, 90 | "max_tokens": max_tokens, 91 | "stream": streaming, 92 | "temperature": 0.0, 93 | } 94 | if ( 95 | "ollama" in self.model 96 | or "huggingface" in self.model 97 | or self.model.startswith("openai/") 98 | ): 99 | completion_params["api_base"] = self.api_base 100 | return completion_params 101 | 102 | def _stream_response(self, completion_params: dict) -> list: 103 | """ 104 | Stream the response from the LLM model. 105 | """ 106 | response_chunks = [] 107 | print("\nStreaming results from LLM model...") 108 | response = completion(**completion_params) 109 | for chunk in response: 110 | print(chunk.choices[0].delta.content or "", end="", flush=True) 111 | response_chunks.append(chunk) 112 | time.sleep( 113 | 0.01 114 | ) # Optional: Delay to simulate more 'natural' response pacing 115 | print("\n") 116 | return response_chunks 117 | 118 | def _non_stream_response(self, completion_params: dict) -> tuple: 119 | """ 120 | Get the non-streamed response from the LLM model. 121 | """ 122 | response = completion(**completion_params) 123 | content = response["choices"][0]["message"]["content"] 124 | prompt_tokens = int(response["usage"]["prompt_tokens"]) 125 | completion_tokens = int(response["usage"]["completion_tokens"]) 126 | return content, prompt_tokens, completion_tokens 127 | 128 | def _process_response(self, response_chunks: list, messages: list) -> tuple: 129 | """ 130 | Process the streamed response chunks into a final response. 131 | """ 132 | model_response = litellm.stream_chunk_builder( 133 | response_chunks, messages=messages 134 | ) 135 | content = model_response["choices"][0]["message"]["content"] 136 | prompt_tokens = int(model_response["usage"]["prompt_tokens"]) 137 | completion_tokens = int(model_response["usage"]["completion_tokens"]) 138 | return content, prompt_tokens, completion_tokens 139 | 140 | def extract_yaml_from_response(self, response: str) -> dict: 141 | response = response.strip().removeprefix("```yaml").removesuffix("```") 142 | max_retries = 3 143 | 144 | for attempt in range(max_retries): 145 | try: 146 | return yaml.safe_load(response) 147 | except Exception as e: 148 | if attempt < max_retries - 1: 149 | print("attempting to fix yaml") 150 | response = self._attempt_yaml_fix(str(e), response) 151 | else: 152 | return [] 153 | 154 | def _attempt_yaml_fix(self, error, content): 155 | user_prompt = self.yaml_prompt.yaml_fixer_user_prompt.render( 156 | { 157 | "content": content, 158 | "error": error, 159 | } 160 | ) 161 | fix_response, _, _ = self.generate_response( 162 | prompt={"system": "", "user": user_prompt}, streaming=False 163 | ) 164 | return fix_response 165 | -------------------------------------------------------------------------------- /src/mutahunter/core/controller.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from subprocess import CompletedProcess 4 | from typing import Any, Dict, List 5 | 6 | from tqdm import tqdm 7 | 8 | from mutahunter.core.analyzer import Analyzer 9 | from mutahunter.core.entities.config import MutationTestControllerConfig 10 | from mutahunter.core.exceptions import ( 11 | MutantKilledError, 12 | MutantSurvivedError, 13 | MutationTestingError, 14 | ReportGenerationError, 15 | UnexpectedTestResultError, 16 | ) 17 | from mutahunter.core.io import FileOperationHandler 18 | from mutahunter.core.llm_mutation_engine import LLMMutationEngine 19 | from mutahunter.core.logger import logger 20 | from mutahunter.core.prompt_factory import MutationTestingPrompt 21 | from mutahunter.core.report import MutantReport 22 | from mutahunter.core.router import LLMRouter 23 | from mutahunter.core.runner import MutantTestRunner 24 | 25 | 26 | class MutationTestController: 27 | def __init__( 28 | self, 29 | config: MutationTestControllerConfig, 30 | analyzer: Analyzer, 31 | test_runner: MutantTestRunner, 32 | router: LLMRouter, 33 | engine: LLMMutationEngine, 34 | mutant_report: MutantReport, 35 | file_handler: FileOperationHandler, 36 | prompt: MutationTestingPrompt, 37 | ) -> None: 38 | self.config = config 39 | self.analyzer = analyzer 40 | self.test_runner = test_runner 41 | self.router = router 42 | self.engine = engine 43 | self.mutant_report = mutant_report 44 | self.file_handler = file_handler 45 | self.prompt = prompt 46 | 47 | # mutant details 48 | self.survived_mutants = 0 49 | self.killed_mutants = 0 50 | self.compile_error_mutants = 0 51 | self.timeout_mutants = 0 52 | 53 | def run(self) -> None: 54 | start = time.time() 55 | try: 56 | self.test_runner.dry_run() 57 | self.run_mutation_testing() 58 | except MutationTestingError as e: 59 | logger.error(f"Mutation testing failed: {str(e)}") 60 | try: 61 | # implement mutation coverage. killed / total mutants 62 | mutation_coverage = self.killed_mutants / ( 63 | self.survived_mutants + self.killed_mutants 64 | ) 65 | self.mutant_report.generate_report( 66 | total_cost=self.router.total_cost, 67 | mutation_coverage=mutation_coverage, 68 | killed_mutants=self.killed_mutants, 69 | survived_mutants=self.survived_mutants, 70 | compile_error_mutants=self.compile_error_mutants, 71 | timeout_mutants=self.timeout_mutants, 72 | ) 73 | except ReportGenerationError as e: 74 | logger.error(f"Report generation failed: {str(e)}") 75 | logger.info(f"Mutation Testing Ended. Took {round(time.time() - start)}s") 76 | 77 | 78 | 79 | def run_mutation_testing(self) -> None: 80 | mutations = self.engine.generate( 81 | source_file_path=self.config.source_path, 82 | )["mutants"] 83 | mutants = self.process_mutations(mutations) 84 | return mutants 85 | 86 | def process_mutations( 87 | self, mutations: List[Dict[str, Any]] 88 | ) -> List[Dict[str, Any]]: 89 | for mutant_data in mutations: 90 | mutant_data["source_path"] = self.config.source_path 91 | try: 92 | mutant_path = self.file_handler.prepare_mutant_file( 93 | mutant_data, self.config.source_path 94 | ) 95 | logger.debug(f"Mutant file prepared: {mutant_path}") 96 | mutant_data["mutant_path"] = mutant_path 97 | self.test_mutant( 98 | source_file_path=self.config.source_path, mutant_path=mutant_path 99 | ) 100 | except MutantSurvivedError as e: 101 | mutant_data["status"] = "SURVIVED" 102 | mutant_data["error_msg"] = str(e) 103 | self.survived_mutants += 1 104 | except MutantKilledError as e: 105 | mutant_data["status"] = "KILLED" 106 | mutant_data["error_msg"] = str(e) 107 | self.killed_mutants += 1 108 | except SyntaxError as e: 109 | logger.error(str(e)) 110 | mutant_data["status"] = "SYNTAX_ERROR" 111 | mutant_data["error_msg"] = str(e) 112 | self.compile_error_mutants += 1 113 | except UnexpectedTestResultError as e: 114 | logger.error(str(e)) 115 | mutant_data["status"] = "UNEXPECTED_TEST_ERROR" 116 | mutant_data["error_msg"] = str(e) 117 | self.unexpected_test_error_mutants += 1 118 | except Exception as e: 119 | logger.error(f"Unexpected error processing mutant: {str(e)}") 120 | mutant_data["status"] = "ERROR" 121 | mutant_data["error_msg"] = str(e) 122 | 123 | def test_mutant( 124 | self, 125 | source_file_path: str, 126 | mutant_path: str, 127 | ) -> None: 128 | 129 | params = { 130 | "module_path": source_file_path, 131 | "replacement_module_path": mutant_path, 132 | "test_command": self.config.test_command, 133 | } 134 | logger.info( 135 | f"'{params['test_command']}' - '{params['replacement_module_path']}'" 136 | ) 137 | result = self.test_runner.run_test(params) 138 | self.process_test_result(result) 139 | 140 | def process_test_result(self, result: CompletedProcess) -> None: 141 | if result.returncode == 0: 142 | logger.info(f"🛡️ Mutant survived 🛡️\n") 143 | logger.info(result.stdout) 144 | raise MutantSurvivedError("Mutant survived the tests") 145 | elif result.returncode == 1: 146 | logger.info(f"🗡️ Mutant killed 🗡️\n") 147 | logger.info(result.stdout) 148 | raise MutantKilledError("Mutant killed by the tests") 149 | else: 150 | error_output = result.stderr + result.stdout 151 | logger.info( 152 | f"⚠️ Unexpected test result (return code: {result.returncode}) ⚠️\n" 153 | ) 154 | raise UnexpectedTestResultError( 155 | f"Unexpected test result. Return code: {result.returncode}. Error output: {error_output}" 156 | ) 157 | -------------------------------------------------------------------------------- /tests/test_analyzer.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from unittest.mock import Mock, mock_open, patch 3 | 4 | import pytest 5 | 6 | from mutahunter.core.analyzer import Analyzer 7 | 8 | 9 | @pytest.fixture 10 | def cobertura_xml_content(): 11 | return """ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | """ 27 | 28 | 29 | @pytest.fixture 30 | def function_blocks(): 31 | return [ 32 | Mock(start_point=(4, 0), end_point=(8, 0)), # This block should be covered 33 | Mock( 34 | start_point=(9, 0), end_point=(14, 0) 35 | ), # This block should be partially covered 36 | Mock( 37 | start_point=(15, 0), end_point=(20, 0) 38 | ), # This block should not be covered 39 | ] 40 | 41 | 42 | @pytest.fixture 43 | def function_blocks_xml_content(): 44 | return """ 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | """ 60 | 61 | 62 | @patch("xml.etree.ElementTree.parse") 63 | @patch("builtins.open", new_callable=mock_open) 64 | @patch.object(Analyzer, "get_function_blocks") 65 | def test_get_covered_function_blocks( 66 | mock_get_function_blocks, 67 | mock_open, 68 | mock_parse, 69 | function_blocks, 70 | function_blocks_xml_content, 71 | ): 72 | source_file_path = "test_file.py" 73 | mock_file_content = "def test_func():\n pass\n" 74 | mock_open.return_value.read.return_value = mock_file_content 75 | mock_parse.return_value = ET.ElementTree(ET.fromstring(function_blocks_xml_content)) 76 | mock_get_function_blocks.return_value = function_blocks 77 | 78 | analyzer = Analyzer() 79 | covered_blocks, covered_block_executed_lines = analyzer.get_covered_function_blocks( 80 | executed_lines=[5, 6, 10], source_file_path=source_file_path 81 | ) 82 | 83 | # Assertions 84 | assert len(covered_blocks) == 2 85 | assert covered_block_executed_lines == [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]] 86 | 87 | 88 | @patch("xml.etree.ElementTree.parse") 89 | @patch("builtins.open", new_callable=mock_open) 90 | @patch.object(Analyzer, "get_method_blocks") 91 | def test_get_covered_method_blocks( 92 | mock_get_method_blocks, 93 | mock_open, 94 | mock_parse, 95 | function_blocks, 96 | function_blocks_xml_content, 97 | ): 98 | source_file_path = "test_file.py" 99 | mock_file_content = "def test_method():\n pass\n" 100 | mock_open.return_value.read.return_value = mock_file_content 101 | mock_parse.return_value = ET.ElementTree(ET.fromstring(function_blocks_xml_content)) 102 | mock_get_method_blocks.return_value = function_blocks 103 | 104 | analyzer = Analyzer() 105 | covered_blocks, covered_block_executed_lines = analyzer.get_covered_method_blocks( 106 | executed_lines=[5, 6, 10], source_file_path=source_file_path 107 | ) 108 | 109 | # Assertions 110 | assert len(covered_blocks) == 2 111 | assert covered_block_executed_lines == [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]] 112 | 113 | 114 | @patch("xml.etree.ElementTree.parse") 115 | @patch("builtins.open", new_callable=mock_open) 116 | @patch("mutahunter.core.analyzer.filename_to_lang", return_value="python") 117 | @patch("mutahunter.core.analyzer.get_parser") 118 | def test_check_syntax( 119 | mock_get_parser, 120 | mock_filename_to_lang, 121 | mock_open, 122 | mock_parse, 123 | cobertura_xml_content, 124 | ): 125 | source_code = "def foo():\n pass" 126 | source_file_path = "test_file.py" 127 | mock_open.return_value.read.return_value = source_code 128 | mock_parse.return_value = ET.ElementTree(ET.fromstring(cobertura_xml_content)) 129 | mock_parser = mock_get_parser.return_value 130 | mock_tree = Mock() 131 | mock_tree.root_node.has_error = False 132 | mock_parser.parse.return_value = mock_tree 133 | 134 | analyzer = Analyzer() 135 | result = analyzer.check_syntax(source_file_path, source_code) 136 | 137 | # Assertions 138 | assert result is True 139 | mock_filename_to_lang.assert_called_once_with(source_file_path) 140 | mock_parser.parse.assert_called_once_with(bytes(source_code, "utf8")) 141 | 142 | 143 | @patch("xml.etree.ElementTree.parse") 144 | @patch("builtins.open", new_callable=mock_open) 145 | def test_get_covered_blocks(mock_open, mock_parse, cobertura_xml_content): 146 | # Define the executed lines 147 | executed_lines = [5, 6, 10] 148 | 149 | # Mock blocks to be returned 150 | mock_blocks = [ 151 | Mock(start_point=(4, 0), end_point=(8, 0)), # This block should be covered 152 | Mock( 153 | start_point=(9, 0), end_point=(14, 0) 154 | ), # This block should be partially covered 155 | Mock( 156 | start_point=(15, 0), end_point=(20, 0) 157 | ), # This block should not be covered 158 | ] 159 | 160 | # Mock the file read operation for the coverage report 161 | mock_file_content = "def test_func():\n pass\n" 162 | mock_open.return_value.read.return_value = mock_file_content 163 | mock_parse.return_value = ET.ElementTree(ET.fromstring(cobertura_xml_content)) 164 | 165 | analyzer = Analyzer() 166 | covered_blocks, covered_block_executed_lines = analyzer._get_covered_blocks( 167 | mock_blocks, executed_lines 168 | ) 169 | 170 | # Assertions 171 | assert len(covered_blocks) == 2 172 | assert covered_block_executed_lines == [ 173 | [1, 2, 3, 4, 5], 174 | [1, 2, 3, 4, 5, 6], 175 | ] 176 | 177 | 178 | @patch("xml.etree.ElementTree.parse") 179 | @patch("builtins.open", new_callable=mock_open) 180 | def test_read_source_file(mock_open, mock_parse, cobertura_xml_content): 181 | source_file_path = "test_file.py" 182 | source_code = b"def foo():\n pass" 183 | mock_open.return_value.read.return_value = source_code 184 | mock_parse.return_value = ET.ElementTree(ET.fromstring(cobertura_xml_content)) 185 | 186 | analyzer = Analyzer() 187 | result = analyzer._read_source_file(source_file_path) 188 | mock_open.assert_called_once_with(source_file_path, "rb") 189 | 190 | # Assertions 191 | assert result == source_code 192 | 193 | 194 | @patch("xml.etree.ElementTree.parse") 195 | @patch("mutahunter.core.analyzer.Analyzer._find_blocks_nodes") 196 | def test_find_method_blocks_nodes( 197 | mock_find_blocks_nodes, mock_parse, cobertura_xml_content 198 | ): 199 | source_code = b"def foo():\n pass" 200 | source_file_path = "test_file.py" 201 | mock_parse.return_value = ET.ElementTree(ET.fromstring(cobertura_xml_content)) 202 | 203 | analyzer = Analyzer() 204 | analyzer.find_method_blocks_nodes(source_file_path, source_code) 205 | mock_find_blocks_nodes.assert_called_once_with( 206 | source_file_path, source_code, ["if_statement", "loop", "return"] 207 | ) 208 | -------------------------------------------------------------------------------- /examples/java_maven/jacoco.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mutahunter/core/analyzer.py: -------------------------------------------------------------------------------- 1 | from importlib import resources 2 | from typing import Any, Dict, List 3 | 4 | from mutahunter.core.parsers import filename_to_lang 5 | from tree_sitter_languages import get_language, get_parser 6 | 7 | from mutahunter.core.logger import logger 8 | 9 | 10 | class Analyzer: 11 | def __init__(self) -> None: 12 | pass 13 | 14 | def get_language_by_filename(self, filename: str) -> str: 15 | """ 16 | Gets the language identifier based on the filename. 17 | 18 | Args: 19 | filename (str): The name of the file. 20 | 21 | Returns: 22 | str: The language identifier. 23 | """ 24 | return filename_to_lang(filename) 25 | 26 | def get_covered_function_blocks( 27 | self, executed_lines: List[int], source_file_path: str 28 | ) -> List[Any]: 29 | """ 30 | Retrieves covered function blocks based on executed lines and source_file_path. 31 | 32 | Args: 33 | executed_lines (List[int]): List of executed line numbers. 34 | source_file_path (str): The name of the file being analyzed. 35 | 36 | Returns: 37 | List[Any]: A list of covered function blocks. 38 | """ 39 | function_blocks = self.get_function_blocks(source_file_path=source_file_path) 40 | return self._get_covered_blocks(function_blocks, executed_lines) 41 | 42 | def get_covered_method_blocks( 43 | self, executed_lines: List[int], source_file_path: str 44 | ) -> List[Any]: 45 | """ 46 | Retrieves covered method blocks based on executed lines and source_file_path. 47 | 48 | Args: 49 | executed_lines (List[int]): List of executed line numbers. 50 | source_file_path (str): The name of the file being analyzed. 51 | 52 | Returns: 53 | List[Any]: A list of covered method blocks. 54 | """ 55 | method_blocks = self.get_method_blocks(source_file_path=source_file_path) 56 | return self._get_covered_blocks(method_blocks, executed_lines) 57 | 58 | def _get_covered_blocks( 59 | self, blocks: List[Any], executed_lines: List[int] 60 | ) -> List[Any]: 61 | """ 62 | Retrieves covered blocks based on executed lines. 63 | 64 | Args: 65 | blocks (List[Any]): List of blocks (function or method). 66 | executed_lines (List[int]): List of executed line numbers. 67 | 68 | Returns: 69 | List[Any]: A list of covered blocks. 70 | """ 71 | covered_blocks = [] 72 | covered_block_executed_lines = [] 73 | 74 | for block in blocks: 75 | # 0 baseed index 76 | start_point = block.start_point 77 | end_point = block.end_point 78 | 79 | start_line = start_point[0] + 1 80 | end_line = end_point[0] + 1 81 | 82 | if any(line in executed_lines for line in range(start_line, end_line + 1)): 83 | block_executed_lines = [ 84 | line - start_line + 1 for line in range(start_line, end_line + 1) 85 | ] 86 | covered_blocks.append(block) 87 | covered_block_executed_lines.append(block_executed_lines) 88 | 89 | return covered_blocks, covered_block_executed_lines 90 | 91 | def get_method_blocks(self, source_file_path: str) -> List[Any]: 92 | """ 93 | Retrieves method blocks from a given file. 94 | 95 | Args: 96 | source_file_path (str): The name of the file being analyzed. 97 | 98 | Returns: 99 | List[Any]: A list of method block nodes. 100 | """ 101 | source_code = self._read_source_file(source_file_path) 102 | return self.find_method_blocks_nodes(source_file_path, source_code) 103 | 104 | def get_function_blocks(self, source_file_path: str) -> List[Any]: 105 | """ 106 | Retrieves function blocks from a given file. 107 | 108 | Args: 109 | source_file_path (str): The name of the file being analyzed. 110 | 111 | Returns: 112 | List[Any]: A list of function block nodes. 113 | """ 114 | source_code = self._read_source_file(source_file_path) 115 | return self.find_function_blocks_nodes(source_file_path, source_code) 116 | 117 | def _read_source_file(self, file_path: str) -> bytes: 118 | """ 119 | Reads the source code from a file. 120 | 121 | Args: 122 | file_path (str): The path to the source file. 123 | 124 | Returns: 125 | bytes: The source code. 126 | """ 127 | with open(file_path, "rb") as f: 128 | return f.read() 129 | 130 | def check_syntax(self, source_file_path: str, source_code: str) -> bool: 131 | """ 132 | Checks the syntax of the provided source code. 133 | 134 | Args: 135 | source_code (str): The source code to check. 136 | 137 | Returns: 138 | bool: True if the syntax is correct, False otherwise. 139 | """ 140 | lang = filename_to_lang(source_file_path) 141 | parser = get_parser(lang) 142 | tree = parser.parse(bytes(source_code, "utf8")) 143 | return not tree.root_node.has_error 144 | 145 | def find_method_blocks_nodes( 146 | self, source_file_path: str, source_code: bytes 147 | ) -> List[Any]: 148 | """ 149 | Finds method block nodes in the provided source code. 150 | 151 | Args: 152 | source_code (bytes): The source code to analyze. 153 | 154 | Returns: 155 | List[Any]: A list of method block nodes. 156 | """ 157 | return self._find_blocks_nodes( 158 | source_file_path, source_code, ["if_statement", "loop", "return"] 159 | ) 160 | 161 | def find_function_blocks_nodes( 162 | self, source_file_path: str, source_code: bytes 163 | ) -> List[Any]: 164 | """ 165 | Finds function block nodes in the provided source code. 166 | 167 | Args: 168 | source_code (bytes): The source code to analyze. 169 | 170 | Returns: 171 | List[Any]: A list of function block nodes. 172 | """ 173 | return self._find_blocks_nodes( 174 | source_file_path, source_code, ["definition.function", "definition.method"] 175 | ) 176 | 177 | def _find_blocks_nodes( 178 | self, source_file_path: str, source_code: bytes, tags: List[str] 179 | ) -> List[Any]: 180 | """ 181 | Finds block nodes (method or function) in the provided source code. 182 | 183 | Args: 184 | source_code (bytes): The source code to analyze. 185 | tags (List[str]): List of tags to identify blocks. 186 | 187 | Returns: 188 | List[Any]: A list of block nodes. 189 | """ 190 | lang = filename_to_lang(source_file_path) 191 | if lang is None: 192 | raise ValueError(f"Language not supported for file: {source_file_path}") 193 | parser = get_parser(lang) 194 | language = get_language(lang) 195 | 196 | tree = parser.parse(source_code) 197 | 198 | # def traverse_tree(node, depth=0): 199 | # print(" " * depth + f"{node.type}: {node.text.decode('utf8')}") 200 | # for child in node.children: 201 | # traverse_tree(child, depth + 1) 202 | 203 | # traverse_tree(tree.root_node) 204 | 205 | query_scm = self._load_query_scm(lang) 206 | if not query_scm: 207 | return [] 208 | 209 | query = language.query(query_scm) 210 | captures = query.captures(tree.root_node) 211 | # for node, tag in captures: 212 | # print(node, tag) 213 | # print code 214 | # print(source_code[node.start_byte : node.end_byte].decode("utf8")) 215 | 216 | if not captures: 217 | logger.error("Tree-sitter query failed to find any captures.") 218 | return [] 219 | return [node for node, tag in captures if tag in tags] 220 | 221 | def _load_query_scm(self, lang: str) -> str: 222 | """ 223 | Loads the query SCM file content. 224 | 225 | Args: 226 | lang (str): The language identifier. 227 | 228 | Returns: 229 | str: The content of the query SCM file. 230 | """ 231 | try: 232 | scm_fname = resources.files(__package__).joinpath( 233 | "queries", f"tree-sitter-{lang}-tags.scm" 234 | ) 235 | except KeyError: 236 | return "" 237 | if not scm_fname.exists(): 238 | return "" 239 | return scm_fname.read_text() 240 | 241 | def find_function_block_by_name( 242 | self, source_file_path: str, method_name: str 243 | ) -> List[Any]: 244 | """ 245 | Finds a function block by its name and returns the start and end lines of the function. 246 | 247 | Args: 248 | source_file_path (str): The path to the source file. 249 | method_name (str): The name of the method to find. 250 | 251 | Returns: 252 | Dict[str, int]: A dictionary with 'start_line' and 'end_line' as keys and their corresponding line numbers as values. 253 | """ 254 | source_code = self._read_source_file(source_file_path) 255 | lang = filename_to_lang(source_file_path) 256 | if lang is None: 257 | raise ValueError(f"Language not supported for file: {source_file_path}") 258 | 259 | parser = get_parser(lang) 260 | language = get_language(lang) 261 | tree = parser.parse(source_code) 262 | 263 | query_scm = self._load_query_scm(lang) 264 | if not query_scm: 265 | raise ValueError( 266 | "Failed to load query SCM file for the specified language." 267 | ) 268 | 269 | query = language.query(query_scm) 270 | captures = query.captures(tree.root_node) 271 | 272 | result = [] 273 | 274 | for node, tag in captures: 275 | if tag == "definition.function" or tag == "definition.method": 276 | if self._is_function_name(node, method_name, source_code): 277 | return node 278 | raise ValueError(f"Function {method_name} not found in file {source_file_path}") 279 | 280 | def _is_function_name(self, node, method_name: str, source_code: bytes) -> bool: 281 | """ 282 | Checks if the given node corresponds to the method_name. 283 | 284 | Args: 285 | node (Node): The AST node to check. 286 | method_name (str): The method name to find. 287 | source_code (bytes): The source code. 288 | 289 | Returns: 290 | bool: True if the node corresponds to the method_name, False otherwise. 291 | """ 292 | node_text = source_code[node.start_byte : node.end_byte].decode("utf8") 293 | return method_name in node_text 294 | 295 | def get_import_nodes(self, source_file_path: str) -> List[Any]: 296 | """ 297 | Retrieves import nodes from a given file. 298 | 299 | Args: 300 | source_file_path (str): The name of the file being analyzed. 301 | 302 | Returns: 303 | List[Any]: A list of import nodes. 304 | """ 305 | source_code = self._read_source_file(source_file_path) 306 | return self._find_blocks_nodes(source_file_path, source_code, ["import"]) 307 | 308 | def get_test_nodes(self, source_file_path: str) -> List[Any]: 309 | """ 310 | Retrieves test nodes from a given file. 311 | 312 | Args: 313 | source_file_path (str): The name of the file being analyzed. 314 | 315 | Returns: 316 | List[Any]: A list of test nodes. 317 | """ 318 | source_code = self._read_source_file(source_file_path) 319 | return self._find_blocks_nodes(source_file_path, source_code, ["test.method"]) 320 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------