Compare commits

..

4 Commits

Author SHA1 Message Date
56ede1b45f refactor: address code review feedback
All checks were successful
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Linux (debian) (pull_request) Successful in 16m1s
CI Code / Linux (arch) (pull_request) Successful in 21m7s
CI Code / Linux (ubuntu) (pull_request) Successful in 16m24s
CI Code / Code Coverage (pull_request) Successful in 15m23s
Makefile.am:
- Replace 4 hardcoded parallel commands with loop over FUNC_TEST_GROUPS
- Easy to extend by adding group numbers to the variable

functionaltests.c:
- Remove test counts from comments (prone to becoming outdated)
- Restore previous comment style with === separators for readability
- Replace switch with groups[] array structure for extensibility

proftest.h:
- Fix comment to describe current state, not the change made

CONTRIBUTING.md:
- Document test groups and parallel execution commands
- Add instructions for adding new test groups
2026-01-21 15:33:48 +03:00
49b552dcb1 feat: add code coverage with branch coverage
All checks were successful
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 15m31s
CI Code / Linux (debian) (pull_request) Successful in 16m1s
CI Code / Linux (arch) (pull_request) Successful in 16m11s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m24s
- Add --enable-coverage configure option
- Add lcov targets for generating coverage reports
- Enable branch coverage (--rc branch_coverage=1)
- Install lcov in all Docker images
- Coverage report displayed via lcov --summary (no Codecov - doesn't work with Gitea)
2026-01-19 20:49:37 +03:00
5a34a4a1d3 perf: optimize CI build performance
- Add -C flag to configure for caching
- Use ccache for faster recompilation
2026-01-19 16:38:34 +03:00
cb2cd1bbf2 feat: parallel functional tests (~3x faster CI)
- Split 69 functional tests into 4 groups for parallel execution
- Add check-functional-parallel target to Makefile.am
- Fix VPATH builds with $(srcdir)/tests path
- Add test failure detection verification in CI
- Update Dockerfiles with parallel make -j$(nproc)
- Add --depth 1 for faster git clone in Dockerfiles
2026-01-19 16:38:19 +03:00
50 changed files with 348 additions and 1662 deletions

View File

@@ -50,9 +50,6 @@ jobs:
run: | run: |
grep -P 'auto_(char|gchar|gcharv|guchar|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true grep -P 'auto_(char|gchar|gcharv|guchar|jid|sqlite|gfd|FILE)[\w *]*;$' -r src && exit -1 || true
- name: Check CWE-134 format string vulnerabilities
run: ./check-cwe134.sh
- name: Install clang-format - name: Install clang-format
run: | run: |
sudo apt-get update sudo apt-get update
@@ -109,5 +106,20 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build and run coverage - name: Build and run coverage
run: | run: |
docker build -f Dockerfile.arch -t profanity-cov . docker build -f Dockerfile.debian -t profanity-cov .
docker run profanity-cov ./ci-build.sh --coverage-only docker run -v ${{ github.workspace }}/coverage:/coverage profanity-cov bash -c '
./bootstrap.sh
./configure --enable-coverage --enable-otr --enable-pgp --enable-omemo --enable-plugins
make -j$(nproc)
make check || true
make check-functional-parallel || true
lcov --capture --directory . --output-file /coverage/coverage.info \
--rc branch_coverage=1 \
--ignore-errors inconsistent
lcov --remove /coverage/coverage.info \
"/usr/include/*" "*/tests/*" \
--output-file /coverage/coverage.info \
--rc branch_coverage=1 \
--ignore-errors inconsistent,empty,unused
lcov --summary /coverage/coverage.info
'

6
.gitignore vendored
View File

@@ -62,8 +62,6 @@ tests/unittests/unittests
tests/unittests/unittests.log tests/unittests/unittests.log
tests/unittests/unittests.trs tests/unittests/unittests.trs
test-suite.log test-suite.log
test-files/
test-logs/
# valgrind output # valgrind output
profval* profval*
@@ -110,7 +108,3 @@ breaks
*.zip *.zip
*.log* *.log*
coverage/ coverage/
*.gcno
*.gcda
*.gcov
coverage.info

View File

@@ -90,24 +90,6 @@ set -e
``` ```
This will run the same tests that the CI runs and refuse the push if it fails. This will run the same tests that the CI runs and refuse the push if it fails.
The CI script runs 4 parallel builds with different configurations:
- **Full** — all features enabled (+ coverage in `--coverage-only` mode)
- **Minimal** — all optional features disabled
- **NoEncrypt** — no encryption (OTR, PGP, OMEMO disabled)
- **Default** — default ./configure options
Each build runs Valgrind and functional tests on Linux.
Use `./ci-build.sh --coverage-only` to run only the Full build with coverage collection.
Output shows test results per build:
```
✓ Full PASSED
Unit tests: 437 passed, 0 failed
Functional tests: 69 passed, 0 failed
Coverage: Lines: 27.5% | Functions: 36.2% | Branches: 18.1%
Duration: 5m39s
```
Note that it will run on the actual content of the repository directory and not Note that it will run on the actual content of the repository directory and not
what may have been staged/committed. what may have been staged/committed.
@@ -144,16 +126,6 @@ scan-build make
scan-view ... scan-view ...
``` ```
### Security checks
We have a static analyzer `check-cwe134.sh` that detects CWE-134 format string vulnerabilities. It runs automatically in CI but you can also run it locally:
```bash
./check-cwe134.sh
```
This checks for unsafe patterns where data could be passed directly as a format string to functions like `printf`, `cons_show`, etc. Never pass a raw string for formatting; use `"%s"` format specifier instead.
### Finding typos ### Finding typos
We include a `.codespellrc` configuration file for `codespell` in the root directory. We include a `.codespellrc` configuration file for `codespell` in the root directory.

View File

@@ -1,10 +1,9 @@
FROM archlinux:latest FROM archlinux
ENV TERM=xterm ENV TERM=xterm
ENV CC="ccache gcc" ENV CC="ccache gcc"
RUN pacman -Syyu --noconfirm RUN pacman -Syu --noconfirm
# reflector is optional - if it fails due to network issues, continue with default mirrorlist # reflector is optional - if it fails due to network issues, continue with default mirrorlist
RUN pacman -S --needed --noconfirm reflector && \ RUN pacman -S --needed --noconfirm reflector && \
(reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true) (reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true)
@@ -14,7 +13,6 @@ RUN pacman -S --needed --noconfirm \
autoconf-archive \ autoconf-archive \
automake \ automake \
base-devel \ base-devel \
ca-certificates \
ccache \ ccache \
check \ check \
cmake \ cmake \
@@ -66,7 +64,7 @@ USER root
RUN pacman -U --noconfirm libstrophe-git/libstrophe-git-*.pkg.tar.zst RUN pacman -U --noconfirm libstrophe-git/libstrophe-git-*.pkg.tar.zst
WORKDIR /usr/src WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber WORKDIR /usr/src/stabber
RUN ./bootstrap.sh RUN ./bootstrap.sh

View File

@@ -9,7 +9,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \ autoconf \
autoconf-archive \ autoconf-archive \
automake \ automake \
ca-certificates \
ccache \ ccache \
gcc \ gcc \
git \ git \
@@ -41,8 +40,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity} RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
RUN git clone --depth 1 https://github.com/strophe/libstrophe RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
WORKDIR /usr/src/stabber WORKDIR /usr/src/stabber
RUN ./bootstrap.sh RUN ./bootstrap.sh

View File

@@ -11,7 +11,6 @@ RUN dnf install -y \
autoconf-archive \ autoconf-archive \
automake \ automake \
awk \ awk \
ca-certificates \
ccache \ ccache \
gcc \ gcc \
git \ git \
@@ -51,7 +50,7 @@ ENV TERM=xterm
RUN mkdir -p /usr/src RUN mkdir -p /usr/src
WORKDIR /usr/src WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber WORKDIR /usr/src/stabber
RUN ./bootstrap.sh RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking RUN ./configure --prefix=/usr --disable-dependency-tracking
@@ -60,7 +59,7 @@ RUN make install
WORKDIR /usr/src WORKDIR /usr/src
RUN mkdir -p /usr/src/libstrophe RUN mkdir -p /usr/src/libstrophe
RUN git clone --depth 1 https://github.com/strophe/libstrophe RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
WORKDIR /usr/src/libstrophe WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh RUN ./bootstrap.sh
RUN ./configure --prefix=/usr RUN ./configure --prefix=/usr

View File

@@ -10,7 +10,6 @@ RUN zypper --non-interactive in --no-recommends \
autoconf \ autoconf \
autoconf-archive \ autoconf-archive \
automake \ automake \
ca-certificates \
ccache \ ccache \
gcc \ gcc \
git \ git \
@@ -50,7 +49,7 @@ ENV TERM=xterm
RUN mkdir -p /usr/src RUN mkdir -p /usr/src
WORKDIR /usr/src WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber WORKDIR /usr/src/stabber
RUN ./bootstrap.sh RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking RUN ./configure --prefix=/usr --disable-dependency-tracking

View File

@@ -8,7 +8,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \ autoconf \
autoconf-archive \ autoconf-archive \
automake \ automake \
ca-certificates \
ccache \ ccache \
gcc \ gcc \
git \ git \
@@ -40,8 +39,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity} RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src WORKDIR /usr/src
RUN git clone --depth 1 https://git.jabber.space/devs/stabber RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
RUN git clone --depth 1 https://github.com/strophe/libstrophe RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
WORKDIR /usr/src/stabber WORKDIR /usr/src/stabber
RUN ./bootstrap.sh RUN ./bootstrap.sh

View File

@@ -185,9 +185,6 @@ functionaltest_sources = \
tests/functionaltests/test_software.c tests/functionaltests/test_software.h \ tests/functionaltests/test_software.c tests/functionaltests/test_software.h \
tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \ tests/functionaltests/test_muc.c tests/functionaltests/test_muc.h \
tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \ tests/functionaltests/test_disconnect.c tests/functionaltests/test_disconnect.h \
tests/functionaltests/test_lastactivity.c tests/functionaltests/test_lastactivity.h \
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
tests/functionaltests/functionaltests.c tests/functionaltests/functionaltests.c
main_source = src/main.c main_source = src/main.c
@@ -316,7 +313,7 @@ FUNC_TEST_GROUPS = 1 2 3 4
check-functional-parallel: tests/functionaltests/functionaltests check-functional-parallel: tests/functionaltests/functionaltests
@echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..." @echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..."
@mkdir -p $(builddir)/test-logs $(builddir)/test-files @mkdir -p $(builddir)/test-logs
@pids=""; \ @pids=""; \
for g in $(FUNC_TEST_GROUPS); do \ for g in $(FUNC_TEST_GROUPS); do \
./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \ ./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \
@@ -375,10 +372,7 @@ clean-local:
rm -f $(git_include) $(git_include).in rm -f $(git_include) $(git_include).in
endif endif
clean-functional-tests: .PHONY: my-prof.supp
rm -rf $(builddir)/test-files $(builddir)/test-logs
.PHONY: my-prof.supp clean-functional-tests
my-prof.supp: my-prof.supp:
@sed '/^# AUTO-GENERATED START/q' prof.supp > $@ @sed '/^# AUTO-GENERATED START/q' prof.supp > $@
@printf "\n\n# glib\n" >> $@ @printf "\n\n# glib\n" >> $@
@@ -394,7 +388,7 @@ check-unit: tests/unittests/unittests
tests/unittests/unittests tests/unittests/unittests
@VALGRIND_CHECK_RULES@ @VALGRIND_CHECK_RULES@
VALGRIND_SUPPRESSIONS_FILES=$(srcdir)/prof.supp VALGRIND_SUPPRESSIONS_FILES=prof.supp
# Code coverage targets (requires --enable-coverage) # Code coverage targets (requires --enable-coverage)
coverage-clean: coverage-clean:

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# check-cwe134.sh - Static analysis for CWE-134 format string vulnerabilities
#
# This script detects potentially unsafe usage of format string functions
# where user-controlled data may be passed without "%s" wrapper.
#
# Usage: ./check-cwe134.sh [directory]
set -e
DIR="${1:-src}"
echo "=== CWE-134 Format String Vulnerability Check ==="
echo "Scanning: $DIR"
echo ""
# Functions that accept format strings
FORMAT_FUNCS="cons_show|cons_debug|cons_show_error|log_info|log_error|log_warning|log_debug|win_println|win_print"
ERRORS=0
echo "Checking for unsafe format string usage..."
echo ""
# Pattern 1: function call with single variable argument (no format string)
# Example: cons_show(variable); - BAD
# Example: cons_show("%s", variable); - OK
# Matches: func(identifier) or func(identifier->member) or func(identifier[index])
RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\(\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*\w+\s*\])?\s*\)\s*;" "$DIR" 2>/dev/null || true)
# Filter out function definitions, declarations, and safe api_* wrappers
RESULTS=$(echo "$RESULTS" | grep -v "const char\|void \|^[^:]*:[0-9]*:[a-z_]*(\|api_cons_show\|api_log_" || true)
if [ -n "$RESULTS" ]; then
echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND:"
echo ""
echo "$RESULTS"
echo ""
ERRORS=$(echo "$RESULTS" | wc -l)
else
echo "✅ No obvious CWE-134 issues found."
fi
# Additional check: GString->str passed directly (not as %s argument)
echo ""
echo "Checking for GString->str passed to format functions..."
GSTRING_RESULTS=$(grep -rn --include="*.c" -P "($FORMAT_FUNCS)\s*\([^)]*->str\s*\)" "$DIR" 2>/dev/null | grep -v '"%s"' || true)
if [ -n "$GSTRING_RESULTS" ]; then
echo "⚠️ GString->str passed without \"%s\" (review manually):"
echo ""
echo "$GSTRING_RESULTS"
echo ""
fi
echo ""
echo "=== Summary ==="
echo "Critical issues: $ERRORS"
if [ "$ERRORS" -gt 0 ]; then
echo ""
echo "Fix by adding \"%s\" format specifier:"
echo " BAD: cons_show(variable);"
echo " GOOD: cons_show(\"%s\", variable);"
exit 1
fi
exit 0

View File

