Aqua.jl

repository·master·Indexed 19 days ago

https://github.com/juliatesting/aqua.jl

An automated quality assurance tool for Julia packages designed to ensure best practices and identify common issues. It provides a suite of checks, including detection of method ambiguities, undefined exports, unbound type parameters, stale dependencies, and type piracy. It also includes utilities to verify Project.toml compatibility entries and detect persistent tasks that may cause precompilation hangs in Julia 1.10+.

Tokens
3K
Snippets
11
Records
17
Agent score
66%

What's inside Aqua.jl

  1. What is type piracy and why should I avoid it?

    master

    Type piracy occurs when a package adds methods to a function defined in a foreign package using only arguments that are also foreign to that function.

    This is considered bad practice because it can cause non-deterministic behavior: the behavior of a dependency might change depending on whether your package is loaded or not. This makes code difficult to reason about and introduces bugs that are hard to track down.

    For more detailed guidance, refer to the Julia style guide on avoiding type piracy.

  2. Understanding Unbound Type Parameters

    master

    An unbound type parameter is a type parameter defined in a where clause that does not appear in the method's signature or serve as a bound for another type parameter. These parameters are semantically redundant and can typically be removed without changing the function's behavior.

    Common scenarios where unbound parameters occur:

    1. Redundant where clauses: A type parameter is declared but not used in the arguments (e.g., f(x::Int) where {T}).
    2. Vararg edge cases: In functions using Vararg (e.g., g(x::T...) where {T}), the type parameter becomes unbound when the function is called with zero arguments, as there are no elements to determine the type T.
    # Example of a redundant unbound parameter
    f(x::Int) where {T} = do_something(x)
    
    # Example of an unbound parameter in a Vararg signature for the zero-argument case
    g(x::T...) where {T} = println(T)
    
    g()
  3. Identify and avoid common type piracy patterns

    master

    Type piracy manifests in several ways, ranging from severe to moderate. Understanding these patterns helps in writing safer Julia code:

    1. Direct Overlap (Worst Case): Adding a method for a type defined in the foreign package. For example, if PkgA defines bar(x::C) and you define bar(x::C) in PkgB, the result of bar(C()) changes depending on whether PkgB is loaded.
    2. Argument-less Methods: Adding a method that takes no arguments (e.g., bar()) when the original function required arguments. This can cause MethodError in one environment and return a value in another.
    3. Union/Container Piracy: Adding methods that handle Union{} or specific Vector types that rely on foreign types, which can change dispatch behavior for empty collections.
    4. Invalidation Risks: Defining methods with specific type parameters (like Vector{D}) that might cause performance invalidations.
    5. Union-based Piracy: Using Union{ForeignType, MyType}. While this might not change immediate dispatch, a future change in the foreign package (e.g., changing a method signature to include a Union) can turn this into an ambiguous dispatch or direct piracy.
    module PkgA
        struct C end
        bar(x::C) = 42
        bar(x::Vector) = 43
    end
    
    module PkgB 
        import PkgA: bar, C
        struct D end
        # Case 1: Worst case (direct overlap)
        bar(x::C) = 1
        
        # Case 2: Bad case (argument-less/signature change)
        bar(xs::D...) = 2
        
        # Case 3: Moderate case (Union/Vector behavior)
        bar(x::Vector{<:D}) = 3
        
        # Case 4: Potential invalidation risk
        bar(x::Vector{D}) = 4 
        
        # Case 5: Slightly bad (Union with foreign type)
        bar(x::Union{C,D}) = 5 
    end
  4. Use compat entries in Project.toml for dependency compatibility

    master
    To ensure your package is compatible with specific versions of Julia and its dependencies, you should define [compat] entries in your Project.toml file. This practice facilitates easier installations and upgrades for your users and protects your package from breaking changes in the Julia ecosystem. For detailed syntax on how to define these ranges, refer to the official Pkg.jl compatibility documentation.
  5. Run comprehensive package tests with Aqua.test_all()

    master

    To run a comprehensive suite of quality assurance tests on your Julia package, use Aqua.test_all(). This function executes most of the individual tests provided by the Aqua.jl module. For most packages, the default settings are sufficient. If your package requires specific configurations or needs to bypass certain checks, you can pass keyword arguments to Aqua.test_all() to customize the test execution.

    using Aqua
    # Run all tests with default settings
    Aqua.test_all()
    
    # Example of customizing tests with keyword arguments
    # Aqua.test_all(some_keyword_arg=value)
  6. How to fix packages that cause precompilation hangs

    master

    If Aqua.test_persistent_tasks fails, you can use one of two primary strategies to fix it:

    1. Conditional execution in __init__

    Modify your __init__ function to check if the current process is performing precompilation. If it is, skip launching persistent tasks. You can check this using ccall(:jl_generating_output, Cint, ()).

    function __init__()
        # Only launch persistent tasks if we are NOT precompiling
        if ccall(:jl_generating_output, Cint, ()) == 0
            # launch persistent tasks here
        end
    end

    2. Implement a clean shutdown

    For tasks running loops, use a global Ref to signal when the task should terminate. This allows external code to shut down the task gracefully.

    const task_should_run = Ref(true)
    
    # Inside your task/loop:
    while task_should_run[]
        # ... loop body ...
    end
    
    # To stop it from outside:
    task_should_run[] = false
    function __init__()
        if ccall(:jl_generating_output, Cint, ()) == 0
            # launch persistent tasks here
        end
    end
  7. Fixing Unbound Type Parameters in Vararg signatures

    master

    When a Vararg method has an unbound type parameter (specifically for the zero-argument case), you can resolve the ambiguity by providing a specific method for the zero-argument case that defines a default type, alongside the variadic method.

    Instead of:

    g(x::T...) where {T} = println(T)

    Use a pattern like this:

    g() = println(Int)  # Explicitly handle zero arguments with a default type
    g(x1::T, x2::T...) where {T} = println(T)
    # Fix: Provide a zero-argument method and a variadic method with at least one argument
    g() = println(Int)
    g(x1::T, x2::T...) where {T} = println(T)
    
    # Now these calls are unambiguous
    g(1.0, 2.0)
    g(1)
    g()
  8. Resolving method ambiguities in Julia

    master

    Method ambiguities occur when multiple methods are applicable to a set of arguments, but none is more specific than the others. This results in a MethodError.

    To resolve an ambiguity, you must add a more specific method that covers the overlapping case. For example, if you have methods for (Int, Integer) and (Integer, Int), you should add a method for (Int, Int) to handle the case where both arguments are integers.

    # Ambiguous case
    f(x::Int, y::Integer) = 1
    f(x::Integer, y::Int) = 2
    
    # This throws MethodError: ambiguity in method selection
    f(1, 2)
    
    # Solution: Add the most specific method
    f(x::Int, y::Int) = 1
    
    # Now this works
    f(1, 2)
  9. Add the Aqua.jl QA badge to your README

    master

    To display the Aqua.jl quality assurance status in your project's README.md, include the following Markdown snippet:

    [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)
  10. Use Aqua.test_piracies to detect type piracy

    master

    You can use the Aqua.test_piracies function to automatically check your package for common type piracy patterns. Currently, this test function specifically checks for the most severe cases (direct method overlap and argument-less method additions).

    # Use the Aqua test function to check for piracy
    Aqua.test_piracies
  11. Check for persistent tasks that block precompilation

    master

    In Julia 1.10+, precompilation waits for all running Tasks to finish. If your package's __init__ function launches persistent tasks (like Timers or background loops), it will cause any package that depends on yours to hang during precompilation.

    You can use Aqua.test_persistent_tasks(MyPackage) to detect if your package causes this issue. By default, this checks if the __init__ function leaves tasks running.

    using Aqua
    Aqua.test_persistent_tasks(MyPackage)