aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorEgor Tensin <egor@tensin.name>2026-10-04 21:41:24 +0500
committerEgor Tensin <egor@tensin.name>2026-10-04 21:41:24 +0500
commit57109eace4733153d4897cce42e44520f4efffae (patch)
treea220463377c288cad059fc587078b08a1e7d7f63
parentdebian: ditch Vagrant, switch to Docker (diff)
parentworkflows/ci: minor formatting fixes (diff)
downloadlinux-status-57109eace4733153d4897cce42e44520f4efffae.tar.gz
linux-status-57109eace4733153d4897cce42e44520f4efffae.zip
Merge tag 'v2.4.13' into debian
v2.4.13
-rw-r--r--.github/workflows/ci.yml46
-rw-r--r--test/lib/log.sh139
-rw-r--r--test/lib/test.sh62
-rwxr-xr-xtest/main.sh77
-rwxr-xr-xtest/test.sh205
-rw-r--r--test/tst-cgi.sh48
-rw-r--r--test/tst-curl.sh80
7 files changed, 420 insertions, 237 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b6dc4e7..6f42ecd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,14 @@ jobs:
test:
strategy:
matrix:
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.x']
+ python-version:
+ - '3.9'
+ - '3.10'
+ - '3.11'
+ - '3.12'
+ - '3.13'
+ - '3.14'
+ - '3.x'
runs-on: ubuntu-latest
name: 'Python ${{ matrix.python-version }}'
steps:
@@ -33,9 +40,9 @@ jobs:
with:
python-version: '${{ matrix.python-version }}'
- name: Run tests as root
- run: sudo ./test/test.sh
+ run: sudo ./test/main.sh
- name: Run tests
- run: ./test/test.sh
+ run: ./test/main.sh
aur:
name: Publish to AUR
@@ -48,6 +55,9 @@ jobs:
&& !startsWith(github.ref, 'refs/tags/debian/')
container:
image: archlinux:base-devel
+ env:
+ SSH_KEY: |
+ ${{ secrets.SSH_KEY }}
steps:
# This step must be done before actions/checkout. From the latter's
# output:
@@ -57,39 +67,11 @@ jobs:
- name: Install dependencies
run: |
pacman -Syu --noconfirm git openssh
-
- name: Checkout
uses: actions/checkout@v7
with:
ref: aur
token: '${{ secrets.GH_TOKEN }}'
-
- # makepkg for no reason hard-forbids running as root. Kill me now.
- - name: Add builder user
- run: |
- useradd -m -s /bin/bash builder
- passwd -d builder
- echo 'builder ALL=(ALL) ALL' >> /etc/sudoers
- chown -R builder:builder -- "$GITHUB_WORKSPACE"
-
- - name: Configure SSH
- run: |
- mkdir -p /home/builder/.ssh/
- cat > /home/builder/.ssh/config <<'EOF'
- StrictHostKeyChecking no
- EOF
- cat > /home/builder/.ssh/id_ed25519 <<'EOF'
- ${{ secrets.SSH_KEY }}
- EOF
- chown -R builder:builder /home/builder/.ssh
- chmod 0600 /home/builder/.ssh/id_ed25519
-
- - name: Configure git
- run: |
- # See https://github.com/actions/checkout#push-a-commit-using-the-built-in-token
- runuser -u builder -- git -C "$GITHUB_WORKSPACE" config user.name "github-actions[bot]"
- runuser -u builder -- git -C "$GITHUB_WORKSPACE" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-
- name: Run maintenance
run: |
- runuser -u builder -- make -C "$GITHUB_WORKSPACE" maintenance
+ make -C "$GITHUB_WORKSPACE" ci
diff --git a/test/lib/log.sh b/test/lib/log.sh
new file mode 100644
index 0000000..35b7a2d
--- /dev/null
+++ b/test/lib/log.sh
@@ -0,0 +1,139 @@
+# Copyright (c) 2026 Egor Tensin <egor@tensin.name>
+# This file is part of the "linux-status" project.
+# For details, see https://github.com/egor-tensin/linux-status
+# Distributed under the MIT License.
+
+log() {
+ local msg
+ for msg; do
+ echo -e "$test_file | $msg" >&2
+ done
+}
+
+log_run() {
+ if [ "$#" -lt 1 ]; then
+ log "usage: ${FUNCNAME[0]} ARGV0 [ARG...]"
+ return 1
+ fi
+
+ local msg='Running:'
+ msg="$msg$( printf -- ' %q' "$@" )"
+ log "$msg"
+}
+
+fail() {
+ local msg
+ for msg; do
+ log "FAIL: $msg"
+ done
+}
+
+fail_details() {
+ local msg
+ for msg; do
+ fail " $msg"
+ done
+}
+
+log_numof_tests_ok=0
+log_numof_tests_failed=0
+declare -a log_tests_failed=()
+declare -A log_tests_should_fail=()
+
+log_test_start() {
+ if [ "$#" -ne 1 ]; then
+ echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2
+ return 1
+ fi
+
+ local test_file="$1"
+
+ if [ -n "${test_should_fail:-}" ]; then
+ log_tests_should_fail["$test_file"]=1
+ fi
+
+ cat <<EOF >&2
+
+======================================================================
+TEST: $test_file
+======================================================================
+EOF
+
+ log "Should fail: ${test_should_fail:-No}"
+}
+
+log_test_ok() {
+ if [ "$#" -ne 1 ]; then
+ echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2
+ return 1
+ fi
+
+ log_numof_tests_ok=$((log_numof_tests_ok + 1))
+
+ cat <<EOF >&2
+----------------------------------------------------------------------
+OK: $1
+----------------------------------------------------------------------
+EOF
+}
+
+log_test_fail() {
+ if [ "$#" -ne 1 ]; then
+ echo "usage: ${FUNCNAME[0]} TEST_FILE" >&2
+ return 1
+ fi
+
+ log_numof_tests_failed=$((log_numof_tests_failed + 1))
+ log_tests_failed+=("$1")
+
+ cat <<EOF >&2
+----------------------------------------------------------------------
+FAIL: $1
+----------------------------------------------------------------------
+EOF
+}
+
+log_test_finish() {
+ if [ "$#" -ne 2 ]; then
+ echo "usage: ${FUNCNAME[0]} TEST_FILE EXIT_CODE" >&2
+ return 1
+ fi
+
+ local test_file="$1"
+ local ret="$2"
+ local test_should_fail
+ test_should_fail="${log_tests_should_fail["$test_file"]:+x}"
+
+ if [ "$ret" -eq 0 ] || [ -n "$test_should_fail" ]; then
+ log_test_ok "$test_file"
+ else
+ log_test_fail "$test_file"
+ fi
+}
+
+log_tests_summary() {
+ cat <<EOF >&2
+
+======================================================================
+SUMMARY
+======================================================================
+OK: $log_numof_tests_ok
+FAIL: $log_numof_tests_failed
+EOF
+
+ local test_file
+ for test_file in ${log_tests_failed[@]+"${log_tests_failed[@]}"}; do
+ cat <<EOF >&2
+FAIL: $test_file
+EOF
+ done
+
+ cat <<EOF >&2
+----------------------------------------------------------------------
+EOF
+
+ local ret=0
+ [ "$log_numof_tests_failed" -gt 0 ] && ret=1
+
+ return "$ret"
+}
diff --git a/test/lib/test.sh b/test/lib/test.sh
new file mode 100644
index 0000000..ee35b05
--- /dev/null
+++ b/test/lib/test.sh
@@ -0,0 +1,62 @@
+# Copyright (c) 2026 Egor Tensin <egor@tensin.name>
+# This file is part of the "linux-status" project.
+# For details, see https://github.com/egor-tensin/linux-status
+# Distributed under the MIT License.
+
+test_should_fail=
+test_root_dir=
+
+test_server_port=35605
+test_server_pid=
+
+_test_run_server() {
+ log "Starting up server..."
+ log_run "$script_dir/../src/server.py" --port "$test_server_port"
+ "$script_dir/../src/server.py" --port "$test_server_port" >&2 &
+ test_server_pid="$!"
+ log "Server's PID: $test_server_pid"
+ sleep 5
+}
+
+_test_kill_server() {
+ if [ -n "$test_server_pid" ]; then
+ log "Stopping server: $test_server_pid"
+ kill "$test_server_pid"
+ log "Waiting for it to terminate..."
+ wait "$test_server_pid" || true
+ fi
+}
+
+test_setup() {
+ test_root_dir="$( mktemp -d )"
+ _test_run_server
+}
+
+test_cleanup_default() {
+ if [ -n "$test_root_dir" ]; then
+ log "Removing test's root directory: $test_root_dir"
+ rm -rf -- "$test_root_dir"
+ fi
+ _test_kill_server
+}
+
+test_check_output() {
+ if [ "$#" -lt 1 ]; then
+ log "usage: ${FUNCNAME[0]} PATH [KEYWORD...]"
+ return 1
+ fi
+
+ local path="$1"
+ shift
+
+ local keyword
+ for keyword; do
+ if ! grep --fixed-strings --quiet -- "$keyword" "$path"; then
+ fail "The following pattern hasn't been found in $path:"
+ fail_details "$keyword"
+ log "Output:"
+ cat -- "$path" >&2
+ return 1
+ fi
+ done
+}
diff --git a/test/main.sh b/test/main.sh
new file mode 100755
index 0000000..a9e2feb
--- /dev/null
+++ b/test/main.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2026 Egor Tensin <egor@tensin.name>
+# This file is part of the "linux-status" project.
+# For details, see https://github.com/egor-tensin/linux-status
+# Distributed under the MIT License.
+
+set -o errexit -o nounset -o pipefail
+shopt -s inherit_errexit lastpipe
+
+script_dir="$( dirname -- "${BASH_SOURCE[0]}" )"
+script_dir="$( cd -- "$script_dir" && pwd )"
+readonly script_dir
+script_name="$( basename -- "${BASH_SOURCE[0]}" )"
+readonly script_name
+
+source "$script_dir/lib/log.sh"
+
+run_test_file() {
+ local test_file
+ for test_file; do
+ source "$script_dir/lib/test.sh"
+ source "$script_dir/$test_file"
+
+ log_test_start "$test_file"
+
+ set +e
+ (
+ set -o errexit -o nounset -o pipefail
+ shopt -s inherit_errexit lastpipe
+
+ if [ "$( type -t test_cleanup )" == function ]; then
+ trap test_cleanup EXIT
+ else
+ trap test_cleanup_default EXIT
+ fi
+
+ test_run
+ )
+ local ret="$?"
+ set -e
+
+ log_test_finish "$test_file" "$ret"
+ done
+}
+
+main() {
+ if [ "$#" -gt 1 ]; then
+ echo "usage: $script_name [PATTERN]" >&2
+ return 1
+ fi
+
+ local -a test_files=()
+ local test_file
+
+ local pattern='*'
+ [ "$#" -gt 0 ] && pattern="$1"
+
+ find "$script_dir" \
+ -mindepth 1 \
+ -maxdepth 1 \
+ -type f \
+ -regex '.*/tst-.*\.sh$' \
+ -regextype posix-basic \
+ -name "$pattern" \
+ -printf '%P\0' \
+ | sort -z \
+ | \
+ while IFS= read -d '' -r test_file; do
+ test_files+=("$test_file")
+ done
+
+ run_test_file ${test_files[@]+"${test_files[@]}"}
+ log_tests_summary
+}
+
+main "$@"
diff --git a/test/test.sh b/test/test.sh
deleted file mode 100755
index 32d2767..0000000
--- a/test/test.sh
+++ /dev/null
@@ -1,205 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021 Egor Tensin <egor@tensin.name>
-# This file is part of the "linux-status" project.
-# For details, see https://github.com/egor-tensin/linux-status
-# Distributed under the MIT License.
-
-set -o errexit -o nounset -o pipefail
-shopt -s inherit_errexit lastpipe
-
-script_dir="$( dirname -- "${BASH_SOURCE[0]}" )"
-script_dir="$( cd -- "$script_dir" && pwd )"
-readonly script_dir
-script_name="$( basename -- "${BASH_SOURCE[0]}" )"
-readonly script_name
-
-readonly server_port=18101
-server_pid=
-curl_header_file=
-curl_output_file=
-
-dump() {
- local msg
- for msg; do
- echo "$script_name: $msg"
- done
-}
-
-run_server() {
- dump "Starting up server..."
- "$script_dir/../src/server.py" --port "$server_port" &
- server_pid="$!"
- dump "Its PID is $server_pid"
- sleep 5
-}
-
-kill_server() {
- [ -z "$server_pid" ] && return
- dump "Killing server with PID $server_pid..."
- kill "$server_pid"
- dump "Waiting for it to terminate..."
- wait "$server_pid" || true
- dump "Done"
-}
-
-create_files() {
- curl_header_file="$( mktemp )"
- curl_output_file="$( mktemp )"
- dump "curl header file: $curl_header_file"
- dump "curl output file: $curl_output_file"
-}
-
-cleanup_files() {
- dump "Cleaning up curl files..."
- [ -n "$curl_header_file" ] && rm -f -- "$curl_header_file"
- [ -n "$curl_output_file" ] && rm -f -- "$curl_output_file"
-}
-
-prepare() {
- run_server
- create_files
-}
-
-cleanup() {
- kill_server || true
- cleanup_files || true
-}
-
-run_curl() {
- if [ "$#" -ne 1 ]; then
- echo "usage: ${FUNCNAME[0]} URL" >&2
- return 1
- fi
- local url="$1"
- curl \
- --silent --show-error \
- --dump-header "$curl_header_file" \
- --output "$curl_output_file" \
- --connect-timeout 3 \
- -- "http://localhost:$server_port$url" || true
-}
-
-curl_check_status() {
- if [ "$#" -ne 1 ]; then
- echo "usage: ${FUNCNAME[0]} HTTP_STATUS" >&2
- return 1
- fi
-
- local expected="$1"
- expected="HTTP/1.0 $expected"$'\r'
- local actual
- actual="$( head -n 1 -- "$curl_header_file" )"
-
- [ "$expected" == "$actual" ] && return 0
-
- dump "Actual HTTP response: $actual" >&2
- dump "Expected: $expected" >&2
-
- dump 'HTTP headers:' >&2
- cat -- "$curl_header_file" >&2
- dump 'HTTP response:' >&2
- cat -- "$curl_output_file" >&2
- return 1
-}
-
-curl_check_keyword() {
- local keyword
- for keyword; do
- if ! grep --fixed-strings --quiet -- "$keyword" "$curl_output_file"; then
- dump "The following pattern hasn't been found:"
- dump "$keyword"
- dump "The output was:"
- cat -- "$curl_output_file"
- return 1
- fi
- done
-}
-
-run_curl_test() {
- if [ "$#" -lt 1 ]; then
- echo "usage: ${FUNCNAME[0]} URL [KEYWORD...]" >&2
- return 1
- fi
-
- local url="$1"
- shift
- dump "Running test for URL: $url"
-
- run_curl "$url"
- curl_check_status '200 OK'
-
- local keyword
- for keyword; do
- curl_check_keyword "$keyword"
- done
-}
-
-run_curl_tests() {
- # / and /index.html are identical:
- run_curl_test '/' '<link rel="stylesheet" href="css/bootstrap.min.css">' 'var status_refresh_interval_seconds'
- run_curl_test '/index.html' '<link rel="stylesheet" href="css/bootstrap.min.css">' 'var status_refresh_interval_seconds'
-
- # /status returns a JSON with a number of fields:
- run_curl_test '/status' '"hostname":' '"thermal":' '"system":' '"user":'
- # /top is `top` output:
- run_curl_test '/top' 'load average:'
- # /thermal is also an endpoint:
- run_curl_test '/thermal'
-}
-
-cgi_check_header() {
- local expected='200 OK'
- local actual
- actual="$( head -n 1 -- "$curl_output_file" )"
-
- [ "$expected" == "$actual" ] && return 0
-
- dump "Actual CGI header: $actual" >&2
- dump "Expected: $expected" >&2
-
- diff <( echo "$actual" ) <( echo "$expected" ) | cat -te
- return 1
-}
-
-run_cgi_test() {
- if [ "$#" -lt 1 ]; then
- echo "usage: ${FUNCNAME[0]} WHAT [KEYWORD...]" >&2
- return 1
- fi
-
- local what="$1"
- shift
-
- local query_string="what=$what"
- dump "Running CGI test for query string: $query_string"
-
- QUERY_STRING="$query_string" "$script_dir/../src/app.py" > "$curl_output_file"
-
- cgi_check_header
-
- local keyword
- for keyword; do
- curl_check_keyword "$keyword"
- done
-}
-
-run_cgi_tests() {
- # Check that app.py still works as a CGI script.
-
- # /status returns a JSON with a number of fields:
- run_cgi_test 'status' '"hostname":' '"thermal":' '"system":' '"user":'
- # /top is `top` output:
- run_cgi_test 'top' 'load average:'
- # /thermal is also an endpoint:
- run_cgi_test 'thermal'
-}
-
-main() {
- trap cleanup EXIT
- prepare
- run_curl_tests
- run_cgi_tests
-}
-
-main "$@"
diff --git a/test/tst-cgi.sh b/test/tst-cgi.sh
new file mode 100644
index 0000000..bb2eb01
--- /dev/null
+++ b/test/tst-cgi.sh
@@ -0,0 +1,48 @@
+test_cgi_response=
+
+_test_cgi_check_status() {
+ local expected='200 OK'
+ local actual
+ actual="$( head -n 1 -- "$test_cgi_response" )"
+
+ [ "$expected" == "$actual" ] && return 0
+
+ fail "Unexpected HTTP status: $actual"
+ fail_details "Expected: $expected"
+
+ diff <( echo "$actual" ) <( echo "$expected" ) | cat -te >&2
+ return 1
+}
+
+_test_cgi() {
+ if [ "$#" -lt 1 ]; then
+ log "usage: ${FUNCNAME[0]} WHAT [KEYWORD...]"
+ return 1
+ fi
+
+ local what="$1"
+ shift
+
+ local query_string="what=$what"
+ log "Running CGI test for query string: $query_string"
+
+ QUERY_STRING="$query_string" "$script_dir/../src/app.py" > "$test_cgi_response"
+
+ _test_cgi_check_status
+ test_check_output "$test_cgi_response" "$@"
+}
+
+test_run() {
+ # Check that app.py still works as a CGI script.
+
+ test_setup
+
+ test_cgi_response="$test_root_dir/response"
+
+ # /status returns a JSON with a number of fields:
+ _test_cgi 'status' '"hostname":' '"thermal":' '"system":' '"user":'
+ # /top is `top` output:
+ _test_cgi 'top' 'load average:'
+ # /thermal is also an endpoint:
+ _test_cgi 'thermal'
+}
diff --git a/test/tst-curl.sh b/test/tst-curl.sh
new file mode 100644
index 0000000..a8f9860
--- /dev/null
+++ b/test/tst-curl.sh
@@ -0,0 +1,80 @@
+test_curl_header=
+test_curl_output=
+
+_curl() {
+ if [ "$#" -ne 1 ]; then
+ log "usage: ${FUNCNAME[0]} URL"
+ return 1
+ fi
+
+ local url="$1"
+
+ curl \
+ --silent --show-error \
+ --dump-header "$test_curl_header" \
+ --output "$test_curl_output" \
+ --connect-timeout 3 \
+ -- \
+ "http://127.0.0.1:$test_server_port$url" \
+ || true
+}
+
+_test_curl_check_status() {
+ if [ "$#" -ne 1 ]; then
+ log "usage: ${FUNCNAME[0]} HTTP_STATUS"
+ return 1
+ fi
+
+ local expected="$1"
+ expected="HTTP/1.0 $expected"$'\r'
+ local actual
+ actual="$( head -n 1 -- "$test_curl_header" )"
+
+ [ "$expected" == "$actual" ] && return 0
+
+ fail "Unexpected HTTP status: $actual"
+ fail_details "Expected: $expected"
+
+ log 'Response headers:'
+ cat -- "$test_curl_header" >&2
+ log 'Response data:'
+ cat -- "$test_curl_output" >&2
+ return 1
+}
+
+_test_curl() {
+ if [ "$#" -lt 1 ]; then
+ log "usage: ${FUNCNAME[0]} URL [KEYWORD...]"
+ return 1
+ fi
+
+ local url="$1"
+ shift
+ log "Running test for URL: $url"
+
+ _curl "$url"
+ _test_curl_check_status '200 OK'
+ test_check_output "$test_curl_output" "$@"
+}
+
+test_run() {
+ test_setup
+
+ test_curl_header="$test_root_dir/header"
+ test_curl_output="$test_root_dir/output"
+
+ # / and /index.html are identical:
+ _test_curl '/' \
+ '<link rel="stylesheet" href="css/bootstrap.min.css">' \
+ 'var status_refresh_interval_seconds'
+ _test_curl '/index.html' \
+ '<link rel="stylesheet" href="css/bootstrap.min.css">' \
+ 'var status_refresh_interval_seconds'
+
+ # /status returns a JSON with a number of fields:
+ _test_curl '/status' '"hostname":' '"thermal":' '"system":' '"user":'
+ # /top is `top` output:
+ _test_curl '/top' 'load average:'
+ # /thermal is also an endpoint:
+ _test_curl '/thermal'
+}