Skip to main content

Tips on Writing Readable Code

Code readability may be defined as the convenience with which the source code is understood and modified. Code readability is of vital importance when maintaining larger legacy codebase and working in teams. If the code is easy to read, it would be easy to debug, refactor, enhance and maintain, thereby reducing the lifecycle cost of software product. As readability directly impacts software quality, researchers have proposed adding a separate phase during development focused on improving code readability. Although, code readability is subjective to human judgement, static analysis tools can provide a fair estimate on various factors impacting readability such as indenting, naming and commenting. As a programmer, follow the tips provided below to create self-documenting software program, which can be understood conveniently by other programmers during it's lifetime.

Automated Software Fault Localization Tools and Techniques

Automated Software Debugging Techniques

Software Debugging is the process of identifying and fixing errors / bugs in a faulty program discovered during testing or reported from production. Bugs may be logical errors, runtime errors or syntax errors which can lead to software crashes or incorrect outputs. The process of debugging involves the following steps: 1. reproduce the failure, 2. locate the bug, 3. identify the root cause, 4. fix the bug, 5. test the fix, and 6. document the process. Software Debugging is extremely time consuming and very expensive, yet essential to improve the overall stability, reliability and performance of a software product. The effectiveness of software debugging depends on developers’ understanding of the program being debugged and how suspicious code containing the bug is identified.

During software debugging, fault localization is the process of identifying exact locations of program faults, which is very expensive and time consuming process. Since fault localization is highly tedious, automated tools are essential to help developers locate programming errors efficiently. Moreover, with the rapid growth and rising complexity of software applications, demand for advanced fault localization tools has increased significantly. Software companies have been investing heavily in these tools, and researchers have been developing innovative solutions to enhance fault localization capabilities, including AI based tools for specialized applications.

One traditional way to locate bugs in a software program is to insert 'print' statements at critical places of suspicious code to print the state of variables. A better way is to use debugging tools integrated within IDEs and set breakpoints at various execution points in a software program and examine the state of variables at each breakpoint. The disadvantage of using both traditional approaches is that it requires a lot of effort from the developer to understand the software program and examine manually along the complete execution path.

Advanced fault localization techniques / tools can reduce the developer's burden by localizing the suspicious code based on the likelihood of containing bugs. Various tools and techniques have been developed to automate and improve the accuracy of fault localization. Here are some of the popular tools and techniques:

1. SPECTRUM-BASED FAULT LOCALIZATION (SBFL)

Spectrum-Based Fault Localization is a technique used to identify faulty code by analyzing the execution patterns of test cases. The idea is to gather information about which parts of the code are executed during both passing and failing test cases, and then calculate a "suspiciousness" score for each code element (e.g., lines, methods) based on the correlation between execution and failure. Some popular SBFL techniques include Tarantula, Ochiai, Zoltar, and DStar.

Example

Let’s assume we have a simple Python program that calculates the sum of numbers in a list but contains a bug:

def calculate_sum(numbers):
    total = 0
    for number in numbers:
        if number >= 0: # Bug: This should not be here
        total += number
    return total

There are following test cases to verify the code above:

def test_calculate_sum():
    assert calculate_sum([1, 2, 3]) == 6  # Pass
    assert calculate_sum([-1, -2, -3]) == -6  # Fail
    assert calculate_sum([0, 0, 0]) == 0  # Pass
    assert calculate_sum([1, -1, 1]) == 1  # Fail

To apply SBFL, in the 1st step, data is collected regarding which lines of code are executed by each test case and whether the test case passes or fails. In the 2nd step, some SBFL metric is calculated, for example Tarantula metric is calculated for code shared above, which calculates the suspiciousness of a line of code as:

Suspiciousness = [failed (𝑒) / total_failed] / [failed (𝑒) / total_failed + passed (𝑒) / total_passed]

Where:

  • failed (𝑒) is the number of failing test cases that executed line 𝑒.
  • passed (𝑒) is the number of passing test cases that executed line e.
  • total_failed is the total number of failing test cases.
  • total_passed is the total number of passing test cases.
Based on the Tarantula metric formula shared above, the suspiciousness score of the example code for each line of code is calculated as under:

    Line 2: Suspiciousness Score = 0.33
    Line 3: Suspiciousness Score = 0.33
    Line 4: Suspiciousness Score = 1.00
    Line 6: Suspiciousness Score = 0.33

Lines with higher scores are more likely to contain the fault. In our example, the bug is on line 4 (if number >= 0:), hence this line has a high suspiciousness score. This example demonstrates the basic principle of Spectrum-Based Fault Localization (SBFL). In real-world scenarios, SBFL tools would handle the collection of execution data and calculation of suspiciousness scores more efficiently, often integrating with automated testing frameworks and providing visualizations to help developers quickly identify faulty code.

Example Tool

One popular tool based on SBFL techniques include GZoltar. It is a tool that provides visualizations to help developers to see which parts of the code are most likely responsible for a failure. GZoltar is an on-going project, currently provided as a Command Line Interface, Ant Task, Maven Plug-in, VS Code Extension, Eclipse Plugin, and JUnit tests.

