Files
daily-blackout-check/tests/test_main.sh
MeghdadFadaee bf09d4d99c
All checks were successful
Test blackout notifier / test (push) Successful in 5s
Hourly blackout check / check (push) Successful in 5s
feat(cli): add height-aware compact outage schedule
2026-07-27 18:03:18 +03:30

270 lines
9.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=../main.sh
source "$ROOT_DIR/main.sh"
TEST_TMP="$(mktemp -d "${TMPDIR:-/tmp}/blackout-cli-tests.XXXXXX")"
trap 'rm -rf -- "$TEST_TMP"' EXIT
assert_equal() {
local expected="$1"
local actual="$2"
local message="$3"
if [[ "$expected" != "$actual" ]]; then
printf 'FAIL: %s\nExpected: %s\nActual: %s\n' "$message" "$expected" "$actual" >&2
exit 1
fi
}
assert_contains() {
local text="$1"
local expected="$2"
local message="$3"
if [[ "$text" != *"$expected"* ]]; then
printf 'FAIL: %s\nMissing: %s\n' "$message" "$expected" >&2
exit 1
fi
}
FIXTURE="$TEST_TMP/outages.json"
cat >"$FIXTURE" <<'JSON'
{
"schema_version": 1,
"updated_at": "2026-07-18T16:00:00+03:30",
"bills": {
"222": {
"events": {
"5": {
"status": "active",
"snapshot": {
"outage_number": "5",
"outage_date": "1405/04/28",
"start_time": "09:00",
"stop_time": "11:00",
"address": "Future planned address",
"reason": "Maintenance",
"is_planned": true
}
}
}
},
"111": {
"events": {
"1": {
"status": "active",
"snapshot": {
"outage_number": "1",
"outage_date": "1405/04/27",
"start_time": "16:00",
"stop_time": "18:00",
"address": "Current address",
"reason": "Load management",
"is_planned": true
}
},
"2": {
"status": "active",
"snapshot": {
"outage_number": "2",
"outage_date": "1405/04/27",
"start_time": "18:00",
"stop_time": "20:00",
"address": "Emergency address",
"reason": "Emergency load management",
"is_planned": false
}
},
"3": {
"status": "cancelled",
"snapshot": {
"outage_number": "3",
"outage_date": "1405/04/27",
"start_time": "19:00",
"stop_time": "21:00",
"address": "Cancelled address",
"reason": "Cancelled maintenance",
"is_planned": true
}
},
"4": {
"status": "active",
"snapshot": {
"outage_number": "4",
"outage_date": "1405/04/27",
"start_time": "10:00",
"stop_time": "11:00",
"address": "Expired address",
"reason": "Old event",
"is_planned": true
}
}
}
}
}
}
JSON
assert_equal "2026-07-18" "$(jalali_to_gregorian "1405/04/27")" \
"Jalali dates should convert to Gregorian dates"
read -r cross_start cross_stop < <(event_epochs "1405/04/27" "23:00" "01:00")
assert_equal "7200" "$((cross_stop - cross_start))" \
"cross-midnight outages should end on the following day"
assert_equal "01 : 01 : 01" "$(banner_duration 3661)" \
"countdowns should include hours, minutes, and seconds"
assert_equal "49 : 30 : 00" "$(banner_duration 178200)" \
"countdowns should use total hours beyond one day"
assert_equal "17" "$(LINES=17 terminal_lines)" \
"the terminal height override should be respected"
FONT_OUTPUT="$(print_large_countdown "00 : 00 : 00" 150)"
assert_contains "$FONT_OUTPUT" ',a8888a,' \
"the countdown should consistently use the univers-style font"
if ! validate_state "$FIXTURE"; then
printf 'FAIL: valid fixture was rejected\n' >&2
exit 1
fi
NOW_EPOCH="$(TZ=Asia/Tehran date -d '2026-07-18 17:00:00' +%s)"
build_schedule "$FIXTURE" "$NOW_EPOCH"
assert_equal "3" "${#SCHEDULE[@]}" \
"only active current and future events should be scheduled"
assert_equal "ONGOING" "${HERO%%$'\t'*}" \
"an ongoing outage should take the hero position"
assert_contains "${SCHEDULE[0]}" '"outage_number":"1"' \
"ongoing events should sort first"
assert_contains "${SCHEDULE[1]}" '"outage_number":"2"' \
"the nearest future event should sort second"
assert_contains "${SCHEDULE[2]}" '"outage_number":"5"' \
"later events should remain in the schedule"
build_schedule "$FIXTURE" "$NOW_EPOCH" "222"
assert_equal "1" "${#SCHEDULE[@]}" \
"selecting a bill should filter the schedule"
assert_equal "UPCOMING" "${HERO%%$'\t'*}" \
"the countdown should ignore another bill's ongoing outage"
assert_contains "$HERO" '"bill_id":"222"' \
"the countdown should use only the selected bill"
SELECTED_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "222")"
assert_contains "$SELECTED_OUTPUT" "COUNTDOWN FOR BILL 222" \
"the selected countdown bill should be labeled"
assert_contains "$SELECTED_OUTPUT" "OUTAGE SCHEDULE (1)" \
"the schedule should contain only the selected bill"
if [[ "$SELECTED_OUTPUT" == *"Bill 111"* ]]; then
printf 'FAIL: selected bill output contains another bill\n' >&2
exit 1
fi
build_schedule "$FIXTURE" "$NOW_EPOCH" "333"
assert_equal "" "$HERO" \
"a bill without upcoming outages should not borrow another bill's countdown"
assert_equal "0" "${#SCHEDULE[@]}" \
"a bill without upcoming outages should have an empty schedule"
NO_HERO_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "333")"
assert_contains "$NO_HERO_OUTPUT" "ALL CLEAR" \
"a selected bill without outages should render the all-clear screen"
build_schedule "$FIXTURE" "$NOW_EPOCH"
OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH")"
if [[ "$OUTPUT" == *"POWER RETURNS IN"* || "$OUTPUT" == *"NEXT OUTAGE IN"* ]]; then
printf 'FAIL: countdown output contains a redundant heading\n' >&2
exit 1
fi
assert_contains "$OUTPUT" "PLANNED" \
"planned events should be labeled"
assert_contains "$OUTPUT" "EMERGENCY" \
"unplanned events should be labeled"
assert_contains "$OUTPUT" "OUTAGE SCHEDULE (3)" \
"the dashboard should include every selected event"
if [[ "$OUTPUT" == *"Address"* || "$OUTPUT" == *"Reason"* \
|| "$OUTPUT" == *"Emergency address"* || "$OUTPUT" == *"Load management"* ]]; then
printf 'FAIL: dashboard output exposes address or reason text\n' >&2
exit 1
fi
if [[ "$OUTPUT" == *$'\033['* ]]; then
printf 'FAIL: NO_COLOR output contains ANSI escapes\n' >&2
exit 1
fi
SHORT_OUTPUT="$(NO_COLOR=1 COLUMNS=80 LINES=12 render_dashboard "$NOW_EPOCH" 80 "" 12)"
SHORT_LINE_COUNT="$(awk 'END { print NR }' <<<"$SHORT_OUTPUT")"
assert_equal "12" "$SHORT_LINE_COUNT" \
"a dashboard with extra outages should fit the terminal height"
assert_contains "$SHORT_OUTPUT" '01 : 00 : 00' \
"the countdown should remain visible when schedule rows are limited"
assert_contains "$SHORT_OUTPUT" "... 2 more outages" \
"the dashboard should report schedule rows that do not fit"
SHORT_BOTTOM_LINE="${SHORT_OUTPUT##*$'\n'}"
assert_equal "$(repeat_character '=' 80)" "$SHORT_BOTTOM_LINE" \
"the compact schedule should end with a full-width rule"
WIDE_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$NOW_EPOCH")"
WIDE_TOP_LINE="${WIDE_OUTPUT%%$'\n'*}"
assert_equal "180" "${#WIDE_TOP_LINE}" \
"the countdown divider should span the full terminal width"
WIDE_BOTTOM_LINE="${WIDE_OUTPUT##*$'\n'}"
assert_equal "180" "${#WIDE_BOTTOM_LINE}" \
"the dashboard bottom divider should span the full terminal width"
assert_equal "$(repeat_character '=' 180)" "$WIDE_BOTTOM_LINE" \
"the dashboard should end with the same rule as the countdown section"
FUTURE_NOW="$(TZ=Asia/Tehran date -d '2026-07-18 15:00:00' +%s)"
build_schedule "$FIXTURE" "$FUTURE_NOW"
assert_equal "UPCOMING" "${HERO%%$'\t'*}" \
"the nearest future event should be the hero when none are ongoing"
EMPTY_NOW="$(TZ=Asia/Tehran date -d '2026-07-20 12:00:00' +%s)"
build_schedule "$FIXTURE" "$EMPTY_NOW"
assert_equal "0" "${#SCHEDULE[@]}" \
"expired schedules should become empty"
EMPTY_OUTPUT="$(NO_COLOR=1 COLUMNS=60 render_dashboard "$EMPTY_NOW")"
assert_contains "$EMPTY_OUTPUT" "ALL CLEAR" \
"an empty schedule should render the all-clear screen"
WIDE_EMPTY_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$EMPTY_NOW")"
WIDE_EMPTY_TOP_LINE="${WIDE_EMPTY_OUTPUT%%$'\n'*}"
WIDE_EMPTY_BOTTOM_LINE="${WIDE_EMPTY_OUTPUT##*$'\n'}"
assert_equal "180" "${#WIDE_EMPTY_TOP_LINE}" \
"the all-clear top divider should span the full terminal width"
assert_equal "180" "${#WIDE_EMPTY_BOTTOM_LINE}" \
"the all-clear bottom divider should span the full terminal width"
WIDE_UNAVAILABLE_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_unavailable 180)"
WIDE_UNAVAILABLE_TOP_LINE="${WIDE_UNAVAILABLE_OUTPUT%%$'\n'*}"
WIDE_UNAVAILABLE_BOTTOM_LINE="${WIDE_UNAVAILABLE_OUTPUT##*$'\n'}"
assert_equal "180" "${#WIDE_UNAVAILABLE_TOP_LINE}" \
"the unavailable top divider should span the full terminal width"
assert_equal "180" "${#WIDE_UNAVAILABLE_BOTTOM_LINE}" \
"the unavailable bottom divider should use the detected terminal width"
TERMINAL_ACTIVE=1
DRAW_OUTPUT="$(draw_frame $'short\nlonger')"
TERMINAL_ACTIVE=0
assert_contains "$DRAW_OUTPUT" $'short\033[K\nlonger\033[K' \
"interactive redraws should erase old content from every line"
MALFORMED="$TEST_TMP/malformed.json"
printf '{"schema_version": 999, "bills": {}}\n' >"$MALFORMED"
if validate_state "$MALFORMED"; then
printf 'FAIL: unsupported schema was accepted\n' >&2
exit 1
fi
if (
curl() { return 22; }
fetch_state "$TEST_TMP/download.json" "123"
); then
printf 'FAIL: download failure was reported as success\n' >&2
exit 1
fi
if main --bill-id invalid >/dev/null 2>&1; then
printf 'FAIL: a non-decimal countdown bill ID was accepted\n' >&2
exit 1
fi
printf 'Bash CLI tests passed\n'