Skill: easonc13/sui-auto-test (version 1.0.0)

--- name: sui-coverage description: Analyze Sui Move test coverage, identify untested code, write missing tests, and perform security audits. Includes Python tools for parsing coverage output and generating reports. --- # Sui Coverage Skill Analyze and automatically improve Sui Move test coverage with security analysis. ## Quick Reference ```bash # Location of tools SKILL_DIR=~/clawd/skills/sui-coverage # Full workflow cd /path/to/move/package sui move test --coverage --trace python3 $SKILL_DIR/analyze_source.py -m <module> -o coverage.md ``` ## Workflow: Auto-Improve Test Coverage ### Step 1: Run Coverage Analysis ```bash cd <package_path> sui move test --coverage --trace python3 ~/clawd/skills/sui-coverage/analyze_source.py -m <module_name> -o coverage.md ``` ### Step 2: Read the Coverage Report Read the generated `coverage.md` to identify: - 🔴 **Uncalled functions** - Functions never executed - 🔴 **Uncovered assertions** - `assert!()` failure paths not tested - 🔴 **Uncovered branches** - `if/else` paths not taken ### Step 3: Write Missing Tests For each uncovered item, write a test: #### A. Uncalled Function ```move #[test] fun test_<function_name>() { // Setup let mut ctx = tx_context::dummy(); // Call the uncovered function <function_name>(...); // Assert expected behavior } ``` #### B. Assertion Failure Path (expect_failure) ```move #[test] #[expected_failure(abort_code = <ERROR_CODE>)] fun test_<function>_fails_when_<condition>() { let mut ctx = tx_context::dummy(); // Setup state that triggers the assertion failure <function_call_that_should_fail>(); } ``` #### C. Branch Coverage (if/else) ```move #[test] fun test_<function>_when_<condition_true>() { ... } #[test] fun test_<function>_when_<condition_false>() { ... } ``` ### Step 4: Verify Coverage Improved ```bash sui move test --coverage --trace python3 ~/clawd/skills/sui-coverage/analyze_source.py -m <module_name> ``` --- ## Tools ### 1. analyze_source.py (Primary Tool) ```bash python3 ~/clawd/skills/sui-coverage/analyze_source.py --module <name> [options] Options: -m, --module Module name (required) -p, --path Package path (default: .) -o, --output Output file (e.g., coverage.md) --json JSON output --markdown Markdown to stdout ``` ### 2. analyze.py (LCOV Statistics) ```bash sui move coverage lcov python3 ~/clawd/skills/sui-coverage/analyze.py lcov.info -f "<package>" -s sources/ Options: -f, --filter Filter by path pattern -s, --source-dir Source directory for context -i, --issues-only Only show files with issues -j, --json JSON output ``` ### 3. parse_bytecode.py (Low-level) ```bash sui move coverage bytecode --module <name> | python3 ~/clawd/skills/sui-coverage/parse_bytecode.py ``` --- ## Common Patterns ### Testing Assertion Failures ```move // Source code: public fun withdraw(balance: &mut u64, amount: u64) { assert!(*balance >= amount, EInsufficientBalance); // ← This failure path *balance = *balance - amount; } // Test for the failure path: #[test] #[expected_failure(abort_code = EInsufficientBalance)] fun test_withdraw_insufficient_balance() { let mut balance = 50; withdraw(&mut balance, 100); // Should fail: 50 < 100 } ``` ### Testing All Branches ```move // Source code: public fun classify(value: u64): u8 { if (value == 0) { 0 } else if (value < 100) { 1 } else { 2 } } // Tests for all branches: #[test] fun test_classify_zero() { assert!(classify(0) == 0, 0); } #[test] fun test_classify_small() { assert!(classify(50) == 1, 0); } #[test] fun test_classify_large() { assert!(classify(100) == 2, 0); } ``` ### Testing Object Lifecycle ```move #[test] fun test_full_lifecycle() { let mut ctx = tx_context::dummy(); // Create let obj = create(&mut ctx); assert!(get_value(&obj) == 0, 0); // Modify increment(&mut obj); assert!(get_value(&obj) == 1, 0); // Destroy destroy(obj); } ``` --- ## Error Code Reference When writing `#[expected_failure]` tests, use the error constant name: ```move // If the module defines: const EInvalidInput: u64 = 1; const ENotAuthorized: u64 = 2; // Use in test: #[expected_failure(abort_code = EInvalidInput)] fun test_invalid_input() { ... } // Or use the module-qualified name: #[expected_failure(abort_code = my_module::EInvalidInput)] fun test_invalid_input() { ... } ``` --- ## Example: Full Auto-Coverage Session ```bash # 1. Analyze current coverage cd ~/project/my_package sui move test --coverage --trace python3 ~/clawd/skills/sui-coverage/analyze_source.py -m my_module -o coverage.md # 2. Review what's missing cat coverage.md # Shows: # - decrement() not called # - assert!(value > 0, EValueZero) failure not tested # 3. Add tests to sources/my_module.move or tests/my_module_tests.move # (write the missing tests) # 4. Verify improvement sui move test --coverage --trace python3 ~/clawd/skills/sui-coverage/analyze_source.py -m my_module # 5. Repeat until 100% coverage ``` --- ## Integration with Agent Workflow When asked to improve test coverage: 1. **Run analysis** - Get current coverage state 2. **Read source** - Understand the module's logic 3. **Identify gaps** - List uncovered functions/branches/assertions 4. **Security review** - Analyze for vulnerabilities while writing tests 5. **Write tests** - Create tests for each gap + security edge cases 6. **Report findings** - Document any security concerns discovered 7. **Verify** - Re-run coverage to confirm improvement Always commit test improvements: ```bash git add sources/ tests/ git commit -m "Improve test coverage for <module>" ``` --- ## Security Analysis During Testing **Writing tests = Understanding the contract = Finding vulnerabilities** When writing tests, actively look for these issues: ### 1. Access Control ``` Questions to ask: - Who can call this function? - Should there be owner/admin checks? - Can unauthorized users manipulate state? Red flags: - Public functions that modify critical state without checks - Missing capability/witness patterns ``` ### 2. Integer Overflow/Underflow ``` Questions to ask: - What happens at u64::MAX? - What happens when subtracting from 0? - Are arithmetic operations checked? Test pattern: #[test] fun test_overflow_boundary() { // Test with max values } ``` ### 3. State Manipulation ``` Questions to ask: - Can state be left in inconsistent state? - Are all state changes atomic? - Can partial failures corrupt data? Red flags: - Multiple state changes without rollback - Shared objects without proper locking ``` ### 4. Economic Exploits ``` Questions to ask: - Can someone extract more value than deposited? - Are there rounding errors that can be exploited? - Flash loan attack vectors? Red flags: - Price calculations without slippage protection - Unbounded loops over user-controlled data ``` ### 5. Denial of Service ``` Questions to ask: - Can someone block legitimate users? - Are there unbounded operations? - Can storage be filled maliciously? Red flags: - Vectors that grow unbounded - Loops over external data ``` ### Security Report Template When analyzing a module, generate a security report: ```markdown ## Security Analysis: <module_name> ### Summary - Risk Level: [Low/Medium/High/Critical] - Issues Found: X ### Findings #### [SEVERITY] Issue Title - **Location:** Line XX - **Description:** What the issue is - **Impact:** What could happen - **Recommendation:** How to fix ### Tested Edge Cases - [ ] Overflow at max values - [ ] Underflow at zero - [ ] Unauthorized access attempts - [ ] Empty/null inputs - [ ] Reentrancy scenarios ``` ### Example: Security-Aware Test ```move // SECURITY: Testing that non-owner cannot withdraw #[test] #[expected_failure(abort_code = ENotOwner)] fun test_unauthorized_withdraw() { // Setup: Create vault owned by ALICE // Action: BOB tries to withdraw // Expected: Should fail with ENotOwner } // SECURITY: Testing overflow protection #[test] fun test_deposit_overflow_protection() { // Deposit near u64::MAX // Verify no overflow occurs } // SECURITY: Testing economic invariant #[test] fun test_total_supply_invariant() { // After any operations: // sum(all_balances) == total_supply } ``` --- ## Full Workflow with Security ```bash # 1. Coverage analysis sui move test --coverage --trace python3 ~/clawd/skills/sui-coverage/analyze_source.py -m <module> -o coverage.md # 2. While writing tests, document security findings # Create SECURITY.md alongside coverage.md # 3. After tests pass, summarize: # - Coverage: X% → 100% # - Security issues found: N # - Recommendations: ... ```