2. STATISTICAL DEBUGGING

Statistical debugging is a technique that identifies likely fault locations in code by analyzing statistical correlations between program behaviors (e.g., predicates or branches taken) and test case outcomes (pass / fail). The goal is to determine which program behaviors are strongly associated with failures, suggesting those parts of the code which may contain faults.

Example

Let's consider a simple program that calculates the factorial of a number but contains a bug:

 def factorial(n):
    if n == 0:
        return 1
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

Here are some test cases for the factorial function:

def test_factorial():
   assert factorial(0) == 1  # Pass
   assert factorial(1) == 1  # Pass
   assert factorial(2) == 2  # Pass
   assert factorial(3) == 6  # Pass
   assert factorial(5) == 120  # Pass
   assert factorial(-1) == "Error"  # Fail

To perform statistical debugging, certain predicates (boolean expressions) are tracked during execution to calculate how often each predicate is true during both passing and failing test cases. The suspiciousness score indicates how likely a predicate is associated with failures. Predicates with higher scores are more likely to be related to the bug.

    Predicate 'n < 0': Suspiciousness Score = 1.00
    Predicate 'result > 0': Suspiciousness Score = 0.50
    Predicate 'n == 0': Suspiciousness Score = 0.00
    Predicate 'n > 0': Suspiciousness Score = 0.00

In this case, the predicate n < 0 has the highest suspiciousness score, suggesting that the program doesn't handle negative inputs correctly, this is the root cause of the failure. Statistical debugging helps highlight the association between program behaviors and failures, making it easier to identify the source of bugs. This simple example illustrates the core idea of statistical debugging: using predicate behavior analysis to find correlations with test failures, thereby guiding developers to potential fault locations in the code.

Example Tool

One fault localization tool that uses statistical debugging is BugEx, which focuses on identifying suspicious predicates and control-flow anomalies.

3. DELTA DEBUGGING

Delta Debugging is technique that isolates minimal difference between a working and a failing version of code or input by systematically testing variations and reducing the scope of the change. It may be integrated with a Version Control System such as Git, and used to identify the specific code changes or inputs that trigger a bug. It's an automated approach that narrows down the cause of a failure by systematically testing variations of the code.

Example

Let's consider a simple function that calculates the sum of a list of numbers, and several recent modifications have been made in the code. The function now fails for some test cases, and the programmer may want to isolate which change introduced the bug.

Below is the initial code before any subsequent changes were made.

def calculate_sum(numbers):
    total = 0
    for number in numbers:
        total += number
    return total

Below is the final code after several changes with a bug induced. The 1st change is done to handle empty input, 2nd change to skip negative numbers (bug introduced here) and 3rd change for logging (not related to the bug).

def calculate_sum(numbers):

    total = 0
    for number in numbers:
        # Change 1: Handle empty input
        if not numbers:
            return 0
        # Change 2: Skip negative numbers (bug here)
        if number < 0:
            continue
        # Change 3: Add logging (not related to the bug)
        print(f"Adding {number} to total.")
        
        total += number
    return total

The automated Delta Debugging tool identifies which of the recent modifications introduced the bug. It starts by testing the complete set of changes, then removes changes one by one to see if the failure persists. The goal is to identify the minimal set of changes that still causes the test to fail. In the example above, the tool will output that the minimal set of changes causing the bug is 2nd change (skipping negative numbers). This technique is particularly useful when dealing with a large codebase with many recent changes, as it can systematically narrow down problematic modification, saving time and effort in debugging.

Example Tool

There are plenty of tools based on Delta Debugging technique such as delta, DD.py, C-Reduce, DustMite, Igor and various Eclipse plug-ins.

4. PROGRAM SLICING

Program slicing involves extracting the relevant parts of the code that affect a particular computation or variable. Forward slicing traces all statements that could be influenced by a particular point in the code, while backward slicing traces the statements that could influence a particular point. Program Slicing techniques helps developers focus on the relevant parts of the code that might be responsible for a bug.

Example

Let's consider a scenario where a function is supposed to perform some arithmetic operations, but due to a bug, it ends up dividing by zero. 

def compute_ratio(a, b, c):
    total = a + b + c
    if total > 10:
        ratio = total / (b - c)  # Potential divide by zero error here
    else:
        ratio = total / (a + 1)
    return ratio

def main():
    result = compute_ratio(5, 10, 10)  # Gives divide by zero error
    print(f"The result is: {result}")

The automated Program Slicing based fault localization tool will isolate the part of the code that is responsible for the ZeroDivisionError. The slicing criterion is the division operation total / (b - c) in the compute_ratio function. After tracing dependencies and slicing code, the tool will find that the error occurs because of the expression b - c being zero, leading to a division by zero.

