Compare commits
33 Commits
fix/test-C
...
fix/functi
| Author | SHA1 | Date | |
|---|---|---|---|
|
737309c888
|
|||
|
0a37dd3fe8
|
|||
|
8e8d715734
|
|||
| 730d46da16 | |||
|
70de0123b0
|
|||
|
569decbbd9
|
|||
|
caac08a7d7
|
|||
|
a179b02f5d
|
|||
|
5e4e28cd13
|
|||
|
4c327bcb0c
|
|||
|
3a77bb1014
|
|||
|
fff902d204
|
|||
|
3307e0eac2
|
|||
|
b02582b19e
|
|||
|
96acaa322d
|
|||
|
c33884ddc4
|
|||
|
b4dd64a0c9
|
|||
| ecf78b26fd | |||
| 0a531ca819 | |||
| 9472662c05 | |||
| 6f94c4bae2 | |||
| e4b6daddf0 | |||
| 036fab364a | |||
| 57e5f30f1a | |||
| ecdf299113 | |||
| 70a1d93821 | |||
| 3685676b0a | |||
| 78bfe2d439 | |||
| d823ca57ce | |||
| 1437aedb2a | |||
| 6f61e5b26c | |||
| c628c2acd5 | |||
| 1305c59b5e |
10
.github/workflows/ci-code.yml
vendored
10
.github/workflows/ci-code.yml
vendored
@@ -98,13 +98,3 @@ jobs:
|
|||||||
- name: Check spelling
|
- name: Check spelling
|
||||||
run: |
|
run: |
|
||||||
codespell
|
codespell
|
||||||
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Code Coverage
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build and run coverage
|
|
||||||
run: |
|
|
||||||
docker build -f Dockerfile.arch -t profanity-cov .
|
|
||||||
docker run profanity-cov ./ci-build.sh --coverage-only
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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*
|
||||||
@@ -109,8 +107,3 @@ breaks
|
|||||||
*.tar.*
|
*.tar.*
|
||||||
*.zip
|
*.zip
|
||||||
*.log*
|
*.log*
|
||||||
coverage/
|
|
||||||
*.gcno
|
|
||||||
*.gcda
|
|
||||||
*.gcov
|
|
||||||
coverage.info
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -156,54 +138,37 @@ You can run the `make spell` command for this.
|
|||||||
`make doublecheck` will run the code formatter, spell checker and unit tests.
|
`make doublecheck` will run the code formatter, spell checker and unit tests.
|
||||||
|
|
||||||
|
|
||||||
### Functional tests
|
### Functional tests: moving away from brittle ID hooks
|
||||||
|
|
||||||
The functional test suite uses [stabber](https://git.jabber.space/devs/stabber) as a mock XMPP server. Tests are located in `tests/functionaltests/`.
|
Historically the functional test suite relied on stabber's id based helpers like `stbbr_for_id("prof_presence_1", ...)` to register canned responses that would be sent once Profanity emitted a stanza carrying that exact `id` attribute. This made the tests fragile:
|
||||||
|
|
||||||
#### Running functional tests
|
* Changes to stanza id generation (sequence, format) broke tests unexpectedly.
|
||||||
|
* Reordering internal requests produced hard-to-debug race conditions when an `id` no longer matched.
|
||||||
|
* Parallel additions of new features could shift which stanzas received a given id causing unrelated test failures.
|
||||||
|
|
||||||
Functional tests require stabber to be installed. Once installed, tests run as part of `make check`:
|
We have migrated to content based stubbing using direct sends (`stbbr_send`) and query hooks (`stbbr_for_query`). Instead of tying a response to a predicted id we now send the required server stanzas explicitly after initiating actions. Example (see `tests/functionaltests/proftest.c`):
|
||||||
|
|
||||||
```bash
|
|
||||||
make check # Run all tests (unit + functional)
|
|
||||||
make check-functional-parallel # Run functional tests in parallel (~3x faster)
|
|
||||||
./tests/functionaltests/functionaltests # Run all functional tests sequentially
|
|
||||||
./tests/functionaltests/functionaltests 1 # Run specific group (1-4)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Test groups
|
|
||||||
|
|
||||||
Tests are organized into 4 groups for parallel execution:
|
|
||||||
|
|
||||||
| Group | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| 1 | Connect, Ping, Rooms, Software |
|
|
||||||
| 2 | Message, Receipts, Roster, Chat Session |
|
|
||||||
| 3 | Presence, Disconnect |
|
|
||||||
| 4 | MUC, Carbons |
|
|
||||||
|
|
||||||
To add a new group:
|
|
||||||
1. Define the test array in `functionaltests.c`
|
|
||||||
2. Add entry to `groups[]` array
|
|
||||||
3. Update `FUNC_TEST_GROUPS` in `Makefile.am`
|
|
||||||
|
|
||||||
#### Writing functional tests
|
|
||||||
|
|
||||||
Use content-based stubbing with stabber:
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Use stbbr_for_query for IQ queries (roster, disco, etc.)
|
// Old brittle approach
|
||||||
stbbr_for_query("jabber:iq:roster", "<iq type='result'>...</iq>");
|
stbbr_for_id("prof_presence_1", "<presence id='prof_presence_1' ...>");
|
||||||
|
|
||||||
// Use stbbr_send for presence, message, and push-style stanzas
|
// New approach: after authentication, send presence directly
|
||||||
stbbr_send("<presence from='buddy@localhost'>...</presence>");
|
stbbr_send("<presence from='stabber@localhost/profanity' to='stabber@localhost/profanity'>...caps...</presence>");
|
||||||
```
|
```
|
||||||
|
|
||||||
Guidelines:
|
Benefits:
|
||||||
|
|
||||||
1. Use `stbbr_for_query(namespace, xml)` for IQ queries where the namespace is stable.
|
* Eliminates dependency on internal id sequencing.
|
||||||
2. Use `stbbr_send(xml)` for presence, message, and other push-style stanzas.
|
* Clearer intent inside test code ("send presence now" vs "register hook and hope client triggers it").
|
||||||
3. Keep assertions tolerant of ordering when possible; use `prof_output_regex()` for flexible matching.
|
* Simplifies adding new tests—no need to inspect logs for generated ids.
|
||||||
4. If timing issues appear, use `prof_timeout()` around critical expectations and reset afterwards.
|
|
||||||
5. When adding new tests, place them in the appropriate group based on functionality.
|
Guidelines when writing new functional tests:
|
||||||
|
|
||||||
|
1. Prefer `stbbr_for_query(namespace, xml)` for IQ roster or disco queries where the namespace is stable.
|
||||||
|
2. Use `stbbr_send(xml)` for presence, message, and other push style stanzas.
|
||||||
|
3. Avoid `stbbr_for_id` unless the protocol flow genuinely requires correlating a specific request/response pair not covered by a namespace query.
|
||||||
|
4. Keep assertions tolerant of ordering when possible; rely on `prof_output_regex()` matches rather than hard-coded positions.
|
||||||
|
5. If timing flakiness appears, temporarily raise `prof_timeout()` around the critical expectation and reset it immediately afterwards.
|
||||||
|
|
||||||
|
The migration from `stbbr_for_id` is complete. All functional tests now use content-based stubbing. When adding new tests, follow the guidelines above.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
FROM archlinux
|
FROM archlinux
|
||||||
|
|
||||||
ENV TERM=xterm
|
ENV TERM=xterm
|
||||||
ENV CC="ccache gcc"
|
|
||||||
|
|
||||||
RUN pacman -Syu --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
|
||||||
@@ -13,12 +12,9 @@ RUN pacman -S --needed --noconfirm \
|
|||||||
autoconf-archive \
|
autoconf-archive \
|
||||||
automake \
|
automake \
|
||||||
base-devel \
|
base-devel \
|
||||||
ca-certificates \
|
|
||||||
ccache \
|
|
||||||
check \
|
check \
|
||||||
cmake \
|
cmake \
|
||||||
cmocka \
|
cmocka \
|
||||||
lcov \
|
|
||||||
curl \
|
curl \
|
||||||
debuginfod \
|
debuginfod \
|
||||||
doxygen \
|
doxygen \
|
||||||
@@ -65,12 +61,12 @@ 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 -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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
WORKDIR /usr/src/profanity
|
WORKDIR /usr/src/profanity
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ FROM debian:testing
|
|||||||
|
|
||||||
ENV DEBIAN_FRONTEND="noninteractive"
|
ENV DEBIAN_FRONTEND="noninteractive"
|
||||||
ENV TERM=xterm
|
ENV TERM=xterm
|
||||||
ENV CC="ccache gcc"
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
autoconf \
|
autoconf \
|
||||||
autoconf-archive \
|
autoconf-archive \
|
||||||
automake \
|
automake \
|
||||||
ca-certificates \
|
|
||||||
ccache \
|
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
lcov \
|
|
||||||
libcmocka-dev \
|
libcmocka-dev \
|
||||||
libcurl3-dev \
|
libcurl3-dev \
|
||||||
libgcrypt-dev \
|
libgcrypt-dev \
|
||||||
@@ -41,19 +37,19 @@ 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 -c http.sslverify=false https://git.jabber.space/devs/stabber
|
||||||
RUN git clone --depth 1 https://github.com/strophe/libstrophe
|
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
|
||||||
|
|
||||||
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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
WORKDIR /usr/src/libstrophe
|
WORKDIR /usr/src/libstrophe
|
||||||
RUN ./bootstrap.sh
|
RUN ./bootstrap.sh
|
||||||
RUN ./configure --prefix=/usr
|
RUN ./configure --prefix=/usr
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
WORKDIR /usr/src/profanity
|
WORKDIR /usr/src/profanity
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# Build the latest Fedora image
|
# Build the latest Fedora image
|
||||||
FROM fedora:latest
|
FROM fedora:latest
|
||||||
|
|
||||||
ENV TERM=xterm
|
|
||||||
ENV CC="ccache gcc"
|
|
||||||
|
|
||||||
# libmicrohttpd - for stabber
|
# libmicrohttpd - for stabber
|
||||||
# glibc-locale - to have en_US locale
|
# glibc-locale - to have en_US locale
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
@@ -11,11 +8,8 @@ RUN dnf install -y \
|
|||||||
autoconf-archive \
|
autoconf-archive \
|
||||||
automake \
|
automake \
|
||||||
awk \
|
awk \
|
||||||
ca-certificates \
|
|
||||||
ccache \
|
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
lcov \
|
|
||||||
glib2-devel \
|
glib2-devel \
|
||||||
glibc-all-langpacks \
|
glibc-all-langpacks \
|
||||||
gtk2-devel \
|
gtk2-devel \
|
||||||
@@ -51,20 +45,20 @@ 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 -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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
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 -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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/profanity
|
RUN mkdir -p /usr/src/profanity
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
# Build the latest openSUSE Tumbleweed image
|
# Build the latest openSUSE Tumbleweed image
|
||||||
FROM opensuse/tumbleweed
|
FROM opensuse/tumbleweed
|
||||||
|
|
||||||
ENV TERM=xterm
|
|
||||||
ENV CC="ccache gcc"
|
|
||||||
|
|
||||||
# libmicrohttpd - for stabber
|
# libmicrohttpd - for stabber
|
||||||
# glibc-locale - to have en_US locale
|
# glibc-locale - to have en_US locale
|
||||||
RUN zypper --non-interactive in --no-recommends \
|
RUN zypper --non-interactive in --no-recommends \
|
||||||
autoconf \
|
autoconf \
|
||||||
autoconf-archive \
|
autoconf-archive \
|
||||||
automake \
|
automake \
|
||||||
ca-certificates \
|
|
||||||
ccache \
|
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
lcov \
|
|
||||||
glib2-devel \
|
glib2-devel \
|
||||||
glibc-locale \
|
glibc-locale \
|
||||||
gtk2-devel \
|
gtk2-devel \
|
||||||
@@ -50,11 +44,11 @@ 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 -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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/profanity
|
RUN mkdir -p /usr/src/profanity
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ FROM ubuntu:latest
|
|||||||
|
|
||||||
ENV DEBIAN_FRONTEND="noninteractive"
|
ENV DEBIAN_FRONTEND="noninteractive"
|
||||||
ENV TERM=xterm
|
ENV TERM=xterm
|
||||||
ENV CC="ccache gcc"
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
autoconf \
|
autoconf \
|
||||||
autoconf-archive \
|
autoconf-archive \
|
||||||
automake \
|
automake \
|
||||||
ca-certificates \
|
|
||||||
ccache \
|
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
lcov \
|
|
||||||
libcmocka-dev \
|
libcmocka-dev \
|
||||||
libcurl3-dev \
|
libcurl3-dev \
|
||||||
libgcrypt-dev \
|
libgcrypt-dev \
|
||||||
@@ -40,19 +36,19 @@ 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 -c http.sslverify=false https://git.jabber.space/devs/stabber
|
||||||
RUN git clone --depth 1 https://github.com/strophe/libstrophe
|
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
|
||||||
|
|
||||||
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
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
WORKDIR /usr/src/libstrophe
|
WORKDIR /usr/src/libstrophe
|
||||||
RUN ./bootstrap.sh
|
RUN ./bootstrap.sh
|
||||||
RUN ./configure --prefix=/usr
|
RUN ./configure --prefix=/usr
|
||||||
RUN make -j$(nproc)
|
RUN make
|
||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
WORKDIR /usr/src/profanity
|
WORKDIR /usr/src/profanity
|
||||||
|
|||||||
48
Makefile.am
48
Makefile.am
@@ -286,7 +286,7 @@ endif
|
|||||||
|
|
||||||
TESTS = tests/unittests/unittests
|
TESTS = tests/unittests/unittests
|
||||||
check_PROGRAMS = tests/unittests/unittests
|
check_PROGRAMS = tests/unittests/unittests
|
||||||
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
|
tests_unittests_unittests_CPPFLAGS = -Itests/
|
||||||
tests_unittests_unittests_SOURCES = $(unittest_sources)
|
tests_unittests_unittests_SOURCES = $(unittest_sources)
|
||||||
tests_unittests_unittests_LDADD = -lcmocka
|
tests_unittests_unittests_LDADD = -lcmocka
|
||||||
|
|
||||||
@@ -302,32 +302,9 @@ if HAVE_FORKPTY
|
|||||||
TESTS += tests/functionaltests/functionaltests
|
TESTS += tests/functionaltests/functionaltests
|
||||||
check_PROGRAMS += tests/functionaltests/functionaltests
|
check_PROGRAMS += tests/functionaltests/functionaltests
|
||||||
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
|
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
|
||||||
tests_functionaltests_functionaltests_CPPFLAGS = -I$(srcdir)/tests
|
tests_functionaltests_functionaltests_CPPFLAGS = -Itests/
|
||||||
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS)
|
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS)
|
||||||
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber @FORKPTY_LIB@
|
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber @FORKPTY_LIB@
|
||||||
|
|
||||||
# Parallel functional tests target (~3x faster than sequential)
|
|
||||||
# Usage: make check-functional-parallel
|
|
||||||
# To add more groups: increase FUNC_TEST_GROUPS and add group in functionaltests.c
|
|
||||||
FUNC_TEST_GROUPS = 1 2 3 4
|
|
||||||
|
|
||||||
check-functional-parallel: tests/functionaltests/functionaltests
|
|
||||||
@echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..."
|
|
||||||
@mkdir -p $(builddir)/test-logs $(builddir)/test-files
|
|
||||||
@pids=""; \
|
|
||||||
for g in $(FUNC_TEST_GROUPS); do \
|
|
||||||
./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \
|
|
||||||
pids="$$pids $$!"; \
|
|
||||||
done; \
|
|
||||||
failed=0; i=1; \
|
|
||||||
for pid in $$pids; do \
|
|
||||||
wait $$pid || { echo "Group $$i FAILED"; cat $(builddir)/test-logs/group$$i.log; failed=1; }; \
|
|
||||||
i=$$((i + 1)); \
|
|
||||||
done; \
|
|
||||||
echo "=== Test Results Summary ==="; \
|
|
||||||
grep -E 'PASSED|FAILED|Running' $(builddir)/test-logs/group*.log || true; \
|
|
||||||
if [ $$failed -ne 0 ]; then echo "FUNCTIONAL TESTS FAILED"; exit 1; fi; \
|
|
||||||
echo "All functional test groups passed!"
|
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -372,10 +349,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" >> $@
|
||||||
@@ -391,21 +365,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)
|
|
||||||
coverage-clean:
|
|
||||||
find . -name '*.gcda' -delete
|
|
||||||
find . -name '*.gcno' -delete
|
|
||||||
rm -rf coverage-html coverage.info
|
|
||||||
|
|
||||||
coverage-report: check
|
|
||||||
lcov --capture --directory . --output-file coverage.info --ignore-errors inconsistent
|
|
||||||
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info --ignore-errors inconsistent
|
|
||||||
genhtml coverage.info --output-directory coverage-html
|
|
||||||
@echo "Coverage report generated in coverage-html/index.html"
|
|
||||||
|
|
||||||
.PHONY: coverage-clean coverage-report
|
|
||||||
|
|
||||||
format: $(all_c_sources)
|
format: $(all_c_sources)
|
||||||
clang-format -i $(all_c_sources)
|
clang-format -i $(all_c_sources)
|
||||||
|
|||||||
406
ci-build.sh
406
ci-build.sh
@@ -15,145 +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
|
|
||||||
# This is a meta-test: it runs a deliberately failing test
|
|
||||||
# and checks that the test framework reports the failure correctly
|
|
||||||
verify_test_failure_detection()
|
|
||||||
{
|
|
||||||
echo
|
|
||||||
echo "==> Verifying test failure detection..."
|
|
||||||
|
|
||||||
# Create a simple failing test
|
|
||||||
cat > /tmp/test_must_fail.c << 'EOF'
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <setjmp.h>
|
|
||||||
#include <cmocka.h>
|
|
||||||
|
|
||||||
static void test_that_must_fail(void **state) {
|
|
||||||
(void)state;
|
|
||||||
assert_true(0); // This MUST fail
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
const struct CMUnitTest tests[] = {
|
|
||||||
cmocka_unit_test(test_that_must_fail),
|
|
||||||
};
|
|
||||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Compile the failing test
|
|
||||||
if ! gcc -o /tmp/test_must_fail /tmp/test_must_fail.c -lcmocka 2>/dev/null; then
|
|
||||||
echo "Warning: Could not compile test failure verification (cmocka not available?)"
|
|
||||||
echo "Skipping test failure detection verification"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 1: Single failing test detection
|
|
||||||
echo " Testing single test failure detection..."
|
|
||||||
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 "This means the test framework is NOT detecting failures correctly!" >&2
|
|
||||||
echo "--- Test output ---" >&2
|
|
||||||
cat /tmp/test_must_fail.log >&2
|
|
||||||
echo "--- End output ---" >&2
|
|
||||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✓ Single test failure correctly detected"
|
|
||||||
|
|
||||||
# Test 2: Parallel failure detection (simulates check-functional-parallel)
|
|
||||||
echo " Testing parallel test failure detection..."
|
|
||||||
failed=0
|
|
||||||
/tmp/test_must_fail > /tmp/p1.log 2>&1 & pid1=$!
|
|
||||||
true > /tmp/p2.log 2>&1 & pid2=$! # This passes
|
|
||||||
/tmp/test_must_fail > /tmp/p3.log 2>&1 & pid3=$!
|
|
||||||
true > /tmp/p4.log 2>&1 & pid4=$! # This passes
|
|
||||||
|
|
||||||
wait $pid1 || failed=$((failed + 1))
|
|
||||||
wait $pid2 || failed=$((failed + 1))
|
|
||||||
wait $pid3 || failed=$((failed + 1))
|
|
||||||
wait $pid4 || failed=$((failed + 1))
|
|
||||||
|
|
||||||
if [ $failed -ne 2 ]; then
|
|
||||||
echo "ERROR: Expected 2 failures in parallel tests, got $failed" >&2
|
|
||||||
echo "Parallel failure detection is broken!" >&2
|
|
||||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✓ Parallel test failures correctly detected (2 of 4 failed as expected)"
|
|
||||||
|
|
||||||
rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log
|
|
||||||
echo "✓ Test failure detection verified"
|
|
||||||
}
|
|
||||||
|
|
||||||
num_cores()
|
num_cores()
|
||||||
{
|
{
|
||||||
# Check for cores, for systems with:
|
# Check for cores, for systems with:
|
||||||
@@ -165,20 +34,6 @@ num_cores()
|
|||||||
|| getconf _NPROCESSORS_ONLN 2>/dev/null
|
|| getconf _NPROCESSORS_ONLN 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run test failure detection verification first
|
|
||||||
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=()
|
||||||
@@ -189,7 +44,7 @@ ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
|
|||||||
|
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
linux*)
|
linux*)
|
||||||
# 4 configurations for parallel CI
|
# Reduced set of configurations for faster CI
|
||||||
tests=(
|
tests=(
|
||||||
# 1. Full build (all features enabled)
|
# 1. Full build (all features enabled)
|
||||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||||
@@ -201,13 +56,15 @@ case "$ARCH" in
|
|||||||
--disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf"
|
--disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf"
|
||||||
# 3. No encryption (disable otr, pgp, omemo)
|
# 3. No encryption (disable otr, pgp, omemo)
|
||||||
"--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode"
|
"--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode"
|
||||||
# 4. Default configuration
|
# 4. No plugins
|
||||||
|
"--disable-plugins --disable-c-plugins --disable-python-plugins"
|
||||||
|
# 5. Default configuration
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
|
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
|
||||||
;;
|
;;
|
||||||
darwin*)
|
darwin*)
|
||||||
# 4 configurations for parallel CI
|
# Reduced set of configurations for faster CI
|
||||||
tests=(
|
tests=(
|
||||||
# 1. Full build (all features enabled)
|
# 1. Full build (all features enabled)
|
||||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||||
@@ -219,7 +76,9 @@ case "$ARCH" in
|
|||||||
--disable-python-plugins"
|
--disable-python-plugins"
|
||||||
# 3. No encryption (disable otr, pgp, omemo)
|
# 3. No encryption (disable otr, pgp, omemo)
|
||||||
"--disable-pgp --disable-otr --disable-omemo"
|
"--disable-pgp --disable-otr --disable-omemo"
|
||||||
# 4. Default configuration
|
# 4. No plugins
|
||||||
|
"--disable-plugins --disable-c-plugins --disable-python-plugins"
|
||||||
|
# 5. Default configuration
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
@@ -231,7 +90,7 @@ case "$ARCH" in
|
|||||||
# src/event/server_events.c:1454:19: error: universal character names are only valid in C++ and C99
|
# src/event/server_events.c:1454:19: error: universal character names are only valid in C++ and C99
|
||||||
CC="egcc -std=gnu99 -fexec-charset=UTF-8"
|
CC="egcc -std=gnu99 -fexec-charset=UTF-8"
|
||||||
|
|
||||||
# 4 configurations for parallel CI
|
# Reduced set of configurations for faster CI
|
||||||
tests=(
|
tests=(
|
||||||
# 1. Full build (all features enabled)
|
# 1. Full build (all features enabled)
|
||||||
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
|
||||||
@@ -243,230 +102,45 @@ case "$ARCH" in
|
|||||||
--disable-python-plugins"
|
--disable-python-plugins"
|
||||||
# 3. No encryption (disable otr, pgp, omemo)
|
# 3. No encryption (disable otr, pgp, omemo)
|
||||||
"--disable-pgp --disable-otr --disable-omemo"
|
"--disable-pgp --disable-otr --disable-omemo"
|
||||||
# 4. Default configuration
|
# 4. No plugins
|
||||||
|
"--disable-plugins --disable-c-plugins --disable-python-plugins"
|
||||||
|
# 5. Default configuration
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Function to build and test a single configuration
|
case "$ARCH" in
|
||||||
build_and_test() {
|
linux*)
|
||||||
local features="$1"
|
echo
|
||||||
local extra_args="$2"
|
echo "--> Building with ./configure ${tests[0]} --enable-valgrind $*"
|
||||||
local idx="$3"
|
echo
|
||||||
local run_valgrind="$4"
|
|
||||||
local run_coverage="$5"
|
|
||||||
local build_dir="build-$idx"
|
|
||||||
local log_file="build-$idx.log"
|
|
||||||
|
|
||||||
{
|
# shellcheck disable=SC2086
|
||||||
echo "=== Build $idx started at $(date) ==="
|
./configure ${tests[0]} --enable-valgrind $*
|
||||||
echo "--> Building in $build_dir with ./configure -C $features $extra_args"
|
|
||||||
|
|
||||||
local start_time=$SECONDS
|
$MAKE CC="${CC}"
|
||||||
|
if grep '^ID=' /etc/os-release | grep -q -e debian; then
|
||||||
mkdir -p "$build_dir"
|
$MAKE check-valgrind
|
||||||
cd "$build_dir"
|
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
if ! ../configure -C $features $extra_args; then
|
|
||||||
echo "ERROR: configure failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! $MAKE CC="${CC}"; then
|
|
||||||
echo "ERROR: make failed"
|
|
||||||
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
|
else
|
||||||
echo "--> Running unit tests..."
|
$MAKE check-valgrind || log_content ./test-suite-memcheck.log
|
||||||
$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
|
fi
|
||||||
# Extract unit test counts from cmocka output
|
$MAKE distclean
|
||||||
unit_passed=$(extract_test_count unit-tests-output.log "$CMOCKA_PASSED_PATTERN")
|
;;
|
||||||
unit_failed=$(extract_test_count unit-tests-output.log "$CMOCKA_FAILED_PATTERN")
|
esac
|
||||||
: "${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,
|
for features in "${tests[@]}"
|
||||||
# build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds.
|
do
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
rm -rf "$build_dir"
|
|
||||||
|
|
||||||
local elapsed=$((SECONDS - start_time))
|
|
||||||
local mins=$((elapsed / 60))
|
|
||||||
local secs=$((elapsed % 60))
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run configurations
|
|
||||||
# Coverage enabled only for build 1 (Full) - it has most code paths
|
|
||||||
echo
|
|
||||||
echo "=== Start build ==="
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [ "$COVERAGE_ONLY" = "yes" ]; then
|
|
||||||
echo "Running coverage-only mode (${BUILD_NAMES[0]} build)..."
|
|
||||||
echo
|
echo
|
||||||
run_valgrind="no"
|
echo "--> Building with ./configure ${features} $*"
|
||||||
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
|
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
|
# shellcheck disable=SC2086
|
||||||
echo "Waiting for builds to complete..."
|
./configure $features $*
|
||||||
echo
|
|
||||||
failed_builds=()
|
$MAKE CC="${CC}"
|
||||||
for i in "${!pids[@]}"; do
|
$MAKE check
|
||||||
idx=$((i + 1))
|
|
||||||
if wait "${pids[$i]}"; then
|
./profanity -v
|
||||||
if [ -f "build-$idx.log" ]; then
|
$MAKE clean
|
||||||
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
|
|
||||||
echo "✗ ${BUILD_NAMES[$i]} FAILED" >&2
|
|
||||||
failed_builds+=("$idx")
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Show failed builds full logs
|
|
||||||
for idx in "${failed_builds[@]}"; do
|
|
||||||
if [ -f "build-$idx.log" ]; then
|
|
||||||
echo "=== ${BUILD_NAMES[$((idx-1))]} FAILURE LOG ===" >&2
|
|
||||||
cat "build-$idx.log" >&2
|
|
||||||
echo >&2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#failed_builds[@]} -gt 0 ]; then
|
|
||||||
echo "RESULT: FAILED (builds ${failed_builds[*]})" >&2
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
if [ "$COVERAGE_ONLY" = "yes" ]; then
|
|
||||||
echo "RESULT: COVERAGE BUILD PASSED ✓"
|
|
||||||
else
|
|
||||||
echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ AC_ARG_ENABLE([gdk-pixbuf],
|
|||||||
[AS_HELP_STRING([--enable-gdk-pixbuf], [enable GDK Pixbuf support to scale avatars before uploading])])
|
[AS_HELP_STRING([--enable-gdk-pixbuf], [enable GDK Pixbuf support to scale avatars before uploading])])
|
||||||
AC_ARG_ENABLE([omemo-qrcode],
|
AC_ARG_ENABLE([omemo-qrcode],
|
||||||
[AS_HELP_STRING([--enable-omemo-qrcode], [enable ability to display omemo qr code])])
|
[AS_HELP_STRING([--enable-omemo-qrcode], [enable ability to display omemo qr code])])
|
||||||
AC_ARG_ENABLE([coverage],
|
|
||||||
[AS_HELP_STRING([--enable-coverage], [enable code coverage analysis])])
|
|
||||||
|
|
||||||
m4_include([m4/ax_valgrind_check.m4])
|
m4_include([m4/ax_valgrind_check.m4])
|
||||||
AX_VALGRIND_DFLT([drd], [off])
|
AX_VALGRIND_DFLT([drd], [off])
|
||||||
@@ -388,11 +386,6 @@ AC_SUBST([FORKPTY_LIB])
|
|||||||
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
|
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
|
||||||
AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
|
AM_LDFLAGS="$AM_LDFLAGS -export-dynamic"
|
||||||
|
|
||||||
AS_IF([test "x$enable_coverage" = xyes],
|
|
||||||
[AM_CFLAGS="$AM_CFLAGS --coverage -O0"
|
|
||||||
AM_LDFLAGS="$AM_LDFLAGS --coverage"
|
|
||||||
AC_MSG_NOTICE([Code coverage analysis enabled])])
|
|
||||||
|
|
||||||
AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
|
AS_IF([test "x$PACKAGE_STATUS" = xdevelopment],
|
||||||
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
|
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
|
||||||
AS_IF([test "x$PLATFORM" = xosx],
|
AS_IF([test "x$PLATFORM" = xosx],
|
||||||
|
|||||||
23
prof.supp
23
prof.supp
@@ -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)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -268,11 +268,7 @@ _show_scrolled(ProfWin* current)
|
|||||||
wattroff(win, bracket_attrs);
|
wattroff(win, bracket_attrs);
|
||||||
|
|
||||||
wattron(win, scrolled_attrs);
|
wattron(win, scrolled_attrs);
|
||||||
if (current->layout->unread_msg == 0) {
|
wprintw(win, "SCROLLED");
|
||||||
wprintw(win, "SCROLLED");
|
|
||||||
} else {
|
|
||||||
wprintw(win, "SCROLLED, NEW MESSAGES");
|
|
||||||
}
|
|
||||||
wattroff(win, scrolled_attrs);
|
wattroff(win, scrolled_attrs);
|
||||||
|
|
||||||
wattron(win, bracket_attrs);
|
wattron(win, bracket_attrs);
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ typedef struct prof_layout_t
|
|||||||
ProfBuff buffer;
|
ProfBuff buffer;
|
||||||
int y_pos;
|
int y_pos;
|
||||||
int paged;
|
int paged;
|
||||||
int unread_msg;
|
|
||||||
} ProfLayout;
|
} ProfLayout;
|
||||||
|
|
||||||
typedef struct prof_layout_simple_t
|
typedef struct prof_layout_simple_t
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ _win_create_simple_layout(void)
|
|||||||
layout->base.buffer = buffer_create();
|
layout->base.buffer = buffer_create();
|
||||||
layout->base.y_pos = 0;
|
layout->base.y_pos = 0;
|
||||||
layout->base.paged = 0;
|
layout->base.paged = 0;
|
||||||
layout->base.unread_msg = 0;
|
|
||||||
scrollok(layout->base.win, TRUE);
|
scrollok(layout->base.win, TRUE);
|
||||||
|
|
||||||
return &layout->base;
|
return &layout->base;
|
||||||
@@ -121,7 +120,6 @@ _win_create_split_layout(void)
|
|||||||
layout->base.buffer = buffer_create();
|
layout->base.buffer = buffer_create();
|
||||||
layout->base.y_pos = 0;
|
layout->base.y_pos = 0;
|
||||||
layout->base.paged = 0;
|
layout->base.paged = 0;
|
||||||
layout->base.unread_msg = 0;
|
|
||||||
scrollok(layout->base.win, TRUE);
|
scrollok(layout->base.win, TRUE);
|
||||||
layout->subwin = NULL;
|
layout->subwin = NULL;
|
||||||
layout->sub_y_pos = 0;
|
layout->sub_y_pos = 0;
|
||||||
@@ -199,7 +197,6 @@ win_create_muc(const char* const roomjid)
|
|||||||
layout->base.buffer = buffer_create();
|
layout->base.buffer = buffer_create();
|
||||||
layout->base.y_pos = 0;
|
layout->base.y_pos = 0;
|
||||||
layout->base.paged = 0;
|
layout->base.paged = 0;
|
||||||
layout->base.unread_msg = 0;
|
|
||||||
scrollok(layout->base.win, TRUE);
|
scrollok(layout->base.win, TRUE);
|
||||||
new_win->window.layout = (ProfLayout*)layout;
|
new_win->window.layout = (ProfLayout*)layout;
|
||||||
|
|
||||||
@@ -701,11 +698,9 @@ void
|
|||||||
win_page_down(ProfWin* window, int scroll_size)
|
win_page_down(ProfWin* window, int scroll_size)
|
||||||
{
|
{
|
||||||
int total_rows = getcury(window->layout->win);
|
int total_rows = getcury(window->layout->win);
|
||||||
int total_rows_with_unread = total_rows + window->layout->unread_msg;
|
|
||||||
int* page_start = &(window->layout->y_pos);
|
int* page_start = &(window->layout->y_pos);
|
||||||
int page_space = getmaxy(stdscr) - 4;
|
int page_space = getmaxy(stdscr) - 4;
|
||||||
int page_start_initial = *page_start;
|
int page_start_initial = *page_start;
|
||||||
|
|
||||||
if (scroll_size == 0)
|
if (scroll_size == 0)
|
||||||
scroll_size = page_space;
|
scroll_size = page_space;
|
||||||
win_scroll_state_t* scroll_state = &window->scroll_state;
|
win_scroll_state_t* scroll_state = &window->scroll_state;
|
||||||
@@ -714,11 +709,7 @@ win_page_down(ProfWin* window, int scroll_size)
|
|||||||
*page_start += scroll_size;
|
*page_start += scroll_size;
|
||||||
|
|
||||||
// Scrolled down after reaching the bottom of the page
|
// Scrolled down after reaching the bottom of the page
|
||||||
gboolean past_bottom = *page_start > total_rows_with_unread - page_space;
|
if ((*page_start > total_rows - page_space || (*page_start == page_space && *page_start >= total_rows)) && window->type == WIN_CHAT) {
|
||||||
gboolean at_page_space_and_past_unread = (*page_start == page_space && *page_start >= total_rows_with_unread);
|
|
||||||
gboolean is_chat = window->type == WIN_CHAT;
|
|
||||||
|
|
||||||
if ((past_bottom || at_page_space_and_past_unread) && is_chat) {
|
|
||||||
int bf_size = buffer_size(window->layout->buffer);
|
int bf_size = buffer_size(window->layout->buffer);
|
||||||
if (bf_size > 0 && *scroll_state != WIN_SCROLL_REACHED_BOTTOM) {
|
if (bf_size > 0 && *scroll_state != WIN_SCROLL_REACHED_BOTTOM) {
|
||||||
// How many lines are left until end of the screen
|
// How many lines are left until end of the screen
|
||||||
@@ -752,16 +743,13 @@ win_page_down(ProfWin* window, int scroll_size)
|
|||||||
window->layout->paged = 1;
|
window->layout->paged = 1;
|
||||||
|
|
||||||
// update only if position has changed
|
// update only if position has changed
|
||||||
if ((page_start_initial != *page_start) || window->layout->unread_msg) {
|
if (page_start_initial != *page_start) {
|
||||||
win_update_virtual(window);
|
win_update_virtual(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Switch off page if no messages left to read.
|
// switch off page if last line and space line visible
|
||||||
* TODO: update buffer end handling to check messages just after last entry.
|
if (total_rows - *page_start == page_space) {
|
||||||
*/
|
|
||||||
if (*scroll_state == WIN_SCROLL_REACHED_BOTTOM) {
|
|
||||||
window->layout->paged = 0;
|
window->layout->paged = 0;
|
||||||
window->layout->unread_msg = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,7 +810,6 @@ win_clear(ProfWin* window)
|
|||||||
int* page_start = &(window->layout->y_pos);
|
int* page_start = &(window->layout->y_pos);
|
||||||
*page_start = y;
|
*page_start = y;
|
||||||
window->layout->paged = 1;
|
window->layout->paged = 1;
|
||||||
window->layout->unread_msg = 0;
|
|
||||||
win_update_virtual(window);
|
win_update_virtual(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,7 +914,6 @@ void
|
|||||||
win_move_to_end(ProfWin* window)
|
win_move_to_end(ProfWin* window)
|
||||||
{
|
{
|
||||||
window->layout->paged = 0;
|
window->layout->paged = 0;
|
||||||
window->layout->unread_msg = 0;
|
|
||||||
|
|
||||||
int rows = getmaxy(stdscr);
|
int rows = getmaxy(stdscr);
|
||||||
int y = getcury(window->layout->win);
|
int y = getcury(window->layout->win);
|
||||||
@@ -1710,13 +1696,6 @@ win_newline(ProfWin* window)
|
|||||||
static void
|
static void
|
||||||
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
|
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
|
||||||
{
|
{
|
||||||
|
|
||||||
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
|
|
||||||
if (window->layout->paged && wins_is_current(window)) {
|
|
||||||
window->layout->unread_msg++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp == NULL) {
|
if (timestamp == NULL) {
|
||||||
timestamp = g_date_time_new_now_local();
|
timestamp = g_date_time_new_now_local();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,31 +14,21 @@
|
|||||||
* flaky tests caused by leftover state. The overhead is acceptable since
|
* flaky tests caused by leftover state. The overhead is acceptable since
|
||||||
* 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:
|
||||||
* Group 1: Connect, Ping, Rooms, Software
|
* Group 1: Connection, Ping, Rooms, Presence
|
||||||
* Group 2: Message, Receipts, Roster, Chat Session
|
* Group 2: Messages, Receipts, Roster management
|
||||||
* Group 3: Presence, Disconnect
|
* Group 3: MUC (Multi-User Chat) functionality
|
||||||
* Group 4: MUC, Carbons
|
* Group 4: Carbons, Chat sessions, Software version, Disconnect
|
||||||
*
|
|
||||||
* Parallel execution:
|
|
||||||
* ./functionaltests - run all tests sequentially
|
|
||||||
* ./functionaltests N - run group N only (N = 1..num_groups)
|
|
||||||
*
|
|
||||||
* For parallel execution, run multiple groups simultaneously:
|
|
||||||
* ./functionaltests 1 & ./functionaltests 2 & ./functionaltests 3 & ./functionaltests 4 & wait
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <string.h>
|
|
||||||
#include "prof_cmocka.h"
|
#include "prof_cmocka.h"
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
#include "proftest.h"
|
#include "proftest.h"
|
||||||
#include "test_connect.h"
|
#include "test_connect.h"
|
||||||
#include "test_ping.h"
|
#include "test_ping.h"
|
||||||
@@ -59,27 +49,13 @@
|
|||||||
int
|
int
|
||||||
main(int argc, char* argv[])
|
main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
int group = 0; /* 0 = all groups */
|
const struct CMUnitTest all_tests[] = {
|
||||||
if (argc > 1) {
|
|
||||||
group = atoi(argv[1]);
|
|
||||||
if (group < 1 || group > 4) {
|
|
||||||
fprintf(stderr, "Usage: %s [group]\n", argv[0]);
|
|
||||||
fprintf(stderr, " group: 1-4 to run specific group, or omit for all\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
char group_env[16];
|
/* ============================================================
|
||||||
snprintf(group_env, sizeof(group_env), "%d", group);
|
* GROUP 1: Connect, Ping, Rooms, Presence
|
||||||
setenv("PROF_TEST_GROUP", group_env, 1);
|
* Basic XMPP session establishment and presence management
|
||||||
|
* ============================================================ */
|
||||||
fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group);
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
* GROUP 1: Connect, Ping, Rooms, Software
|
|
||||||
* Basic XMPP session establishment and server queries
|
|
||||||
* ============================================================ */
|
|
||||||
const struct CMUnitTest group1_tests[] = {
|
|
||||||
/* Connection tests - verify login, roster, bookmarks */
|
/* Connection tests - verify login, roster, bookmarks */
|
||||||
PROF_FUNC_TEST(connect_jid_requests_roster),
|
PROF_FUNC_TEST(connect_jid_requests_roster),
|
||||||
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
|
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
|
||||||
@@ -97,20 +73,28 @@ main(int argc, char* argv[])
|
|||||||
/* Room discovery - XEP-0045 */
|
/* Room discovery - XEP-0045 */
|
||||||
PROF_FUNC_TEST(rooms_query),
|
PROF_FUNC_TEST(rooms_query),
|
||||||
|
|
||||||
/* Software Version - XEP-0092 */
|
/* Presence tests - online/away/xa/dnd/chat status */
|
||||||
PROF_FUNC_TEST(send_software_version_request),
|
PROF_FUNC_TEST(presence_online),
|
||||||
PROF_FUNC_TEST(display_software_version_result),
|
PROF_FUNC_TEST(presence_online_with_message),
|
||||||
PROF_FUNC_TEST(shows_message_when_software_version_error),
|
PROF_FUNC_TEST(presence_away),
|
||||||
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
|
PROF_FUNC_TEST(presence_away_with_message),
|
||||||
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
|
PROF_FUNC_TEST(presence_xa),
|
||||||
PROF_FUNC_TEST(display_software_version_result_in_chat),
|
PROF_FUNC_TEST(presence_xa_with_message),
|
||||||
};
|
PROF_FUNC_TEST(presence_dnd),
|
||||||
|
PROF_FUNC_TEST(presence_dnd_with_message),
|
||||||
|
PROF_FUNC_TEST(presence_chat),
|
||||||
|
PROF_FUNC_TEST(presence_chat_with_message),
|
||||||
|
PROF_FUNC_TEST(presence_set_priority),
|
||||||
|
PROF_FUNC_TEST(presence_includes_priority),
|
||||||
|
PROF_FUNC_TEST(presence_keeps_status),
|
||||||
|
PROF_FUNC_TEST(presence_received),
|
||||||
|
PROF_FUNC_TEST(presence_missing_resource_defaults),
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* GROUP 2: Message, Receipts, Roster
|
||||||
|
* Core messaging and contact management
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
* GROUP 2: Message, Receipts, Roster, Chat Session
|
|
||||||
* Core messaging and contact management
|
|
||||||
* ============================================================ */
|
|
||||||
const struct CMUnitTest group2_tests[] = {
|
|
||||||
/* Basic message send/receive */
|
/* Basic message send/receive */
|
||||||
PROF_FUNC_TEST(message_send),
|
PROF_FUNC_TEST(message_send),
|
||||||
PROF_FUNC_TEST(message_receive_console),
|
PROF_FUNC_TEST(message_receive_console),
|
||||||
@@ -128,54 +112,21 @@ main(int argc, char* argv[])
|
|||||||
PROF_FUNC_TEST(sends_remove_item_nick),
|
PROF_FUNC_TEST(sends_remove_item_nick),
|
||||||
PROF_FUNC_TEST(sends_nick_change),
|
PROF_FUNC_TEST(sends_nick_change),
|
||||||
|
|
||||||
/* Chat session management - bare/full JID routing */
|
/* ============================================================
|
||||||
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
|
* GROUP 3: MUC (Multi-User Chat)
|
||||||
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
|
* XEP-0045 conference room functionality
|
||||||
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
|
* ============================================================ */
|
||||||
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
|
|
||||||
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
|
|
||||||
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ============================================================
|
/* Room join with various options */
|
||||||
* GROUP 3: Presence, Disconnect
|
|
||||||
* Online/away/xa/dnd/chat status management
|
|
||||||
* ============================================================ */
|
|
||||||
const struct CMUnitTest group3_tests[] = {
|
|
||||||
PROF_FUNC_TEST(presence_online),
|
|
||||||
PROF_FUNC_TEST(presence_online_with_message),
|
|
||||||
PROF_FUNC_TEST(presence_away),
|
|
||||||
PROF_FUNC_TEST(presence_away_with_message),
|
|
||||||
PROF_FUNC_TEST(presence_xa),
|
|
||||||
PROF_FUNC_TEST(presence_xa_with_message),
|
|
||||||
PROF_FUNC_TEST(presence_dnd),
|
|
||||||
PROF_FUNC_TEST(presence_dnd_with_message),
|
|
||||||
PROF_FUNC_TEST(presence_chat),
|
|
||||||
PROF_FUNC_TEST(presence_chat_with_message),
|
|
||||||
PROF_FUNC_TEST(presence_set_priority),
|
|
||||||
PROF_FUNC_TEST(presence_includes_priority),
|
|
||||||
PROF_FUNC_TEST(presence_keeps_status),
|
|
||||||
PROF_FUNC_TEST(presence_received),
|
|
||||||
PROF_FUNC_TEST(presence_missing_resource_defaults),
|
|
||||||
|
|
||||||
/* Disconnect - clean session termination */
|
|
||||||
PROF_FUNC_TEST(disconnect_ends_session),
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
* GROUP 4: MUC, Carbons
|
|
||||||
* Multi-user chat and message synchronization
|
|
||||||
* ============================================================ */
|
|
||||||
const struct CMUnitTest group4_tests[] = {
|
|
||||||
/* MUC room join with various options - XEP-0045 */
|
|
||||||
PROF_FUNC_TEST(sends_room_join),
|
PROF_FUNC_TEST(sends_room_join),
|
||||||
PROF_FUNC_TEST(sends_room_join_with_nick),
|
PROF_FUNC_TEST(sends_room_join_with_nick),
|
||||||
PROF_FUNC_TEST(sends_room_join_with_password),
|
PROF_FUNC_TEST(sends_room_join_with_password),
|
||||||
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
|
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
|
||||||
|
|
||||||
/* MUC room information display */
|
/* Room information display */
|
||||||
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
|
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
|
||||||
PROF_FUNC_TEST(shows_subject_on_join),
|
PROF_FUNC_TEST(shows_subject_on_join),
|
||||||
|
// PROF_FUNC_TEST(shows_history_message), // temporarily disabled due to timing issues in CI
|
||||||
PROF_FUNC_TEST(shows_occupant_join),
|
PROF_FUNC_TEST(shows_occupant_join),
|
||||||
|
|
||||||
/* MUC messaging */
|
/* MUC messaging */
|
||||||
@@ -183,11 +134,16 @@ main(int argc, char* argv[])
|
|||||||
PROF_FUNC_TEST(shows_me_message_from_occupant),
|
PROF_FUNC_TEST(shows_me_message_from_occupant),
|
||||||
PROF_FUNC_TEST(shows_me_message_from_self),
|
PROF_FUNC_TEST(shows_me_message_from_self),
|
||||||
|
|
||||||
/* MUC console notification settings */
|
/* Console notification settings for MUC */
|
||||||
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
|
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
|
||||||
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),
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* GROUP 4: Carbons, Chat Session, Software, Disconnect
|
||||||
|
* Message synchronization and session management
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
/* 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),
|
||||||
@@ -195,34 +151,26 @@ main(int argc, char* argv[])
|
|||||||
PROF_FUNC_TEST(receive_carbon),
|
PROF_FUNC_TEST(receive_carbon),
|
||||||
PROF_FUNC_TEST(receive_self_carbon),
|
PROF_FUNC_TEST(receive_self_carbon),
|
||||||
PROF_FUNC_TEST(receive_private_carbon),
|
PROF_FUNC_TEST(receive_private_carbon),
|
||||||
|
|
||||||
|
/* Chat session management - bare/full JID routing */
|
||||||
|
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
|
||||||
|
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
|
||||||
|
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
|
||||||
|
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
|
||||||
|
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
|
||||||
|
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
|
||||||
|
|
||||||
|
/* Software Version - XEP-0092 */
|
||||||
|
PROF_FUNC_TEST(send_software_version_request),
|
||||||
|
PROF_FUNC_TEST(display_software_version_result),
|
||||||
|
PROF_FUNC_TEST(shows_message_when_software_version_error),
|
||||||
|
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
|
||||||
|
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
|
||||||
|
PROF_FUNC_TEST(display_software_version_result_in_chat),
|
||||||
|
|
||||||
|
/* Disconnect - clean session termination */
|
||||||
|
PROF_FUNC_TEST(disconnect_ends_session),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Test group registry for easy extension */
|
return cmocka_run_group_tests(all_tests, NULL, NULL);
|
||||||
struct {
|
|
||||||
const char* name;
|
|
||||||
const struct CMUnitTest* tests;
|
|
||||||
size_t count;
|
|
||||||
} groups[] = {
|
|
||||||
{ "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 3: Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
|
|
||||||
{ "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) },
|
|
||||||
};
|
|
||||||
const int num_groups = ARRAY_SIZE(groups);
|
|
||||||
|
|
||||||
int result = 0;
|
|
||||||
|
|
||||||
if (group > 0 && group <= num_groups) {
|
|
||||||
/* Run specific group */
|
|
||||||
result = _cmocka_run_group_tests(groups[group - 1].name, groups[group - 1].tests,
|
|
||||||
groups[group - 1].count, NULL, NULL);
|
|
||||||
} else {
|
|
||||||
/* Run all groups sequentially */
|
|
||||||
for (int i = 0; i < num_groups; i++) {
|
|
||||||
result |= _cmocka_run_group_tests(groups[i].name, groups[i].tests,
|
|
||||||
groups[i].count, NULL, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -27,13 +24,6 @@ int fd = 0;
|
|||||||
int stub_port = 5230;
|
int stub_port = 5230;
|
||||||
pid_t child_pid = 0;
|
pid_t child_pid = 0;
|
||||||
|
|
||||||
/*
|
|
||||||
* Dynamic XDG paths based on stub_port for parallel test execution.
|
|
||||||
* Each test instance gets unique directories to avoid file conflicts.
|
|
||||||
*/
|
|
||||||
char xdg_config_home[256];
|
|
||||||
char xdg_data_home[256];
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Buffer for accumulating output from profanity.
|
* Buffer for accumulating output from profanity.
|
||||||
* 64KB is sufficient for typical test output while keeping memory usage
|
* 64KB is sufficient for typical test output while keeping memory usage
|
||||||
@@ -87,7 +77,7 @@ _mkdir_recursive(const char *dir)
|
|||||||
void
|
void
|
||||||
_create_config_dir(void)
|
_create_config_dir(void)
|
||||||
{
|
{
|
||||||
GString *profanity_dir = g_string_new(xdg_config_home);
|
GString *profanity_dir = g_string_new(XDG_CONFIG_HOME);
|
||||||
g_string_append(profanity_dir, "/profanity");
|
g_string_append(profanity_dir, "/profanity");
|
||||||
|
|
||||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||||
@@ -100,7 +90,7 @@ _create_config_dir(void)
|
|||||||
void
|
void
|
||||||
_create_data_dir(void)
|
_create_data_dir(void)
|
||||||
{
|
{
|
||||||
GString *profanity_dir = g_string_new(xdg_data_home);
|
GString *profanity_dir = g_string_new(XDG_DATA_HOME);
|
||||||
g_string_append(profanity_dir, "/profanity");
|
g_string_append(profanity_dir, "/profanity");
|
||||||
|
|
||||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||||
@@ -113,7 +103,7 @@ _create_data_dir(void)
|
|||||||
void
|
void
|
||||||
_create_chatlogs_dir(void)
|
_create_chatlogs_dir(void)
|
||||||
{
|
{
|
||||||
GString *chatlogs_dir = g_string_new(xdg_data_home);
|
GString *chatlogs_dir = g_string_new(XDG_DATA_HOME);
|
||||||
g_string_append(chatlogs_dir, "/profanity/chatlogs");
|
g_string_append(chatlogs_dir, "/profanity/chatlogs");
|
||||||
|
|
||||||
if (!_mkdir_recursive(chatlogs_dir->str)) {
|
if (!_mkdir_recursive(chatlogs_dir->str)) {
|
||||||
@@ -126,7 +116,7 @@ _create_chatlogs_dir(void)
|
|||||||
void
|
void
|
||||||
_create_logs_dir(void)
|
_create_logs_dir(void)
|
||||||
{
|
{
|
||||||
GString *logs_dir = g_string_new(xdg_data_home);
|
GString *logs_dir = g_string_new(XDG_DATA_HOME);
|
||||||
g_string_append(logs_dir, "/profanity/logs");
|
g_string_append(logs_dir, "/profanity/logs");
|
||||||
|
|
||||||
if (!_mkdir_recursive(logs_dir->str)) {
|
if (!_mkdir_recursive(logs_dir->str)) {
|
||||||
@@ -139,15 +129,7 @@ _create_logs_dir(void)
|
|||||||
void
|
void
|
||||||
_cleanup_dirs(void)
|
_cleanup_dirs(void)
|
||||||
{
|
{
|
||||||
const char *group_env = getenv("PROF_TEST_GROUP");
|
int res = system("rm -rf ./tests/functionaltests/files");
|
||||||
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];
|
|
||||||
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id);
|
|
||||||
int res = system(cmd);
|
|
||||||
if (res == -1) {
|
if (res == -1) {
|
||||||
assert_true(FALSE);
|
assert_true(FALSE);
|
||||||
}
|
}
|
||||||
@@ -230,83 +212,33 @@ 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.
|
|
||||||
* 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),
|
|
||||||
"./test-files/%d/xdg_config_home", dir_id);
|
|
||||||
snprintf(xdg_data_home, sizeof(xdg_data_home),
|
|
||||||
"./test-files/%d/xdg_data_home", dir_id);
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
config_orig = getenv("XDG_CONFIG_HOME");
|
config_orig = getenv("XDG_CONFIG_HOME");
|
||||||
data_orig = getenv("XDG_DATA_HOME");
|
data_orig = getenv("XDG_DATA_HOME");
|
||||||
|
|
||||||
setenv("XDG_CONFIG_HOME", xdg_config_home, 1);
|
setenv("XDG_CONFIG_HOME", XDG_CONFIG_HOME, 1);
|
||||||
setenv("XDG_DATA_HOME", xdg_data_home, 1);
|
setenv("XDG_DATA_HOME", XDG_DATA_HOME, 1);
|
||||||
|
|
||||||
_cleanup_dirs();
|
_cleanup_dirs();
|
||||||
|
|
||||||
@@ -454,16 +386,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(®ex);
|
regfree(®ex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
#ifndef __H_PROFTEST
|
#ifndef __H_PROFTEST
|
||||||
#define __H_PROFTEST
|
#define __H_PROFTEST
|
||||||
|
|
||||||
/*
|
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
|
||||||
* XDG paths are dynamic and generated per-test based on stub_port.
|
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
|
||||||
* Each test instance uses unique directories (./tests/functionaltests/files/{port}/xdg_*)
|
|
||||||
* to allow parallel test execution without file conflicts.
|
|
||||||
*/
|
|
||||||
extern char xdg_config_home[256];
|
|
||||||
extern char xdg_data_home[256];
|
|
||||||
|
|
||||||
extern int stub_port;
|
extern int stub_port;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user