path: analyze.py
content: #!/usr/bin/env python3
"""
Sui Move Coverage Analyzer

Parses LCOV format output from `sui move coverage lcov` and identifies
uncovered code sections to help write more comprehensive tests.
"""

import argparse
import json
import os
import sys
from dataclasses import dataclass, field, asdict
from typing import Optional


@dataclass
class BranchInfo:
    line: int
    block: int
    branch: int
    taken: bool
    count: int  # -1 means never taken


@dataclass
class FunctionInfo:
    name: str
    line: int
    call_count: int


@dataclass
class FileCoverage:
    path: str
    functions: list[FunctionInfo] = field(default_factory=list)
    line_hits: dict[int, int] = field(default_factory=dict)  # line -> hit count
    branches: list[BranchInfo] = field(default_factory=list)
    # Summary stats
    functions_found: int = 0
    functions_hit: int = 0
    lines_found: int = 0
    lines_hit: int = 0
    branches_found: int = 0
    branches_hit: int = 0


def parse_lcov(lcov_path: str) -> list[FileCoverage]:
    """Parse LCOV file and return coverage data per source file."""
    files = []
    current = None
    fn_lines = {}  # name -> line number
    fn_counts = {}  # name -> call count (for current file)

    with open(lcov_path, 'r') as f:
        for line in f:
            line = line.strip()
            if not line:
                continue

            if line.startswith('SF:'):
                # New source file
                current = FileCoverage(path=line[3:])
                fn_lines = {}
                fn_counts = {}
            elif line.startswith('FN:'):
                # Function definition: FN:line,name
                parts = line[3:].split(',', 1)
                if len(parts) == 2:
                    fn_line, fn_name = int(parts[0]), parts[1]
                    fn_lines[fn_name] = fn_line
            elif line.startswith('FNDA:'):
                # Function execution: FNDA:count,name
                parts = line[5:].split(',', 1)
                if len(parts) == 2 and current:
                    count, name = int(parts[0]), parts[1]
                    fn_counts[name] = count
            elif line.startswith('DA:'):
                # Line data: DA:line,count
                parts = line[3:].split(',')
                if len(parts) == 2 and current:
                    ln, count = int(parts[0]), int(parts[1])
                    current.line_hits[ln] = count
            elif line.startswith('BRDA:'):
                # Branch data: BRDA:line,block,branch,count
                parts = line[5:].split(',')
                if len(parts) == 4 and current:
                    ln, block, branch = int(parts[0]), int(parts[1]), int(parts[2])
                    count_str = parts[3]
                    if count_str == '-':
                        count = -1
                        taken = False
                    else:
                        count = int(count_str)
                        taken = count > 0
                    current.branches.append(BranchInfo(line=ln, block=block, branch=branch, taken=taken, count=count))
            elif line.startswith('FNF:'):
                if current:
                    current.functions_found = int(line[4:])
            elif line.startswith('FNH:'):
                if current:
                    current.functions_hit = int(line[4:])
            elif line.startswith('LF:'):
                if current:
                    current.lines_found = int(line[3:])
            elif line.startswith('LH:'):
                if current:
                    current.lines_hit = int(line[3:])
            elif line.startswith('BRF:'):
                if current:
                    current.branches_found = int(line[4:])
            elif line.startswith('BRH:'):
                if current:
                    current.branches_hit = int(line[4:])
            elif line == 'end_of_record':
                if current:
                    # Finalize functions for this file
                    for name, fn_line in fn_lines.items():
                        count = fn_counts.get(name, 0)
                        current.functions.append(FunctionInfo(name=name, line=fn_line, call_count=count))
                    files.append(current)
                    current = None
                    fn_lines = {}
                    fn_counts = {}

    # Handle file without end_of_record
    if current:
        for name, fn_line in fn_lines.items():
            count = fn_counts.get(name, 0)
            current.functions.append(FunctionInfo(name=name, line=fn_line, call_count=count))
        files.append(current)

    return files