The program slice isolates the code responsible for the divide by zero error, specifically the line ratio = total / (b - c). This technique helps in understanding the cause of the error by focusing only on the relevant parts of the code. In real-world scenarios, where codebases are much larger, program slicing can be a powerful tool to localize faults effectively.

Example Tool

Some tools based on Program Slicing technique include CodeSurfer (for C++) and Indus (for Java).

5. MUTATION-BASED FAULT LOCALIZATION

This technique involves creating many mutated versions of the program (each with a slight modification) and then analyzing which mutations cause the program to pass or fail tests. The differences in behavior can point to the location of the fault.

Example

Let's consider a simple program that checks if a number is positive. However, there's a bug in the implementation.

def is_positive(number):
    return number >= 0  #Bug here: should be > 0

def main():
    is_positive(5)
    
is_positive(-3)
    is_positive(0)

The function is_positive should return True only for positive numbers (greater than 0). However, it incorrectly returns True for 0 because of the >= operator instead of >. Create a mutant by changing the >= operator to > and check how this change affects the test results. This is a simple mutation that could potentially fix the bug.

def is_positive_mutant(number):
    return number > 0  # Mutated line

Compare the test results of the original and mutant functions to see if the mutant behaves differently. In this simple example, the original function is_positive contains a bug because it incorrectly identifies 0 as positive. By creating a mutant (is_positive_mutant) that changes the comparison from >= to >, we can see that the mutant passes all the tests and correctly identifies 0 as not positive.

This process shows how mutation-based fault localization can help identify and fix bugs by systematically altering the code and observing the effects on the test results. In real-world applications, many mutants might be generated and tested automatically to pinpoint the exact location of a fault.

Example Tool

Mutation-based fault localization is a versatile technique with applications across various industries. It plays a crucial role in ensuring the reliability and robustness of software systems, particularly in environments where failures can have significant consequences. A prominent real-world application tool that leverages mutation-based fault localization is PIT (Pitest), which is a mutation testing tool for Java. PIT is widely used in the industry to ensure the quality and reliability of Java codebases.

6. MACHINE LEARNING-BASED FAULT LOCALIZATION

Machine learning-based fault localization is an advanced technique used to identify the source of bugs in software systems by leveraging machine learning (ML) algorithms. This approach aims to improve the accuracy and efficiency of fault localization by learning patterns from past data, such as execution traces, program spectra, and code features, and then using those patterns to predict the locations of faults in new or unseen software.

Example

Let's consider a buggy implementation of a sorting algorithm. The goal is to use machine learning to help locate the bug based on the behavior of the code during different test cases.

def buggy_bubble_sort(arr):

    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
        if arr[j] < arr[j+1]: #Bug here: should be arr[j] > arr[j+1]
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

The execution data is collected during test runs which is used to train a machine learning model. In practice, tools like coverage.py may be used to collect data and train a simple Random Forest model to predict which lines are suspicious based on the collected data. The model outputs a list of the most suspicious lines of code based on their contribution to test failures. In our example code above, the model ranks line 6 (if arr[j] < arr[j+1]:) and line 7 (arr[j], arr[j+1] = arr[j+1], arr[j]) as the most suspicious. These are directly related to the bug in our example, where the comparison operator is incorrect.

This example is a simplified version of machine learning-based fault localization. In real-world scenarios, more sophisticated data collection, feature extraction, and model tuning techniques are employed. Nonetheless, this example illustrates the core idea of using machine learning to identify potential bug locations in code.

Example Tool

Some fault localization tools based on machine learning technique include DeepCode, Microsoft’s CodeBERT and Facebook’s SapFix.

OTHER FAULT LOCALIZATION TECHNIQUES 

7. MODEL-BASED FAULT LOCALIZATION

Model-based fault localization uses formal models of the system to reason about the possible states and transitions that could lead to a failure. It involves model checking and analyzing the discrepancies between expected and actual behaviors.

8. FAULT LOCALIZATION VIA PROGRAM DEPENDENCY GRAPHS

Program dependence graphs capture data and control dependencies in the code. Fault localization techniques analyze these graphs to identify code segments that influence faulty behavior.

9. CAUSAL INFERENCE-BASED FAULT LOCALIZATION

Causal inference-based uses techniques from causal inference to establish causal relationships between different parts of the code and the observed failures. It identifies the most likely causes of failure based on these relationships.

10. EXECUTION TRACE ANALYSIS

Execution trace analysis examines detailed execution traces (logs of program execution) to identify patterns or anomalies that correlate with failures. This can involve pattern matching, clustering, or statistical analysis of traces.

CONCLUSION

Automated debugging techniques enhance software reliability, reduce the time and effort required for manual debugging, and help maintain high code quality. Integrating these techniques into the development workflow can significantly improve the efficiency and effectiveness of the software development process.

In conclusion, even with the presence of so many different fault localization methods, fault localization is far from perfect. While these methods are constantly advancing, software too is becoming increasingly more complex which means the challenges posed by fault localization are also growing. Thus, there is a significant amount of research still to be done, and a large number of breakthroughs yet to be made. 

Comments