@@ -15,72 +15,14 @@ error_handler()
log_content ./test-suite.log log_content ./test-suite.log
log_content ./test-suite-memcheck.log log_content ./test-suite-memcheck.log
echo >&2 echo
echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." >&2 echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting."
echo >&2 echo
exit ${ERR_CODE} exit ${ERR_CODE}
} }
trap error_handler ERR trap error_handler ERR
# =============================================================================
# Constants
# =============================================================================
# Number of parallel build configurations
readonly TEST_BUILDS=4
# Human-readable names for each build configuration
readonly BUILD_NAMES=(
"Full" # 1. All features enabled
"Minimal" # 2. All optional features disabled
"NoEncrypt" # 3. No encryption (otr, pgp, omemo disabled)
"Default" # 4. Default ./configure options
)
# Regex patterns for parsing test output
readonly CMOCKA_PASSED_PATTERN='^\[ PASSED \] [0-9]+ test'
readonly CMOCKA_FAILED_PATTERN='^\[ FAILED \] [0-9]+ test'
# Coverage extraction patterns (matches both Docker and CI paths)
readonly COVERAGE_PATTERNS='*/profanity/src/* */src/src/*'
# =============================================================================
# Helper Functions
# =============================================================================
# Parse STATS line from build log and set global variables
# Usage: parse_build_stats <log_file>
parse_build_stats() {
local log_file="$1"
local stats_line
stats_line=$(grep "^STATS:" "$log_file" 2>/dev/null | tail -1)
STAT_UNIT_P=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2)
STAT_UNIT_F=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2)
STAT_FUNC_P=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2)
STAT_FUNC_F=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2)
STAT_COV_LINES=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2)
STAT_COV_FUNCS=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2)
STAT_COV_BRANCHES=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2)
STAT_TIME=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2)
: "${STAT_UNIT_P:=0}"
: "${STAT_UNIT_F:=0}"
: "${STAT_FUNC_P:=0}"
: "${STAT_FUNC_F:=0}"
}
# Extract test count from log file
# Usage: extract_test_count <log_file> <pattern>
extract_test_count() {
grep -E "$2" "$1" 2>/dev/null | grep -oE "[0-9]+" | head -1
}
# =============================================================================
# Test Verification
# =============================================================================
# Verify that test failures are properly detected # Verify that test failures are properly detected
# This is a meta-test: it runs a deliberately failing test # This is a meta-test: it runs a deliberately failing test
# and checks that the test framework reports the failure correctly # and checks that the test framework reports the failure correctly
@@ -119,11 +61,11 @@ EOF
# Test 1: Single failing test detection # Test 1: Single failing test detection
echo " Testing single test failure detection..." echo " Testing single test failure detection..."
if /tmp/test_must_fail > /tmp/test_must_fail.log 2>&1; then if /tmp/test_must_fail > /tmp/test_must_fail.log 2>&1; then
echo "ERROR: Test that should fail returned success (exit code 0)" >&2 echo "ERROR: Test that should fail returned success (exit code 0)"
echo "This means the test framework is NOT detecting failures correctly!" >&2 echo "This means the test framework is NOT detecting failures correctly!"
echo "--- Test output ---" >&2 echo "--- Test output ---"
cat /tmp/test_must_fail.log >&2 cat /tmp/test_must_fail.log
echo "--- End output ---" >&2 echo "--- End output ---"
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log
exit 1 exit 1
fi fi
@@ -143,8 +85,8 @@ EOF
wait $pid4 || failed=$((failed + 1)) wait $pid4 || failed=$((failed + 1))
if [ $failed -ne 2 ]; then if [ $failed -ne 2 ]; then
echo "ERROR: Expected 2 failures in parallel tests, got $failed" >&2 echo "ERROR: Expected 2 failures in parallel tests, got $failed"
echo "Parallel failure detection is broken!" >&2 echo "Parallel failure detection is broken!"
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
exit 1 exit 1
fi fi
@@ -168,17 +110,6 @@ num_cores()
# Run test failure detection verification first # Run test failure detection verification first
verify_test_failure_detection verify_test_failure_detection
# Parse arguments
COVERAGE_ONLY=no
for arg in "$@"; do
case "$arg" in
--coverage-only)
COVERAGE_ONLY=yes
shift
;;
esac
done
./bootstrap.sh ./bootstrap.sh
tests=() tests=()
@@ -249,13 +180,30 @@ case "$ARCH" in
;; ;;
esac esac
case "$ARCH" in
linux*)
echo
echo "--> Building with ./configure -C ${tests[0]} --enable-valgrind $*"
echo
# shellcheck disable=SC2086
./configure -C ${tests[0]} --enable-valgrind $*
$MAKE CC="${CC}"
if grep '^ID=' /etc/os-release | grep -q -e debian; then
$MAKE check-valgrind
else
$MAKE check-valgrind || log_content ./test-suite-memcheck.log
fi
$MAKE distclean
;;
esac
# Function to build and test a single configuration # Function to build and test a single configuration
build_and_test() { build_and_test() {
local features="$1" local features="$1"
local extra_args="$2" local extra_args="$2"
local idx="$3" local idx="$3"
local run_valgrind="$4"
local run_coverage="$5"
local build_dir="build-$idx" local build_dir="build-$idx"
local log_file="build-$idx.log" local log_file="build-$idx.log"
@@ -263,210 +211,61 @@ build_and_test() {
echo "=== Build $idx started at $(date) ===" echo "=== Build $idx started at $(date) ==="
echo "--> Building in $build_dir with ./configure -C $features $extra_args" echo "--> Building in $build_dir with ./configure -C $features $extra_args"
local start_time=$SECONDS
mkdir -p "$build_dir" mkdir -p "$build_dir"
cd "$build_dir" cd "$build_dir"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if ! ../configure -C $features $extra_args; then ../configure -C $features $extra_args
echo "ERROR: configure failed"
exit 1
fi
if ! $MAKE CC="${CC}"; then $MAKE CC="${CC}"
echo "ERROR: make failed" $MAKE check-functional-parallel
exit 1
fi
# Run unit tests
local unit_passed=0 unit_failed=0
if [ "$run_valgrind" = "yes" ]; then
echo "--> Running unit tests under Valgrind..."
# Build unit tests first
$MAKE tests/unittests/unittests
# Run valgrind directly to capture cmocka output
valgrind --error-exitcode=1 --leak-check=full \
--suppressions=../prof.supp \
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
valgrind_exit=${PIPESTATUS[0]}
if [ $valgrind_exit -ne 0 ]; then
echo "ERROR: Valgrind unit tests failed (exit code $valgrind_exit)"
exit 1
fi
else
echo "--> Running unit tests..."
$MAKE tests/unittests/unittests
tests/unittests/unittests 2>&1 | tee unit-tests-output.log
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "ERROR: Unit tests failed"
exit 1
fi
fi
# Extract unit test counts from cmocka output
unit_passed=$(extract_test_count unit-tests-output.log "$CMOCKA_PASSED_PATTERN")
unit_failed=$(extract_test_count unit-tests-output.log "$CMOCKA_FAILED_PATTERN")
: "${unit_passed:=0}" "${unit_failed:=0}"
echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed"
# Set build index for port allocation: build 1 uses ports 5230-5233,
# build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds.
export PROF_BUILD_INDEX=$idx
local func_passed=0 func_failed=0
if ! $MAKE check-functional-parallel; then
echo "ERROR: functional tests failed"
exit 1
fi
# Extract functional test counts from group logs
echo "=== Functional test results ==="
for glog in ./test-logs/group*.log; do
if [ -f "$glog" ]; then
cnt=$(extract_test_count "$glog" "$CMOCKA_PASSED_PATTERN")
[ -n "$cnt" ] && func_passed=$((func_passed + cnt))
cnt=$(extract_test_count "$glog" "$CMOCKA_FAILED_PATTERN")
[ -n "$cnt" ] && func_failed=$((func_failed + cnt))
fi
done
echo "FUNC_TESTS: passed=$func_passed failed=$func_failed"
# Collect coverage data if enabled (lines, functions, branches)
# Must be done BEFORE make clean which removes .gcda files
local cov_lines="n/a" cov_funcs="n/a" cov_branches="n/a"
if [ "$run_coverage" = "yes" ]; then
echo "--> Collecting coverage data..."
if command -v lcov >/dev/null 2>&1; then
lcov --capture --directory . --output-file coverage-full.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
# Extract only production code from src/ directory, exclude tests
# shellcheck disable=SC2086
lcov --extract coverage-full.info $COVERAGE_PATTERNS \
--output-file coverage.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true
if [ -f coverage.info ] && [ -s coverage.info ]; then
local summary
summary=$(lcov --summary coverage.info \
--rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true)
cov_lines=$(echo "$summary" | grep -E "lines\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
cov_funcs=$(echo "$summary" | grep -E "functions\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
cov_branches=$(echo "$summary" | grep -E "branches\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
[ -z "$cov_lines" ] && cov_lines="n/a"
[ -z "$cov_funcs" ] && cov_funcs="n/a"
[ -z "$cov_branches" ] && cov_branches="n/a"
else
echo "WARNING: coverage.info is empty or not created"
fi
else
echo "WARNING: lcov not found"
fi
echo "COVERAGE: lines=$cov_lines funcs=$cov_funcs branches=$cov_branches"
fi
./profanity -v ./profanity -v
# Save coverage.info before cleanup (for CI artifact)
# Only copy in CI environment to avoid leaving artifacts during local runs
if [ "$run_coverage" = "yes" ] && [ -f coverage.info ] && [ -n "$CI" ]; then
cp coverage.info ../coverage.info
echo "Coverage report saved to coverage.info"
fi
$MAKE clean $MAKE clean
cd .. cd ..
rm -rf "$build_dir" rm -rf "$build_dir"
local elapsed=$((SECONDS - start_time))
local mins=$((elapsed / 60))
local secs=$((elapsed % 60))
echo "=== Build $idx completed at $(date) ===" echo "=== Build $idx completed at $(date) ==="
echo "STATS: unit_passed=$unit_passed unit_failed=$unit_failed func_passed=$func_passed func_failed=$func_failed cov_lines=$cov_lines cov_funcs=$cov_funcs cov_branches=$cov_branches time=${mins}m${secs}s"
} > "$log_file" 2>&1 } > "$log_file" 2>&1
} }
# Run configurations # Run all 4 configurations in parallel
# Coverage enabled only for build 1 (Full) - it has most code paths echo "Starting parallel builds..."
echo pids=()
echo "=== Start build ===" for idx in 1 2 3 4; do
echo if [ $idx -le ${#tests[@]} ]; then
build_and_test "${tests[$((idx-1))]}" "$*" "$idx" &
if [ "$COVERAGE_ONLY" = "yes" ]; then pids+=("$!")
echo "Running coverage-only mode (${BUILD_NAMES[0]} build)..." echo "Started build $idx (PID: $!)"
echo fi
run_valgrind="no" done
run_coverage="yes"
extra_flags="--enable-coverage"
build_and_test "${tests[0]}" "$* $extra_flags" "1" "$run_valgrind" "$run_coverage" &
pids=("$!")
echo "${BUILD_NAMES[0]}: ${tests[0]} [+Coverage]"
else
echo "Starting $TEST_BUILDS parallel build configurations..."
echo
pids=()
for idx in $(seq 1 $TEST_BUILDS); do
if [ $idx -le ${#tests[@]} ]; then
# All builds run Valgrind on Linux
if [ "$ARCH" = "linux" ]; then
run_valgrind="yes"
extra_flags="--enable-valgrind"
else
run_valgrind="no"
extra_flags=""
fi
run_coverage="no"
build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" &
pids+=("$!")
flags_desc=""
[ "$run_valgrind" = "yes" ] && flags_desc=" [+Valgrind]"
echo "${BUILD_NAMES[$((idx-1))]}: ${tests[$((idx-1))]}$flags_desc"
fi
done
fi
echo
# Wait for all builds and check exit codes # Wait for all builds and check exit codes
echo "Waiting for builds to complete..." failed=0
echo
failed_builds=()
for i in "${!pids[@]}"; do for i in "${!pids[@]}"; do
idx=$((i + 1)) idx=$((i + 1))
if wait "${pids[$i]}"; then if wait "${pids[$i]}"; then
if [ -f "build-$idx.log" ]; then echo "✓ Build $idx passed"
parse_build_stats "build-$idx.log"
echo "${BUILD_NAMES[$i]} PASSED"
echo " Unit tests: $STAT_UNIT_P passed, $STAT_UNIT_F failed"
echo " Functional tests: $STAT_FUNC_P passed, $STAT_FUNC_F failed"
if [ "$STAT_COV_LINES" != "n/a" ] && [ -n "$STAT_COV_LINES" ]; then
echo " Coverage: Lines: $STAT_COV_LINES | Functions: $STAT_COV_FUNCS | Branches: $STAT_COV_BRANCHES"
fi
echo " Duration: ${STAT_TIME:-?}"
else
echo "${BUILD_NAMES[$i]} passed (no stats available)"
fi
else else
echo "${BUILD_NAMES[$i]} FAILED" >&2 echo "Build $idx failed"
failed_builds+=("$idx") echo "--- Log for build $idx ---"
cat "build-$idx.log"
echo "--- End log ---"
failed=1
fi fi
done
# Show all logs on success too
if [ $failed -eq 0 ]; then
echo echo
done echo "All builds passed!"
for idx in 1 2 3 4; do
# Show failed builds full logs if [ -f "build-$idx.log" ]; then
for idx in "${failed_builds[@]}"; do echo "--- Log for build $idx ---"
if [ -f "build-$idx.log" ]; then cat "build-$idx.log"
echo "=== ${BUILD_NAMES[$((idx-1))]} FAILURE LOG ===" >&2 fi
cat "build-$idx.log" >&2 done
echo >&2
fi
done
if [ ${#failed_builds[@]} -gt 0 ]; then
echo "RESULT: FAILED (builds ${failed_builds[*]})" >&2
exit 1
else else
if [ "$COVERAGE_ONLY" = "yes" ]; then exit 1
echo "RESULT: COVERAGE BUILD PASSED ✓"
else
echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓"
fi
fi fi

View File

@@ -8,29 +8,6 @@
# * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp # * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
# #
# ============================================
# glibc AVX2 optimizations (false positives)
# See: https://sourceware.org/bugzilla/show_bug.cgi?id=19796
# ============================================
{
glibc_wcpncpy_avx2
Memcheck:Addr32
fun:__wcpncpy_avx2
fun:wcsxfrm_l
fun:g_utf8_collate_key
...
}
{
glibc_wcsxfrm_avx2
Memcheck:Addr32
...
fun:wcsxfrm_l
fun:g_utf8_collate_key
...
}
# ============================================ # ============================================
# Functional tests suppressions (stabber/pthread) # Functional tests suppressions (stabber/pthread)
# ============================================ # ============================================

View File

@@ -2825,15 +2825,16 @@ cmd_search_index_any(char* term)
int terms_len = g_strv_length(processed_terms); int terms_len = g_strv_length(processed_terms);
for (int i = 0; i < terms_len; i++) { for (int i = 0; i < terms_len; i++) {
GHashTableIter iter; GList* index_keys = g_hash_table_get_keys(search_index);
gpointer key, value; GList* curr = index_keys;
g_hash_table_iter_init(&iter, search_index); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { char* index_entry = g_hash_table_lookup(search_index, curr->data);
char* index_entry = (char*)value;
if (g_str_match_string(processed_terms[i], index_entry, FALSE)) { if (g_str_match_string(processed_terms[i], index_entry, FALSE)) {
results = g_list_append(results, key); results = g_list_append(results, curr->data);
} }
curr = g_list_next(curr);
} }
g_list_free(index_keys);
} }
return results; return results;
@@ -2847,14 +2848,13 @@ cmd_search_index_all(char* term)
auto_gcharv gchar** terms = g_str_tokenize_and_fold(term, NULL, NULL); auto_gcharv gchar** terms = g_str_tokenize_and_fold(term, NULL, NULL);
int terms_len = g_strv_length(terms); int terms_len = g_strv_length(terms);
GHashTableIter iter; GList* commands = g_hash_table_get_keys(search_index);
gpointer key, value; GList* curr = commands;
g_hash_table_iter_init(&iter, search_index); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { char* command = curr->data;
char* command = (char*)key;
char* command_index = (char*)value;
int matches = 0; int matches = 0;
for (int i = 0; i < terms_len; i++) { for (int i = 0; i < terms_len; i++) {
char* command_index = g_hash_table_lookup(search_index, command);
if (g_str_match_string(terms[i], command_index, FALSE)) { if (g_str_match_string(terms[i], command_index, FALSE)) {
matches++; matches++;
} }
@@ -2862,8 +2862,11 @@ cmd_search_index_all(char* term)
if (matches == terms_len) { if (matches == terms_len) {
results = g_list_append(results, command); results = g_list_append(results, command);
} }
curr = g_list_next(curr);
} }
g_list_free(commands);
return results; return results;
} }
@@ -2986,18 +2989,7 @@ command_docgen(void)
} }
FILE* toc_fragment = fopen("toc_fragment.html", "w"); FILE* toc_fragment = fopen("toc_fragment.html", "w");
if (!toc_fragment) {
log_error("command_docgen(): unable to open toc_fragment.html for writing: %s", g_strerror(errno));
g_list_free(cmds);
return;
}
FILE* main_fragment = fopen("main_fragment.html", "w"); FILE* main_fragment = fopen("main_fragment.html", "w");
if (!main_fragment) {
log_error("command_docgen(): unable to open main_fragment.html for writing: %s", g_strerror(errno));
fclose(toc_fragment);
g_list_free(cmds);
return;
}
fputs("<ul><li><ul><li>\n", toc_fragment); fputs("<ul><li><ul><li>\n", toc_fragment);
fputs("<hr>\n", main_fragment); fputs("<hr>\n", main_fragment);
@@ -3102,11 +3094,6 @@ command_mangen(void)
return; return;
} }
FILE* manpage = fopen(filename, "w"); FILE* manpage = fopen(filename, "w");
if (!manpage) {
log_error("command_mangen(): unable to open %s for writing: %s", filename, g_strerror(errno));
curr = g_list_next(curr);
continue;
}
fprintf(manpage, "%s\n", header); fprintf(manpage, "%s\n", header);
fputs(".SH NAME\n", manpage); fputs(".SH NAME\n", manpage);

View File

@@ -184,7 +184,7 @@ _string_matches_one_of(const char* what, const char* is, bool is_can_be_null, co
} }
va_end(ap); va_end(ap);
if (s > 0) if (s > 0)
cons_show("%s", errmsg); cons_show(errmsg);
} }
return ret; return ret;
} }
@@ -418,7 +418,7 @@ cmd_connect(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL; auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg); gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) { if (!res) {
cons_show("%s", err_msg); cons_show(err_msg);
cons_show(""); cons_show("");
port = 0; port = 0;
options_destroy(options); options_destroy(options);
@@ -711,7 +711,7 @@ _account_set_port(char* account_name, char* port)
auto_char char* err_msg = NULL; auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port, &porti, 1, 65535, &err_msg); gboolean res = strtoi_range(port, &porti, 1, 65535, &err_msg);
if (!res) { if (!res) {
cons_show("%s", err_msg); cons_show(err_msg);
cons_show(""); cons_show("");
} else { } else {
accounts_set_port(account_name, porti); accounts_set_port(account_name, porti);
@@ -903,7 +903,7 @@ _account_set_max_sessions(char* account_name, char* max_sessions_raw)
auto_char char* err_msg = NULL; auto_char char* err_msg = NULL;
gboolean res = strtoi_range(max_sessions_raw, &max_sessions, 0, INT_MAX, &err_msg); gboolean res = strtoi_range(max_sessions_raw, &max_sessions, 0, INT_MAX, &err_msg);
if (!res) { if (!res) {
cons_show("%s", err_msg); cons_show(err_msg);
cons_show(""); cons_show("");
return TRUE; return TRUE;
} }
@@ -924,7 +924,7 @@ _account_set_presence_priority(char* account_name, char* presence, char* priorit
auto_char char* err_msg = NULL; auto_char char* err_msg = NULL;
gboolean res = strtoi_range(priority, &intval, -128, 127, &err_msg); gboolean res = strtoi_range(priority, &intval, -128, 127, &err_msg);
if (!res) { if (!res) {
cons_show("%s", err_msg); cons_show(err_msg);
return TRUE; return TRUE;
} }
@@ -1580,7 +1580,7 @@ _cmd_list_commands(GList* commands)
while (curr) { while (curr) {
gchar* cmd = curr->data; gchar* cmd = curr->data;
if (count == 5) { if (count == 5) {
cons_show("%s", cmds->str); cons_show(cmds->str);
g_string_free(cmds, TRUE); g_string_free(cmds, TRUE);
cmds = g_string_new(""); cmds = g_string_new("");
count = 0; count = 0;
@@ -1589,7 +1589,7 @@ _cmd_list_commands(GList* commands)
curr = g_list_next(curr); curr = g_list_next(curr);
count++; count++;
} }
cons_show("%s", cmds->str); cons_show(cmds->str);
g_string_free(cmds, TRUE); g_string_free(cmds, TRUE);
g_list_free(curr); g_list_free(curr);
@@ -2407,7 +2407,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
} }
return TRUE; return TRUE;
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
return TRUE; return TRUE;
} }
@@ -2467,7 +2467,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster contact indent set to: %d", intval); cons_show("Roster contact indent set to: %d", intval);
rosterwin_roster(); rosterwin_roster();
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
} }
} else { } else {
@@ -2501,7 +2501,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster resource indent set to: %d", intval); cons_show("Roster resource indent set to: %d", intval);
rosterwin_roster(); rosterwin_roster();
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
} }
} else if (g_strcmp0(args[1], "join") == 0) { } else if (g_strcmp0(args[1], "join") == 0) {
@@ -2527,7 +2527,7 @@ cmd_roster(ProfWin* window, const char* const command, gchar** args)
cons_show("Roster presence indent set to: %d", intval); cons_show("Roster presence indent set to: %d", intval);
rosterwin_roster(); rosterwin_roster();
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
} }
} else { } else {
@@ -4371,7 +4371,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
wins_resize_all(); wins_resize_all();
return TRUE; return TRUE;
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
return TRUE; return TRUE;
} }
} }
@@ -4391,7 +4391,7 @@ cmd_occupants(ProfWin* window, const char* const command, gchar** args)
occupantswin_occupants_all(); occupantswin_occupants_all();
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
} }
@@ -4970,8 +4970,8 @@ cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
alt_scheme = OMEMO_AESGCM_URL_SCHEME; alt_scheme = OMEMO_AESGCM_URL_SCHEME;
alt_fragment = _add_omemo_stream(&fd, &fh, &err); alt_fragment = _add_omemo_stream(&fd, &fh, &err);
if (err != NULL) { if (err != NULL) {
cons_show_error("%s", err); cons_show_error(err);
win_println(window, THEME_ERROR, "-", "%s", err); win_println(window, THEME_ERROR, "-", err);
goto out; goto out;
} }
#endif #endif
@@ -5836,7 +5836,7 @@ cmd_inpblock(ProfWin* window, const char* const command, gchar** args)
prefs_set_inpblock(intval); prefs_set_inpblock(intval);
inp_nonblocking(FALSE); inp_nonblocking(FALSE);
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
@@ -6063,7 +6063,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize(); ui_resize();
return TRUE; return TRUE;
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
cons_bad_cmd_usage(command); cons_bad_cmd_usage(command);
return TRUE; return TRUE;
} }
@@ -6094,7 +6094,7 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
ui_resize(); ui_resize();
return TRUE; return TRUE;
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
cons_bad_cmd_usage(command); cons_bad_cmd_usage(command);
return TRUE; return TRUE;
} }
@@ -6254,7 +6254,7 @@ cmd_log(ProfWin* window, const char* const command, gchar** args)
prefs_set_max_log_size(intval); prefs_set_max_log_size(intval);
cons_show("Log maximum size set to %d bytes", intval); cons_show("Log maximum size set to %d bytes", intval);
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
} }
@@ -6304,7 +6304,7 @@ cmd_reconnect(ProfWin* window, const char* const command, gchar** args)
cons_show("Reconnect interval set to %d seconds.", intval); cons_show("Reconnect interval set to %d seconds.", intval);
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
cons_bad_cmd_usage(command); cons_bad_cmd_usage(command);
} }
@@ -6330,7 +6330,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping interval set to %d seconds.", intval); cons_show("Autoping interval set to %d seconds.", intval);
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
cons_bad_cmd_usage(command); cons_bad_cmd_usage(command);
} }
@@ -6346,7 +6346,7 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args)
cons_show("Autoping timeout set to %d seconds.", intval); cons_show("Autoping timeout set to %d seconds.", intval);
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
cons_bad_cmd_usage(command); cons_bad_cmd_usage(command);
} }
@@ -6416,7 +6416,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
cons_show("Auto away time set to: %d minutes.", minutesval); cons_show("Auto away time set to: %d minutes.", minutesval);
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
@@ -6439,7 +6439,7 @@ cmd_autoaway(ProfWin* window, const char* const command, gchar** args)
} }
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
@@ -6506,7 +6506,7 @@ cmd_priority(ProfWin* window, const char* const command, gchar** args)
cl_ev_presence_send(last_presence, 0); cl_ev_presence_send(last_presence, 0);
cons_show("Priority set to %d.", intval); cons_show("Priority set to %d.", intval);
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
@@ -6576,7 +6576,7 @@ cmd_tray(ProfWin* window, const char* const command, gchar** args)
tray_set_timer(intval); tray_set_timer(intval);
} }
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
return TRUE; return TRUE;
@@ -7181,21 +7181,22 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
} }
cons_show("PGP keys:"); cons_show("PGP keys:");
GHashTableIter iter; GList* keylist = g_hash_table_get_keys(keys);
gpointer key, value; GList* curr = keylist;
g_hash_table_iter_init(&iter, keys); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
ProfPGPKey* pgp_key = (ProfPGPKey*)value; cons_show(" %s", key->name);
cons_show(" %s", pgp_key->name); cons_show(" ID : %s", key->id);
cons_show(" ID : %s", pgp_key->id); auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
cons_show(" Fingerprint : %s", format_fp); cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) { if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE"); cons_show(" Type : PUBLIC, PRIVATE");
} else { } else {
cons_show(" Type : PUBLIC"); cons_show(" Type : PUBLIC");
} }
curr = g_list_next(curr);
} }
g_list_free(keylist);
p_gpg_free_keys(keys); p_gpg_free_keys(keys);
return TRUE; return TRUE;
} }
@@ -7236,24 +7237,25 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
return TRUE; return TRUE;
} }
GHashTable* pubkeys = p_gpg_pubkeys(); GHashTable* pubkeys = p_gpg_pubkeys();
if (!pubkeys || g_hash_table_size(pubkeys) == 0) { GList* jids = g_hash_table_get_keys(pubkeys);
if (!jids) {
cons_show("No contacts found with PGP public keys assigned."); cons_show("No contacts found with PGP public keys assigned.");
return TRUE; return TRUE;
} }
cons_show("Assigned PGP public keys:"); cons_show("Assigned PGP public keys:");
GHashTableIter iter; GList* curr = jids;
gpointer key, value; while (curr) {
g_hash_table_iter_init(&iter, pubkeys); char* jid = curr->data;
while (g_hash_table_iter_next(&iter, &key, &value)) { ProfPGPPubKeyId* pubkeyid = g_hash_table_lookup(pubkeys, jid);
char* jid = (char*)key;
ProfPGPPubKeyId* pubkeyid = (ProfPGPPubKeyId*)value;
if (pubkeyid->received) { if (pubkeyid->received) {
cons_show(" %s: %s (received)", jid, pubkeyid->id); cons_show(" %s: %s (received)", jid, pubkeyid->id);
} else { } else {
cons_show(" %s: %s (stored)", jid, pubkeyid->id); cons_show(" %s: %s (stored)", jid, pubkeyid->id);
} }
curr = g_list_next(curr);
} }
g_list_free(jids);
return TRUE; return TRUE;
} }
@@ -7466,21 +7468,22 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
} }
cons_show("OpenPGP keys:"); cons_show("OpenPGP keys:");
GHashTableIter iter; GList* keylist = g_hash_table_get_keys(keys);
gpointer key, value; GList* curr = keylist;
g_hash_table_iter_init(&iter, keys); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
ProfPGPKey* pgp_key = (ProfPGPKey*)value; cons_show(" %s", key->name);
cons_show(" %s", pgp_key->name); cons_show(" ID : %s", key->id);
cons_show(" ID : %s", pgp_key->id); auto_char char* format_fp = p_gpg_format_fp_str(key->fp);
auto_char char* format_fp = p_gpg_format_fp_str(pgp_key->fp);
cons_show(" Fingerprint : %s", format_fp); cons_show(" Fingerprint : %s", format_fp);
if (pgp_key->secret) { if (key->secret) {
cons_show(" Type : PUBLIC, PRIVATE"); cons_show(" Type : PUBLIC, PRIVATE");
} else { } else {
cons_show(" Type : PUBLIC"); cons_show(" Type : PUBLIC");
} }
curr = g_list_next(curr);
} }
g_list_free(keylist);
p_gpg_free_keys(keys); p_gpg_free_keys(keys);
return TRUE; return TRUE;
} }
@@ -7488,8 +7491,8 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
else if (g_strcmp0(args[0], "contacts") == 0) { else if (g_strcmp0(args[0], "contacts") == 0) {
GHashTable* keys = ox_gpg_public_keys(); GHashTable* keys = ox_gpg_public_keys();
cons_show("OpenPGP keys:"); cons_show("OpenPGP keys:");
GHashTableIter iter; GList* keylist = g_hash_table_get_keys(keys);
gpointer key, value; GList* curr = keylist;
GSList* roster_list = NULL; GSList* roster_list = NULL;
jabber_conn_status_t conn_status = connection_get_status(); jabber_conn_status_t conn_status = connection_get_status();
@@ -7499,16 +7502,15 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
roster_list = roster_get_contacts(ROSTER_ORD_NAME); roster_list = roster_get_contacts(ROSTER_ORD_NAME);
} }
g_hash_table_iter_init(&iter, keys); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { ProfPGPKey* key = g_hash_table_lookup(keys, curr->data);
ProfPGPKey* pgp_key = (ProfPGPKey*)value;
PContact contact = NULL; PContact contact = NULL;
if (roster_list) { if (roster_list) {
GSList* curr_c = roster_list; GSList* curr_c = roster_list;
while (!contact && curr_c) { while (!contact && curr_c) {
contact = curr_c->data; contact = curr_c->data;
auto_gchar gchar* xmppuri = g_strdup_printf("xmpp:%s", p_contact_barejid(contact)); auto_gchar gchar* xmppuri = g_strdup_printf("xmpp:%s", p_contact_barejid(contact));
if (g_strcmp0(pgp_key->name, xmppuri)) { if (g_strcmp0(key->name, xmppuri)) {
contact = NULL; contact = NULL;
} }
curr_c = g_slist_next(curr_c); curr_c = g_slist_next(curr_c);
@@ -7516,10 +7518,11 @@ cmd_ox(ProfWin* window, const char* const command, gchar** args)
} }
if (contact) { if (contact) {
cons_show("%s - %s", pgp_key->fp, pgp_key->name); cons_show("%s - %s", key->fp, key->name);
} else { } else {
cons_show("%s - %s (not in roster)", pgp_key->fp, pgp_key->name); cons_show("%s - %s (not in roster)", key->fp, key->name);
} }
curr = g_list_next(curr);
} }
} else if (g_strcmp0(args[0], "start") == 0) { } else if (g_strcmp0(args[0], "start") == 0) {
@@ -9609,7 +9612,7 @@ cmd_register(ProfWin* window, const char* const command, gchar** args)
auto_char char* err_msg = NULL; auto_char char* err_msg = NULL;
gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg); gboolean res = strtoi_range(port_str, &port, 1, 65535, &err_msg);
if (!res) { if (!res) {
cons_show("%s", err_msg); cons_show(err_msg);
cons_show(""); cons_show("");
port = 0; port = 0;
options_destroy(options); options_destroy(options);
@@ -9679,7 +9682,7 @@ cmd_strophe(ProfWin* window, const char* const command, gchar** args)
prefs_set_string(PREF_STROPHE_VERBOSITY, args[1]); prefs_set_string(PREF_STROPHE_VERBOSITY, args[1]);
return TRUE; return TRUE;
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
} else if (g_strcmp0(args[0], "sm") == 0) { } else if (g_strcmp0(args[0], "sm") == 0) {
if (g_strcmp0(args[1], "no-resend") == 0) { if (g_strcmp0(args[1], "no-resend") == 0) {

View File

@@ -143,7 +143,7 @@ auto_close_gfd(gint* fd)
return; return;
if (close(*fd) == EOF) if (close(*fd) == EOF)
log_error("%s", g_strerror(errno)); log_error(g_strerror(errno));
} }
/** /**
@@ -158,7 +158,7 @@ auto_close_FILE(FILE** fd)
return; return;
if (fclose(*fd) == EOF) if (fclose(*fd) == EOF)
log_error("%s", g_strerror(errno)); log_error(g_strerror(errno));
} }
static gboolean static gboolean

View File

@@ -65,38 +65,6 @@ static gboolean _check_available_space_for_db_migration(char* path_to_db);
static const int latest_version = 2; static const int latest_version = 2;
// Helper: close DB handle (if any), warn on busy, and shutdown SQLite
static void
_db_teardown(const char* ctx)
{
if (g_chatlog_database) {
int rc = sqlite3_close_v2(g_chatlog_database);
if (rc != SQLITE_OK) {
log_warning("sqlite3_close_v2 in %s returned %d; database may still have active statements.",
ctx ? ctx : "db_teardown", rc);
}
g_chatlog_database = NULL;
}
// Safe to call unconditionally; no-op if not initialized.
// See: https://www.sqlite.org/c3ref/initialize.html
sqlite3_shutdown();
}
// Helper: prepare a statement and log a contextual error on failure
static gboolean
_db_prepare_ctx(const char* query, sqlite3_stmt** stmt, const char* ctx)
{
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in %s: (error code: %d) %s",
ctx ? ctx : "sqlite3_prepare_v2",
rc,
sqlite3_errmsg(g_chatlog_database));
return FALSE;
}
return TRUE;
}
static char* static char*
_db_strdup(const char* str) _db_strdup(const char* str)
{ {
@@ -130,19 +98,17 @@ log_database_init(ProfAccount* account)
auto_char char* filename = _get_db_filename(account); auto_char char* filename = _get_db_filename(account);
if (!filename) { if (!filename) {
sqlite3_shutdown();
return FALSE; return FALSE;
} }
ret = sqlite3_open(filename, &g_chatlog_database); ret = sqlite3_open(filename, &g_chatlog_database);
if (ret != SQLITE_OK) { if (ret != SQLITE_OK) {
const char* err_msg = g_chatlog_database ? sqlite3_errmsg(g_chatlog_database) : "(no handle)"; const char* err_msg = sqlite3_errmsg(g_chatlog_database);
log_error("Error opening SQLite database: %s", err_msg); log_error("Error opening SQLite database: %s", err_msg);
_db_teardown("log_database_init(open)");
return FALSE; return FALSE;
} }
char* err_msg = NULL; char* err_msg;
int db_version = _get_db_version(); int db_version = _get_db_version();
if (db_version == latest_version) { if (db_version == latest_version) {
@@ -250,7 +216,6 @@ out:
} else { } else {
log_error("Unknown SQLite error in log_database_init()."); log_error("Unknown SQLite error in log_database_init().");
} }
_db_teardown("log_database_init(out)");
return FALSE; return FALSE;
} }
@@ -258,7 +223,11 @@ void
log_database_close(void) log_database_close(void)
{ {
log_debug("log_database_close() called"); log_debug("log_database_close() called");
_db_teardown("log_database_close"); if (g_chatlog_database) {
sqlite3_close(g_chatlog_database);
sqlite3_shutdown();
g_chatlog_database = NULL;
}
} }
void void
@@ -312,15 +281,8 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
{ {
sqlite3_stmt* stmt = NULL; sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid(); const Jid* myjid = connection_get_jid();
// Always return a valid ProfMessage to avoid NULL dereferences in callers if (!myjid->str)
ProfMessage* msg = message_init(); return NULL;
if (!myjid || !myjid->str) {
// If caller requested the last message and we have no context, fall back to now
if (is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
}
const char* order = is_last ? "DESC" : "ASC"; const char* order = is_last ? "DESC" : "ASC";
auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE " auto_sqlite char* query = sqlite3_mprintf("SELECT `archive_id`, `timestamp` FROM `ChatLogs` WHERE "
@@ -331,19 +293,17 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
if (!query) { if (!query) {
log_error("Could not allocate memory for SQL query in log_database_get_limits_info()"); log_error("Could not allocate memory for SQL query in log_database_get_limits_info()");
if (is_last) { return NULL;
msg->timestamp = g_date_time_new_now_utc();
}
return msg;
} }
if (!_db_prepare_ctx(query, &stmt, "log_database_get_limits_info()")) { int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (is_last) { if (rc != SQLITE_OK) {
msg->timestamp = g_date_time_new_now_utc(); log_error("Unknown SQLite error in log_database_get_last_info().");
} return NULL;
return msg;
} }
ProfMessage* msg = message_init();
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
char* archive_id = (char*)sqlite3_column_text(stmt, 0); char* archive_id = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1); char* date = (char*)sqlite3_column_text(stmt, 1);
@@ -353,11 +313,6 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
// If nothing was found and caller expects the last message, provide a sane default
if (!msg->timestamp && is_last) {
msg->timestamp = g_date_time_new_now_utc();
}
return msg; return msg;
} }
@@ -402,7 +357,9 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const gchar*
return DB_RESPONSE_ERROR; return DB_RESPONSE_ERROR;
} }
if (!_db_prepare_ctx(query, &stmt, "log_database_get_previous_chat()")) { int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in log_database_get_previous_chat(): (error code: %d) %s", rc, sqlite3_errmsg(g_chatlog_database));
return DB_RESPONSE_ERROR; return DB_RESPONSE_ERROR;
} }
@@ -546,7 +503,9 @@ _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Ji
} }
sqlite3_stmt* lmc_stmt = NULL; sqlite3_stmt* lmc_stmt = NULL;
if (!_db_prepare_ctx(replace_check_query, &lmc_stmt, "_add_to_db(replace_check)")) {
if (SQLITE_OK != sqlite3_prepare_v2(g_chatlog_database, replace_check_query, -1, &lmc_stmt, NULL)) {
log_error("SQLite error in _add_to_db() on selecting original message: %s", sqlite3_errmsg(g_chatlog_database));
return; return;
} }
@@ -583,7 +542,8 @@ _add_to_db(ProfMessage* message, char* type, const Jid* const from_jid, const Ji
} }
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, duplicate_check_query, -1, &stmt, NULL)) {
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain); log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid); cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
@@ -639,7 +599,8 @@ _get_db_version(void)
int current_version = -1; int current_version = -1;
const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1"; const char* query = "SELECT `version` FROM `DbVersion` LIMIT 1";
sqlite3_stmt* statement; sqlite3_stmt* statement;
if (_db_prepare_ctx(query, &statement, "_get_db_version()")) {
if (sqlite3_prepare_v2(g_chatlog_database, query, -1, &statement, NULL) == SQLITE_OK) {
if (sqlite3_step(statement) == SQLITE_ROW) { if (sqlite3_step(statement) == SQLITE_ROW) {
current_version = sqlite3_column_int(statement, 0); current_version = sqlite3_column_int(statement, 0);
} }

View File

@@ -532,13 +532,11 @@ omemo_set_device_list(const char* const from, GList* device_list)
for (device_id = device_list; device_id != NULL; device_id = device_id->next) { for (device_id = device_list; device_id != NULL; device_id = device_id->next) {
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid->barejid); GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid->barejid);
if (known_identities) { if (known_identities) {
GHashTableIter iter; GList* fp = NULL;
gpointer key, value; for (fp = g_hash_table_get_keys(known_identities); fp != NULL; fp = fp->next) {
g_hash_table_iter_init(&iter, known_identities); if (device_id->data == g_hash_table_lookup(known_identities, fp->data)) {
while (g_hash_table_iter_next(&iter, &key, &value)) { cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(fp->data));
if (device_id->data == value) { omemo_trust(jid->barejid, omemo_format_fingerprint(fp->data));
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, omemo_format_fingerprint(key));
omemo_trust(jid->barejid, omemo_format_fingerprint(key));
} }
} }
} }

View File

@@ -345,13 +345,14 @@ p_gpg_list_keys(void)
// TODO: move autocomplete in other place // TODO: move autocomplete in other place
autocomplete_clear(key_ac); autocomplete_clear(key_ac);
GHashTableIter iter; GList* ids = g_hash_table_get_keys(result);
gpointer key, value; GList* curr = ids;
g_hash_table_iter_init(&iter, result); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { ProfPGPKey* key = g_hash_table_lookup(result, curr->data);
ProfPGPKey* pgp_key = (ProfPGPKey*)value; autocomplete_add(key_ac, key->id);
autocomplete_add(key_ac, pgp_key->id); curr = curr->next;
} }
g_list_free(ids);
return result; return result;
} }

View File

@@ -141,16 +141,18 @@ autocompleters_complete(const char* const input, gboolean previous)
while (curr_hash) { while (curr_hash) {
GHashTable* key_to_ac = curr_hash->data; GHashTable* key_to_ac = curr_hash->data;
GHashTableIter iter; GList* keys = g_hash_table_get_keys(key_to_ac);
gpointer key, value; GList* curr = keys;
g_hash_table_iter_init(&iter, key_to_ac); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { result = autocomplete_param_with_ac(input, curr->data, g_hash_table_lookup(key_to_ac, curr->data), TRUE, previous);
result = autocomplete_param_with_ac(input, key, value, TRUE, previous);
if (result) { if (result) {
g_list_free(ac_hashes); g_list_free(ac_hashes);
g_list_free(keys);
return result; return result;
} }
curr = g_list_next(curr);
} }
g_list_free(keys);
curr_hash = g_list_next(curr_hash); curr_hash = g_list_next(curr_hash);
} }
@@ -160,19 +162,22 @@ autocompleters_complete(const char* const input, gboolean previous)
curr_hash = filepath_hashes; curr_hash = filepath_hashes;
while (curr_hash) { while (curr_hash) {
GHashTable* prefixes_hash = curr_hash->data; GHashTable* prefixes_hash = curr_hash->data;
GHashTableIter iter; GList* prefixes = g_hash_table_get_keys(prefixes_hash);
gpointer key, value; GList* curr_prefix = prefixes;
g_hash_table_iter_init(&iter, prefixes_hash); while (curr_prefix) {
while (g_hash_table_iter_next(&iter, &key, &value)) { char* prefix = curr_prefix->data;
char* prefix = (char*)key;
if (g_str_has_prefix(input, prefix)) { if (g_str_has_prefix(input, prefix)) {
result = cmd_ac_complete_filepath(input, prefix, previous); result = cmd_ac_complete_filepath(input, prefix, previous);
if (result) { if (result) {
g_list_free(filepath_hashes); g_list_free(filepath_hashes);
g_list_free(prefixes);
return result; return result;
} }
} }
curr_prefix = g_list_next(curr_prefix);
} }
g_list_free(prefixes);
curr_hash = g_list_next(curr_hash); curr_hash = g_list_next(curr_hash);
} }

View File

@@ -149,14 +149,15 @@ callbacks_remove(const char* const plugin_name)
{ {
GHashTable* command_hash = g_hash_table_lookup(p_commands, plugin_name); GHashTable* command_hash = g_hash_table_lookup(p_commands, plugin_name);
if (command_hash) { if (command_hash) {
GHashTableIter iter; GList* commands = g_hash_table_get_keys(command_hash);
gpointer key, value; GList* curr = commands;
g_hash_table_iter_init(&iter, command_hash); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { char* command = curr->data;
char* command = (char*)key;
cmd_ac_remove(command); cmd_ac_remove(command);
cmd_ac_remove_help(&command[1]); cmd_ac_remove_help(&command[1]);
curr = g_list_next(curr);
} }
g_list_free(commands);
} }
g_hash_table_remove(p_commands, plugin_name); g_hash_table_remove(p_commands, plugin_name);
@@ -164,12 +165,13 @@ callbacks_remove(const char* const plugin_name)
GHashTable* tag_to_win_cb_hash = g_hash_table_lookup(p_window_callbacks, plugin_name); GHashTable* tag_to_win_cb_hash = g_hash_table_lookup(p_window_callbacks, plugin_name);
if (tag_to_win_cb_hash) { if (tag_to_win_cb_hash) {
GHashTableIter iter; GList* tags = g_hash_table_get_keys(tag_to_win_cb_hash);
gpointer key, value; GList* curr = tags;
g_hash_table_iter_init(&iter, tag_to_win_cb_hash); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { wins_close_plugin(curr->data);
wins_close_plugin(key); curr = g_list_next(curr);
} }
g_list_free(tags);
} }
g_hash_table_remove(p_window_callbacks, plugin_name); g_hash_table_remove(p_window_callbacks, plugin_name);

View File

@@ -107,11 +107,10 @@ disco_remove_features(const char* plugin_name)
return; return;
} }
GHashTableIter iter; GList* plugin_feature_list = g_hash_table_get_keys(plugin_features_set);
gpointer key, value; GList* curr = plugin_feature_list;
g_hash_table_iter_init(&iter, plugin_features_set); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { char* feature = curr->data;
char* feature = (char*)key;
if (g_hash_table_contains(features, feature)) { if (g_hash_table_contains(features, feature)) {
void* refcountp = g_hash_table_lookup(features, feature); void* refcountp = g_hash_table_lookup(features, feature);
int refcount = GPOINTER_TO_INT(refcountp); int refcount = GPOINTER_TO_INT(refcountp);
@@ -122,7 +121,10 @@ disco_remove_features(const char* plugin_name)
g_hash_table_replace(features, strdup(feature), GINT_TO_POINTER(refcount)); g_hash_table_replace(features, strdup(feature), GINT_TO_POINTER(refcount));
} }
} }
curr = g_list_next(curr);
} }
g_list_free(plugin_feature_list);
} }
GList* GList*

View File

@@ -887,8 +887,8 @@ _python_undefined_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook); g_string_append(err_msg, hook);
g_string_append(err_msg, "(): return value undefined, expected "); g_string_append(err_msg, "(): return value undefined, expected ");
g_string_append(err_msg, type); g_string_append(err_msg, type);
log_error("%s", err_msg->str); log_error(err_msg->str);
cons_show_error("%s", err_msg->str); cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE); g_string_free(err_msg, TRUE);
} }
@@ -901,8 +901,8 @@ _python_type_error(ProfPlugin* plugin, char* hook, char* type)
g_string_append(err_msg, hook); g_string_append(err_msg, hook);
g_string_append(err_msg, "(): incorrect return type, expected "); g_string_append(err_msg, "(): incorrect return type, expected ");
g_string_append(err_msg, type); g_string_append(err_msg, type);
log_error("%s", err_msg->str); log_error(err_msg->str);
cons_show_error("%s", err_msg->str); cons_show_error(err_msg->str);
g_string_free(err_msg, TRUE); g_string_free(err_msg, TRUE);
} }

View File

@@ -135,7 +135,7 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
*/ */
min_runtime += waittime; min_runtime += waittime;
} else { } else {
log_error("%s", err_msg); log_error(err_msg);
g_free(err_msg); g_free(err_msg);
commands = NULL; commands = NULL;
} }
@@ -245,7 +245,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
if (prof_log_level == PROF_LEVEL_DEBUG) { if (prof_log_level == PROF_LEVEL_DEBUG) {
ProfWin* console = wins_get_console(); ProfWin* console = wins_get_console();
win_println(console, THEME_DEFAULT, "-", "Debug mode enabled! Logging to: "); win_println(console, THEME_DEFAULT, "-", "Debug mode enabled! Logging to: ");
win_println(console, THEME_DEFAULT, "-", "%s", get_log_file_location()); win_println(console, THEME_DEFAULT, "-", get_log_file_location());
} }
session_init(); session_init();
cmd_init(); cmd_init();

