{
  "name": "long-context-test-generation",
  "description": "Write comprehensive test cases for a 100+ line Python module with 6-8 functions, covering edge cases",
  "dataset": "terminal-bench-local",
  "difficulty": "medium",
  "instruction": "You are given text_processor.py, a module with 8 functions for processing and analyzing text. Your task is to create a file called test_text_processor.py with comprehensive unit tests using Python's unittest module.\n\nRequirements:\n1. Write tests for ALL 8 functions in the module\n2. Each function should have at least 3 test cases covering: normal input, edge cases (empty strings, None, special characters), and boundary conditions\n3. Test names should be descriptive (e.g., test_word_count_empty_string)\n4. All tests must pass when run\n5. Tests should verify both correct return values AND that appropriate exceptions are raised for invalid input\n\nRead text_processor.py carefully to understand what each function does, its expected inputs/outputs, and what errors it should raise. Then create comprehensive tests.\n\nRun python3 verify_tests.py to check your work.",
  "setup_files": {
    "text_processor.py": "\"\"\"Text processing utilities.\n\nA collection of functions for analyzing, transforming, and processing text.\nAll functions are well-documented with expected behavior including edge cases.\n\"\"\"\n\nimport re\nfrom collections import Counter\n\n\ndef word_count(text):\n    \"\"\"Count the number of words in text.\n    \n    Words are separated by whitespace. Multiple consecutive whitespace\n    characters count as a single separator.\n    \n    Args:\n        text: A string to count words in.\n        \n    Returns:\n        An integer count of words.\n        \n    Raises:\n        TypeError: If text is not a string.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    stripped = text.strip()\n    if not stripped:\n        return 0\n    return len(stripped.split())\n\n\ndef find_most_common_words(text, n=5):\n    \"\"\"Find the n most common words in text.\n    \n    Words are lowercased and stripped of punctuation before counting.\n    \n    Args:\n        text: A string of text.\n        n: Number of top words to return (default 5).\n        \n    Returns:\n        A list of (word, count) tuples sorted by count descending,\n        then alphabetically for ties.\n        \n    Raises:\n        TypeError: If text is not a string.\n        ValueError: If n is less than 1.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    if n < 1:\n        raise ValueError(f\"n must be >= 1, got {n}\")\n    \n    words = re.findall(r'[a-zA-Z]+', text.lower())\n    if not words:\n        return []\n    \n    counts = Counter(words)\n    sorted_words = sorted(counts.items(), key=lambda x: (-x[1], x[0]))\n    return sorted_words[:n]\n\n\ndef truncate_text(text, max_length, suffix='...'):\n    \"\"\"Truncate text to max_length characters, adding suffix if truncated.\n    \n    If text is shorter than or equal to max_length, returns it unchanged.\n    If truncated, tries to break at the last word boundary before max_length.\n    The suffix is included in the max_length count.\n    \n    Args:\n        text: The string to truncate.\n        max_length: Maximum length of the result including suffix.\n        suffix: String to append when truncating (default '...').\n        \n    Returns:\n        The truncated string.\n        \n    Raises:\n        TypeError: If text is not a string.\n        ValueError: If max_length is less than len(suffix).\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    if max_length < len(suffix):\n        raise ValueError(f\"max_length ({max_length}) must be >= suffix length ({len(suffix)})\")\n    \n    if len(text) <= max_length:\n        return text\n    \n    available = max_length - len(suffix)\n    truncated = text[:available]\n    \n    last_space = truncated.rfind(' ')\n    if last_space > 0:\n        truncated = truncated[:last_space]\n    \n    return truncated.rstrip() + suffix\n\n\ndef extract_emails(text):\n    \"\"\"Extract all email addresses from text.\n    \n    Uses a simple regex pattern that matches common email formats.\n    \n    Args:\n        text: A string that may contain email addresses.\n        \n    Returns:\n        A list of email address strings found in the text.\n        Emails are returned in the order they appear, with duplicates removed.\n        \n    Raises:\n        TypeError: If text is not a string.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    \n    pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'\n    found = re.findall(pattern, text)\n    \n    seen = set()\n    result = []\n    for email in found:\n        lower_email = email.lower()\n        if lower_email not in seen:\n            seen.add(lower_email)\n            result.append(email)\n    return result\n\n\ndef caesar_cipher(text, shift):\n    \"\"\"Apply Caesar cipher to text.\n    \n    Only shifts ASCII letters (a-z, A-Z). Preserves case.\n    Non-letter characters are left unchanged.\n    \n    Args:\n        text: The string to encrypt/decrypt.\n        shift: Integer shift amount. Positive shifts right, negative shifts left.\n        \n    Returns:\n        The shifted string.\n        \n    Raises:\n        TypeError: If text is not a string or shift is not an int.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str for text, got {type(text).__name__}\")\n    if not isinstance(shift, int):\n        raise TypeError(f\"Expected int for shift, got {type(shift).__name__}\")\n    \n    result = []\n    for char in text:\n        if char.isalpha():\n            base = ord('A') if char.isupper() else ord('a')\n            shifted = (ord(char) - base + shift) % 26 + base\n            result.append(chr(shifted))\n        else:\n            result.append(char)\n    return ''.join(result)\n\n\ndef text_statistics(text):\n    \"\"\"Compute various statistics about the text.\n    \n    Args:\n        text: A string to analyze.\n        \n    Returns:\n        A dict with keys:\n            - 'char_count': total characters including spaces\n            - 'word_count': number of words\n            - 'sentence_count': number of sentences (split on .!?)\n            - 'avg_word_length': average word length (float, 0.0 if no words)\n            - 'longest_word': the longest word (empty string if no words)\n            \n    Raises:\n        TypeError: If text is not a string.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    \n    words = re.findall(r'[a-zA-Z]+', text)\n    sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]\n    \n    avg_len = sum(len(w) for w in words) / len(words) if words else 0.0\n    longest = max(words, key=len) if words else ''\n    \n    return {\n        'char_count': len(text),\n        'word_count': len(words),\n        'sentence_count': len(sentences),\n        'avg_word_length': round(avg_len, 2),\n        'longest_word': longest\n    }\n\n\ndef wrap_text(text, width=80):\n    \"\"\"Wrap text to specified width.\n    \n    Breaks lines at word boundaries. If a single word is longer than width,\n    it is placed on its own line (not broken).\n    \n    Args:\n        text: The string to wrap.\n        width: Maximum line width (default 80).\n        \n    Returns:\n        A string with newlines inserted at wrap points.\n        \n    Raises:\n        TypeError: If text is not a string.\n        ValueError: If width is less than 1.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str, got {type(text).__name__}\")\n    if width < 1:\n        raise ValueError(f\"width must be >= 1, got {width}\")\n    \n    words = text.split()\n    if not words:\n        return ''\n    \n    lines = []\n    current_line = words[0]\n    \n    for word in words[1:]:\n        if len(current_line) + 1 + len(word) <= width:\n            current_line += ' ' + word\n        else:\n            lines.append(current_line)\n            current_line = word\n    \n    lines.append(current_line)\n    return '\\n'.join(lines)\n\n\ndef replace_pattern(text, pattern, replacement, case_sensitive=True):\n    \"\"\"Replace all occurrences of a regex pattern in text.\n    \n    Args:\n        text: The input string.\n        pattern: A regex pattern string.\n        replacement: The replacement string.\n        case_sensitive: If False, matches case-insensitively (default True).\n        \n    Returns:\n        The string with replacements made.\n        \n    Raises:\n        TypeError: If text, pattern, or replacement is not a string.\n        re.error: If pattern is an invalid regex.\n    \"\"\"\n    if not isinstance(text, str):\n        raise TypeError(f\"Expected str for text, got {type(text).__name__}\")\n    if not isinstance(pattern, str):\n        raise TypeError(f\"Expected str for pattern, got {type(pattern).__name__}\")\n    if not isinstance(replacement, str):\n        raise TypeError(f\"Expected str for replacement, got {type(replacement).__name__}\")\n    \n    flags = 0 if case_sensitive else re.IGNORECASE\n    return re.sub(pattern, replacement, text, flags=flags)\n",
    "verify_tests.py": "\"\"\"Verify that test_text_processor.py exists, is comprehensive, and passes.\"\"\"\nimport sys\nimport os\nimport ast\nimport subprocess\n\nerrors = []\n\n# Check file exists\nif not os.path.exists('test_text_processor.py'):\n    print(\"FAIL: test_text_processor.py does not exist\")\n    sys.exit(1)\n\n# Parse the test file AST to check coverage\nwith open('test_text_processor.py', 'r') as f:\n    source = f.read()\n\ntry:\n    tree = ast.parse(source)\nexcept SyntaxError as e:\n    print(f\"FAIL: test_text_processor.py has syntax error: {e}\")\n    sys.exit(1)\n\n# Find all test methods\ntest_methods = []\nfor node in ast.walk(tree):\n    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n        if node.name.startswith('test_'):\n            test_methods.append(node.name)\n\n# Check minimum test count\nif len(test_methods) < 24:\n    errors.append(f\"Expected at least 24 test methods (3 per function x 8 functions), found {len(test_methods)}\")\n\n# Check that tests cover all 8 functions\nfunction_names = [\n    'word_count', 'find_most_common_words', 'truncate_text', 'extract_emails',\n    'caesar_cipher', 'text_statistics', 'wrap_text', 'replace_pattern'\n]\n\nfor func_name in function_names:\n    matching_tests = [t for t in test_methods if func_name in t]\n    if len(matching_tests) < 3:\n        errors.append(f\"Function '{func_name}' has {len(matching_tests)} tests, need at least 3\")\n\n# Check for edge case testing patterns in the source\nedge_case_patterns = ['empty', 'none', 'special', 'error', 'raises', 'invalid', 'boundary', 'zero', 'single', 'negative']\nedge_tests_found = 0\nfor t in test_methods:\n    t_lower = t.lower()\n    for pattern in edge_case_patterns:\n        if pattern in t_lower:\n            edge_tests_found += 1\n            break\n\nif edge_tests_found < 8:\n    errors.append(f\"Found {edge_tests_found} edge case tests, expected at least 8\")\n\n# Check that assertRaises or with self.assertRaises is used (for TypeError/ValueError tests)\nif 'assertRaises' not in source and 'raises' not in source.lower():\n    errors.append(\"No assertRaises found - tests should check exception handling\")\n\n# Now actually run the tests\nresult = subprocess.run(\n    [sys.executable, '-m', 'pytest', 'test_text_processor.py', '-v', '--tb=short'],\n    capture_output=True, text=True, timeout=30\n)\n\n# If pytest not available, try unittest\nif result.returncode != 0 and 'No module named pytest' in result.stderr:\n    result = subprocess.run(\n        [sys.executable, '-m', 'unittest', 'test_text_processor', '-v'],\n        capture_output=True, text=True, timeout=30\n    )\n\nif result.returncode != 0:\n    # Check if it's a test failure vs crash\n    output = result.stdout + result.stderr\n    if 'FAILED' in output or 'FAIL:' in output or 'Error' in output:\n        errors.append(f\"Some tests failed. Output:\\n{output[-500:]}\")\n    else:\n        errors.append(f\"Tests could not run. Output:\\n{output[-500:]}\")\n\n# Check how many tests actually ran\noutput = result.stdout + result.stderr\nimport re\nran_match = re.search(r'Ran (\\d+) test', output)\nif ran_match:\n    ran_count = int(ran_match.group(1))\n    if ran_count < 24:\n        errors.append(f\"Only {ran_count} tests ran, expected at least 24\")\nelse:\n    # Try pytest format\n    pytest_match = re.search(r'(\\d+) passed', output)\n    if pytest_match:\n        ran_count = int(pytest_match.group(1))\n        if ran_count < 24:\n            errors.append(f\"Only {ran_count} tests passed, expected at least 24\")\n\nif errors:\n    print(f\"FAILED with {len(errors)} error(s):\")\n    for e in errors:\n        print(f\"  - {e}\")\n    sys.exit(1)\nelse:\n    print(\"ALL CHECKS PASSED\")\n    print(f\"  - {len(test_methods)} test methods found\")\n    print(f\"  - All 8 functions covered with 3+ tests each\")\n    print(f\"  - Edge case tests present\")\n    print(f\"  - Exception handling tested\")\n    print(f\"  - All tests pass\")\n    sys.exit(0)\n"
  },
  "verify": "cd $BENCH_WORK_DIR && python3 verify_tests.py",
  "timeout": 360000,
  "tags": ["long-context", "test-generation", "python", "coverage"]
}