270 lines
9.4 KiB
Bash
Executable File
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'
|