View File

@@ -311,7 +311,7 @@ http_file_put(void* userdata)
} }
win_update_entry_message(upload->window, upload->put_url, err_msg); win_update_entry_message(upload->window, upload->put_url, err_msg);
} }
cons_show_error("%s", err_msg); cons_show_error(err_msg);
} else { } else {
if (!upload->cancel) { if (!upload->cancel) {
auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': 100%%", upload->filename); auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': 100%%", upload->filename);
@@ -327,7 +327,7 @@ http_file_put(void* userdata)
if (!fail_msg) { if (!fail_msg) {
fail_msg = g_strdup(FALLBACK_MSG); fail_msg = g_strdup(FALLBACK_MSG);
} }
cons_show_error("%s", fail_msg); cons_show_error(fail_msg);
} else { } else {
switch (upload->window->type) { switch (upload->window->type) {
case WIN_CHAT: case WIN_CHAT:

View File

@@ -140,10 +140,6 @@ void
buffer_remove_entry(ProfBuff buffer, int entry) buffer_remove_entry(ProfBuff buffer, int entry)
{ {
GSList* node = g_slist_nth(buffer->entries, entry); GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
// Index out of range; nothing to remove
return;
}
ProfBuffEntry* e = node->data; ProfBuffEntry* e = node->data;
buffer->lines -= e->_lines; buffer->lines -= e->_lines;
_free_entry(e); _free_entry(e);
@@ -172,9 +168,6 @@ ProfBuffEntry*
buffer_get_entry(ProfBuff buffer, int entry) buffer_get_entry(ProfBuff buffer, int entry)
{ {
GSList* node = g_slist_nth(buffer->entries, entry); GSList* node = g_slist_nth(buffer->entries, entry);
if (node == NULL) {
return NULL;
}
return node->data; return node->data;
} }
@@ -196,7 +189,6 @@ buffer_get_entry_by_id(ProfBuff buffer, const char* const id)
static ProfBuffEntry* static ProfBuffEntry*
_create_entry(const char* show_char, int pad_indent, GDateTime* time, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message, DeliveryReceipt* receipt, const char* const id, int y_start_pos, int y_end_pos) _create_entry(const char* show_char, int pad_indent, GDateTime* time, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message, DeliveryReceipt* receipt, const char* const id, int y_start_pos, int y_end_pos)
{ {
assert(time != NULL);
ProfBuffEntry* e = malloc(sizeof(struct prof_buff_entry_t)); ProfBuffEntry* e = malloc(sizeof(struct prof_buff_entry_t));
e->show_char = STRDUP_OR_NULL(show_char); e->show_char = STRDUP_OR_NULL(show_char);
e->pad_indent = pad_indent; e->pad_indent = pad_indent;

View File

@@ -592,11 +592,8 @@ chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* e
{ {
auto_gchar gchar* _end_time = NULL; auto_gchar gchar* _end_time = NULL;
if (!end_time && buffer_size(((ProfWin*)chatwin)->layout->buffer) > 0) { if (!end_time && buffer_size(((ProfWin*)chatwin)->layout->buffer) > 0) {
ProfBuffEntry* first = buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0); _end_time = g_date_time_format_iso8601(buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0)->time);
if (first && first->time) { end_time = _end_time;
_end_time = g_date_time_format_iso8601(first->time);
end_time = _end_time;
}
} }
GSList* history = NULL; GSList* history = NULL;

View File

@@ -152,7 +152,7 @@ cons_bad_cmd_usage(const char* const cmd)
g_string_printf(msg, "Invalid usage, see '/help %s' for details.", &cmd[1]); g_string_printf(msg, "Invalid usage, see '/help %s' for details.", &cmd[1]);
cons_show(""); cons_show("");
cons_show("%s", msg->str); cons_show(msg->str);
g_string_free(msg, TRUE); g_string_free(msg, TRUE);
} }
@@ -773,7 +773,7 @@ cons_show_disco_info(const char* jid, GSList* identities, GSList* features)
if (identity->category) { if (identity->category) {
identity_str = g_string_append(identity_str, identity->category); identity_str = g_string_append(identity_str, identity->category);
} }
cons_show("%s", identity_str->str); cons_show(identity_str->str);
g_string_free(identity_str, TRUE); g_string_free(identity_str, TRUE);
identities = g_slist_next(identities); identities = g_slist_next(identities);
} }
@@ -938,7 +938,7 @@ cons_show_account_list(gchar** accounts)
theme_item_t presence_colour = theme_main_presence_attrs(string_from_resource_presence(presence)); theme_item_t presence_colour = theme_main_presence_attrs(string_from_resource_presence(presence));
win_println(console, presence_colour, "-", "%s", accounts[i]); win_println(console, presence_colour, "-", "%s", accounts[i]);
} else { } else {
cons_show("%s", accounts[i]); cons_show(accounts[i]);
} }
} }
cons_show(""); cons_show("");
@@ -1019,7 +1019,7 @@ cons_show_account(ProfAccount* account)
} }
curr = curr->next; curr = curr->next;
} }
cons_show("%s", manual->str); cons_show(manual->str);
g_string_free(manual, TRUE); g_string_free(manual, TRUE);
} }
if (g_list_length(account->otr_opportunistic) > 0) { if (g_list_length(account->otr_opportunistic) > 0) {
@@ -1032,7 +1032,7 @@ cons_show_account(ProfAccount* account)
} }
curr = curr->next; curr = curr->next;
} }
cons_show("%s", opportunistic->str); cons_show(opportunistic->str);
g_string_free(opportunistic, TRUE); g_string_free(opportunistic, TRUE);
} }
if (g_list_length(account->otr_always) > 0) { if (g_list_length(account->otr_always) > 0) {
@@ -1045,7 +1045,7 @@ cons_show_account(ProfAccount* account)
} }
curr = curr->next; curr = curr->next;
} }
cons_show("%s", always->str); cons_show(always->str);
g_string_free(always, TRUE); g_string_free(always, TRUE);
} }
@@ -2297,7 +2297,7 @@ cons_show_themes(GSList* themes)
} else { } else {
cons_show("Available themes:"); cons_show("Available themes:");
while (themes) { while (themes) {
cons_show("%s", themes->data); cons_show(themes->data);
themes = g_slist_next(themes); themes = g_slist_next(themes);
} }
} }
@@ -2315,7 +2315,7 @@ cons_show_scripts(GSList* scripts)
} else { } else {
cons_show("Scripts:"); cons_show("Scripts:");
while (scripts) { while (scripts) {
cons_show("%s", scripts->data); cons_show(scripts->data);
scripts = g_slist_next(scripts); scripts = g_slist_next(scripts);
} }
} }