def get_uncovered_lines(coverage: FileCoverage) -> list[int]:
    """Return list of lines with 0 hit count."""
    return sorted([ln for ln, count in coverage.line_hits.items() if count == 0])


def get_untaken_branches(coverage: FileCoverage) -> list[dict]:
    """Return branches that were never taken."""
    return [
        {'line': b.line, 'block': b.block, 'branch': b.branch}
        for b in coverage.branches if not b.taken
    ]


def get_uncalled_functions(coverage: FileCoverage) -> list[dict]:
    """Return functions that were never called."""
    return [
        {'name': f.name, 'line': f.line}
        for f in coverage.functions if f.call_count == 0
    ]


def read_source_lines(source_path: str) -> dict[int, str]:
    """Read source file and return line number -> content mapping."""
    lines = {}
    try:
        with open(source_path, 'r') as f:
            for i, line in enumerate(f, 1):
                lines[i] = line.rstrip()
    except Exception:
        pass
    return lines


def generate_suggestions(coverage: FileCoverage, source_lines: Optional[dict] = None) -> list[dict]:
    """Generate actionable suggestions for improving coverage."""
    suggestions = []

    # Uncalled functions
    for f in coverage.functions:
        if f.call_count == 0:
            suggestion = {
                'type': 'uncalled_function',
                'priority': 'high',
                'function': f.name,
                'line': f.line,
                'action': f'Write a test that calls `{f.name}()`'
            }
            if source_lines and f.line in source_lines:
                suggestion['source'] = source_lines[f.line]
            suggestions.append(suggestion)

    # Untaken branches (grouped by line)
    branch_lines = {}
    for b in coverage.branches:
        if not b.taken:
            if b.line not in branch_lines:
                branch_lines[b.line] = []
            branch_lines[b.line].append(b)

    for line, branches in branch_lines.items():
        suggestion = {
            'type': 'untaken_branch',
            'priority': 'medium',
            'line': line,
            'branches': len(branches),
            'action': f'Add test case to cover alternate branch at line {line}'
        }
        if source_lines and line in source_lines:
            suggestion['source'] = source_lines[line]
        suggestions.append(suggestion)

    # Uncovered lines (grouped into ranges)
    uncovered = get_uncovered_lines(coverage)
    if uncovered:
        ranges = []
        start = end = uncovered[0]
        for ln in uncovered[1:]:
            if ln == end + 1:
                end = ln
            else:
                ranges.append((start, end))
                start = end = ln
        ranges.append((start, end))

        for start, end in ranges:
            if start == end:
                line_desc = f'line {start}'
            else:
                line_desc = f'lines {start}-{end}'
            suggestion = {
                'type': 'uncovered_lines',
                'priority': 'low',
                'start_line': start,
                'end_line': end,
                'action': f'Write test to execute {line_desc}'
            }
            if source_lines:
                snippet = []
                for ln in range(start, min(end + 1, start + 5)):
                    if ln in source_lines:
                        snippet.append(f'{ln}: {source_lines[ln]}')
                if snippet:
                    suggestion['source_snippet'] = snippet
            suggestions.append(suggestion)

    return suggestions


def analyze(lcov_path: str, source_dir: Optional[str] = None) -> dict:
    """Main analysis function."""
    files = parse_lcov(lcov_path)

    results = {
        'summary': {
            'total_files': len(files),
            'total_functions_found': sum(f.functions_found for f in files),
            'total_functions_hit': sum(f.functions_hit for f in files),
            'total_lines_found': sum(f.lines_found for f in files),
            'total_lines_hit': sum(f.lines_hit for f in files),
            'total_branches_found': sum(f.branches_found for f in files),
            'total_branches_hit': sum(f.branches_hit for f in files),
        },
        'files': []
    }

    # Calculate percentages
    s = results['summary']
    s['function_coverage'] = f"{s['total_functions_hit']}/{s['total_functions_found']}" if s['total_functions_found'] else "N/A"
    s['line_coverage'] = f"{s['total_lines_hit']}/{s['total_lines_found']}" if s['total_lines_found'] else "N/A"
    s['branch_coverage'] = f"{s['total_branches_hit']}/{s['total_branches_found']}" if s['total_branches_found'] else "N/A"

    if s['total_lines_found']:
        s['line_coverage_pct'] = round(100 * s['total_lines_hit'] / s['total_lines_found'], 1)
    if s['total_branches_found']:
        s['branch_coverage_pct'] = round(100 * s['total_branches_hit'] / s['total_branches_found'], 1)

    for cov in files:
        source_lines = None
        if source_dir:
            # Try to find source file
            basename = os.path.basename(cov.path)
            source_path = os.path.join(source_dir, basename)
            if os.path.exists(source_path):
                source_lines = read_source_lines(source_path)
            elif os.path.exists(cov.path):
                source_lines = read_source_lines(cov.path)

        file_result = {
            'path': cov.path,
            'coverage': {
                'functions': f"{cov.functions_hit}/{cov.functions_found}",
                'lines': f"{cov.lines_hit}/{cov.lines_found}",
                'branches': f"{cov.branches_hit}/{cov.branches_found}",
            },
            'uncovered_lines': get_uncovered_lines(cov),
            'untaken_branches': get_untaken_branches(cov),
            'uncalled_functions': get_uncalled_functions(cov),
            'suggestions': generate_suggestions(cov, source_lines),
        }
        results['files'].append(file_result)

    return results


