mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
Two files under the cap. test_security_hardening.sh (423) keeps its filename -- shell-tests.yml runs the suite by name -- and its five sections move into three sourced libs. They are top-level blocks, not functions, so each is wrapped and called in the original order; the counters and LIVE_SECTION stay in the entry, which sourcing shares. Verified by diffing a full run against the same suite from a detached worktree at HEAD: byte-identical output, same exit status, still 18/1/5 on this host. The one failure is pre-existing. Linting a lib alone turns a bare LIVE_SECTION=1 into SC2034, since its only reader is test_result in the entry. A live_section() setter makes the write a real reference, so no suppression was needed. The mutation-driver split was already in the tree from the previous session and did not run: the operators had moved to _session_autopsy_mutations.py but the driver still named Mutation without importing it, so mypy and pylint both reported undefined-variable. Restored the import; the script now reports all 12 tests killed. block_compulsive_opening.sh (705) is deliberately untouched and now says so in the contract. install_all copies the running script to /usr/local/bin, so an entry sourcing lib/ would resolve SCRIPT_DIR there, find nothing, and leave beeper, signal-desktop and discord unlaunchable with a failing pacman hook. Splitting it means teaching the installer to copy the libs -- a refactor, not a line-count split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.2 KiB
Python
Executable File
101 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""The mutation operators: one named defect each, injected into a hook copy.
|
|
|
|
Split out of ``mutate_session_autopsy_hooks.py`` to keep both files under the
|
|
repository's 250-line cap. Every function here takes the text of one hook and
|
|
returns it with exactly one guard broken; the driver applies them one at a time
|
|
and records which tests notice.
|
|
|
|
Nothing here touches the filesystem -- these are pure text transforms, which is
|
|
what makes the driver's "the deployed hooks are never modified" guarantee easy
|
|
to see.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, NamedTuple
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
END_HOOK = "session_autopsy_end.sh"
|
|
START_HOOK = "session_autopsy_start.sh"
|
|
_GUTTED = "#!/bin/bash\nexit 0\n"
|
|
|
|
|
|
def _gut(_text: str) -> str:
|
|
"""Replace an entire hook body with a bare successful exit."""
|
|
return _GUTTED
|
|
|
|
|
|
def _drop_transcript_guard(text: str) -> str:
|
|
"""Remove the SessionEnd guard that requires an existing transcript."""
|
|
return re.sub(
|
|
r'if \[\[ -z "\$transcript".*?\nfi\n',
|
|
"",
|
|
text,
|
|
flags=re.DOTALL,
|
|
)
|
|
|
|
|
|
def _widen_count(text: str) -> str:
|
|
"""Make a zero candidate count announce itself."""
|
|
return text.replace("(( count > 0 ))", "(( count >= 0 ))")
|
|
|
|
|
|
def _drop_numeric_guard(text: str) -> str:
|
|
"""Drop the count regex guard, leaving the arithmetic syntactically valid."""
|
|
return text.replace(
|
|
'[[ "$count" =~ ^[0-9]+$ ]] && (( count > 0 ))',
|
|
"(( count > 0 ))",
|
|
)
|
|
|
|
|
|
def _drop_readable_guard(text: str) -> str:
|
|
"""Remove the early exit taken when the state file cannot be read."""
|
|
return re.sub(
|
|
r'if \[\[ ! -r "\$STATE" \]\]; then\n.*?\nfi\n',
|
|
"",
|
|
text,
|
|
flags=re.DOTALL,
|
|
)
|
|
|
|
|
|
def _always_announce(text: str) -> str:
|
|
"""Announce unconditionally -- the defect every silence test exists to catch."""
|
|
return re.sub(r'if \[\[ "\$count".*?; then', "if true; then", text)
|
|
|
|
|
|
def _unguarded_and_always_announce(text: str) -> str:
|
|
"""Drop the readability guard AND announce unconditionally.
|
|
|
|
Needed because the readability guard is redundant on its own: jq's stderr is
|
|
suppressed and an empty count already fails the arithmetic, so removing it
|
|
changes nothing observable. Only removing it together with the count
|
|
condition can reach the no-state and unreadable-state cases.
|
|
"""
|
|
return _always_announce(_drop_readable_guard(text))
|
|
|
|
|
|
class Mutation(NamedTuple):
|
|
"""One named defect injected into one hook."""
|
|
|
|
name: str
|
|
hook: str
|
|
apply: Callable[[str], str]
|
|
|
|
|
|
MUTATIONS = (
|
|
Mutation("gut-end", END_HOOK, _gut),
|
|
Mutation("gut-start", START_HOOK, _gut),
|
|
Mutation("end-no-transcript-guard", END_HOOK, _drop_transcript_guard),
|
|
Mutation("start-count-ge-zero", START_HOOK, _widen_count),
|
|
Mutation("start-no-numeric-guard", START_HOOK, _drop_numeric_guard),
|
|
Mutation("start-no-readable-guard", START_HOOK, _drop_readable_guard),
|
|
Mutation("start-always-announce", START_HOOK, _always_announce),
|
|
Mutation("start-unguarded-announce", START_HOOK, _unguarded_and_always_announce),
|
|
)
|
|
|
|
__all__ = ["END_HOOK", "MUTATIONS", "START_HOOK", "Mutation"]
|