mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 17:07:50 +00:00
Both get a JPEG profile image made as the apps make them (quality 85, scaled down until the data URI is at most 12500 characters), red for ci_group and green for ci_channel. With them the search results no longer fit in one service response (10737 bytes, compressed if needed), so the directory splits them; the probe follows the cursor, prints each page's entries and sizes, and requires each result once, with its whole image, over at least two pages.
301 lines
11 KiB
Bash
Executable File
301 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Test backend on the app runner: a chat relay and a directory service on the preset servers, with one group
|
|
# and one channel registered and approved by a CLI peer, and a search request that must find both.
|
|
# Both have profile images as large as the apps make them, so their search results don't fit in one service
|
|
# response, and the search must page through them.
|
|
# Usage:
|
|
# scripts/ci/desktop-linux/backend.sh start <backend .tar.gz> <work dir> writes the directory's short address to <work dir>/directory-link
|
|
# scripts/ci/desktop-linux/backend.sh stop <work dir> stops the relay and the directory, prints their logs
|
|
# The relay and the directory keep running between start and stop.
|
|
|
|
set -euo pipefail
|
|
|
|
directory_name="SimpleX Directory"
|
|
admin=ci_admin
|
|
search_text="ci seed"
|
|
# the longest profile image data URI the apps make and the core accepts (maxProfileImageSize)
|
|
max_image_size=12500
|
|
# the largest service response body, compressed if it has to be (maxServiceBodyLength)
|
|
max_service_body=10737
|
|
group_colour="rgb(200,40,40)"
|
|
channel_colour="rgb(40,170,60)"
|
|
|
|
# Runs one chat command in a CLI client that stays online for <secs> after it; the output is in $work/out.
|
|
chat() {
|
|
local db=$1 secs=$2 cmd=$3
|
|
shift 3
|
|
local status=0
|
|
"$bin/simplex-chat" -d "$work/$db" -e "$cmd" -t "$secs" --execute-log all "$@" < /dev/null > "$work/out" 2>&1 || status=$?
|
|
cat "$work/out" >> "$work/$db-cli.log"
|
|
# commands with images are thousands of characters long
|
|
echo "::group::$db: ${cmd:0:200}"
|
|
cat "$work/out"
|
|
echo "::endgroup::"
|
|
return "$status"
|
|
}
|
|
|
|
admin() {
|
|
chat "$admin" "$1" "$2" --user-display-name "$admin"
|
|
}
|
|
|
|
expect_out() {
|
|
if ! grep -qE "$1" "$work/out"; then
|
|
echo "error: \"$1\" not in the output"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Each run keeps the client online for a few seconds, which also lets the handshakes it waits for progress.
|
|
admin_until() {
|
|
local pattern=$1 secs=$2 cmd=$3
|
|
local deadline=$((SECONDS + secs))
|
|
until admin 5 "$cmd" && grep -qE "$pattern" "$work/out"; do
|
|
if [ "$SECONDS" -ge "$deadline" ]; then
|
|
echo "error: \"$pattern\" not shown by \"$cmd\" after $secs s"
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
db_query() {
|
|
sqlite3 "$work/$1_chat.db" "$2"
|
|
}
|
|
|
|
# Prints a profile image data URI made as the apps make one: JPEG at quality 85, scaled down by the square root
|
|
# of its excess, at most halved per step, until it is at most max_image_size characters. The source is noise
|
|
# over the colour, which JPEG can't compress much, so the image ends up near the limit.
|
|
avatar() {
|
|
local colour=$1 source=$work/avatar-source.png file=$work/avatar.jpg size=512 uri
|
|
convert -size 512x512 xc:gray50 -seed 1 +noise Random -modulate 100,0 -fill "$colour" -colorize 70% "$source"
|
|
while :; do
|
|
convert "$source" -resize "${size}x${size}" -quality 85 "$file"
|
|
uri="data:image/jpg;base64,$(base64 -w 0 "$file")"
|
|
[ "${#uri}" -gt "$max_image_size" ] || break
|
|
size=$(awk -v s="$size" -v n="${#uri}" -v m="$max_image_size" 'BEGIN { r = sqrt(n / m); if (r > 2) r = 2; print int(s / r) }')
|
|
done
|
|
echo "avatar over $colour: ${size}x${size} px, ${#uri} characters, mean colour $(convert "$file" -resize '1x1!' -format '%[pixel:p{0,0}]' info:)" >&2
|
|
echo "$uri"
|
|
}
|
|
|
|
start_relay() {
|
|
nohup "$bin/simplex-chat" -d "$work/relay" --relay --headless --user-display-name ci_relay < /dev/null > "$work/relay.log" 2>&1 &
|
|
echo $! > "$work/relay.pid"
|
|
local deadline=$((SECONDS + 120))
|
|
until grep -qE '^https://[^ ]+/r#' "$work/relay.log"; do
|
|
if ! kill -0 "$(cat "$work/relay.pid")" 2> /dev/null || [ "$SECONDS" -ge "$deadline" ]; then
|
|
echo "error: the relay did not create its address"
|
|
cat "$work/relay.log"
|
|
return 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
relay_link=$(grep -m 1 -oE '^https://[^ ]+/r#[^ ]+' "$work/relay.log")
|
|
echo "relay address: $relay_link"
|
|
}
|
|
|
|
# The address is created here, like the directory tests do, with auto-accept on before the service subscribes to it.
|
|
create_directory() {
|
|
chat directory 1 "/ad pq_ratchet=on" --create-bot-display-name "$directory_name"
|
|
directory_link=$(awk '/Your new chat address is created!/ {getline; getline; print; exit}' "$work/out")
|
|
case "$directory_link" in
|
|
https://*/a#*) ;;
|
|
*)
|
|
echo "error: no short directory address in the output"
|
|
return 1
|
|
;;
|
|
esac
|
|
chat directory 1 "/auto_accept on"
|
|
expect_out "^auto_accept on"
|
|
echo "$directory_link" > "$work/directory-link"
|
|
echo "directory address: $directory_link"
|
|
}
|
|
|
|
start_directory() {
|
|
# -y: the service adds its own tables to the database the CLI created, and would otherwise ask on stdin
|
|
nohup "$bin/simplex-directory-service" -d "$work/directory" -y --super-users "$1" < /dev/null >> "$work/directory.log" 2>&1 &
|
|
echo $! > "$work/directory.pid"
|
|
sleep 5
|
|
if ! kill -0 "$(cat "$work/directory.pid")" 2> /dev/null; then
|
|
echo "error: the directory service exited"
|
|
cat "$work/directory.log"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_directory() {
|
|
local pid deadline
|
|
pid=$(cat "$work/directory.pid")
|
|
kill -INT "$pid" 2> /dev/null || true
|
|
deadline=$((SECONDS + 30))
|
|
while kill -0 "$pid" 2> /dev/null; do
|
|
if [ "$SECONDS" -ge "$deadline" ]; then
|
|
kill -KILL "$pid" 2> /dev/null || true
|
|
fi
|
|
sleep 1
|
|
done
|
|
}
|
|
|
|
# A super user is known by contact ID, which exists only after the admin connects, so the service starts
|
|
# with nobody and is restarted with the admin.
|
|
connect_admin() {
|
|
admin 10 "/c $directory_link"
|
|
expect_out "connection request sent!"
|
|
admin_until "Welcome to $directory_name" 180 "/tail @'$directory_name' 10"
|
|
local id
|
|
id=$(db_query directory "SELECT contact_id FROM contacts WHERE local_display_name = '$admin'")
|
|
stop_directory
|
|
start_directory "$id:$admin"
|
|
echo "directory restarted with super user $id:$admin"
|
|
}
|
|
|
|
# The directory sends each registration to super users as a message with the exact /approve command.
|
|
approve() {
|
|
local name=$1 reply=$2
|
|
admin_until "/approve [0-9]+:$name [0-9]+" 300 "/tail @'$directory_name' 20"
|
|
local cmd
|
|
cmd=$(grep -oE "/approve [0-9]+:$name [0-9]+" "$work/out" | tail -n 1)
|
|
admin 5 "@'$directory_name' $cmd"
|
|
admin_until "$reply" 120 "/tail @'$directory_name' 20"
|
|
}
|
|
|
|
# The CLI's group commands can't set an image, so groups are created with the API commands, with the profiles
|
|
# that "/g" and "/public group" make plus the image.
|
|
group_json() {
|
|
local name=$1 descr=$2 image=$3 prefs=$4
|
|
echo "{\"displayName\":\"$name\",\"fullName\":\"\",\"shortDescr\":\"$descr\",\"image\":\"$image\",\"groupPreferences\":{\"directMessages\":{\"enable\":\"on\"},\"history\":{\"enable\":\"on\"}$prefs}}"
|
|
}
|
|
|
|
seed_group() {
|
|
local user_id
|
|
user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'")
|
|
group_image=$(avatar "$group_colour")
|
|
admin 1 "/_group $user_id $(group_json ci_group "CI seed group" "$group_image" "")"
|
|
expect_out "group #ci_group \(CI seed group\) is created"
|
|
admin 5 "/a ci_group '$directory_name' admin"
|
|
expect_out "invitation to join the group #ci_group sent to"
|
|
approve ci_group "Group approved!"
|
|
}
|
|
|
|
seed_channel() {
|
|
admin 1 "/relays name=ci_relay $relay_link"
|
|
expect_out "^ok$"
|
|
local user_id relay_id
|
|
user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'")
|
|
relay_id=$(db_query "$admin" "SELECT chat_relay_id FROM chat_relays WHERE display_name = 'ci_relay' AND deleted = 0")
|
|
channel_image=$(avatar "$channel_colour")
|
|
admin 10 "/_public group $user_id $relay_id $(group_json ci_channel "CI seed channel" "$channel_image" ",\"support\":{\"enable\":\"off\"}")"
|
|
expect_out "group #ci_channel \(CI seed channel\) is created"
|
|
local group_id
|
|
group_id=$(db_query "$admin" "SELECT group_id FROM groups WHERE local_display_name = 'ci_channel'")
|
|
admin_until "relay id [0-9]+: active" 180 "/_get relays #$group_id"
|
|
admin 5 "/share chat #ci_channel @'$directory_name'"
|
|
expect_out "link to join channel #ci_channel"
|
|
approve ci_channel "Channel approved!"
|
|
}
|
|
|
|
page_summary() {
|
|
local resp=$1 compressed=""
|
|
if command -v zstd > /dev/null; then
|
|
compressed=", $(printf '%s' "$resp" | zstd -3 -q -c | wc -c) bytes zstd-compressed"
|
|
fi
|
|
jq -r '[.entries[] | "\(.displayName) (image: \(.image // "" | length) characters)"] | join(", ") | "entries: " + .' <<< "$resp"
|
|
echo "body: ${#resp} bytes$compressed, limit $max_service_body"
|
|
}
|
|
|
|
# What the app will send, from a client that never connected to the directory, following the cursor to the last page.
|
|
# The images make the results too large for one response, so the directory must split them, and each must arrive
|
|
# once and with its whole image.
|
|
probe_search() {
|
|
local cursor="" pages=0 started resp req
|
|
: > "$work/probe-entries"
|
|
while :; do
|
|
pages=$((pages + 1))
|
|
if [ "$pages" -gt 10 ]; then
|
|
echo "error: the search is still returning pages after 10"
|
|
return 1
|
|
fi
|
|
req="{\"type\":\"search\",\"searchText\":\"$search_text\"${cursor:+,\"searchCursor\":$cursor}}"
|
|
started=$SECONDS
|
|
chat probe 1 "/_service_request 1 $directory_link timeout=30 $req" --user-display-name ci_probe
|
|
resp=$(sed -n 's/^service response: //p' "$work/out")
|
|
if [ -z "$resp" ]; then
|
|
echo "error: no service response for page $pages"
|
|
return 1
|
|
fi
|
|
echo "page $pages, in $((SECONDS - started)) s:"
|
|
page_summary "$resp"
|
|
jq -r '.entries[] | "\(.displayName) \(.image // "" | length)"' <<< "$resp" >> "$work/probe-entries"
|
|
cursor=$(jq -c '.searchCursor // empty' <<< "$resp")
|
|
[ -n "$cursor" ] || break
|
|
done
|
|
local expected="ci_channel ${#channel_image}
|
|
ci_group ${#group_image}"
|
|
if [ "$(sort "$work/probe-entries")" != "$expected" ]; then
|
|
echo "error: expected each result once with its whole image (name, image length):"
|
|
echo "$expected"
|
|
echo "got:"
|
|
sort "$work/probe-entries"
|
|
return 1
|
|
fi
|
|
if [ "$pages" -lt 2 ]; then
|
|
echo "error: the results fit in one response, so the search no longer tests paging"
|
|
return 1
|
|
fi
|
|
echo "search for \"$search_text\" found ci_group and ci_channel with their images, in $pages pages"
|
|
}
|
|
|
|
dump_directory() {
|
|
echo "::group::directory registrations"
|
|
db_query directory "SELECT group_id, group_reg_status FROM sx_directory_group_regs" || true
|
|
echo "::endgroup::"
|
|
}
|
|
|
|
start() {
|
|
bin=$work/bin
|
|
mkdir -p "$bin"
|
|
tar -C "$bin" -xzf "$1"
|
|
"$bin/simplex-chat" --version
|
|
"$bin/simplex-directory-service" --version
|
|
|
|
start_relay
|
|
create_directory
|
|
start_directory "0:nobody"
|
|
trap dump_directory EXIT
|
|
connect_admin
|
|
seed_group
|
|
seed_channel
|
|
probe_search
|
|
}
|
|
|
|
stop() {
|
|
local p
|
|
for p in directory relay; do
|
|
if [ -f "$work/$p.pid" ]; then
|
|
kill -INT "$(cat "$work/$p.pid")" 2> /dev/null || true
|
|
fi
|
|
done
|
|
sleep 3
|
|
for p in directory relay; do
|
|
echo "::group::$p log"
|
|
cat "$work/$p.log" 2> /dev/null || true
|
|
echo "::endgroup::"
|
|
done
|
|
}
|
|
|
|
case "${1:-}" in
|
|
start)
|
|
work=$3
|
|
mkdir -p "$work"
|
|
start "$2"
|
|
;;
|
|
stop)
|
|
work=$2
|
|
stop
|
|
;;
|
|
*)
|
|
echo "usage: $0 start <backend .tar.gz> <work dir> | stop <work dir>"
|
|
exit 1
|
|
;;
|
|
esac
|