def print_human_readable(results: dict):
    """Print results in a human-readable format."""
    s = results['summary']
    print("=" * 60)
    print("SUI MOVE COVERAGE ANALYSIS")
    print("=" * 60)
    print(f"\nFiles analyzed: {s['total_files']}")
    print(f"Function coverage: {s['function_coverage']}")
    print(f"Line coverage: {s['line_coverage']} ({s.get('line_coverage_pct', 'N/A')}%)")
    print(f"Branch coverage: {s['branch_coverage']} ({s.get('branch_coverage_pct', 'N/A')}%)")

    for file_data in results['files']:
        print(f"\n{'─' * 60}")
        print(f"📄 {file_data['path']}")
        print(f"   Lines: {file_data['coverage']['lines']}, "
              f"Branches: {file_data['coverage']['branches']}, "
              f"Functions: {file_data['coverage']['functions']}")

        if file_data['uncalled_functions']:
            print("\n   ⚠️  Uncalled functions:")
            for f in file_data['uncalled_functions']:
                print(f"      - {f['name']} (line {f['line']})")

        if file_data['untaken_branches']:
            print("\n   ⚠️  Untaken branches:")
            lines = set(b['line'] for b in file_data['untaken_branches'])
            for ln in sorted(lines):
                count = sum(1 for b in file_data['untaken_branches'] if b['line'] == ln)
                print(f"      - Line {ln}: {count} branch(es) not taken")

        if file_data['uncovered_lines']:
            print(f"\n   ⚠️  Uncovered lines: {file_data['uncovered_lines']}")

        if file_data['suggestions']:
            print("\n   💡 Suggestions:")
            for i, sug in enumerate(file_data['suggestions'], 1):
                priority_emoji = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}.get(sug['priority'], '⚪')
                print(f"      {i}. {priority_emoji} {sug['action']}")
                if 'source' in sug:
                    print(f"         └─ {sug['source'][:80]}...")
                if 'source_snippet' in sug:
                    for snippet_line in sug['source_snippet'][:3]:
                        print(f"         │ {snippet_line[:70]}")

    print("\n" + "=" * 60)


def filter_results(results: dict, path_filter: str = None, issues_only: bool = False) -> dict:
    """Filter results by path pattern and/or issues only."""
    filtered_files = []
    
    for file_data in results['files']:
        # Path filter
        if path_filter and path_filter not in file_data['path']:
            continue
        
        # Issues only filter
        if issues_only:
            has_issues = (
                file_data['uncovered_lines'] or 
                file_data['untaken_branches'] or 
                file_data['uncalled_functions']
            )
            if not has_issues:
                continue
        
        filtered_files.append(file_data)
    
    # Recalculate summary for filtered files
    filtered_results = {
        'summary': {
            'total_files': len(filtered_files),
            'total_functions_found': sum(
                int(f['coverage']['functions'].split('/')[1]) for f in filtered_files
            ),
            'total_functions_hit': sum(
                int(f['coverage']['functions'].split('/')[0]) for f in filtered_files
            ),
            'total_lines_found': sum(
                int(f['coverage']['lines'].split('/')[1]) for f in filtered_files
            ),
            'total_lines_hit': sum(
                int(f['coverage']['lines'].split('/')[0]) for f in filtered_files
            ),
            'total_branches_found': sum(
                int(f['coverage']['branches'].split('/')[1]) for f in filtered_files
            ),
            'total_branches_hit': sum(
                int(f['coverage']['branches'].split('/')[0]) for f in filtered_files
            ),
        },
        'files': filtered_files
    }
    
    s = filtered_results['summary']
    s['function_coverage'] = f"{s['total_functions_hit']}/{s['total_functions_found']}"
    s['line_coverage'] = f"{s['total_lines_hit']}/{s['total_lines_found']}"
    s['branch_coverage'] = f"{s['total_branches_hit']}/{s['total_branches_found']}"
    
    if s['total_lines_found']:
        s['line_coverage_pct'] = round(100 * s['total_lines_hit'] / s['total_lines_found'], 1)
    if s['total_branches_found']:
        s['branch_coverage_pct'] = round(100 * s['total_branches_hit'] / s['total_branches_found'], 1)
    
    return filtered_results


def main():
    parser = argparse.ArgumentParser(description='Analyze Sui Move test coverage')
    parser.add_argument('lcov_file', help='Path to lcov.info file')
    parser.add_argument('--source-dir', '-s', help='Directory containing Move source files')
    parser.add_argument('--json', '-j', action='store_true', help='Output as JSON')
    parser.add_argument('--filter', '-f', help='Only show files matching this path pattern')
    parser.add_argument('--issues-only', '-i', action='store_true', help='Only show files with coverage issues')
    args = parser.parse_args()

    if not os.path.exists(args.lcov_file):
        print(f"Error: File not found: {args.lcov_file}", file=sys.stderr)
        sys.exit(1)

    results = analyze(args.lcov_file, args.source_dir)
    
    # Apply filters
    if args.filter or args.issues_only:
        results = filter_results(results, args.filter, args.issues_only)

    if args.json:
        print(json.dumps(results, indent=2))
    else:
        print_human_readable(results)


