240 lines
5.8 KiB
Bash
Executable File
240 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-test-quality-gate.sh — Integration-style tests for validate-test-quality.sh.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
PASS_COUNT=0
|
|
FAIL_COUNT=0
|
|
declare -a TMP_REPOS=()
|
|
|
|
log() {
|
|
echo "[test-quality-test] $*"
|
|
}
|
|
|
|
pass() {
|
|
PASS_COUNT=$((PASS_COUNT + 1))
|
|
log "PASS: $*"
|
|
}
|
|
|
|
fail() {
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
log "FAIL: $*"
|
|
}
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "Missing required command: $1"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_expect_success() {
|
|
local label="$1"
|
|
shift
|
|
if "$@" >/tmp/test-quality-gate.out 2>&1; then
|
|
pass "$label"
|
|
else
|
|
fail "$label"
|
|
cat /tmp/test-quality-gate.out
|
|
fi
|
|
}
|
|
|
|
run_expect_failure() {
|
|
local label="$1"
|
|
shift
|
|
if "$@" >/tmp/test-quality-gate.out 2>&1; then
|
|
fail "$label (expected failure but command succeeded)"
|
|
cat /tmp/test-quality-gate.out
|
|
else
|
|
pass "$label"
|
|
fi
|
|
}
|
|
|
|
create_fixture_repo() {
|
|
local repo
|
|
repo="$(mktemp -d)"
|
|
|
|
mkdir -p "$repo/scripts" \
|
|
"$repo/audit" \
|
|
"$repo/androidApp/src/androidTest/kotlin/example" \
|
|
"$repo/iosApp/iosAppUITests"
|
|
|
|
cp "$PROJECT_ROOT/scripts/validate-test-quality.sh" "$repo/scripts/"
|
|
chmod +x "$repo/scripts/validate-test-quality.sh"
|
|
|
|
cat > "$repo/androidApp/src/androidTest/kotlin/example/ExampleUiTest.kt" <<'EOF'
|
|
package example
|
|
|
|
import org.junit.Test
|
|
|
|
class ExampleUiTest {
|
|
@Test
|
|
fun usesAntiPatternsForFixture() {
|
|
Thread.sleep(5)
|
|
try {
|
|
// fixture-only
|
|
} catch (e: AssertionError) {
|
|
// fixture-only
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "$repo/iosApp/iosAppUITests/ExampleUiTests.swift" <<'EOF'
|
|
import XCTest
|
|
|
|
final class ExampleUiTests: XCTestCase {
|
|
func testFixtureUsesAntiPatterns() {
|
|
sleep(1)
|
|
if XCUIApplication().buttons["Example"].exists {
|
|
XCTAssertTrue(true)
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "$repo/audit/test-quality-baseline.json" <<'EOF'
|
|
{
|
|
"version": 1,
|
|
"generated_at": "2026-02-20T16:00:00Z",
|
|
"metrics": [
|
|
{
|
|
"id": "android_thread_sleep_calls",
|
|
"description": "Android Thread.sleep",
|
|
"mode": "rg",
|
|
"root": "androidApp/src/androidTest",
|
|
"glob": "*.kt",
|
|
"pattern": "Thread\\.sleep\\(",
|
|
"baseline": 1,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "android_assertionerror_catches",
|
|
"description": "Android AssertionError catches",
|
|
"mode": "rg",
|
|
"root": "androidApp/src/androidTest",
|
|
"glob": "*.kt",
|
|
"pattern": "catch \\([^\\)]*AssertionError",
|
|
"baseline": 1,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "ios_sleep_calls",
|
|
"description": "iOS sleep calls",
|
|
"mode": "rg",
|
|
"root": "iosApp/iosAppUITests",
|
|
"glob": "*.swift",
|
|
"pattern": "\\bsleep\\(",
|
|
"baseline": 1,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "ios_conditional_exists_guards_in_test_bodies",
|
|
"description": "iOS conditional exists checks in test bodies",
|
|
"mode": "swift_test_body_pattern",
|
|
"root": "iosApp/iosAppUITests",
|
|
"glob": "*.swift",
|
|
"pattern": "if[[:space:]]+[^\\n]*\\.exists",
|
|
"baseline": 1,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "ios_noop_assert_true",
|
|
"description": "iOS no-op assertTrue(true) in test bodies",
|
|
"mode": "swift_test_body_pattern",
|
|
"root": "iosApp/iosAppUITests",
|
|
"glob": "*.swift",
|
|
"pattern": "XCTAssertTrue\\(true\\)",
|
|
"baseline": 1,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "ios_empty_test_bodies",
|
|
"description": "iOS empty or comment-only test bodies",
|
|
"mode": "rg_multiline",
|
|
"root": "iosApp/iosAppUITests",
|
|
"glob": "*.swift",
|
|
"pattern": "(?s)func\\s+test[[:alnum:]_]+\\s*\\([^)]*\\)\\s*(?:throws\\s*)?\\{\\s*(?:(?://[^\\n]*\\n)\\s*)*\\}",
|
|
"baseline": 0,
|
|
"allowed_growth": 0
|
|
},
|
|
{
|
|
"id": "ios_placeholder_test_markers",
|
|
"description": "iOS placeholder markers in test bodies",
|
|
"mode": "swift_test_body_pattern",
|
|
"root": "iosApp/iosAppUITests",
|
|
"glob": "*.swift",
|
|
"pattern": "(TODO|FIXME|placeholder|no-op)",
|
|
"baseline": 0,
|
|
"allowed_growth": 0
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
|
|
TMP_REPOS+=("$repo")
|
|
echo "$repo"
|
|
}
|
|
|
|
test_baseline_pass() {
|
|
local repo
|
|
repo="$(create_fixture_repo)"
|
|
run_expect_success "validate-test-quality passes when metrics match baseline" \
|
|
bash -lc "cd '$repo' && scripts/validate-test-quality.sh"
|
|
}
|
|
|
|
test_growth_fails() {
|
|
local repo
|
|
repo="$(create_fixture_repo)"
|
|
echo "Thread.sleep(10)" >> "$repo/androidApp/src/androidTest/kotlin/example/ExampleUiTest.kt"
|
|
run_expect_failure "validate-test-quality fails when metric grows past threshold" \
|
|
bash -lc "cd '$repo' && scripts/validate-test-quality.sh"
|
|
}
|
|
|
|
test_allowed_growth_passes() {
|
|
local repo
|
|
repo="$(create_fixture_repo)"
|
|
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
jq '(.metrics[] | select(.id == "ios_sleep_calls") | .allowed_growth) = 1' \
|
|
"$repo/audit/test-quality-baseline.json" > "$tmp"
|
|
mv "$tmp" "$repo/audit/test-quality-baseline.json"
|
|
|
|
echo "sleep(1)" >> "$repo/iosApp/iosAppUITests/ExampleUiTests.swift"
|
|
|
|
run_expect_success "validate-test-quality honors allowed_growth threshold" \
|
|
bash -lc "cd '$repo' && scripts/validate-test-quality.sh"
|
|
}
|
|
|
|
main() {
|
|
require_cmd jq
|
|
require_cmd rg
|
|
require_cmd awk
|
|
|
|
test_baseline_pass
|
|
test_growth_fails
|
|
test_allowed_growth_passes
|
|
|
|
log "Summary: pass=$PASS_COUNT fail=$FAIL_COUNT"
|
|
if [[ "$FAIL_COUNT" -gt 0 ]]; then
|
|
exit 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
cleanup() {
|
|
local repo
|
|
for repo in "${TMP_REPOS[@]:-}"; do
|
|
[[ -d "$repo" ]] && rm -rf "$repo"
|
|
done
|
|
rm -f /tmp/test-quality-gate.out
|
|
return 0
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
main "$@"
|