View File

@@ -445,7 +445,7 @@ ui_handle_error(const char* const err_msg)
GString* msg = g_string_new(""); GString* msg = g_string_new("");
g_string_printf(msg, "Error %s", err_msg); g_string_printf(msg, "Error %s", err_msg);
cons_show_error("%s", msg->str); cons_show_error(msg->str);
g_string_free(msg, TRUE); g_string_free(msg, TRUE);
} }
@@ -461,7 +461,7 @@ ui_invalid_command_usage(const char* const cmd, void (*setting_func)(void))
(*setting_func)(); (*setting_func)();
} else { } else {
cons_show(""); cons_show("");
cons_show("%s", msg->str); cons_show(msg->str);
ProfWin* current = wins_get_current(); ProfWin* current = wins_get_current();
if (current->type == WIN_CHAT) { if (current->type == WIN_CHAT) {
win_println(current, THEME_DEFAULT, "-", "%s", msg->str); win_println(current, THEME_DEFAULT, "-", "%s", msg->str);

View File

@@ -144,14 +144,10 @@ void
create_input_window(void) create_input_window(void)
{ {
/* MB_CUR_MAX is evaluated at runtime depending on the current /* MB_CUR_MAX is evaluated at runtime depending on the current
* locale; ensure our own compiled-in maximum is sufficient. * locale, therefore we check that our own version is big enough
* Fail gracefully instead of aborting in production. * and bail out if it isn't.
*/ */
if (MB_CUR_MAX > PROF_MB_CUR_MAX) { assert(MB_CUR_MAX <= PROF_MB_CUR_MAX);
log_error("Locale MB_CUR_MAX (%zu) exceeds compiled limit (%d)", (size_t)MB_CUR_MAX, PROF_MB_CUR_MAX);
cons_show_error("Unsupported locale. Before running, execute in terminal: export LC_ALL=C.UTF-8");
return;
}
#ifdef NCURSES_REENTRANT #ifdef NCURSES_REENTRANT
set_escdelay(25); set_escdelay(25);
#else #else
@@ -167,10 +163,6 @@ create_input_window(void)
rl_callback_handler_install(NULL, _inp_rl_linehandler); rl_callback_handler_install(NULL, _inp_rl_linehandler);
inp_win = newpad(1, INP_WIN_MAX); inp_win = newpad(1, INP_WIN_MAX);
if (!inp_win) {
log_error("Failed to allocate input window pad");
return;
}
wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT)); wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT));
keypad(inp_win, TRUE); keypad(inp_win, TRUE);
wmove(inp_win, 0, 0); wmove(inp_win, 0, 0);
@@ -246,9 +238,6 @@ inp_readline(void)
void void
inp_win_resize(void) inp_win_resize(void)
{ {
if (!inp_win) {
return;
}
int col = getcurx(inp_win); int col = getcurx(inp_win);
int wcols = getmaxx(stdscr); int wcols = getmaxx(stdscr);
@@ -296,10 +285,8 @@ void
inp_close(void) inp_close(void)
{ {
rl_callback_handler_remove(); rl_callback_handler_remove();
if (inp_win) { delwin(inp_win);
delwin(inp_win); inp_win = NULL;
inp_win = NULL;
}
fclose(discard); fclose(discard);
discard = NULL; discard = NULL;
} }
@@ -307,9 +294,6 @@ inp_close(void)
char* char*
inp_get_line(void) inp_get_line(void)
{ {
if (!inp_win) {
return NULL;
}
werase(inp_win); werase(inp_win);
wmove(inp_win, 0, 0); wmove(inp_win, 0, 0);
_inp_win_update_virtual(); _inp_win_update_virtual();
@@ -334,9 +318,6 @@ inp_set_line(const char* const new_line)
char* char*
inp_get_password(void) inp_get_password(void)
{ {
if (!inp_win) {
return NULL;
}
werase(inp_win); werase(inp_win);
wmove(inp_win, 0, 0); wmove(inp_win, 0, 0);
_inp_win_update_virtual(); _inp_win_update_virtual();

View File

@@ -40,8 +40,6 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include "log.h"
#ifdef HAVE_NCURSESW_NCURSES_H #ifdef HAVE_NCURSESW_NCURSES_H
#include <ncursesw/ncurses.h> #include <ncursesw/ncurses.h>
#elif HAVE_NCURSES_H #elif HAVE_NCURSES_H
@@ -113,24 +111,16 @@ status_bar_init(void)
int row = screen_statusbar_row(); int row = screen_statusbar_row();
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_init: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
statusbar_win = newwin(1, cols, row, 0); statusbar_win = newwin(1, cols, row, 0);
if (statusbar_win) { status_bar_draw();
status_bar_draw();
}
} }
void void
status_bar_close(void) status_bar_close(void)
{ {
if (statusbar_win) { delwin(statusbar_win);
delwin(statusbar_win); statusbar_win = NULL;
statusbar_win = NULL;
}
if (statusbar) { if (statusbar) {
if (statusbar->time) { if (statusbar->time) {
g_free(statusbar->time); g_free(statusbar->time);
@@ -155,14 +145,7 @@ status_bar_close(void)
void void
status_bar_resize(void) status_bar_resize(void)
{ {
if (!statusbar_win) {
return;
}
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
if (cols <= 0) {
log_warning("status_bar_resize: invalid cols %d, defaulting to 1", cols);
cols = 1;
}
werase(statusbar_win); werase(statusbar_win);
int row = screen_statusbar_row(); int row = screen_statusbar_row();
wresize(statusbar_win, 1, cols); wresize(statusbar_win, 1, cols);
@@ -302,9 +285,6 @@ status_bar_clear_fulljid(void)
void void
status_bar_draw(void) status_bar_draw(void)
{ {
if (!statusbar_win) {
return;
}
werase(statusbar_win); werase(statusbar_win);
wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT)); wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT));
@@ -694,13 +674,8 @@ _display_name(StatusBarTab* tab)
fullname = g_strconcat(mucwin_title, " conf", NULL); fullname = g_strconcat(mucwin_title, " conf", NULL);
} else if (tab->window_type == WIN_PRIVATE) { } else if (tab->window_type == WIN_PRIVATE) {
auto_jid Jid* jid = jid_create(tab->identifier); auto_jid Jid* jid = jid_create(tab->identifier);
if (jid) { auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE);
auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE); fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL);
} else {
// Fallback: use identifier directly if JID parsing failed
fullname = strdup(tab->identifier);
}
} else { } else {
fullname = strdup("window"); fullname = strdup("window");
} }

View File

@@ -73,16 +73,9 @@ void
create_title_bar(void) create_title_bar(void)
{ {
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
int row = screen_titlebar_row(); int row = screen_titlebar_row();
win = newwin(1, cols, row, 0); win = newwin(1, cols, row, 0);
if (!win) {
// Failed to create title bar window; skip initialization to avoid NULL deref
return;
}
wbkgd(win, theme_attrs(THEME_TITLE_TEXT)); wbkgd(win, theme_attrs(THEME_TITLE_TEXT));
title_bar_console(); title_bar_console();
title_bar_set_presence(CONTACT_OFFLINE); title_bar_set_presence(CONTACT_OFFLINE);
@@ -95,18 +88,13 @@ create_title_bar(void)
void void
free_title_bar(void) free_title_bar(void)
{ {
if (win) { delwin(win);
delwin(win); win = NULL;
win = NULL;
}
} }
void void
title_bar_update_virtual(void) title_bar_update_virtual(void)
{ {
if (!win) {
return;
}
ProfWin* window = wins_get_current(); ProfWin* window = wins_get_current();
if (window->type != WIN_CONSOLE) { if (window->type != WIN_CONSOLE) {
if (typing_elapsed) { if (typing_elapsed) {
@@ -126,13 +114,7 @@ title_bar_update_virtual(void)
void void
title_bar_resize(void) title_bar_resize(void)
{ {
if (!win) {
return;
}
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
if (cols <= 0) {
cols = 1;
}
werase(win); werase(win);
@@ -149,9 +131,6 @@ title_bar_resize(void)
void void
title_bar_console(void) title_bar_console(void)
{ {
if (!win) {
return;
}
werase(win); werase(win);
if (typing_elapsed) { if (typing_elapsed) {
g_timer_destroy(typing_elapsed); g_timer_destroy(typing_elapsed);
@@ -213,9 +192,6 @@ title_bar_set_typing(gboolean is_typing)
static void static void
_title_bar_draw(void) _title_bar_draw(void)
{ {
if (!win) {
return;
}
int pos; int pos;
int maxrightpos; int maxrightpos;
ProfWin* current = wins_get_current(); ProfWin* current = wins_get_current();

View File

@@ -75,22 +75,12 @@ static void _win_print_internal(ProfWin* window, const char* show_char, int pad_
int flags, theme_item_t theme_item, const char* const from, const char* const message, DeliveryReceipt* receipt); int flags, theme_item_t theme_item, const char* const from, const char* const message, DeliveryReceipt* receipt);
static void _win_print_wrapped(WINDOW* win, const char* const message, size_t indent, int pad_indent); static void _win_print_wrapped(WINDOW* win, const char* const message, size_t indent, int pad_indent);
// Helper: clamp a subwindow width to a sane range [1, cols-1] if possible
static int
_check_subwin_width(int cols, int width)
{
return cols <= 1 ? 1 : CLAMP(width, 1, cols - 1);
}
int int
win_roster_cols(void) win_roster_cols(void)
{ {
int roster_win_percent = prefs_get_roster_size(); int roster_win_percent = prefs_get_roster_size();
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * roster_win_percent); return CEILING((((double)cols) / 100) * roster_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
} }
int int
@@ -98,10 +88,7 @@ win_occpuants_cols(void)
{ {
int occupants_win_percent = prefs_get_occupants_size(); int occupants_win_percent = prefs_get_occupants_size();
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
int width = CEILING((((double)cols) / 100) * occupants_win_percent); return CEILING((((double)cols) / 100) * occupants_win_percent);
// Clamp to a sane range to avoid zero/full-width pads
width = _check_subwin_width(cols, width);
return width;
} }
static ProfLayout* static ProfLayout*
@@ -157,7 +144,6 @@ win_create_console(void)
ProfWin* ProfWin*
win_create_chat(const char* const barejid) win_create_chat(const char* const barejid)
{ {
assert(barejid != NULL);
ProfChatWin* new_win = malloc(sizeof(ProfChatWin)); ProfChatWin* new_win = malloc(sizeof(ProfChatWin));
new_win->window.type = WIN_CHAT; new_win->window.type = WIN_CHAT;
new_win->window.scroll_state = WIN_SCROLL_INNER; new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -189,7 +175,6 @@ win_create_chat(const char* const barejid)
ProfWin* ProfWin*
win_create_muc(const char* const roomjid) win_create_muc(const char* const roomjid)
{ {
assert(roomjid != NULL);
ProfMucWin* new_win = malloc(sizeof(ProfMucWin)); ProfMucWin* new_win = malloc(sizeof(ProfMucWin));
int cols = getmaxx(stdscr); int cols = getmaxx(stdscr);
@@ -248,8 +233,6 @@ win_create_muc(const char* const roomjid)
ProfWin* ProfWin*
win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata) win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata)
{ {
assert(roomjid != NULL);
assert(form != NULL);
ProfConfWin* new_win = malloc(sizeof(ProfConfWin)); ProfConfWin* new_win = malloc(sizeof(ProfConfWin));
new_win->window.type = WIN_CONFIG; new_win->window.type = WIN_CONFIG;
new_win->window.scroll_state = WIN_SCROLL_INNER; new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -268,7 +251,6 @@ win_create_config(const char* const roomjid, DataForm* form, ProfConfWinCallback
ProfWin* ProfWin*
win_create_private(const char* const fulljid) win_create_private(const char* const fulljid)
{ {
assert(fulljid != NULL);
ProfPrivateWin* new_win = malloc(sizeof(ProfPrivateWin)); ProfPrivateWin* new_win = malloc(sizeof(ProfPrivateWin));
new_win->window.type = WIN_PRIVATE; new_win->window.type = WIN_PRIVATE;
new_win->window.scroll_state = WIN_SCROLL_INNER; new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -299,8 +281,6 @@ win_create_xmlconsole(void)
ProfWin* ProfWin*
win_create_plugin(const char* const plugin_name, const char* const tag) win_create_plugin(const char* const plugin_name, const char* const tag)
{ {
assert(plugin_name != NULL);
assert(tag != NULL);
ProfPluginWin* new_win = malloc(sizeof(ProfPluginWin)); ProfPluginWin* new_win = malloc(sizeof(ProfPluginWin));
new_win->window.type = WIN_PLUGIN; new_win->window.type = WIN_PLUGIN;
new_win->window.scroll_state = WIN_SCROLL_INNER; new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -317,7 +297,6 @@ win_create_plugin(const char* const plugin_name, const char* const tag)
ProfWin* ProfWin*
win_create_vcard(vCard* vcard) win_create_vcard(vCard* vcard)
{ {
assert(vcard != NULL);
ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin)); ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin));
new_win->window.type = WIN_VCARD; new_win->window.type = WIN_VCARD;
new_win->window.scroll_state = WIN_SCROLL_INNER; new_win->window.scroll_state = WIN_SCROLL_INNER;
@@ -369,7 +348,7 @@ win_get_title(ProfWin* window)
const ProfConfWin* confwin = (ProfConfWin*)window; const ProfConfWin* confwin = (ProfConfWin*)window;
assert(confwin->memcheck == PROFCONFWIN_MEMCHECK); assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
auto_gchar gchar* mucwin_title = mucwin_generate_title(confwin->roomjid, PREF_TITLEBAR_MUC_TITLE); auto_gchar gchar* mucwin_title = mucwin_generate_title(confwin->roomjid, PREF_TITLEBAR_MUC_TITLE);
if (confwin->form && confwin->form->modified) { if (confwin->form->modified) {
return g_strconcat(mucwin_title, " config *", NULL); return g_strconcat(mucwin_title, " config *", NULL);
} }
return g_strconcat(mucwin_title, " config", NULL); return g_strconcat(mucwin_title, " config", NULL);
@@ -577,25 +556,7 @@ win_show_subwin(ProfWin* window)
} }
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout; ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// If a subwindow already exists (e.g. repeated call), destroy it to avoid leaks
if (layout->subwin) {
delwin(layout->subwin);
layout->subwin = NULL;
}
// Ensure minimum width to avoid creating a zero-width pad
if (subwin_cols <= 0) {
subwin_cols = 1;
}
layout->subwin = newpad(PAD_SIZE, subwin_cols); layout->subwin = newpad(PAD_SIZE, subwin_cols);
if (layout->subwin == NULL) {
// Failed to allocate subwindow; keep base window resized to full width
log_error("Failed to create subwindow pad (cols=%d)", subwin_cols);
wresize(layout->base.win, PAD_SIZE, cols);
win_redraw(window);
return;
}
wbkgd(layout->subwin, theme_attrs(THEME_TEXT)); wbkgd(layout->subwin, theme_attrs(THEME_TEXT));
wresize(layout->base.win, PAD_SIZE, cols - subwin_cols); wresize(layout->base.win, PAD_SIZE, cols - subwin_cols);
win_redraw(window); win_redraw(window);
@@ -949,11 +910,6 @@ win_refresh_with_subwin(ProfWin* window)
int row_end = screen_mainwin_row_end(); int row_end = screen_mainwin_row_end();
ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout; ProfLayoutSplit* layout = (ProfLayoutSplit*)window->layout;
// Safety: if subwindow is not active, nothing to refresh
if (layout == NULL || layout->subwin == NULL) {
return;
}
if (window->type == WIN_MUC) { if (window->type == WIN_MUC) {
subwin_cols = win_occpuants_cols(); subwin_cols = win_occpuants_cols();
} else if (window->type == WIN_CONSOLE) { } else if (window->type == WIN_CONSOLE) {
@@ -2082,14 +2038,7 @@ win_print_loading_history(ProfWin* window)
gboolean is_buffer_empty = buffer_size(window->layout->buffer) == 0; gboolean is_buffer_empty = buffer_size(window->layout->buffer) == 0;
if (!is_buffer_empty) { if (!is_buffer_empty) {
ProfBuffEntry* first = buffer_get_entry(window->layout->buffer, 0); timestamp = buffer_get_entry(window->layout->buffer, 0)->time;
if (first && first->time) {
timestamp = first->time;
} else {
// Fallback to current time if entry/time is unavailable
timestamp = g_date_time_new_now_local();
is_buffer_empty = TRUE; // ensure we unref fallback timestamp below
}
} else { } else {
timestamp = g_date_time_new_now_local(); timestamp = g_date_time_new_now_local();
} }
@@ -2289,7 +2238,7 @@ void
win_handle_command_exec_result_note(ProfWin* window, const char* const type, const char* const value) win_handle_command_exec_result_note(ProfWin* window, const char* const type, const char* const value)
{ {
assert(window != NULL); assert(window != NULL);
win_println(window, THEME_DEFAULT, "!", "%s", value); win_println(window, THEME_DEFAULT, "!", value);
} }
void void

View File

@@ -407,10 +407,8 @@ wins_get_by_string(const char* str)
if (barejid) { if (barejid) {
ProfChatWin* chatwin = wins_get_chat(barejid); ProfChatWin* chatwin = wins_get_chat(barejid);
if (chatwin) { if (chatwin) {
free(barejid);
return (ProfWin*)chatwin; return (ProfWin*)chatwin;
} }
free(barejid);
} }
} }
@@ -605,9 +603,6 @@ wins_new_xmlconsole(void)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_xmlconsole(); ProfWin* newwin = win_create_xmlconsole();
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, "xmlconsole"); autocomplete_add(wins_ac, "xmlconsole");
autocomplete_add(wins_close_ac, "xmlconsole"); autocomplete_add(wins_close_ac, "xmlconsole");
@@ -619,9 +614,6 @@ wins_new_chat(const char* const barejid)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_chat(barejid); ProfWin* newwin = win_create_chat(barejid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, barejid); autocomplete_add(wins_ac, barejid);
@@ -645,9 +637,6 @@ wins_new_muc(const char* const roomjid)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_muc(roomjid); ProfWin* newwin = win_create_muc(roomjid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, roomjid); autocomplete_add(wins_ac, roomjid);
autocomplete_add(wins_close_ac, roomjid); autocomplete_add(wins_close_ac, roomjid);
@@ -662,9 +651,6 @@ wins_new_config(const char* const roomjid, DataForm* form, ProfConfWinCallback s
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_config(roomjid, form, submit, cancel, userdata); ProfWin* newwin = win_create_config(roomjid, form, submit, cancel, userdata);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin; return newwin;
@@ -675,9 +661,6 @@ wins_new_private(const char* const fulljid)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_private(fulljid); ProfWin* newwin = win_create_private(fulljid);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, fulljid); autocomplete_add(wins_ac, fulljid);
autocomplete_add(wins_close_ac, fulljid); autocomplete_add(wins_close_ac, fulljid);
@@ -692,9 +675,6 @@ wins_new_plugin(const char* const plugin_name, const char* const tag)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_plugin(plugin_name, tag); ProfWin* newwin = win_create_plugin(plugin_name, tag);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
autocomplete_add(wins_ac, tag); autocomplete_add(wins_ac, tag);
autocomplete_add(wins_close_ac, tag); autocomplete_add(wins_close_ac, tag);
@@ -706,9 +686,6 @@ wins_new_vcard(vCard* vcard)
{ {
int result = _wins_get_next_available_num(keys); int result = _wins_get_next_available_num(keys);
ProfWin* newwin = win_create_vcard(vcard); ProfWin* newwin = win_create_vcard(vcard);
if (!newwin) {
return NULL;
}
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin); _wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
return newwin; return newwin;

View File

@@ -171,15 +171,16 @@ caps_get_features(void)
{ {
GList* result = NULL; GList* result = NULL;
GHashTableIter iter; GList* features_as_list = g_hash_table_get_keys(prof_features);
gpointer key, value; GList* curr = features_as_list;
g_hash_table_iter_init(&iter, prof_features); while (curr) {
while (g_hash_table_iter_next(&iter, &key, &value)) { result = g_list_append(result, strdup(curr->data));
result = g_list_append(result, strdup((char*)key)); curr = g_list_next(curr);
} }
g_list_free(features_as_list);
GList* plugin_features = plugins_get_disco_features(); GList* plugin_features = plugins_get_disco_features();
GList* curr = plugin_features; curr = plugin_features;
while (curr) { while (curr) {
result = g_list_append(result, strdup(curr->data)); result = g_list_append(result, strdup(curr->data));
curr = g_list_next(curr); curr = g_list_next(curr);

View File

@@ -148,7 +148,7 @@ connection_init(void)
if (string_to_verbosity(v, &verbosity, &err_msg)) { if (string_to_verbosity(v, &verbosity, &err_msg)) {
xmpp_ctx_set_verbosity(conn.xmpp_ctx, verbosity); xmpp_ctx_set_verbosity(conn.xmpp_ctx, verbosity);
} else { } else {
cons_show("%s", err_msg); cons_show(err_msg);
} }
conn.xmpp_conn = xmpp_conn_new(conn.xmpp_ctx); conn.xmpp_conn = xmpp_conn_new(conn.xmpp_ctx);
@@ -638,18 +638,22 @@ gboolean
connection_supports(const char* const feature) connection_supports(const char* const feature)
{ {
gboolean ret = FALSE; gboolean ret = FALSE;
GHashTableIter iter; GList* jids = g_hash_table_get_keys(conn.features_by_jid);
gpointer key, value;
g_hash_table_iter_init(&iter, conn.features_by_jid); GList* curr = jids;
while (g_hash_table_iter_next(&iter, &key, &value)) { while (curr) {
GHashTable* features = (GHashTable*)value; char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) { if (features && g_hash_table_lookup(features, feature)) {
ret = TRUE; ret = TRUE;
break; break;
} }
curr = g_list_next(curr);
} }
g_list_free(jids);
return ret; return ret;
} }
@@ -660,17 +664,22 @@ connection_jid_for_feature(const char* const feature)
return NULL; return NULL;
} }
GHashTableIter iter; GList* jids = g_hash_table_get_keys(conn.features_by_jid);
gpointer key, value;
g_hash_table_iter_init(&iter, conn.features_by_jid); GList* curr = jids;
while (g_hash_table_iter_next(&iter, &key, &value)) { while (curr) {
GHashTable* features = (GHashTable*)value; char* jid = curr->data;
GHashTable* features = g_hash_table_lookup(conn.features_by_jid, jid);
if (features && g_hash_table_lookup(features, feature)) { if (features && g_hash_table_lookup(features, feature)) {
return (const char*)key; g_list_free(jids);
return jid;
} }
curr = g_list_next(curr);
} }
g_list_free(jids);
return NULL; return NULL;
} }
@@ -1025,7 +1034,7 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
conn.sm_state = xmpp_conn_get_sm_state(conn.xmpp_conn); conn.sm_state = xmpp_conn_get_sm_state(conn.xmpp_conn);
if (send_queue_len > 0 && prefs_get_boolean(PREF_STROPHE_SM_RESEND)) { if (send_queue_len > 0 && prefs_get_boolean(PREF_STROPHE_SM_RESEND)) {
conn.queued_messages = calloc(send_queue_len + 1, sizeof(*conn.queued_messages)); conn.queued_messages = calloc(send_queue_len + 1, sizeof(*conn.queued_messages));
for (int n = 0; n < send_queue_len; ++n) { for (int n = 0; n < send_queue_len && conn.queued_messages[n]; ++n) {
conn.queued_messages[n] = xmpp_conn_send_queue_drop_element(conn.xmpp_conn, XMPP_QUEUE_OLDEST); conn.queued_messages[n] = xmpp_conn_send_queue_drop_element(conn.xmpp_conn, XMPP_QUEUE_OLDEST);
} }
} else if (send_queue_len > 0) { } else if (send_queue_len > 0) {
@@ -1180,13 +1189,12 @@ connection_debug_print_features()
continue; continue;
} }
GHashTableIter feature_iter; GList* feature_keys = g_hash_table_get_keys(features);
gpointer feature_key, feature_value; for (GList* l = feature_keys; l != NULL; l = l->next) {
g_hash_table_iter_init(&feature_iter, features); const char* feature = (const char*)l->data;
while (g_hash_table_iter_next(&feature_iter, &feature_key, &feature_value)) {
const char* feature = (const char*)feature_key;
log_debug("%s:\t%s", jid, feature); log_debug("%s:\t%s", jid, feature);
} }
g_list_free(feature_keys);
} }
log_debug("=== End of Features ==="); log_debug("=== End of Features ===");

View File

@@ -437,7 +437,18 @@ form_get_form_type_field(DataForm* form)
gboolean gboolean
form_tag_exists(DataForm* form, const char* const tag) form_tag_exists(DataForm* form, const char* const tag)
{ {
return g_hash_table_contains(form->tag_to_var, tag); GList* tags = g_hash_table_get_keys(form->tag_to_var);
GList* curr = tags;
while (curr) {
if (g_strcmp0(curr->data, tag) == 0) {
g_list_free(tags);
return TRUE;
}
curr = g_list_next(curr);
}
g_list_free(tags);
return FALSE;
} }
form_field_type_t form_field_type_t

View File

@@ -2532,6 +2532,9 @@ _disco_items_result_handler(xmpp_stanza_t* const stanza)
} }
xmpp_stanza_t* child = xmpp_stanza_get_children(query); xmpp_stanza_t* child = xmpp_stanza_get_children(query);
if (child == NULL) {
return;
}
while (child) { while (child) {
const char* stanza_name = xmpp_stanza_get_name(child); const char* stanza_name = xmpp_stanza_get_name(child);

View File

@@ -868,7 +868,7 @@ _handle_error(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error="); g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg); g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str); log_info(log_msg->str);
g_string_free(log_msg, TRUE); g_string_free(log_msg, TRUE);

View File

@@ -326,7 +326,7 @@ _ox_metadata_result(xmpp_stanza_t* const stanza, void* const userdata)
if (fingerprint) { if (fingerprint) {
if (strlen(fingerprint) == KEYID_LENGTH) { if (strlen(fingerprint) == KEYID_LENGTH) {
cons_show("%s", fingerprint); cons_show(fingerprint);
} else { } else {
cons_show("OX: Wrong char size of public key"); cons_show("OX: Wrong char size of public key");
log_error("[OX] Wrong chat size of public key %s", fingerprint); log_error("[OX] Wrong chat size of public key %s", fingerprint);

View File

@@ -455,7 +455,7 @@ _presence_error_handler(xmpp_stanza_t* const stanza)
g_string_append(log_msg, " error="); g_string_append(log_msg, " error=");
g_string_append(log_msg, err_msg); g_string_append(log_msg, err_msg);
log_info("%s", log_msg->str); log_info(log_msg->str);
g_string_free(log_msg, TRUE); g_string_free(log_msg, TRUE);

View File

@@ -15,9 +15,9 @@
* functional tests run less frequently than unit tests. * functional tests run less frequently than unit tests.
* *
* Tests are organized into groups for better maintainability and parallel execution: * Tests are organized into groups for better maintainability and parallel execution:
* Group 1: Connect, Ping, Autoping (fast), Rooms, Software, Last Activity * Group 1: Connect, Ping, Rooms, Software
* Group 2: Message, Receipts, Roster, Chat Session * Group 2: Message, Receipts, Roster, Chat Session
* Group 3: Presence, Disconnect, Autoping (slow) * Group 3: Presence, Disconnect
* Group 4: MUC, Carbons * Group 4: MUC, Carbons
* *
* Parallel execution: * Parallel execution:
@@ -52,9 +52,6 @@
#include "test_software.h" #include "test_software.h"
#include "test_muc.h" #include "test_muc.h"
#include "test_disconnect.h" #include "test_disconnect.h"
#include "test_lastactivity.h"
#include "test_autoping.h"
#include "test_disco.h"
/* Macro to wrap each test with setup/teardown functions */ /* Macro to wrap each test with setup/teardown functions */
#define PROF_FUNC_TEST(test) cmocka_unit_test_setup_teardown(test, init_prof_test, close_prof_test) #define PROF_FUNC_TEST(test) cmocka_unit_test_setup_teardown(test, init_prof_test, close_prof_test)
@@ -72,12 +69,6 @@ main(int argc, char* argv[])
} }
} }
char group_env[16];
snprintf(group_env, sizeof(group_env), "%d", group);
setenv("PROF_TEST_GROUP", group_env, 1);
fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group);
/* ============================================================ /* ============================================================
* GROUP 1: Connect, Ping, Rooms, Software * GROUP 1: Connect, Ping, Rooms, Software
* Basic XMPP session establishment and server queries * Basic XMPP session establishment and server queries
@@ -107,20 +98,6 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart), PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource), PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat), PROF_FUNC_TEST(display_software_version_result_in_chat),
/* Last Activity - XEP-0012 */
PROF_FUNC_TEST(responds_to_last_activity_request),
PROF_FUNC_TEST(last_activity_request_to_contact),
/* Autoping command tests - fast, no waiting */
PROF_FUNC_TEST(autoping_set_interval),
PROF_FUNC_TEST(autoping_set_zero_disables),
PROF_FUNC_TEST(autoping_timeout_set),
PROF_FUNC_TEST(autoping_timeout_zero_disables),
/* Autoping slow tests - require sleep for timer triggers (~2s each) */
PROF_FUNC_TEST(autoping_sends_ping_after_interval),
PROF_FUNC_TEST(autoping_server_not_supporting_ping),
}; };
/* ============================================================ /* ============================================================
@@ -155,7 +132,7 @@ main(int argc, char* argv[])
}; };
/* ============================================================ /* ============================================================
* GROUP 3: Presence, Disconnect, Disco * GROUP 3: Presence, Disconnect
* Online/away/xa/dnd/chat status management * Online/away/xa/dnd/chat status management
* ============================================================ */ * ============================================================ */
const struct CMUnitTest group3_tests[] = { const struct CMUnitTest group3_tests[] = {
@@ -177,22 +154,6 @@ main(int argc, char* argv[])
/* Disconnect - clean session termination */ /* Disconnect - clean session termination */
PROF_FUNC_TEST(disconnect_ends_session), PROF_FUNC_TEST(disconnect_ends_session),
/* Service Discovery - XEP-0030 */
PROF_FUNC_TEST(disco_info_shows_identity),
PROF_FUNC_TEST(disco_info_shows_features),
PROF_FUNC_TEST(disco_info_to_server),
PROF_FUNC_TEST(disco_info_to_jid),
PROF_FUNC_TEST(disco_info_not_found),
PROF_FUNC_TEST(disco_items_shows_items),
PROF_FUNC_TEST(disco_items_empty_result),
PROF_FUNC_TEST(disco_requires_connection),
PROF_FUNC_TEST(disco_items_to_jid),
PROF_FUNC_TEST(disco_info_empty_result),
PROF_FUNC_TEST(disco_info_multiple_identities),
PROF_FUNC_TEST(disco_info_without_name),
PROF_FUNC_TEST(disco_items_without_name),
PROF_FUNC_TEST(disco_info_service_unavailable),
}; };
/* ============================================================ /* ============================================================
@@ -221,10 +182,6 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed), PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed), PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
/* MUC moderation - XEP-0045 room admin */
PROF_FUNC_TEST(sends_affiliation_list_request),
PROF_FUNC_TEST(sends_kick_request),
/* Message Carbons - XEP-0280 (message sync across devices) */ /* Message Carbons - XEP-0280 (message sync across devices) */
PROF_FUNC_TEST(send_enable_carbons), PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled), PROF_FUNC_TEST(connect_with_carbons_enabled),
@@ -240,9 +197,9 @@ main(int argc, char* argv[])
const struct CMUnitTest* tests; const struct CMUnitTest* tests;
size_t count; size_t count;
} groups[] = { } groups[] = {
{ "Group 1: Connect/Ping/Rooms/Software/Autoping", group1_tests, ARRAY_SIZE(group1_tests) }, { "Group 1: Connect/Ping/Rooms/Software", group1_tests, ARRAY_SIZE(group1_tests) },
{ "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) }, { "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) },
{ "Group 3: Presence/Disconnect/Disco", group3_tests, ARRAY_SIZE(group3_tests) }, { "Group 3: Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
{ "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) }, { "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) },
}; };
const int num_groups = ARRAY_SIZE(groups); const int num_groups = ARRAY_SIZE(groups);

View File

@@ -17,9 +17,6 @@
#include "proftest.h" #include "proftest.h"
/* Number of parallel test groups for CI builds */
#define TEST_GROUPS 4
char *config_orig; char *config_orig;
char *data_orig; char *data_orig;
@@ -139,14 +136,8 @@ _create_logs_dir(void)
void void
_cleanup_dirs(void) _cleanup_dirs(void)
{ {
const char *group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
printf("[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id);
char cmd[512]; char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id); snprintf(cmd, sizeof(cmd), "rm -rf ./tests/functionaltests/files/%d", stub_port);
int res = system(cmd); int res = system(cmd);
if (res == -1) { if (res == -1) {
assert_true(FALSE); assert_true(FALSE);
@@ -230,74 +221,30 @@ prof_start(void)
/* Set non-blocking mode for reading */ /* Set non-blocking mode for reading */
int flags = fcntl(fd, F_GETFL, 0); int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); fcntl(fd, F_SETFL, flags | O_NONBLOCK);
/* Brief wait for process to initialize */
usleep(50000); /* 50ms */
} }
int int
init_prof_test(void **state) init_prof_test(void **state)
{ {
/* Get test group from environment for static resource allocation */
const char *group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
/* Get build index for port offset (for parallel CI builds) */
const char *build_env = getenv("PROF_BUILD_INDEX");
int build_idx = build_env ? atoi(build_env) : 0;
/* Calculate port base: each build uses a different range of TEST_GROUPS ports.
* Build 0 (local/default): 5230-5233, Full: 5230-5233, Minimal: 5234-5237, etc.
* Build 0 and Full share the same range because build 0 is for local runs or sequential run (no parallel builds),
* while Full/Minimal/NoEncrypt/Default are used in CI where they run in parallel. */
int port_base = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS);
/* Static resource allocation to avoid conflicts in parallel execution.
* Group 1-4: use static port assignment.
* Group 0 (all groups): use dynamic allocation as fallback. */
gboolean started = FALSE; gboolean started = FALSE;
for (int p = 5230; p < 5250; ++p) {
if (group >= 1 && group <= TEST_GROUPS) { int ret = stbbr_start(STBBR_LOGDEBUG, p, 0);
/* Static allocation: each group gets a dedicated port */ if (ret == 0) {
stub_port = port_base + group - 1; stub_port = p;
printf("[PROF_TEST] Build %d, Group %d: trying port %d\n", build_idx, group, stub_port);
if (stbbr_start(STBBR_LOGDEBUG, stub_port, 0) == 0) {
started = TRUE; started = TRUE;
printf("[PROF_TEST] Started stabber on port %d\n", stub_port); break;
} else {
printf("[PROF_TEST] Failed to start stabber on port %d\n", stub_port);
} }
} }
/* Fallback to dynamic allocation if static failed or group=0 */
if (!started) { if (!started) {
printf("[PROF_TEST] Using dynamic port allocation\n"); assert_true(FALSE); // could not start stabber on any port in range
for (int p = port_base; p < port_base + 20; ++p) {
if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) {
stub_port = p;
started = TRUE;
printf("[PROF_TEST] Started stabber on port %d\n", stub_port);
break;
}
}
}
if (!started) {
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
return -1; return -1;
} }
/* Generate unique XDG paths based on group for parallel execution. // Generate unique XDG paths based on stub_port for parallel execution
* Use ./test-files/ in current (build) directory for out-of-tree builds compatibility. */
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
snprintf(xdg_config_home, sizeof(xdg_config_home), snprintf(xdg_config_home, sizeof(xdg_config_home),
"./test-files/%d/xdg_config_home", dir_id); "./tests/functionaltests/files/%d/xdg_config_home", stub_port);
snprintf(xdg_data_home, sizeof(xdg_data_home), snprintf(xdg_data_home, sizeof(xdg_data_home),
"./test-files/%d/xdg_data_home", dir_id); "./tests/functionaltests/files/%d/xdg_data_home", stub_port);
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
group, xdg_config_home, xdg_data_home);
// Give stabber server thread time to start listening // Give stabber server thread time to start listening
usleep(100000); // 100ms usleep(100000); // 100ms
@@ -362,6 +309,7 @@ close_prof_test(void **state)
fd = 0; fd = 0;
child_pid = 0; child_pid = 0;
} }
_cleanup_dirs();
if (config_orig) { if (config_orig) {
setenv("XDG_CONFIG_HOME", config_orig, 1); setenv("XDG_CONFIG_HOME", config_orig, 1);
@@ -453,16 +401,6 @@ prof_output_regex(const char *pattern)
usleep(50000); /* 50ms */ usleep(50000); /* 50ms */
} }
/* Timeout reached - log diagnostic info */
fprintf(stderr, "Timeout waiting for regex '%s' after %d seconds. Last output:\n", pattern, expect_timeout);
size_t len = strlen(output_buffer);
if (len > 500) {
fprintf(stderr, "...%s", output_buffer + len - 500);
} else {
fprintf(stderr, "%s", output_buffer);
}
fprintf(stderr, "\n");
regfree(&regex); regfree(&regex);
return 0; return 0;
} }

View File

@@ -1,114 +0,0 @@
#include <glib.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stabber.h>
#include "proftest.h"
void
autoping_set_interval(void** state)
{
prof_connect();
prof_input("/autoping set 60");
assert_true(prof_output_exact("Autoping interval set to 60 seconds."));
}
void
autoping_set_zero_disables(void** state)
{
prof_connect();
prof_input("/autoping set 0");
assert_true(prof_output_exact("Autoping disabled."));
}
void
autoping_timeout_set(void** state)
{
prof_connect();
prof_input("/autoping timeout 30");
assert_true(prof_output_exact("Autoping timeout set to 30 seconds."));
}
void
autoping_timeout_zero_disables(void** state)
{
prof_connect();
prof_input("/autoping timeout 0");
assert_true(prof_output_exact("Autoping timeout disabled."));
}
void
autoping_sends_ping_after_interval(void** state)
{
/*
* This test verifies that autoping sends a ping IQ after the configured
* interval. We set a short interval (1 second) and verify the ping is sent.
*/
// Register disco#info response with ping support
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Stabber'/>"
"<feature var='urn:xmpp:ping'/>"
"</query>"
"</iq>"
);
// Register ping response
stbbr_for_query("urn:xmpp:ping",
"<iq from='localhost' to='stabber@localhost/profanity' type='result'/>"
);
prof_connect();
// Set short autoping interval
prof_input("/autoping set 1");
assert_true(prof_output_exact("Autoping interval set to 1 seconds."));
// Wait for autoping to trigger (interval + some buffer)
sleep(2);
// Verify ping was sent (no 'to' attribute means server ping)
assert_true(stbbr_received(
"<iq id='*' type='get'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
}
void
autoping_server_not_supporting_ping(void** state)
{
/*
* When server doesn't support ping, autoping should show error.
*/
// Register disco#info response WITHOUT ping support
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Stabber'/>"
"</query>"
"</iq>"
);
prof_connect();
// Set short autoping interval
prof_input("/autoping set 1");
assert_true(prof_output_exact("Autoping interval set to 1 seconds."));
// Wait for autoping to trigger
sleep(2);
// Should show error about ping not being supported
assert_true(prof_output_regex("Server ping not supported"));
}

View File

@@ -1,6 +0,0 @@
void autoping_set_interval(void** state);
void autoping_set_zero_disables(void** state);
void autoping_timeout_set(void** state);
void autoping_timeout_zero_disables(void** state);
void autoping_sends_ping_after_interval(void** state);
void autoping_server_not_supporting_ping(void** state);

View File

@@ -1,420 +0,0 @@
/*
* test_disco.c
*
* Functional tests for /disco command (XEP-0030 Service Discovery).
* Tests cover:
* - /disco info [jid] - query entity capabilities and features
* - /disco items [jid] - query entity items/services
*
* XEP-0030: https://xmpp.org/extensions/xep-0030.html
*/
#include <glib.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stabber.h>
#include "proftest.h"
void
disco_info_shows_identity(void **state)
{
/*
* Test that /disco info displays identity information correctly.
* Identity includes: name, type, category
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='Prosody'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info");
prof_timeout(10);
assert_true(prof_output_exact("Service discovery info for localhost"));
assert_true(prof_output_exact("Identities"));
assert_true(prof_output_regex("Prosody.*im.*server"));
prof_timeout_reset();
}
void
disco_info_shows_features(void **state)
{
/*
* Test that /disco info displays feature list.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='TestServer'/>"
"<feature var='urn:xmpp:ping'/>"
"<feature var='http://jabber.org/protocol/disco#info'/>"
"<feature var='http://jabber.org/protocol/disco#items'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info");
prof_timeout(10);
assert_true(prof_output_exact("Features:"));
assert_true(prof_output_exact("urn:xmpp:ping"));
assert_true(prof_output_exact("http://jabber.org/protocol/disco#info"));
prof_timeout_reset();
}
void
disco_info_to_server(void **state)
{
/*
* Test that /disco info without arguments queries the server (domainpart).
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='LocalServer'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info");
/* Verify request was sent to server (localhost) */
prof_timeout(10);
assert_true(stbbr_received(
"<iq id='*' to='localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info'/>"
"</iq>"
));
prof_timeout_reset();
}
void
disco_info_to_jid(void **state)
{
/*
* Test that /disco info <jid> queries the specified JID.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='conference.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='conference' type='text' name='MUC Service'/>"
"<feature var='http://jabber.org/protocol/muc'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info conference.localhost");
prof_timeout(10);
/* Verify request was sent to specified JID */
assert_true(stbbr_received(
"<iq id='*' to='conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info'/>"
"</iq>"
));
assert_true(prof_output_exact("Service discovery info for conference.localhost"));
prof_timeout_reset();
}
void
disco_info_not_found(void **state)
{
/*
* Test error handling when disco info returns item-not-found.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='error' from='unknown.localhost'>"
"<error type='cancel'>"
"<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"</error>"
"</iq>"
);
prof_connect();
prof_input("/disco info unknown.localhost");
prof_timeout(10);
assert_true(prof_output_regex("Service discovery failed.*item-not-found"));
prof_timeout_reset();
}
void
disco_items_shows_items(void **state)
{
/*
* Test that /disco items displays items list with JID and name.
*/
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'>"
"<item jid='conference.localhost' name='Chat Rooms'/>"
"<item jid='pubsub.localhost' name='Publish-Subscribe'/>"
"<item jid='proxy.localhost' name='SOCKS5 Bytestreams'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco items");
prof_timeout(10);
assert_true(prof_output_exact("Service discovery items for localhost:"));
assert_true(prof_output_regex("conference.localhost.*Chat Rooms"));
assert_true(prof_output_regex("pubsub.localhost.*Publish-Subscribe"));
assert_true(prof_output_regex("proxy.localhost.*SOCKS5 Bytestreams"));
/* Verify IQ was sent with correct id */
assert_true(stbbr_received(
"<iq id='discoitemsreq' to='localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>"
));
prof_timeout_reset();
}
void
disco_items_empty_result(void **state)
{
/*
* Test that /disco items handles empty result gracefully.
* Per XEP-0030: "if an entity has no associated items, it MUST return
* an empty <query/> element (rather than an error)"
*/
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>"
);
prof_connect();
prof_input("/disco items");
prof_timeout(10);
assert_true(prof_output_exact("No service discovery items for localhost"));
prof_timeout_reset();
}
void
disco_requires_connection(void **state)
{
/*
* Test that /disco info and /disco items require an active connection.
* First test without any connection, then after connect/disconnect.
*/
/* Without connection */
prof_input("/disconnect");
assert_true(prof_output_exact("You are not currently connected."));
prof_input("/disco info");
assert_true(prof_output_exact("You are not currently connected."));
prof_input("/disco items");
assert_true(prof_output_exact("You are not currently connected."));
/* After connect and disconnect */
prof_connect();
prof_input("/disconnect");
assert_true(prof_output_exact("stabber@localhost logged out successfully."));
prof_input("/disco info");
assert_true(prof_output_exact("You are not currently connected."));
prof_input("/disco items");
assert_true(prof_output_exact("You are not currently connected."));
}
void
disco_items_to_jid(void **state)
{
/*
* Test that /disco items <jid> queries the specified JID.
*/
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq to='stabber@localhost/profanity' type='result' from='conference.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'>"
"<item jid='room1@conference.localhost' name='General Chat'/>"
"<item jid='room2@conference.localhost' name='Support'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco items conference.localhost");
prof_timeout(10);
/* Verify request was sent to specified JID */
assert_true(stbbr_received(
"<iq id='discoitemsreq' to='conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>"
));
assert_true(prof_output_exact("Service discovery items for conference.localhost:"));
assert_true(prof_output_regex("room1@conference.localhost.*General Chat"));
prof_timeout_reset();
}
void
disco_info_empty_result(void **state)
{
/*
* Test that /disco info handles empty result (no identities/features).
* This can happen with minimal server configurations.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='minimal.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'/>"
"</iq>"
);
prof_connect();
prof_input("/disco info minimal.localhost");
prof_timeout(10);
/* Verify request was sent */
assert_true(stbbr_received(
"<iq id='*' to='minimal.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info'/>"
"</iq>"
));
/* Empty result should not crash and should not show "Service discovery info" */
prof_timeout_reset();
}
void
disco_info_multiple_identities(void **state)
{
/*
* Test that /disco info displays multiple identities correctly.
* Entities can have multiple identities (e.g., server + gateway).
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='gateway.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='gateway' type='irc' name='IRC Gateway'/>"
"<identity category='directory' type='chatroom' name='Room Directory'/>"
"<identity category='automation' type='command-node' name='Ad-Hoc Commands'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info gateway.localhost");
prof_timeout(10);
assert_true(prof_output_exact("Service discovery info for gateway.localhost"));
assert_true(prof_output_exact("Identities"));
assert_true(prof_output_regex("IRC Gateway.*irc.*gateway"));
assert_true(prof_output_regex("Room Directory.*chatroom.*directory"));
assert_true(prof_output_regex("Ad-Hoc Commands.*command-node.*automation"));
prof_timeout_reset();
}
void
disco_info_without_name(void **state)
{
/*
* Test that /disco info handles identity without name attribute.
* Per XEP-0030: name is OPTIONAL, only category and type are REQUIRED.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco info");
prof_timeout(10);
assert_true(prof_output_exact("Service discovery info for localhost"));
assert_true(prof_output_exact("Identities"));
/* Should show type and category even without name */
assert_true(prof_output_regex("im.*server"));
prof_timeout_reset();
}
void
disco_items_without_name(void **state)
{
/*
* Test that /disco items handles items without name attribute.
* Per XEP-0030: name is OPTIONAL for items, only jid is REQUIRED.
*/
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq to='stabber@localhost/profanity' type='result' from='localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'>"
"<item jid='conference.localhost'/>"
"<item jid='pubsub.localhost' name='PubSub Service'/>"
"<item jid='upload.localhost'/>"
"</query>"
"</iq>"
);
prof_connect();
prof_input("/disco items");
prof_timeout(10);
assert_true(prof_output_exact("Service discovery items for localhost:"));
/* Items without name should still display their JID */
assert_true(prof_output_exact("conference.localhost"));
assert_true(prof_output_regex("pubsub.localhost.*PubSub Service"));
assert_true(prof_output_exact("upload.localhost"));
prof_timeout_reset();
}
void
disco_info_service_unavailable(void **state)
{
/*
* Test error handling when disco info returns service-unavailable.
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='error' from='offline.localhost'>"
"<error type='cancel'>"
"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
"</error>"
"</iq>"
);
prof_connect();
prof_input("/disco info offline.localhost");
prof_timeout(10);
assert_true(prof_output_regex("Service discovery failed.*service-unavailable"));
prof_timeout_reset();
}

View File

@@ -1,19 +0,0 @@
/* test_disco.h
*
* Functional tests for /disco command (XEP-0030 Service Discovery)
*/
void disco_info_shows_identity(void **state);
void disco_info_shows_features(void **state);
void disco_info_to_server(void **state);
void disco_info_to_jid(void **state);
void disco_info_not_found(void **state);
void disco_items_shows_items(void **state);
void disco_items_empty_result(void **state);
void disco_requires_connection(void **state);
void disco_items_to_jid(void **state);
void disco_info_empty_result(void **state);
void disco_info_multiple_identities(void **state);
void disco_info_without_name(void **state);
void disco_items_without_name(void **state);
void disco_info_service_unavailable(void **state);

View File

@@ -1,63 +0,0 @@
/*
* test_lastactivity.c
* Functional tests for Last Activity (XEP-0012)
*/
#include <glib.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include "proftest.h"
void
responds_to_last_activity_request(void **state)
{
prof_connect();
// Send incoming last activity request
stbbr_send(
"<iq id='last1' type='get' to='stabber@localhost/profanity' from='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
);
// Verify that CProof responds with last activity info
// The 'seconds' attribute indicates idle time
assert_true(stbbr_received(
"<iq id='last1' type='result' to='buddy1@localhost/mobile'>"
"<query xmlns='jabber:iq:last' seconds='*'/>"
"</iq>"
));
}
void
last_activity_request_to_contact(void **state)
{
prof_connect();
stbbr_send(
"<presence to='stabber@localhost' from='buddy1@localhost/mobile'>"
"<priority>10</priority>"
"<status>I'm here</status>"
"</presence>"
);
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
// Register response for last activity query
stbbr_for_query("jabber:iq:last",
"<iq id='*' type='result' from='buddy1@localhost/mobile' to='stabber@localhost/profanity'>"
"<query xmlns='jabber:iq:last' seconds='120'/>"
"</iq>"
);
prof_input("/lastactivity get buddy1@localhost/mobile");
// Verify the request was sent
assert_true(stbbr_received(
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='jabber:iq:last'/>"
"</iq>"
));
}

View File

@@ -1,7 +0,0 @@
/*
* test_lastactivity.h
* Header for Last Activity tests (XEP-0012)
*/
void responds_to_last_activity_request(void **state);
void last_activity_request_to_contact(void **state);

View File

@@ -393,83 +393,3 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
assert_false(prof_output_regex("testroom@conference\\.localhost \\(win 2\\)")); assert_false(prof_output_regex("testroom@conference\\.localhost \\(win 2\\)"));
prof_timeout_reset(); prof_timeout_reset();
} }
void
sends_affiliation_list_request(void **state)
{
prof_connect();
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='owner'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: owner"));
prof_input("/affiliation owner list");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item affiliation='owner'/>"
"</query>"
"</iq>"
));
}
void
sends_kick_request(void **state)
{
prof_connect();
// Enable MUC presence messages to see occupant join/leave
prof_input("/presence room all");
assert_true(prof_output_regex("All presence updates will appear"));
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='admin'/>"
"</x>"
"<status code='110'/>"
"</presence>"
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_regex("-> You have joined the room as stabber, role: moderator, affiliation: admin"));
// Simulate another user in the room
stbbr_send(
"<presence to='stabber@localhost/profanity' from='testroom@conference.localhost/baduser'>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='baduser@localhost/phone' affiliation='none'/>"
"</x>"
"</presence>"
);
sleep(1);
assert_true(prof_output_regex("baduser has joined"));
// Register success response for kick
stbbr_for_query("http://jabber.org/protocol/muc#admin",
"<iq id='*' type='result' from='testroom@conference.localhost'/>"
);
prof_input("/kick baduser \"spamming\"");
assert_true(stbbr_received(
"<iq id='*' to='testroom@conference.localhost' type='set'>"
"<query xmlns='http://jabber.org/protocol/muc#admin'>"
"<item nick='baduser' role='none'>"
"<reason>spamming</reason>"
"</item>"
"</query>"
"</iq>"
));
}

View File

@@ -12,5 +12,3 @@ void shows_me_message_from_self(void **state);
void shows_all_messages_in_console_when_window_not_focussed(void **state); void shows_all_messages_in_console_when_window_not_focussed(void **state);
void shows_first_message_in_console_when_window_not_focussed(void **state); void shows_first_message_in_console_when_window_not_focussed(void **state);
void shows_no_message_in_console_when_window_not_focussed(void **state); void shows_no_message_in_console_when_window_not_focussed(void **state);
void sends_affiliation_list_request(void **state);
void sends_kick_request(void **state);