if __name__ == '__main__':
    main()


path: analyze_source.py
content: #!/usr/bin/env python3
"""
Analyze Sui Move source coverage with precise uncovered code detection.

Usage:
    python3 analyze_source.py --module <module_name> [--path <package_path>]

This script uses PTY to capture colored output from `sui move coverage source`.
"""

import os
import pty
import sys
import re
import json
import argparse
from dataclasses import dataclass, field
from typing import List, Optional

# ANSI patterns (with \x1b escape)
ANSI_CODE = re.compile(r'\x1b\[[\d;]*m')
RED_PATTERN = re.compile(r'\x1b\[1?;?31m')
GREEN_PATTERN = re.compile(r'\x1b\[32m')
RESET_PATTERN = re.compile(r'\x1b\[0m|\x1b\[39m')


def run_coverage_with_pty(module_name: str, package_path: str = '.') -> str:
    """Run sui move coverage source with PTY to preserve colors."""
    master, slave = pty.openpty()
    pid = os.fork()
    
    if pid == 0:
        # Child process
        os.close(master)
        os.setsid()
        os.dup2(slave, 0)
        os.dup2(slave, 1) 
        os.dup2(slave, 2)
        os.close(slave)
        os.chdir(package_path)
        os.execvp('sui', ['sui', 'move', 'coverage', 'source', '--module', module_name])
    else:
        # Parent process
        os.close(slave)
        output = b''
        while True:
            try:
                data = os.read(master, 4096)
                if not data:
                    break
                output += data
            except OSError:
                break
        os.close(master)
        os.waitpid(pid, 0)
        return output.decode('utf-8', errors='replace')


@dataclass
class UncoveredSegment:
    line_num: int
    full_line: str
    uncovered_text: str
    context_before: str
    context_after: str


def parse_colored_output(output: str) -> List[UncoveredSegment]:
    """Parse colored output to find uncovered (red) segments."""
    uncovered = []
    lines = output.split('\n')
    
    for line_num, line in enumerate(lines, 1):
        # Skip notes and empty lines
        if line.startswith('[NOTE]') or line.startswith('[['):
            continue
        if not line.strip():
            continue
            
        # Find red segments in this line
        # Pattern: text before red, red content, text after red
        
        # Remove all ANSI codes to get clean line
        clean_line = ANSI_CODE.sub('', line)
        
        # Find red sections
        # Split by color codes and track current color
        parts = re.split(r'(\x1b\[[\d;]*m)', line)
        
        current_color = None
        position = 0
        red_segments = []
        
        for part in parts:
            if RED_PATTERN.match(part):
                current_color = 'red'
            elif GREEN_PATTERN.match(part):
                current_color = 'green'
            elif RESET_PATTERN.match(part):
                current_color = None
            elif part and not part.startswith('\x1b'):
                # This is actual text
                if current_color == 'red' and part.strip():
                    # Find position in clean line
                    start = clean_line.find(part, position)
                    if start >= 0:
                        # Get context (chars before and after)
                        ctx_start = max(0, start - 20)
                        ctx_end = min(len(clean_line), start + len(part) + 20)
                        
                        before = clean_line[ctx_start:start]
                        after = clean_line[start + len(part):ctx_end]
                        
                        red_segments.append({
                            'text': part,
                            'before': before,
                            'after': after,
                        })
                        position = start + len(part)
        
        for seg in red_segments:
            uncovered.append(UncoveredSegment(
                line_num=line_num,
                full_line=clean_line.strip(),
                uncovered_text=seg['text'],
                context_before=seg['before'],
                context_after=seg['after'],
            ))
    
    return uncovered


def group_by_function(uncovered: List[UncoveredSegment], source_lines: List[str]) -> dict:
    """Group uncovered segments by function."""
    functions = {}
    current_func = None
    
    # Find function boundaries
    func_pattern = re.compile(r'(?:public\s+)?fun\s+(\w+)')
    
    func_starts = {}
    for i, line in enumerate(source_lines, 1):
        match = func_pattern.search(line)
        if match:
            func_starts[i] = match.group(1)
    
    # Assign each uncovered segment to a function
    sorted_funcs = sorted(func_starts.items())
    
    for seg in uncovered:
        func_name = None
        for start_line, name in reversed(sorted_funcs):
            if seg.line_num >= start_line:
                func_name = name
                break
        
        if func_name:
            if func_name not in functions:
                functions[func_name] = []
            functions[func_name].append(seg)
    
    return functions


def print_report(uncovered: List[UncoveredSegment], module_name: str):
    """Print human-readable report."""
    print("=" * 70)
    print(f"SOURCE COVERAGE ANALYSIS: {module_name}")
    print("=" * 70)
    
    if not uncovered:
        print("\n✅ All code is covered!")
        return
    
    print(f"\n🔴 Found {len(uncovered)} uncovered code segment(s)")
    print("-" * 70)
    
    # Group by line
    by_line = {}
    for seg in uncovered:
        if seg.line_num not in by_line:
            by_line[seg.line_num] = {
                'full_line': seg.full_line,
                'segments': []
            }
        by_line[seg.line_num]['segments'].append(seg)
    
    for line_num in sorted(by_line.keys()):
        info = by_line[line_num]
        print(f"\n📍 Line {line_num}:")
        print(f"   {info['full_line']}")
        
        for seg in info['segments']:
            # Show the uncovered part with markers
            print(f"   └─ 🔴 Uncovered: \"{seg.uncovered_text}\"")
            if seg.context_before or seg.context_after:
                ctx = f"...{seg.context_before}[{seg.uncovered_text}]{seg.context_after}..."
                print(f"      Context: {ctx}")
    
    print("\n" + "=" * 70)
    
    # Summary of what to test
    print("\n💡 SUGGESTIONS:")
    print("-" * 70)
    
    # Identify patterns
    assertions = [s for s in uncovered if 'assert!' in s.uncovered_text]
    func_names = [s for s in uncovered if s.uncovered_text.isidentifier()]
    
    if assertions:
        print("\n🧪 Test assertion failure paths:")
        for seg in assertions:
            print(f"   - Line {seg.line_num}: {seg.uncovered_text}")
            print(f"     → Write a test where this assertion FAILS")
    
    if func_names:
        print("\n🧪 Call these uncovered functions:")
        for seg in func_names:
            print(f"   - {seg.uncovered_text}()")
    
    print()


def print_json(uncovered: List[UncoveredSegment]):
    """Print JSON output."""
    data = {
        'uncovered_count': len(uncovered),
        'uncovered': [
            {
                'line': s.line_num,
                'full_line': s.full_line,
                'uncovered_text': s.uncovered_text,
                'context_before': s.context_before,
                'context_after': s.context_after,
            }
            for s in uncovered
        ]
    }
    print(json.dumps(data, indent=2))


def generate_markdown(uncovered: List[UncoveredSegment], module_name: str) -> str:
    """Generate markdown report."""
    lines = []
    lines.append(f"# Coverage Report: {module_name}")
    lines.append("")
    
    if not uncovered:
        lines.append("✅ **All code is covered!**")
        return '\n'.join(lines)
    
    lines.append(f"🔴 **Found {len(uncovered)} uncovered code segment(s)**")
    lines.append("")
    
    # Group by line
    by_line = {}
    for seg in uncovered:
        if seg.line_num not in by_line:
            by_line[seg.line_num] = {
                'full_line': seg.full_line,
                'segments': []
            }
        by_line[seg.line_num]['segments'].append(seg)
    
    lines.append("## Uncovered Code")
    lines.append("")
    
    for line_num in sorted(by_line.keys()):
        info = by_line[line_num]
        lines.append(f"### Line {line_num}")
        lines.append("")
        lines.append("```move")
        lines.append(info['full_line'])
        lines.append("```")
        lines.append("")
        
        for seg in info['segments']:
            lines.append(f"- ❌ **Uncovered:** `{seg.uncovered_text}`")
        lines.append("")
    
    # Suggestions
    lines.append("## Suggestions")
    lines.append("")
    
    assertions = [s for s in uncovered if 'assert!' in s.uncovered_text]
    func_names = [s for s in uncovered if re.match(r'^[a-z_][a-z0-9_]*$', s.uncovered_text) and len(s.uncovered_text) > 1]
    
    if assertions:
        lines.append("### Test Assertion Failure Paths")
        lines.append("")
        for seg in assertions:
            lines.append(f"- [ ] Line {seg.line_num}: `{seg.uncovered_text}`")
            lines.append(f"  - Write a test where this assertion **fails**")
        lines.append("")
    
    if func_names:
        lines.append("### Call Uncovered Functions")
        lines.append("")
        seen = set()
        for seg in func_names:
            if seg.uncovered_text not in seen:
                seen.add(seg.uncovered_text)
                lines.append(f"- [ ] `{seg.uncovered_text}()`")
        lines.append("")
    
    return '\n'.join(lines)


def main():
    parser = argparse.ArgumentParser(description='Analyze Sui Move source coverage')
    parser.add_argument('--module', '-m', required=True, help='Module name to analyze')
    parser.add_argument('--path', '-p', default='.', help='Package path (default: current dir)')
    parser.add_argument('--json', '-j', action='store_true', help='Output as JSON')
    parser.add_argument('--markdown', '--md', action='store_true', help='Output as Markdown')
    parser.add_argument('--output', '-o', help='Output file path (e.g., coverage.md)')
    args = parser.parse_args()
    
    # Run coverage command with PTY
    print(f"Running: sui move coverage source --module {args.module}", file=sys.stderr)
    output = run_coverage_with_pty(args.module, args.path)
    
    # Parse the colored output
    uncovered = parse_colored_output(output)
    
    # Generate output
    if args.json:
        result = json.dumps({
            'uncovered_count': len(uncovered),
            'uncovered': [
                {
                    'line': s.line_num,
                    'full_line': s.full_line,
                    'uncovered_text': s.uncovered_text,
                }
                for s in uncovered
            ]
        }, indent=2)
    elif args.markdown or (args.output and args.output.endswith('.md')):
        result = generate_markdown(uncovered, args.module)
    else:
        # Default: print to console
        print_report(uncovered, args.module)
        return
    
    # Output to file or stdout
    if args.output:
        with open(args.output, 'w') as f:
            f.write(result)
        print(f"Report saved to: {args.output}", file=sys.stderr)
    else:
        print(result)


if __name__ == '__main__':
    main()


path: parse_bytecode.py
content: #!/usr/bin/env python3
"""
Parse Sui Move bytecode coverage output (ANSI colors) to identify uncovered code.

Usage:
    sui move coverage bytecode --module <name> 2>&1 | python3 parse_bytecode.py
"""

import re
import sys
from dataclasses import dataclass
from typing import Optional

# ANSI color codes
GREEN = '\033[32m'  # Covered
RED = '\033[31m'    # Uncovered
RESET = '\033[39m'

@dataclass
class BytecodeInstruction:
    line_num: Optional[int]  # Source line if available
    offset: int
    instruction: str
    covered: bool  # True = green, False = red


def parse_bytecode_coverage(input_text: str) -> dict:
    """Parse bytecode coverage output and return structured data."""
    
    # Pattern to match bytecode lines with optional line numbers
    # Examples:
    #   [32m[3]	0: CopyLoc[0](max_value)  <- covered, source line 3
    #   [31m	5: Pop                       <- uncovered, no source line
    
    results = {
        'functions': [],
        'summary': {
            'total_instructions': 0,
            'covered_instructions': 0,
            'uncovered_instructions': 0,
        },
        'uncovered_details': []
    }
    
    current_function = None
    current_instructions = []
    
    lines = input_text.split('\n')
    
    for line in lines:
        # Check for function header 
        # Format: "public new(args): RetType {" or "fun name(...) {"
        func_match = re.match(r'^(?:public\s+)?(\w+)\s*\([^)]*\)(?:\s*:\s*\w+)?\s*\{', line.strip())
        if func_match:
            # Save previous function
            if current_function and current_instructions:
                results['functions'].append({
                    'name': current_function,
                    'instructions': current_instructions,
                    'covered': sum(1 for i in current_instructions if i['covered']),
                    'total': len(current_instructions),
                })
            
            current_function = func_match.group(1)
            current_instructions = []
            continue
        
        # Parse bytecode instruction lines
        # Look for color codes
        is_covered = None
        if '\033[32m' in line or '[32m' in line:
            is_covered = True
        elif '\033[31m' in line or '[31m' in line:
            is_covered = False
        
        if is_covered is not None:
            # Extract source line number if present (e.g., [3])
            source_line = None
            line_match = re.search(r'\[(\d+)\]\s*\t', line)
            if line_match:
                source_line = int(line_match.group(1))
            
            # Extract instruction offset and content
            instr_match = re.search(r'(\d+):\s+(.+?)(?:\033\[|$)', line)
            if instr_match:
                offset = int(instr_match.group(1))
                instruction = instr_match.group(2).strip()
                
                instr_data = {
                    'source_line': source_line,
                    'offset': offset,
                    'instruction': instruction,
                    'covered': is_covered,
                }
                current_instructions.append(instr_data)
                
                results['summary']['total_instructions'] += 1
                if is_covered:
                    results['summary']['covered_instructions'] += 1
                else:
                    results['summary']['uncovered_instructions'] += 1
                    results['uncovered_details'].append({
                        'function': current_function,
                        **instr_data
                    })
    
    # Save last function
    if current_function and current_instructions:
        results['functions'].append({
            'name': current_function,
            'instructions': current_instructions,
            'covered': sum(1 for i in current_instructions if i['covered']),
            'total': len(current_instructions),
        })
    
    return results


def print_report(results: dict):
    """Print human-readable coverage report."""
    s = results['summary']
    total = s['total_instructions']
    covered = s['covered_instructions']
    uncovered = s['uncovered_instructions']
    
    print("=" * 60)
    print("BYTECODE COVERAGE ANALYSIS")
    print("=" * 60)
    print(f"\nTotal instructions: {total}")
    print(f"Covered (green):    {covered} ({100*covered//total if total else 0}%)")
    print(f"Uncovered (red):    {uncovered} ({100*uncovered//total if total else 0}%)")
    
    print("\n" + "-" * 60)
    print("FUNCTION BREAKDOWN")
    print("-" * 60)
    
    for func in results['functions']:
        pct = 100 * func['covered'] // func['total'] if func['total'] else 0
        status = "✅" if pct == 100 else "⚠️" if pct > 0 else "❌"
        print(f"{status} {func['name']}: {func['covered']}/{func['total']} ({pct}%)")
    
    if results['uncovered_details']:
        print("\n" + "-" * 60)
        print("UNCOVERED INSTRUCTIONS (RED)")
        print("-" * 60)
        
        # Group by function
        by_func = {}
        for item in results['uncovered_details']:
            fn = item['function'] or 'unknown'
            if fn not in by_func:
                by_func[fn] = []
            by_func[fn].append(item)
        
        for fn, items in by_func.items():
            print(f"\n🔴 {fn}():")
            
            # Group by source line
            by_line = {}
            for item in items:
                line = item['source_line'] or 'N/A'
                if line not in by_line:
                    by_line[line] = []
                by_line[line].append(item)
            
            for line, instrs in sorted(by_line.items(), key=lambda x: (x[0] is None, x[0])):
                line_str = f"Line {line}" if line != 'N/A' else "No source line"
                print(f"   {line_str}:")
                for instr in instrs:
                    print(f"      [{instr['offset']}] {instr['instruction']}")
    
    print("\n" + "=" * 60)


def main():
    if sys.stdin.isatty():
        print("Usage: sui move coverage bytecode --module <name> | python3 parse_bytecode.py")
        print("       python3 parse_bytecode.py < bytecode_output.txt")
        sys.exit(1)
    
    input_text = sys.stdin.read()
    results = parse_bytecode_coverage(input_text)
    
    if '--json' in sys.argv:
        import json
        print(json.dumps(results, indent=2))
    else:
        print_report(results)


if __name__ == '__main__':
    main()


path: parse_source.py
content: #!/usr/bin/env python3
"""
Parse Sui Move source coverage output (ANSI colors) to identify uncovered code.

Usage:
    sui move coverage source --module <name> 2>&1 | python3 parse_source.py

Forces color output even when piped:
    script -q /dev/null sui move coverage source --module <name> | python3 parse_source.py
"""

import re
import sys
import json
from dataclasses import dataclass, field
from typing import List, Optional, Tuple

# ANSI color code patterns
ANSI_PATTERN = re.compile(r'\x1b\[(\d+)m')
GREEN_CODE = '32'
RED_CODE = '31'
YELLOW_CODE = '33'
RESET_CODE = '39'

@dataclass
class CodeSegment:
    text: str
    covered: Optional[bool]  # True=green, False=red, None=no color/neutral


@dataclass 
class LineAnalysis:
    line_num: int
    raw_text: str
    segments: List[CodeSegment] = field(default_factory=list)
    has_uncovered: bool = False
    uncovered_text: List[str] = field(default_factory=list)


def parse_ansi_line(line: str) -> List[CodeSegment]:
    """Parse a line with ANSI codes into segments."""
    segments = []
    current_color = None  # None=neutral, True=green, False=red
    
    # Split by ANSI codes
    parts = ANSI_PATTERN.split(line)
    
    i = 0
    current_text = ""
    
    while i < len(parts):
        part = parts[i]
        
        # Check if this is a color code
        if part == GREEN_CODE:
            if current_text:
                segments.append(CodeSegment(text=current_text, covered=current_color))
                current_text = ""
            current_color = True
        elif part == RED_CODE:
            if current_text:
                segments.append(CodeSegment(text=current_text, covered=current_color))
                current_text = ""
            current_color = False
        elif part == YELLOW_CODE:
            if current_text:
                segments.append(CodeSegment(text=current_text, covered=current_color))
                current_text = ""
            current_color = None  # Yellow = partial/neutral
        elif part in (RESET_CODE, '0', '39'):
            if current_text:
                segments.append(CodeSegment(text=current_text, covered=current_color))
                current_text = ""
            current_color = None
        else:
            # Regular text
            current_text += part
        
        i += 1
    
    # Don't forget remaining text
    if current_text:
        segments.append(CodeSegment(text=current_text, covered=current_color))
    
    return segments


def analyze_coverage(input_text: str) -> dict:
    """Analyze source coverage and return structured data."""
    lines = input_text.split('\n')
    
    results = {
        'lines': [],
        'uncovered_summary': [],
        'stats': {
            'total_lines': 0,
            'lines_with_uncovered': 0,
            'fully_covered_lines': 0,
        }
    }
    
    for line_num, line in enumerate(lines, 1):
        if not line.strip():
            continue
            
        segments = parse_ansi_line(line)
        
        # Check for uncovered (red) segments
        uncovered_texts = [seg.text for seg in segments if seg.covered == False and seg.text.strip()]
        has_uncovered = len(uncovered_texts) > 0
        
        # Check if any covered code
        has_covered = any(seg.covered == True for seg in segments)
        
        if has_covered or has_uncovered:
            results['stats']['total_lines'] += 1
            
            if has_uncovered:
                results['stats']['lines_with_uncovered'] += 1
                
                # Get clean line text (remove ANSI codes)
                clean_line = ANSI_PATTERN.sub('', line)
                
                line_analysis = {
                    'line': line_num,
                    'code': clean_line.strip(),
                    'uncovered_parts': uncovered_texts,
                }
                results['uncovered_summary'].append(line_analysis)
            else:
                results['stats']['fully_covered_lines'] += 1
    
    return results


def print_report(results: dict):
    """Print human-readable coverage report."""
    stats = results['stats']
    
    print("=" * 70)
    print("SOURCE COVERAGE ANALYSIS")
    print("=" * 70)
    
    total = stats['total_lines']
    covered = stats['fully_covered_lines']
    uncovered = stats['lines_with_uncovered']
    
    if total > 0:
        print(f"\nLines with executable code: {total}")
        print(f"Fully covered lines:        {covered} ({100*covered//total}%)")
        print(f"Lines with uncovered code:  {uncovered} ({100*uncovered//total}%)")
    
    if results['uncovered_summary']:
        print("\n" + "-" * 70)
        print("UNCOVERED CODE (RED SECTIONS)")
        print("-" * 70)
        
        for item in results['uncovered_summary']:
            print(f"\n📍 Line {item['line']}:")
            print(f"   {item['code']}")
            print(f"   🔴 Uncovered: {item['uncovered_parts']}")
    
    print("\n" + "=" * 70)


def print_json(results: dict):
    """Print JSON output."""
    print(json.dumps(results, indent=2))


def main():
    # Check for --help
    if '--help' in sys.argv or '-h' in sys.argv:
        print(__doc__)
        print("\nOptions:")
        print("  --json    Output as JSON")
        print("  --help    Show this help")
        sys.exit(0)
    
    # Read from stdin or show usage
    if sys.stdin.isatty():
        print("Usage: sui move coverage source --module <name> 2>&1 | python3 parse_source.py")
        print("\nTo preserve colors when piping, use one of:")
        print("  script -q /dev/null sui move coverage source --module <name> | python3 parse_source.py")
        print("  unbuffer sui move coverage source --module <name> | python3 parse_source.py")
        sys.exit(1)
    
    input_text = sys.stdin.read()
    
    # Check if we got any ANSI codes
    if '\x1b[' not in input_text and '[32m' not in input_text:
        print("Warning: No ANSI color codes detected in input.", file=sys.stderr)
        print("The coverage output may have lost colors during piping.", file=sys.stderr)
        print("Try: script -q /dev/null sui move coverage source --module <name> | python3 parse_source.py", file=sys.stderr)
    
    results = analyze_coverage(input_text)
    
    if '--json' in sys.argv:
        print_json(results)
    else:
        print_report(results)


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)