Compare commits

..

41 Commits

Author SHA1 Message Date
01c3205f5d revert: remove cmocka 2.0 compatibility workaround
Some checks failed
CI Code / Code Coverage (pull_request) Has been cancelled
CI Code / Linux (arch) (pull_request) Has been cancelled
CI Code / Linux (ubuntu) (pull_request) Has been cancelled
CI Code / Check coding style (pull_request) Has been cancelled
CI Code / Check spelling (pull_request) Has been cancelled
CI Code / Linux (debian) (pull_request) Has been cancelled
Restore original prof_cmocka.h - the CMOCKA_DISABLE_DEPRECATION_WARNINGS
workaround is not needed for this project.
2026-01-19 16:33:22 +03:00
4789ada834 ci: fix deprecated lcov option
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 15m29s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m0s
CI Code / Linux (debian) (pull_request) Successful in 18m19s
CI Code / Linux (arch) (pull_request) Successful in 20m21s
Change lcov_branch_coverage to branch_coverage (new syntax)
2026-01-19 16:29:38 +03:00
f6b621ad40 ci: add branch coverage to lcov
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 37s
CI Code / Code Coverage (pull_request) Successful in 15m56s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m20s
CI Code / Linux (debian) (pull_request) Successful in 18m42s
CI Code / Linux (arch) (pull_request) Successful in 20m9s
Enable branch coverage with --rc lcov_branch_coverage=1.
This provides more detailed coverage metrics:
- Line coverage: % of code lines executed
- Function coverage: % of functions called
- Branch coverage: % of if/else/switch branches taken

Branch coverage is a step towards MC/DC (Modified Condition/Decision Coverage)
used in safety-critical systems.

Also add coverage/ to .gitignore.
2026-01-19 15:53:52 +03:00
d80092821b ci: add functional tests to coverage + fix lcov errors
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 15m28s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m1s
CI Code / Linux (debian) (pull_request) Successful in 18m16s
CI Code / Linux (arch) (pull_request) Successful in 20m20s
1. Add 'make check-functional-parallel' to coverage job
   - Unit tests alone cover ~27% (mostly utilities)
   - Functional tests exercise UI, XMPP, and full application flow
   - Should significantly increase coverage

2. Fix lcov errors:
   - Remove /usr/lib/* pattern (unused, causes error)
   - Add 'unused' to --ignore-errors
2026-01-17 19:10:24 +03:00
ca5835c58e ci: fix coverage lcov exclude pattern
Some checks failed
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Failing after 14m46s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m1s
CI Code / Linux (debian) (pull_request) Successful in 18m16s
CI Code / Linux (arch) (pull_request) Successful in 20m5s
The code is built in /usr/src/profanity/ inside Docker, so
excluding '/usr/*' removes all source files!

Fix: exclude only system headers/libs (/usr/include/*, /usr/lib/*),
not the entire /usr/ tree.

Also add --ignore-errors empty to handle edge cases.
2026-01-17 18:37:01 +03:00
1d113c4d64 ci: run coverage on PRs too, not just master push
Some checks failed
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m7s
CI Code / Linux (debian) (pull_request) Successful in 18m25s
CI Code / Linux (arch) (pull_request) Successful in 20m32s
CI Code / Code Coverage (pull_request) Failing after 12m34s
2026-01-17 18:13:47 +03:00
e40b89ad92 ci: improve test failure detection in parallel tests
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Code Coverage (pull_request) Has been skipped
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m10s
CI Code / Linux (debian) (pull_request) Successful in 18m23s
CI Code / Linux (arch) (pull_request) Successful in 20m2s
1. ci-build.sh: Add verify_test_failure_detection() function
   - Tests that single test failures are properly detected
   - Tests that parallel test failures are properly detected
   - Runs at CI start to catch framework issues early

2. Makefile.am: Fix check-functional-parallel exit code handling
   - OLD: used 'wait' without checking individual exit codes
   - NEW: capture each PID and check exit code with 'wait $pid'
   - On failure: print the log file and set failed flag
   - Exit with error if any group failed

This ensures CI correctly fails when:
- Any unit test fails
- Any of the 4 functional test groups fails
- Any parallel build configuration fails
2026-01-17 18:03:07 +03:00
f41888b7a5 feat: add code coverage support
All checks were successful
CI Code / Code Coverage (pull_request) Has been skipped
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m16s
CI Code / Linux (debian) (pull_request) Successful in 18m27s
CI Code / Linux (arch) (pull_request) Successful in 20m58s
- Add --enable-coverage option to configure.ac
- Add coverage and coverage-html targets to Makefile.am
- Add coverage CI job with Codecov upload
- Add lcov to all Dockerfiles (arch, debian, fedora, tumbleweed, ubuntu)

Usage:
  ./configure --enable-coverage
  make check
  make coverage-html
2026-01-17 17:45:34 +03:00
c125746f6e perf: optimize build with configure cache and parallel make
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Linux (ubuntu) (pull_request) Successful in 17m55s
CI Code / Linux (debian) (pull_request) Successful in 18m8s
CI Code / Linux (arch) (pull_request) Successful in 20m12s
- Add -C flag to ./configure for caching results
- Use --depth 1 for git clone (faster)
- Use make -j$(nproc) for stabber/libstrophe builds

Borrowed optimizations from build/multicore branch.
2026-01-16 19:33:56 +03:00
16d4efbccc ci: disable Docker cache to test cmocka 2.0 breakage
Add --no-cache to docker build to force fresh image build with
latest cmocka 2.0.1 from debian:testing.
2026-01-16 18:15:45 +03:00
e1a9f55a32 test: remove cmocka 2.0 fix to verify CI failure
All checks were successful
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Linux (debian) (pull_request) Successful in 15m51s
CI Code / Linux (ubuntu) (pull_request) Successful in 15m58s
CI Code / Linux (arch) (pull_request) Successful in 16m8s
Temporarily removing CMOCKA_DISABLE_DEPRECATION_WARNINGS to demonstrate
that tests fail on debian:testing (cmocka 2.0.1) without this fix.
2026-01-16 17:38:21 +03:00
7765f65be1 fix(build): use $(srcdir)/tests for VPATH builds
All checks were successful
CI Code / Linux (arch) (pull_request) Successful in 16m40s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Linux (ubuntu) (pull_request) Successful in 18m20s
CI Code / Linux (debian) (pull_request) Successful in 18m41s
Fix include paths for out-of-tree builds (e.g., build-1/, build-2/)
to correctly find tests/prof_cmocka.h header file.
2026-01-16 17:11:51 +03:00
5153e04f96 feat: update compatibility header for cmocka 2.0 deprecation handling
All checks were successful
CI Code / Linux (debian) (pull_request) Successful in 12m52s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Linux (ubuntu) (pull_request) Successful in 15m26s
CI Code / Linux (arch) (pull_request) Successful in 18m7s
2026-01-16 16:47:48 +03:00
bed046e7ed feat(tests): update Dockerfile and CI script for parallel builds and improved caching
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Linux (ubuntu) (pull_request) Successful in 15m23s
CI Code / Linux (debian) (pull_request) Successful in 15m35s
CI Code / Linux (arch) (pull_request) Successful in 18m11s
2026-01-15 17:20:19 +03:00
fa17173e5c feat(tests): enable parallel execution of functional tests
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Linux (ubuntu) (pull_request) Successful in 30m9s
CI Code / Linux (arch) (pull_request) Successful in 33m47s
CI Code / Linux (debian) (pull_request) Successful in 33m9s
2026-01-13 17:46:03 +03:00
a90eef1cb2 docs: update CONTRIBUTING.md to clarify functional test guidelines
All checks were successful
CI Code / Check coding style (pull_request) Successful in 29s
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Linux (debian) (pull_request) Successful in 1h5m29s
CI Code / Check coding style (push) Successful in 31s
CI Code / Check spelling (push) Successful in 17s
CI Code / Linux (arch) (push) Successful in 1h3m52s
CI Code / Linux (ubuntu) (pull_request) Successful in 1h4m41s
CI Code / Linux (arch) (pull_request) Successful in 1h7m14s
CI Code / Linux (debian) (push) Successful in 1h4m50s
CI Code / Linux (ubuntu) (push) Successful in 1h5m37s
2026-01-07 11:46:40 +01:00
48ab4b9360 feat: add Valgrind suppressions and update CI/docs
- Add prof.supp with pthread TLS suppressions
- Update ci-build.sh with test configurations
- Document functional test best practices in CONTRIBUTING.md
2026-01-07 11:46:40 +01:00
8f580f91a8 refactor(tests): migrate to content-based stubbing
- Replace stbbr_for_id() with stbbr_for_query()/stbbr_send()
- Content-based stubbing matches stanzas by namespace instead of ID
- Use regex assertions for flexible output matching
- Fix timing issues in chat_session and presence tests
2026-01-07 11:46:40 +01:00
2d7de2caf6 refactor(tests): replace libexpect with forkpty() for PTY handling
- Remove dependency on libexpect/tcl
- Implement native PTY handling with forkpty()
- Add prof_output_exact/regex for flexible output matching
- Improve timeout handling and synchronization
2026-01-07 11:46:40 +01:00
889a6e2b63 build: enable functional tests unconditionally
- Remove conditional compilation for functional tests
- Always build with stabber/cmocka when available
- Simplify test configuration in Makefile.am
2026-01-07 11:46:40 +01:00
44de29a199 build(docker): add TERM env, fix SSL and reflector handling
- Add TERM=xterm-256color for PTY support in functional tests
- Disable SSL verification for git clone in restricted networks
- Add error handling for Arch reflector installation
2026-01-07 11:46:40 +01:00
e31240a4be Merge branch 'fix/connect_max_args'
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 45s
CI Code / Linux (arch) (push) Successful in 13m40s
CI Code / Linux (debian) (push) Successful in 14m5s
CI Code / Linux (ubuntu) (push) Successful in 14m20s
2026-01-07 10:14:38 +01:00
88b48000f8 fix(cmd): increase /connect max args from 7 to 9
All checks were successful
CI Code / Check spelling (pull_request) Successful in 27s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Linux (arch) (pull_request) Successful in 14m23s
CI Code / Linux (debian) (pull_request) Successful in 15m33s
CI Code / Linux (ubuntu) (pull_request) Successful in 15m46s
The /connect command can take: account + server <s> + port <p> + tls <t> + auth <a>
which totals 9 arguments, not 7. This fixes argument parsing for full command usage.
2026-01-06 17:22:53 +03:00
f446f48d07 feat(ui,window): add accurate unread indicators and history paging support
Some checks failed
CI Code / Check spelling (push) Successful in 18s
CI Code / Linux (arch) (push) Has been cancelled
CI Code / Linux (debian) (push) Has been cancelled
CI Code / Linux (ubuntu) (push) Has started running
CI Code / Check coding style (push) Has been cancelled
- Track unread message indicators and paging state in the window component
- Clear and restore unread markers correctly when navigating between conversations
- Suppress buffer updates and message printing while viewing history to prevent
  unwanted scroll jumps and viewport lock

Also includes minor code-style, whitespace, and comment cleanups as well as
updated documentation for metrics and buffer handling.
2025-12-02 17:59:25 +01:00
20af44196b fix(cmd): cmd_time: Avoid extra loop iterations for single option
All checks were successful
CI Code / Check spelling (push) Successful in 19s
CI Code / Check coding style (push) Successful in 36s
CI Code / Linux (arch) (push) Successful in 10m17s
CI Code / Linux (debian) (push) Successful in 13m28s
CI Code / Linux (ubuntu) (push) Successful in 13m47s
2025-11-27 11:38:49 +01:00
84d6253561 fix(xmpp): format debug logging statements for consistency
All checks were successful
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 33s
CI Code / Linux (arch) (push) Successful in 9m54s
CI Code / Linux (debian) (push) Successful in 13m35s
CI Code / Linux (ubuntu) (push) Successful in 13m56s
Add debug output tracking connection lifecycle.
    - Track disconnects and reconnects
    - Record session login, logout, and reconnection attempts
    - Use [CONNDBG] tag for easy log filtering
2025-11-21 10:03:50 +01:00
74cfd32c1e Improve arch build: sort mirrors by speed in reflector
All checks were successful
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Linux (debian) (pull_request) Successful in 12m53s
CI Code / Linux (ubuntu) (pull_request) Successful in 13m14s
CI Code / Linux (arch) (pull_request) Successful in 13m22s
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 32s
CI Code / Linux (debian) (push) Successful in 13m35s
CI Code / Linux (arch) (push) Successful in 14m24s
CI Code / Linux (ubuntu) (push) Successful in 15m59s
2025-10-29 20:45:37 +01:00
fe6912a30c Fix arch build by adding pacman-key --init and updating gtk2 to gtk3
Some checks failed
CI Code / Linux (arch) (pull_request) Failing after 1m46s
CI Code / Check coding style (pull_request) Successful in 1m57s
CI Code / Check spelling (pull_request) Successful in 43s
CI Code / Linux (ubuntu) (pull_request) Successful in 13m52s
CI Code / Linux (debian) (pull_request) Successful in 16m11s
2025-10-29 20:27:55 +01:00
266f5aa046 feat(api): add get_current_window call
Some checks failed
CI API Docs / Test C API Documentation Generation (pull_request) Successful in 27s
CI API Docs / Test Python API Documentation Generation (pull_request) Successful in 29s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Linux (debian) (pull_request) Successful in 13m22s
CI Code / Linux (arch) (pull_request) Successful in 13m27s
CI Code / Linux (ubuntu) (pull_request) Successful in 13m36s
CI API Docs / Test C API Documentation Generation (push) Successful in 1m2s
CI Code / Check spelling (push) Has been cancelled
CI Code / Check coding style (push) Has been cancelled
CI Code / Linux (ubuntu) (push) Has been cancelled
CI Code / Linux (debian) (push) Has been cancelled
CI API Docs / Test Python API Documentation Generation (push) Successful in 1m8s
CI Code / Linux (arch) (push) Successful in 9m24s
Add prof_get_current_window API to retrieve the title of the currently active window, matching the titlebar display.

The feature is especially useful to track currently used plugin window.
2025-10-20 19:00:48 +02:00
3738be4af1 fix: prevent autoping disable on false negative ping support check
All checks were successful
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Linux (arch) (pull_request) Successful in 12m57s
CI Code / Linux (ubuntu) (pull_request) Successful in 13m59s
CI Code / Linux (debian) (pull_request) Successful in 16m35s
CI Code / Check spelling (push) Successful in 19s
CI Code / Check coding style (push) Successful in 38s
CI Code / Linux (arch) (push) Successful in 10m10s
CI Code / Linux (debian) (push) Successful in 13m50s
CI Code / Linux (ubuntu) (push) Successful in 16m42s
Remove autodisable logic (prefs_set_autoping(0)) and early return in autoping
to treat intermittent false negatives from connection_supports(XMPP_FEATURE_PING).
Add one-time error display with debug features print for monitoring; warn on first failing
check without aborting to maintain functionality while investigating root cause.
2025-10-14 12:42:00 +02:00
ad95edb2b9 feat: make /log level work with non-default files 2025-10-13 20:47:34 +02:00
fac1e224bc fix(scroll): handle edge case of extremely long messages
All checks were successful
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Check spelling (pull_request) Successful in 45s
CI Code / Linux (arch) (pull_request) Successful in 10m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 13m11s
CI Code / Linux (debian) (pull_request) Successful in 16m46s
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 32s
CI Code / Linux (arch) (push) Successful in 12m56s
CI Code / Linux (debian) (push) Successful in 13m28s
CI Code / Linux (ubuntu) (push) Successful in 16m20s
Prior commit (6ad8a190) did not properly handle overflow by long (9000 lines) message,
to address this issue, multiple changes were made:
- Add lines recalculation on win_redraw
- Move `prof_buff_t` struct to header file so its lines could be externally changed
- Call win_redraw once overflow is detected: it allows to recalculate sizes in lines and apply changes to the buffer
2025-10-08 13:00:32 +02:00
6ad8a19053 fix(buffer): use dynamic buffer size to prevent unnecessary buffer cleanups
All checks were successful
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Check coding style (pull_request) Successful in 38s
CI Code / Linux (arch) (pull_request) Successful in 13m4s
CI Code / Linux (debian) (pull_request) Successful in 13m32s
CI Code / Linux (ubuntu) (pull_request) Successful in 16m23s
CI Code / Check spelling (push) Successful in 19s
CI Code / Check coding style (push) Successful in 37s
CI Code / Linux (arch) (push) Successful in 13m8s
CI Code / Linux (ubuntu) (push) Successful in 16m2s
CI Code / Linux (debian) (push) Successful in 16m26s
Refactor buffer trimming logic to use dynamic line counts (_lines per entry)
against PAD_SIZE (10k) instead of fixed entry count (MAX_BUFFER_SIZE=200). This
prevents premature cleanups for short messages, reduces scrolling disruptions
during history loads, and scales better with rendered content size.

Delete oldest/newest entry from opposite end only when total lines approach
ncurses pad limit, minimizing unnecessary deletions.
Simplify overflow warning log by removing unused MAX_BUFFER_SIZE reference.

Update buffer.c header for improved DX:
- Add detailed module overview explaining role (in-memory UI buffer vs. DB
persistence), key features (trimming, metadata), and integration (ncurses).
- Shorten copyright/license boilerplate to concise pointer (LICENSE ref).
- Add fork notice; preserve original copyrights per GPLv3.

Partially addresses #36 (stuck scroll mitigation via fewer cleanups)
Related to #36
2025-10-07 15:12:20 +02:00
40dd773c6a docs(apidocs/c): improve profapi.h and profhooks.h comments
All checks were successful
CI API Docs / Test C API Documentation Generation (pull_request) Successful in 23s
CI API Docs / Test Python API Documentation Generation (pull_request) Successful in 26s
CI Code / Check coding style (pull_request) Successful in 33s
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Linux (debian) (pull_request) Successful in 12m26s
CI Code / Linux (ubuntu) (pull_request) Successful in 12m46s
CI Code / Linux (arch) (pull_request) Successful in 15m17s
CI API Docs / Test C API Documentation Generation (push) Successful in 28s
CI API Docs / Test Python API Documentation Generation (push) Successful in 29s
- Enhanced file-level comments with concise descriptions
- Added usage examples and aligned with Python prof module style
- Updated function comments to remove tables, improve clarity
- Replaced Profanity with CProof, preserved paths and method names
2025-09-23 14:07:32 +02:00
8849d9b79e docs: move callbacks down
All checks were successful
CI API Docs / Test C API Documentation Generation (pull_request) Successful in 26s
CI API Docs / Test Python API Documentation Generation (pull_request) Successful in 28s
CI Code / Check coding style (pull_request) Successful in 36s
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Linux (debian) (pull_request) Successful in 13m40s
CI Code / Linux (ubuntu) (pull_request) Successful in 14m9s
CI Code / Linux (arch) (pull_request) Successful in 21m18s
To improve readability of the docs
2025-09-22 23:05:45 +02:00
92cb930810 docs: Fix indentation
All checks were successful
CI API Docs / Test C API Documentation Generation (pull_request) Successful in 26s
CI API Docs / Test Python API Documentation Generation (pull_request) Successful in 28s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Linux (debian) (pull_request) Successful in 13m38s
CI Code / Linux (ubuntu) (pull_request) Successful in 16m18s
CI Code / Linux (arch) (pull_request) Successful in 16m46s
2025-09-17 01:37:10 +02:00
9fd62e2304 ci: restructure and optimize execution conditions
Some checks failed
CI API Docs / Test C API Documentation Generation (pull_request) Successful in 26s
CI API Docs / Test Python API Documentation Generation (pull_request) Failing after 28s
CI Code / Check coding style (pull_request) Has been cancelled
CI Code / Check spelling (pull_request) Has been cancelled
CI Code / Linux (arch) (pull_request) Has been cancelled
CI Code / Linux (debian) (pull_request) Has been cancelled
CI Code / Linux (ubuntu) (pull_request) Has been cancelled
- The code tests will ignore doc changes and vice versa.
- The CI is split in 2 parts for clarity
2025-09-17 01:00:30 +02:00
c94263486f docs(python): fix formatting to ensure correct parsing
Fixes issues introduced in 052e168, 4cdcf9a, and other commits from PR #31.
2025-09-17 00:34:37 +02:00
353b6bdb11 Merge branch 'build/add-api-docs-gen-tests'
All checks were successful
CI / Check spelling (push) Successful in 20s
CI / Test C API Documentation Generation (push) Successful in 23s
CI / Check coding style (push) Successful in 33s
CI / Test Python API Documentation Generation (push) Successful in 28s
CI / Linux (debian) (push) Successful in 13m38s
CI / Linux (ubuntu) (push) Successful in 14m7s
CI / Linux (arch) (push) Successful in 17m5s
2025-09-15 16:54:33 +02:00
105c496ab1 docs(sphinx): fix conf.py and index.rst to resolve warnings
All checks were successful
CI / Check spelling (pull_request) Successful in 19s
CI / Test C API Documentation Generation (pull_request) Successful in 23s
CI / Check coding style (pull_request) Successful in 31s
CI / Test Python API Documentation Generation (pull_request) Successful in 29s
CI / Linux (debian) (pull_request) Successful in 13m34s
CI / Linux (ubuntu) (pull_request) Successful in 14m4s
CI / Linux (arch) (pull_request) Successful in 17m4s
- Removed unnecessary list item (*) from :ref:`genindex`.
- Added blank line after :ref:`genindex` to terminate markup block.
- Rebranded configs
2025-09-15 16:12:47 +02:00
4780fdd4a1 tests: update cmocka include in forced encryption tests
All checks were successful
CI / Check spelling (pull_request) Successful in 21s
CI / Check coding style (pull_request) Successful in 36s
CI / Linux (debian) (pull_request) Successful in 15m53s
CI / Linux (ubuntu) (pull_request) Successful in 15m58s
CI / Linux (arch) (pull_request) Successful in 16m35s
CI / Check spelling (push) Successful in 19s
CI / Check coding style (push) Successful in 34s
CI / Linux (ubuntu) (push) Successful in 14m46s
CI / Linux (debian) (push) Successful in 15m26s
CI / Linux (arch) (push) Successful in 17m52s
Includes were updated in 988d366, but the change did not propagate to forced_encryption.c due to the gap between the upstream Profanity and the CProof.
2025-09-10 15:50:26 +02:00
56 changed files with 2826 additions and 1812 deletions

44
.github/workflows/ci-api-docs.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CI API Docs
on:
push:
branches: [master]
paths:
- 'apidocs/**'
pull_request:
branches: [master]
paths:
- 'apidocs/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test-c-api-docs:
runs-on: ubuntu-latest
name: Test C API Documentation Generation
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends make doxygen
- name: Test C API docs generation
run: |
cd apidocs/c/
doxygen c-prof.conf
test-python-api-docs:
runs-on: ubuntu-latest
name: Test Python API Documentation Generation
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends make python3-sphinx
- name: Test Python API docs generation
run: |
cd apidocs/python/
sphinx-apidoc -f -o . src && make -j$(nproc) xml SPHINXOPTS="-W --keep-going -n"

View File

@@ -1,10 +1,18 @@
name: CI
name: CI Code
on:
push:
branches: [master]
paths-ignore:
- 'docs/**'
- 'apidocs/**'
- 'README.md'
pull_request:
branches: [master]
paths-ignore:
- 'docs/**'
- 'apidocs/**'
- 'README.md'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -23,7 +31,7 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
run: |
docker build -f Dockerfile.${{ matrix.flavor }} -t profanity .
docker build --no-cache -f Dockerfile.${{ matrix.flavor }} -t profanity .
docker run profanity ./ci-build.sh
code-style:
@@ -83,7 +91,7 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: install dependencies
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y --no-install-recommends codespell
@@ -91,30 +99,32 @@ jobs:
run: |
codespell
test-c-api-docs:
coverage:
runs-on: ubuntu-latest
name: Test C API Documentation Generation
name: Code Coverage
steps:
- uses: actions/checkout@v4
- name: Install dependencies
- name: Build and run coverage
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends make doxygen
- name: Test C API docs generation
run: |
cd apidocs/c/
doxygen c-prof.conf
test-python-api-docs:
runs-on: ubuntu-latest
name: Test Python API Documentation Generation
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends make python3-sphinx
- name: Test Python API docs generation
run: |
cd apidocs/python/
sphinx-apidoc -f -o . src && make -j$(nproc) xml SPHINXOPTS="-W --keep-going -n"
docker build -f Dockerfile.debian -t profanity-cov .
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
'
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage.info
fail_ci_if_error: false
verbose: true

1
.gitignore vendored
View File

@@ -107,3 +107,4 @@ breaks
*.tar.*
*.zip
*.log*
coverage/

View File

@@ -137,3 +137,35 @@ You can run the `make spell` command for this.
`make doublecheck` will run the code formatter, spell checker and unit tests.
### Functional tests
The functional test suite uses [stabber](https://git.jabber.space/devs/stabber) as a mock XMPP server. Tests are located in `tests/functionaltests/`.
#### Running functional tests
Functional tests require stabber to be installed. Once installed, tests run as part of `make check`:
```bash
make check
```
#### Writing functional tests
Use content-based stubbing with stabber:
```c
// Use stbbr_for_query for IQ queries (roster, disco, etc.)
stbbr_for_query("jabber:iq:roster", "<iq type='result'>...</iq>");
// Use stbbr_send for presence, message, and push-style stanzas
stbbr_send("<presence from='buddy@localhost'>...</presence>");
```
Guidelines:
1. Use `stbbr_for_query(namespace, xml)` for IQ queries where the namespace is stable.
2. Use `stbbr_send(xml)` for presence, message, and other push-style stanzas.
3. Keep assertions tolerant of ordering when possible; use `prof_output_regex()` for flexible matching.
4. If timing issues appear, use `prof_timeout()` around critical expectations and reset afterwards.

View File

@@ -1,13 +1,23 @@
FROM archlinux
RUN pacman -Syu --noconfirm && pacman -S --needed --noconfirm \
ENV TERM=xterm
ENV CC="ccache gcc"
RUN pacman -Syu --noconfirm
# reflector is optional - if it fails due to network issues, continue with default mirrorlist
RUN pacman -S --needed --noconfirm reflector && \
(reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist || true)
RUN pacman-key --init
RUN pacman -S --needed --noconfirm \
autoconf \
autoconf-archive \
automake \
base-devel \
ccache \
check \
cmake \
cmocka \
lcov \
curl \
debuginfod \
doxygen \
@@ -15,7 +25,7 @@ RUN pacman -Syu --noconfirm && pacman -S --needed --noconfirm \
gcc \
git \
gpgme \
gtk2 \
gtk3 \
libgcrypt \
libmicrohttpd \
libnotify \
@@ -54,13 +64,13 @@ USER root
RUN pacman -U --noconfirm libstrophe-git/libstrophe-git-*.pkg.tar.zst
WORKDIR /usr/src
#RUN git clone https://github.com/boothj5/stabber
RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src/profanity
COPY . /usr/src/profanity

View File

@@ -1,13 +1,18 @@
# Build the latest Debian testing image
FROM debian:testing
ENV DEBIAN_FRONTEND="noninteractive"
ENV TERM=xterm
ENV CC="ccache gcc"
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
autoconf-archive \
automake \
expect \
ccache \
gcc \
git \
lcov \
libcmocka-dev \
libcurl3-dev \
libgcrypt-dev \
@@ -35,19 +40,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src
#RUN git clone https://github.com/boothj5/stabber
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr
RUN make
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src/profanity

View File

@@ -1,7 +1,9 @@
# Build the latest Fedora image
FROM fedora:latest
# expect - for functional tests
ENV TERM=xterm
ENV CC="ccache gcc"
# libmicrohttpd - for stabber
# glibc-locale - to have en_US locale
RUN dnf install -y \
@@ -9,9 +11,10 @@ RUN dnf install -y \
autoconf-archive \
automake \
awk \
expect-devel \
ccache \
gcc \
git \
lcov \
glib2-devel \
glibc-all-langpacks \
gtk2-devel \
@@ -42,25 +45,25 @@ RUN dnf install -y \
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TERM=xterm
RUN mkdir -p /usr/src
WORKDIR /usr/src
#RUN mkdir -p /usr/src/stabber
#RUN git clone https://github.com/boothj5/stabber
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src
RUN mkdir -p /usr/src/libstrophe
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr
RUN make
RUN make -j$(nproc)
RUN make install
RUN mkdir -p /usr/src/profanity

View File

@@ -1,16 +1,19 @@
# Build the latest openSUSE Tumbleweed image
FROM opensuse/tumbleweed
# expect - for functional tests
ENV TERM=xterm
ENV CC="ccache gcc"
# libmicrohttpd - for stabber
# glibc-locale - to have en_US locale
RUN zypper --non-interactive in --no-recommends \
autoconf \
autoconf-archive \
automake \
expect-devel \
ccache \
gcc \
git \
lcov \
glib2-devel \
glibc-locale \
gtk2-devel \
@@ -41,17 +44,17 @@ RUN zypper --non-interactive in --no-recommends \
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TERM=xterm
RUN mkdir -p /usr/src
WORKDIR /usr/src
#RUN mkdir -p /usr/src/stabber
#RUN git clone git://github.com/boothj5/stabber.git
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
RUN mkdir -p /usr/src/profanity
WORKDIR /usr/src/profanity

View File

@@ -1,14 +1,17 @@
FROM ubuntu:latest
ENV DEBIAN_FRONTEND="noninteractive"
ENV TERM=xterm
ENV CC="ccache gcc"
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
autoconf-archive \
automake \
expect \
ccache \
gcc \
git \
lcov \
libcmocka-dev \
libcurl3-dev \
libgcrypt-dev \
@@ -36,20 +39,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /usr/src/{stabber,libstrophe,profanity}
WORKDIR /usr/src
#RUN git clone https://github.com/boothj5/stabber
RUN git clone -c http.sslverify=false https://github.com/strophe/libstrophe
RUN git clone --depth 1 -c http.sslverify=false https://git.jabber.space/devs/stabber
RUN git clone --depth 1 -c http.sslverify=false https://github.com/strophe/libstrophe
# TODO: Re-enable once libmicrohttpd-dev has been updated.
#WORKDIR /usr/src/stabber
#RUN ./bootstrap.sh
#RUN ./configure --prefix=/usr --disable-dependency-tracking
#RUN make
#RUN make install
WORKDIR /usr/src/stabber
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr --disable-dependency-tracking
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src/libstrophe
RUN ./bootstrap.sh
RUN ./configure --prefix=/usr
RUN make
RUN make -j$(nproc)
RUN make install
WORKDIR /usr/src/profanity

View File

@@ -286,25 +286,46 @@ endif
TESTS = tests/unittests/unittests
check_PROGRAMS = tests/unittests/unittests
tests_unittests_unittests_CPPFLAGS = -Itests/
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
tests_unittests_unittests_SOURCES = $(unittest_sources)
tests_unittests_unittests_LDADD = -lcmocka
# Functional test were commented out because of:
# https://github.com/profanity-im/profanity/pull/1010
# An issue was raised for stabber:
# https://github.com/profanity-im/stabber/issues/5
# Once this issue is resolved functional tests should be enabled again
# Functional tests require libstabber.
# They are only built when libstabber is available.
# See: https://github.com/profanity-im/profanity/pull/1010
# https://github.com/profanity-im/stabber/issues/5
#
#if HAVE_STABBER
#if HAVE_EXPECT
#TESTS += tests/functionaltests/functionaltests
#check_PROGRAMS += tests/functionaltests/functionaltests
#tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
#tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS) -I/usr/include/tcl8.6 -I/usr/include/tcl8.5
#tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber -lexpect
#endif
#endif
# Note: We use forkpty() instead of libexpect for PTY handling.
if HAVE_STABBER
if HAVE_FORKPTY
TESTS += tests/functionaltests/functionaltests
check_PROGRAMS += tests/functionaltests/functionaltests
tests_functionaltests_functionaltests_SOURCES = $(functionaltest_sources)
tests_functionaltests_functionaltests_CPPFLAGS = -I$(srcdir)/tests
tests_functionaltests_functionaltests_CFLAGS = $(AM_CFLAGS)
tests_functionaltests_functionaltests_LDADD = -lcmocka -lstabber @FORKPTY_LIB@
# Parallel functional tests target (~3x faster than sequential)
# Usage: make check-functional-parallel
check-functional-parallel: tests/functionaltests/functionaltests
@echo "Running functional tests in parallel (4 groups)..."
@mkdir -p $(builddir)/test-logs
@failed=0; \
./tests/functionaltests/functionaltests 1 > $(builddir)/test-logs/group1.log 2>&1 & pid1=$$!; \
./tests/functionaltests/functionaltests 2 > $(builddir)/test-logs/group2.log 2>&1 & pid2=$$!; \
./tests/functionaltests/functionaltests 3 > $(builddir)/test-logs/group3.log 2>&1 & pid3=$$!; \
./tests/functionaltests/functionaltests 4 > $(builddir)/test-logs/group4.log 2>&1 & pid4=$$!; \
wait $$pid1 || { echo "Group 1 FAILED (exit $$?)"; cat $(builddir)/test-logs/group1.log; failed=1; }; \
wait $$pid2 || { echo "Group 2 FAILED (exit $$?)"; cat $(builddir)/test-logs/group2.log; failed=1; }; \
wait $$pid3 || { echo "Group 3 FAILED (exit $$?)"; cat $(builddir)/test-logs/group3.log; failed=1; }; \
wait $$pid4 || { echo "Group 4 FAILED (exit $$?)"; cat $(builddir)/test-logs/group4.log; failed=1; }; \
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
man1_MANS = $(man1_sources)
@@ -365,6 +386,20 @@ check-unit: tests/unittests/unittests
@VALGRIND_CHECK_RULES@
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)
clang-format -i $(all_c_sources)

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +1,247 @@
/** @file
C Hooks.
*/
/** @file profhooks.h
* @brief C Plugin Hooks for CProof.
*
* This header defines optional callback hooks that CProof plugins can implement to
* handle events such as startup, shutdown, message processing, and presence updates in
* CProof, a console-based XMPP client. These hooks are called by CProof when the
* corresponding events occur, allowing plugins to customize behavior. All hooks are
* optional; plugins can define only the hooks they need, and CProof will automatically
* invoke them when appropriate.
*
* To use these hooks, define the functions in your plugin with the signatures provided
* in this header. For example, to handle the startup event:
*
* @code
* #include "profhooks.h"
* void prof_on_start(void) {
* // Handle CProof startup
* }
* @endcode
*
* For additional plugin functionality, such as displaying messages or registering
* commands, see the CProof API functions in profapi.h.
*
* @see profapi.h
*/
/** @mainpage CProof Plugins API and Hooks
* List of all API functions available to plugins: @ref profapi.h
* List of all hooks which plugins may implement: @ref profhooks.h
*/
/**
Called when a plugin is loaded, either when profanity is started, or when the /plugins load or /plugins install commands are called
@param version the version of Profanity
@param status the package status of Profanity, "development" or "release"
@param account_name account name of the currently logged in account, or NULL if not logged in
@param fulljid the users full Jabber ID (barejid and resource) if logged in, NULL otherwise
*/
* Called when a plugin is loaded, either when CProof is started, or when the /plugins load or /plugins install commands are called
* @param version The version of CProof
* @param status The package status of CProof, "development" or "release"
* @param account_name Account name of the currently logged in account, or NULL if not logged in
* @param fulljid The user's full Jabber ID (barejid and resource) if logged in, NULL otherwise
*/
void prof_init(const char * const version, const char * const status, const char *const account_name, const char *const fulljid);
/**
Called when Profanity is started
*/
* Called when CProof is started
*/
void prof_on_start(void);
/**
Called when the user quits Profanity
*/
* Called when the user quits CProof
*/
void prof_on_shutdown(void);
/**
Called when a plugin is unloaded with the /plugins unload command
*/
* Called when a plugin is unloaded with the /plugins unload command
*/
void prof_on_unload(void);
/**
Called when the user connects with an account
@param account_name account name of the account used for logging in
@param fulljid the full Jabber ID (barejid and resource) of the account
*/
* Called when the user connects with an account
* @param account_name Account name of the account used for logging in
* @param fulljid The full Jabber ID (barejid and resource) of the account
*/
void prof_on_connect(const char * const account_name, const char * const fulljid);
/**
Called when the user disconnects an account
@param account_name account name of the account being disconnected
@param fulljid the full Jabber ID (barejid and resource) of the account
*/
* Called when the user disconnects an account
* @param account_name Account name of the account being disconnected
* @param fulljid The full Jabber ID (barejid and resource) of the account
*/
void prof_on_disconnect(const char * const account_name, const char * const fulljid);
/**
Called before a chat message is displayed
@param barejid Jabber ID of the message sender
@param resource resource of the message sender
@param message the received message
@return the new message to display, or NULL to preserve the original message
*/
* Called before a chat message is displayed
* @param barejid Jabber ID of the message sender
* @param resource Resource of the message sender
* @param message The received message
* @return The new message to display, or NULL to preserve the original message
*/
char* prof_pre_chat_message_display(const char * const barejid, const char *const resource, const char *message);
/**
Called after a chat message is displayed
@param barejid Jabber ID of the message sender
@param resource resource of the message sender
@param message the received message
*/
* Called after a chat message is displayed
* @param barejid Jabber ID of the message sender
* @param resource Resource of the message sender
* @param message The received message
*/
void prof_post_chat_message_display(const char * const barejid, const char *const resource, const char *message);
/**
Called before a chat message is sent
@param barejid Jabber ID of the message recipient
@param message the message to be sent
@return the modified or original message to send, or NULL to cancel sending of the message
*/
* Called before a chat message is sent
* @param barejid Jabber ID of the message recipient
* @param message The message to be sent
* @return The modified or original message to send, or NULL to cancel sending of the message
*/
char* prof_pre_chat_message_send(const char * const barejid, const char *message);
/**
Called after a chat message has been sent
@param barejid Jabber ID of the message recipient
@param message the sent message
*/
* Called after a chat message has been sent
* @param barejid Jabber ID of the message recipient
* @param message The sent message
*/
void prof_post_chat_message_send(const char * const barejid, const char *message);
/**
Called before a chat room message is displayed
@param barejid Jabber ID of the room
@param nick nickname of message sender
@param message the received message
@return the new message to display, or NULL to preserve the original message
*/
* Called before a chat room message is displayed
* @param barejid Jabber ID of the room
* @param nick Nickname of message sender
* @param message The received message
* @return The new message to display, or NULL to preserve the original message
*/
char* prof_pre_room_message_display(const char * const barejid, const char * const nick, const char *message);
/**
Called after a chat room message is displayed
@param barejid Jabber ID of the room
@param nick nickname of the message sender
@param message the received message
*/
* Called after a chat room message is displayed
* @param barejid Jabber ID of the room
* @param nick Nickname of the message sender
* @param message The received message
*/
void prof_post_room_message_display(const char * const barejid, const char * const nick, const char *message);
/**
Called before a chat room message is sent
@param barejid Jabber ID of the room
@param message the message to be sent
@return the modified or original message to send, or NULL to cancel sending of the message
*/
* Called before a chat room message is sent
* @param barejid Jabber ID of the room
* @param message The message to be sent
* @return The modified or original message to send, or NULL to cancel sending of the message
*/
char* prof_pre_room_message_send(const char * const barejid, const char *message);
/**
Called after a chat room message has been sent
@param barejid Jabber ID of the room
@param message the sent message
*/
* Called after a chat room message has been sent
* @param barejid Jabber ID of the room
* @param message The sent message
*/
void prof_post_room_message_send(const char * const barejid, const char *message);
/**
Called when the server sends a chat room history message
@param barejid Jabber ID of the room
@param nick nickname of the message sender
@param message the message to be sent
@param timestamp time the message was originally sent to the room, in ISO8601 format
*/
* Called when the server sends a chat room history message
* @param barejid Jabber ID of the room
* @param nick Nickname of the message sender
* @param message The message to be sent
* @param timestamp Time the message was originally sent to the room, in ISO8601 format
*/
void prof_on_room_history_message(const char * const barejid, const char *const nick, const char *const message, const char *const timestamp);
/**
Called before a private chat room message is displayed
@param barejid Jabber ID of the room
@param nick nickname of message sender
@param message the received message
@return the new message to display, or NULL to preserve the original message
*/
* Called before a private chat room message is displayed
* @param barejid Jabber ID of the room
* @param nick Nickname of message sender
* @param message The received message
* @return The new message to display, or NULL to preserve the original message
*/
char* prof_pre_priv_message_display(const char * const barejid, const char * const nick, const char *message);
/**
Called after a private chat room message is displayed
@param barejid Jabber ID of the room
@param nick nickname of the message sender
@param message the received message
*/
* Called after a private chat room message is displayed
* @param barejid Jabber ID of the room
* @param nick Nickname of the message sender
* @param message The received message
*/
void prof_post_priv_message_display(const char * const barejid, const char * const nick, const char *message);
/**
Called before a private chat room message is sent
@param barejid Jabber ID of the room
@param nick nickname of message recipient
@param message the message to be sent
@return the modified or original message to send, or NULL to cancel sending of the message
*/
* Called before a private chat room message is sent
* @param barejid Jabber ID of the room
* @param nick Nickname of message recipient
* @param message The message to be sent
* @return The modified or original message to send, or NULL to cancel sending of the message
*/
char* prof_pre_priv_message_send(const char * const barejid, const char * const nick, const char *message);
/**
Called after a private chat room message has been sent
@param barejid Jabber ID of the room
@param nick nickname of the message recipient
@param message the sent message
*/
* Called after a private chat room message has been sent
* @param barejid Jabber ID of the room
* @param nick Nickname of the message recipient
* @param message The sent message
*/
void prof_post_priv_message_send(const char * const barejid, const char * const nick, const char *message);
/**
Called before an XMPP message stanza is sent
@param stanza The stanza to send
@return The new stanza to send, or NULL to preserve the original stanza
*/
* Called before an XMPP message stanza is sent
* @param stanza The stanza to send
* @return The new stanza to send, or NULL to preserve the original stanza
*/
char* prof_on_message_stanza_send(const char *const stanza);
/**
Called when an XMPP message stanza is received
@param stanza The stanza received
@return 1 if Profanity should continue to process the message stanza, 0 otherwise
*/
* Called when an XMPP message stanza is received
* @param stanza The stanza received
* @return 1 if CProof should continue to process the message stanza, 0 otherwise
*/
int prof_on_message_stanza_receive(const char *const stanza);
/**
Called before an XMPP presence stanza is sent
@param stanza The stanza to send
@return The new stanza to send, or NULL to preserve the original stanza
*/
* Called before an XMPP presence stanza is sent
* @param stanza The stanza to send
* @return The new stanza to send, or NULL to preserve the original stanza
*/
char* prof_on_presence_stanza_send(const char *const stanza);
/**
Called when an XMPP presence stanza is received
@param stanza The stanza received
@return 1 if Profanity should continue to process the presence stanza, 0 otherwise
*/
* Called when an XMPP presence stanza is received
* @param stanza The stanza received
* @return 1 if CProof should continue to process the presence stanza, 0 otherwise
*/
int prof_on_presence_stanza_receive(const char *const stanza);
/**
Called before an XMPP iq stanza is sent
@param stanza The stanza to send
@return The new stanza to send, or NULL to preserve the original stanza
*/
* Called before an XMPP iq stanza is sent
* @param stanza The stanza to send
* @return The new stanza to send, or NULL to preserve the original stanza
*/
char* prof_on_iq_stanza_send(const char *const stanza);
/**
Called when an XMPP iq stanza is received
@param stanza The stanza received
@return 1 if Profanity should continue to process the iq stanza, 0 otherwise
*/
* Called when an XMPP iq stanza is received
* @param stanza The stanza received
* @return 1 if CProof should continue to process the iq stanza, 0 otherwise
*/
int prof_on_iq_stanza_receive(const char *const stanza);
/**
Called when a contact goes offline
@param barejid Jabber ID of the contact
@param resource the resource being disconnected
@param status the status message received with the offline presence, or NULL
*/
* Called when a contact goes offline
* @param barejid Jabber ID of the contact
* @param resource The resource being disconnected
* @param status The status message received with the offline presence, or NULL
*/
void prof_on_contact_offline(const char *const barejid, const char *const resource, const char *const status);
/**
Called when a presence notification is received from a contact
@param barejid Jabber ID of the contact
@param resource the resource being disconnected
@param presence presence of the contact, one of "chat", "online", "away", "xa" or "dnd"
@param status the status message received with the presence, or NULL
@param priority the priority associated with the resource
*/
* Called when a presence notification is received from a contact
* @param barejid Jabber ID of the contact
* @param resource The resource being disconnected
* @param presence Presence of the contact, one of "chat", "online", "away", "xa" or "dnd"
* @param status The status message received with the presence, or NULL
* @param priority The priority associated with the resource
*/
void prof_on_contact_presence(const char *const barejid, const char *const resource, const char *const presence, const char *const status, const int priority);
/**
Called when a chat window is focused
@param barejid Jabber ID of the chat window recipient
*/
* Called when a chat window is focused
* @param barejid Jabber ID of the chat window recipient
*/
void prof_on_chat_win_focus(const char *const barejid);
/**
Called when a chat room window is focused
@param barejid Jabber ID of the room
*/
void prof_on_room_win_focus(const char *const barejid);
* Called when a chat room window is focused
* @param barejid Jabber ID of the room
*/
void prof_on_room_win_focus(const char *const barejid);

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
# Profanity Python Plugins API documentation build configuration file, created by
# sphinx-quickstart on Thu Mar 17 23:01:31 2016.
# CProof Python Plugins API documentation build configuration file.
#
# This file is execfile()d with the current directory set to its
# containing dir.
@@ -47,25 +46,25 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'Profanity Python Plugins API'
copyright = u'2016 - 2018, boothj5'
author = u'boothj5'
project = u'CProof Python Plugins API'
copyright = u'2025, CProof, based on Profanity by boothj5 et al.'
author = u'CProof Developers'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.5.0'
version = u'0.15.0'
# The full version, including alpha/beta/rc tags.
release = u'0.5.0'
release = u'0.15.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -75,7 +74,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ['_build', 'modules.rst']
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -122,7 +121,7 @@ html_theme = 'default'
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
html_title = "Profanity Python Plugins"
html_title = "CProof Python Plugins"
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
@@ -139,7 +138,7 @@ html_title = "Profanity Python Plugins"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@@ -202,7 +201,7 @@ html_show_copyright = False
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'ProfanityPythonPluginsAPIdoc'
htmlhelp_basename = 'CProofPythonPluginsAPIdoc'
# -- Options for LaTeX output ---------------------------------------------
@@ -224,8 +223,8 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'ProfanityPythonPluginsAPI.tex', u'Profanity Python Plugins API Documentation',
u'boothj5', 'manual'),
(master_doc, 'CProofPythonPluginsAPI.tex', u'CProof Python Plugins API Documentation',
u'CProof Developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -254,7 +253,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'profanitypythonpluginsapi', u'Profanity Python Plugins API Documentation',
(master_doc, 'cproofpythonpluginsapi', u'CProof Python Plugins API Documentation',
[author], 1)
]

View File

@@ -1,8 +1,8 @@
Profanity Python Plugins API
============================
CProof Python Plugins API
===========================
The ``prof`` module describes functions that plugins may call to interact with Profanity.
The ``plugin`` module describes functions that plugins may define to be notified of various events in Profanity.
The ``prof`` module describes functions that plugins may call to interact with CProof.
The ``plugin`` module describes functions that plugins may define to be notified of various events in CProof.
Contents:
@@ -11,4 +11,5 @@ Contents:
prof
plugin
* :ref:`genindex`
:ref:`genindex`

View File

@@ -3,12 +3,14 @@ The plugin module defines optional callback functions that CProof plugins can
implement to handle events such as startup, shutdown, message processing, and
presence updates.
::
All functions are optional and are called by CProof when the corresponding event
occurs. You need to define them in your module and they will automatically be used
for callback events.
To utilize full functionality, such as the ``prof.cons_show`` function, see ``prof``
documentation and use::
import prof
All functions are optional and are called by CProof when the corresponding event
occurs. String parameters and return values are Python 3 str types.
"""
# Initialization and Lifecycle
@@ -20,18 +22,13 @@ def prof_init(version: str, status: str, account_name: str | None, fulljid: str
Called when CProof starts or when the plugin is loaded via the ``/plugins load``
or ``/plugins install`` commands.
Args:
version: The version of CProof (e.g., ``"1.0.0"``).
status: The package status of CProof (``"development"`` or ``"release"``).
account_name: The account name of the logged-in user, or None if not logged in.
fulljid: The full Jabber ID (barejid/resource) of the logged-in user, or None if not logged in.
:param version: The version of CProof (e.g., ``"1.0.0"``).
:param status: The package status of CProof (``"development"`` or ``"release"``).
:param account_name: The account name of the logged-in user, or None if not logged in.
:param fulljid: The full Jabber ID (barejid/resource) of the logged-in user, or None if not logged in.
:return: None
Returns:
None
Example:
::
Example::
def prof_init(version: str, status: str, account_name: str | None, fulljid: str | None) -> None:
prof.cons_show(f"MyPlugin for CProof {version} ({status}) has been loaded, account: {account_name}")
@@ -43,15 +40,9 @@ def prof_on_start() -> None:
Use this to perform setup tasks that should occur at application startup.
Args:
None
:return: None
Returns:
None
Example:
::
Example::
def prof_on_start() -> None:
prof.cons_show("CProof has started...")
@@ -63,15 +54,9 @@ def prof_on_shutdown() -> None:
Use this to perform cleanup tasks before the application exits.
Args:
None
:return: None
Returns:
None
Example:
::
Example::
def prof_on_shutdown() -> None:
prof.cons_show("CProof is shutting down...")
@@ -83,15 +68,9 @@ def prof_on_unload() -> None:
Use this to clean up plugin-specific resources.
Args:
None
:return: None
Returns:
None
Example:
::
Example::
def prof_on_unload() -> None:
prof.cons_show("Plugin unloaded")
@@ -101,16 +80,11 @@ def prof_on_unload() -> None:
def prof_on_connect(account_name: str, fulljid: str) -> None:
"""Called when a user connects to CProof with an account.
Args:
account_name: The account name used for login.
fulljid: The full Jabber ID (barejid/resource) of the connected account.
:param account_name: The account name used for login.
:param fulljid: The full Jabber ID (barejid/resource) of the connected account.
:return: None
Returns:
None
Example:
::
Example::
def prof_on_connect(account_name: str, fulljid: str) -> None:
prof.cons_show(f"Connected as {account_name} ({fulljid})")
@@ -120,16 +94,11 @@ def prof_on_connect(account_name: str, fulljid: str) -> None:
def prof_on_disconnect(account_name: str, fulljid: str) -> None:
"""Called when a user disconnects an account from CProof.
Args:
account_name: The account name being disconnected.
fulljid: The full Jabber ID (barejid/resource) of the disconnected account.
:param account_name: The account name being disconnected.
:param fulljid: The full Jabber ID (barejid/resource) of the disconnected account.
:return: None
Returns:
None
Example:
::
Example::
def prof_on_disconnect(account_name: str, fulljid: str) -> None:
prof.cons_show(f"Disconnected {account_name} ({fulljid})")
@@ -144,17 +113,12 @@ def prof_pre_chat_message_display(barejid: str, resource: str, message: str) ->
Allows the plugin to modify or cancel the message display.
Args:
barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
resource: The sender's resource (e.g., ``laptop``).
message: The received message.
:param barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
:param resource: The sender's resource (e.g., ``laptop``).
:param message: The received message.
:return: The modified message to display, or None to preserve the original.
Returns:
str | None: The modified message to display, or None to preserve the original.
Example:
::
Example::
def prof_pre_chat_message_display(barejid: str, resource: str, message: str) -> str | None:
new_message = f"{message} (from {barejid})"
@@ -167,17 +131,12 @@ def prof_post_chat_message_display(barejid: str, resource: str, message: str) ->
Use this to perform actions after the message is shown.
Args:
barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
resource: The sender's resource (e.g., ``laptop``).
message: The displayed message.
:param barejid: The Jabber ID of the message sender (e.g., ``bob@example.com``).
:param resource: The sender's resource (e.g., ``laptop``).
:param message: The displayed message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_chat_message_display(barejid: str, resource: str, message: str) -> None:
prof.cons_show(f"Displayed message from {barejid}/{resource}: {message}")
@@ -189,16 +148,11 @@ def prof_pre_chat_message_send(barejid: str, message: str) -> str | None:
Allows the plugin to modify or cancel the message.
Args:
barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
message: The message to be sent.
:param barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
:param message: The message to be sent.
:return: The modified message to send, or None to cancel sending.
Returns:
str | None: The modified message to send, or None to cancel sending.
Example:
::
Example::
def prof_pre_chat_message_send(barejid: str, message: str) -> str | None:
return f"{message} (sent by plugin)"
@@ -210,16 +164,11 @@ def prof_post_chat_message_send(barejid: str, message: str) -> None:
Use this to log or react to sent messages.
Args:
barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
message: The sent message.
:param barejid: The Jabber ID of the recipient (e.g., ``bob@example.com``).
:param message: The sent message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_chat_message_send(barejid: str, message: str) -> None:
prof.cons_show(f"Sent to {barejid}: {message}")
@@ -234,17 +183,12 @@ def prof_pre_room_message_display(barejid: str, nick: str, message: str) -> str
Allows the plugin to modify or cancel the message display.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message sender.
message: The received message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message sender.
:param message: The received message.
:return: The modified message to display, or None to preserve the original.
Returns:
str | None: The modified message to display, or None to preserve the original.
Example:
::
Example::
def prof_pre_room_message_display(barejid: str, nick: str, message: str) -> str | None:
return f"{message} (from {nick})"
@@ -256,17 +200,12 @@ def prof_post_room_message_display(barejid: str, nick: str, message: str) -> Non
Use this to perform actions after the message is shown.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message sender.
message: The displayed message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message sender.
:param message: The displayed message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_room_message_display(barejid: str, nick: str, message: str) -> None:
prof.cons_show(f"Displayed in {barejid} from {nick}: {message}")
@@ -278,16 +217,11 @@ def prof_pre_room_message_send(barejid: str, message: str) -> str | None:
Allows the plugin to modify or cancel the message.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
message: The message to be sent.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param message: The message to be sent.
:return: The modified message to send, or None to cancel sending.
Returns:
str | None: The modified message to send, or None to cancel sending.
Example:
::
Example::
def prof_pre_room_message_send(barejid: str, message: str) -> str | None:
return f"{message} (sent by plugin)"
@@ -299,16 +233,11 @@ def prof_post_room_message_send(barejid: str, message: str) -> None:
Use this to log or react to sent messages.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
message: The sent message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param message: The sent message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_room_message_send(barejid: str, message: str) -> None:
prof.cons_show(f"Sent to {barejid}: {message}")
@@ -318,18 +247,13 @@ def prof_post_room_message_send(barejid: str, message: str) -> None:
def prof_on_room_history_message(barejid: str, nick: str, message: str, timestamp: str) -> None:
"""Called when a chat room history message is received from the server.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message sender.
message: The received message.
timestamp: The message's original send time in ISO 8601 format (e.g., ``2025-09-10T19:45:00Z``).
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message sender.
:param message: The received message.
:param timestamp: The message's original send time in ISO 8601 format (e.g., ``2025-09-10T19:45:00Z``).
:return: None
Returns:
None
Example:
::
Example::
def prof_on_room_history_message(barejid: str, nick: str, message: str, timestamp: str) -> None:
prof.cons_show(f"History in {barejid} from {nick} at {timestamp}: {message}")
@@ -344,17 +268,12 @@ def prof_pre_priv_message_display(barejid: str, nick: str, message: str) -> str
Allows the plugin to modify or cancel the message display.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message sender.
message: The received message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message sender.
:param message: The received message.
:return: The modified message to display, or None to preserve the original.
Returns:
str | None: The modified message to display, or None to preserve the original.
Example:
::
Example::
def prof_pre_priv_message_display(barejid: str, nick: str, message: str) -> str | None:
return f"{message} (private from {nick})"
@@ -366,17 +285,12 @@ def prof_post_priv_message_display(barejid: str, nick: str, message: str) -> Non
Use this to perform actions after the message is shown.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message sender.
message: The displayed message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message sender.
:param message: The displayed message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_priv_message_display(barejid: str, nick: str, message: str) -> None:
prof.cons_show(f"Displayed private in {barejid} from {nick}: {message}")
@@ -388,17 +302,12 @@ def prof_pre_priv_message_send(barejid: str, nick: str, message: str) -> str | N
Allows the plugin to modify or cancel the message.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message recipient.
message: The message to be sent.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message recipient.
:param message: The message to be sent.
:return: The modified message to send, or None to cancel sending.
Returns:
str | None: The modified message to send, or None to cancel sending.
Example:
::
Example::
def prof_pre_priv_message_send(barejid: str, nick: str, message: str) -> str | None:
return f"{message} (private to {nick})"
@@ -410,17 +319,12 @@ def prof_post_priv_message_send(barejid: str, nick: str, message: str) -> None:
Use this to log or react to sent messages.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
nick: The nickname of the message recipient.
message: The sent message.
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param nick: The nickname of the message recipient.
:param message: The sent message.
:return: None
Returns:
None
Example:
::
Example::
def prof_post_priv_message_send(barejid: str, nick: str, message: str) -> None:
prof.cons_show(f"Sent private to {nick} in {barejid}: {message}")
@@ -435,15 +339,10 @@ def prof_on_message_stanza_send(stanza: str) -> str | None:
Allows the plugin to modify or cancel the stanza.
Args:
stanza: The XMPP message stanza to send.
:param stanza: The XMPP message stanza to send.
:return: The modified stanza to send, or None to preserve the original.
Returns:
str | None: The modified stanza to send, or None to preserve the original.
Example:
::
Example::
def prof_on_message_stanza_send(stanza: str) -> str | None:
prof.cons_show(f"Sending message stanza: {stanza}")
@@ -456,15 +355,10 @@ def prof_on_message_stanza_receive(stanza: str) -> bool:
Allows the plugin to control whether CProof processes the stanza.
Args:
stanza: The received XMPP message stanza.
:param stanza: The received XMPP message stanza.
:return: True to allow CProof to process the stanza, False to block processing.
Returns:
bool: True to allow CProof to process the stanza, False to block processing.
Example:
::
Example::
def prof_on_message_stanza_receive(stanza: str) -> bool:
prof.cons_show(f"Received message stanza: {stanza}")
@@ -477,15 +371,10 @@ def prof_on_presence_stanza_send(stanza: str) -> str | None:
Allows the plugin to modify or cancel the stanza.
Args:
stanza: The XMPP presence stanza to send.
:param stanza: The XMPP presence stanza to send.
:return: The modified stanza to send, or None to preserve the original.
Returns:
str | None: The modified stanza to send, or None to preserve the original.
Example:
::
Example::
def prof_on_presence_stanza_send(stanza: str) -> str | None:
prof.cons_show(f"Sending presence stanza: {stanza}")
@@ -498,15 +387,10 @@ def prof_on_presence_stanza_receive(stanza: str) -> bool:
Allows the plugin to control whether CProof processes the stanza.
Args:
stanza: The received XMPP presence stanza.
:param stanza: The received XMPP presence stanza.
:return: True to allow CProof to process the stanza, False to block processing.
Returns:
bool: True to allow CProof to process the stanza, False to block processing.
Example:
::
Example::
def prof_on_presence_stanza_receive(stanza: str) -> bool:
prof.cons_show(f"Received presence stanza: {stanza}")
@@ -519,15 +403,10 @@ def prof_on_iq_stanza_send(stanza: str) -> str | None:
Allows the plugin to modify or cancel the stanza.
Args:
stanza: The XMPP IQ stanza to send.
:param stanza: The XMPP IQ stanza to send.
:return: The modified stanza to send, or None to preserve the original.
Returns:
str | None: The modified stanza to send, or None to preserve the original.
Example:
::
Example::
def prof_on_iq_stanza_send(stanza: str) -> str | None:
prof.cons_show(f"Sending IQ stanza: {stanza}")
@@ -540,15 +419,10 @@ def prof_on_iq_stanza_receive(stanza: str) -> bool:
Allows the plugin to control whether CProof processes the stanza.
Args:
stanza: The received XMPP IQ stanza.
:param stanza: The received XMPP IQ stanza.
:return: True to allow CProof to process the stanza, False to block processing.
Returns:
bool: True to allow CProof to process the stanza, False to block processing.
Example:
::
Example::
def prof_on_iq_stanza_receive(stanza: str) -> bool:
prof.cons_show(f"Received IQ stanza: {stanza}")
@@ -562,17 +436,12 @@ def prof_on_iq_stanza_receive(stanza: str) -> bool:
def prof_on_contact_offline(barejid: str, resource: str, status: str | None) -> None:
"""Called when a contact goes offline.
Args:
barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
resource: The resource being disconnected (e.g., ``laptop``).
status: The status message received with the offline presence, or None.
:param barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
:param resource: The resource being disconnected (e.g., ``laptop``).
:param status: The status message received with the offline presence, or None.
:return: None
Returns:
None
Example:
::
Example::
def prof_on_contact_offline(barejid: str, resource: str, status: str | None) -> None:
prof.cons_show(f"{barejid}/{resource} went offline: {status or 'No status'}")
@@ -582,19 +451,14 @@ def prof_on_contact_offline(barejid: str, resource: str, status: str | None) ->
def prof_on_contact_presence(barejid: str, resource: str, presence: str, status: str | None, priority: int) -> None:
"""Called when a presence notification is received from a contact.
Args:
barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
resource: The resource of the contact (e.g., ``laptop``).
presence: The contact's presence (``"chat"``, ``"online"``, ``"away"``, ``"xa"``, or ``"dnd"``).
status: The status message received with the presence, or None.
priority: The priority associated with the resource.
:param barejid: The Jabber ID of the contact (e.g., ``bob@example.com``).
:param resource: The resource of the contact (e.g., ``laptop``).
:param presence: The contact's presence (``"chat"``, ``"online"``, ``"away"``, ``"xa"``, or ``"dnd"``).
:param status: The status message received with the presence, or None.
:param priority: The priority associated with the resource.
:return: None
Returns:
None
Example:
::
Example::
def prof_on_contact_presence(barejid: str, resource: str, presence: str, status: str | None, priority: int) -> None:
prof.notify(f"{barejid} is {presence}", 5000, "Presence")
@@ -607,15 +471,10 @@ def prof_on_contact_presence(barejid: str, resource: str, presence: str, status:
def prof_on_chat_win_focus(barejid: str) -> None:
"""Called when a chat window is focused.
Args:
barejid: The Jabber ID of the chat window recipient (e.g., ``bob@example.com``).
:param barejid: The Jabber ID of the chat window recipient (e.g., ``bob@example.com``).
:return: None
Returns:
None
Example:
::
Example::
def prof_on_chat_win_focus(barejid: str) -> None:
prof.cons_show(f"Focused chat window for {barejid}")
@@ -625,17 +484,12 @@ def prof_on_chat_win_focus(barejid: str) -> None:
def prof_on_room_win_focus(barejid: str) -> None:
"""Called when a chat room window is focused.
Args:
barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:param barejid: The Jabber ID of the room (e.g., ``chat@conference.example.com``).
:return: None
Returns:
None
Example:
::
Example::
def prof_on_room_win_focus(barejid: str) -> None:
prof.cons_show(f"Focused room window for {barejid}")
"""
pass
pass

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,79 @@ error_handler()
trap error_handler ERR
# 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)"
echo "This means the test framework is NOT detecting failures correctly!"
echo "--- Test output ---"
cat /tmp/test_must_fail.log
echo "--- End output ---"
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"
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
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()
{
# Check for cores, for systems with:
@@ -34,6 +107,9 @@ num_cores()
|| getconf _NPROCESSORS_ONLN 2>/dev/null
}
# Run test failure detection verification first
verify_test_failure_detection
./bootstrap.sh
tests=()
@@ -44,49 +120,39 @@ ARCH="$(uname | tr '[:upper:]' '[:lower:]')"
case "$ARCH" in
linux*)
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins --with-xscreensaver --enable-omemo-qrcode --enable-gdk-pixbuf"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins --without-xscreensaver"
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo --disable-omemo-qrcode"
"--disable-pgp --disable-otr"
"--disable-pgp --disable-otr --disable-omemo"
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"--without-xscreensaver"
"--disable-gdk-pixbuf"
"")
--disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode"
# 4. Default configuration
""
)
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
;;
darwin*)
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins"
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo"
"--disable-pgp --disable-otr"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-pgp --disable-otr --disable-omemo"
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"")
# 4. Default configuration
""
)
;;
openbsd*)
MAKE="gmake"
@@ -96,36 +162,32 @@ case "$ARCH" in
# 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"
# 4 configurations for parallel CI
tests=(
# 1. Full build (all features enabled)
"--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp
--enable-omemo --enable-plugins --enable-c-plugins
--enable-python-plugins"
# 2. Minimal build (all optional features disabled)
"--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp
--disable-omemo --disable-plugins --disable-c-plugins
--disable-python-plugins"
"--disable-notifications"
"--disable-icons-and-clipboard"
"--disable-otr"
"--disable-pgp"
"--disable-omemo"
"--disable-pgp --disable-otr"
# 3. No encryption (disable otr, pgp, omemo)
"--disable-pgp --disable-otr --disable-omemo"
"--disable-plugins"
"--disable-python-plugins"
"--disable-c-plugins"
"--disable-c-plugins --disable-python-plugins"
"")
# 4. Default configuration
""
)
;;
esac
case "$ARCH" in
linux*)
echo
echo "--> Building with ./configure ${tests[0]} --enable-valgrind $*"
echo "--> Building with ./configure -C ${tests[0]} --enable-valgrind $*"
echo
# shellcheck disable=SC2086
./configure ${tests[0]} --enable-valgrind $*
./configure -C ${tests[0]} --enable-valgrind $*
$MAKE CC="${CC}"
if grep '^ID=' /etc/os-release | grep -q -e debian; then
@@ -137,18 +199,73 @@ case "$ARCH" in
;;
esac
for features in "${tests[@]}"
do
echo
echo "--> Building with ./configure ${features} $*"
echo
# Function to build and test a single configuration
build_and_test() {
local features="$1"
local extra_args="$2"
local idx="$3"
local build_dir="build-$idx"
local log_file="build-$idx.log"
# shellcheck disable=SC2086
./configure $features $*
{
echo "=== Build $idx started at $(date) ==="
echo "--> Building in $build_dir with ./configure -C $features $extra_args"
$MAKE CC="${CC}"
$MAKE check
mkdir -p "$build_dir"
cd "$build_dir"
./profanity -v
$MAKE clean
# shellcheck disable=SC2086
../configure -C $features $extra_args
$MAKE CC="${CC}"
$MAKE check-functional-parallel
./profanity -v
$MAKE clean
cd ..
rm -rf "$build_dir"
echo "=== Build $idx completed at $(date) ==="
} > "$log_file" 2>&1
}
# Run all 4 configurations in parallel
echo "Starting parallel builds..."
pids=()
for idx in 1 2 3 4; do
if [ $idx -le ${#tests[@]} ]; then
build_and_test "${tests[$((idx-1))]}" "$*" "$idx" &
pids+=("$!")
echo "Started build $idx (PID: $!)"
fi
done
# Wait for all builds and check exit codes
failed=0
for i in "${!pids[@]}"; do
idx=$((i + 1))
if wait "${pids[$i]}"; then
echo "✓ Build $idx passed"
else
echo "✗ Build $idx failed"
echo "--- Log for build $idx ---"
cat "build-$idx.log"
echo "--- End log ---"
failed=1
fi
done
# Show all logs on success too
if [ $failed -eq 0 ]; then
echo
echo "All builds passed!"
for idx in 1 2 3 4; do
if [ -f "build-$idx.log" ]; then
echo "--- Log for build $idx ---"
cat "build-$idx.log"
fi
done
else
exit 1
fi

View File

@@ -69,6 +69,8 @@ AC_ARG_ENABLE([gdk-pixbuf],
[AS_HELP_STRING([--enable-gdk-pixbuf], [enable GDK Pixbuf support to scale avatars before uploading])])
AC_ARG_ENABLE([omemo-qrcode],
[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])
AX_VALGRIND_DFLT([drd], [off])
@@ -374,14 +376,23 @@ PKG_CHECK_MODULES([cmocka], [cmocka], [],
AM_CONDITIONAL([HAVE_STABBER], [false])
AC_CHECK_LIB([stabber], [stbbr_start], [AM_CONDITIONAL([HAVE_STABBER], [true])],
[AC_MSG_NOTICE([libstabber not found, will not be able to run functional tests])])
AM_CONDITIONAL([HAVE_EXPECT], [false])
AC_CHECK_LIB([expect], [exp_expectl], [AM_CONDITIONAL([HAVE_EXPECT], [true])],
[AC_MSG_NOTICE([libexpect not found, will not be able to run functional tests])])
dnl Check for forkpty (needed for functional tests PTY handling)
dnl On Linux it's in libutil, on some BSDs it's in libc
AM_CONDITIONAL([HAVE_FORKPTY], [false])
AC_CHECK_LIB([util], [forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB="-lutil"],
[AC_CHECK_FUNC([forkpty], [AM_CONDITIONAL([HAVE_FORKPTY], [true]) FORKPTY_LIB=""],
[AC_MSG_NOTICE([forkpty not found, will not be able to run functional tests])])])
AC_SUBST([FORKPTY_LIB])
## Default parameters
AM_CFLAGS="$AM_CFLAGS -Wall -Wno-deprecated-declarations -std=gnu99 -ggdb3"
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],
[AM_CFLAGS="$AM_CFLAGS -Wunused -Werror"])
AS_IF([test "x$PLATFORM" = xosx],

358
prof.supp
View File

@@ -8,6 +8,83 @@
# * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
#
# ============================================
# Functional tests suppressions (stabber/pthread)
# ============================================
{
stabber_pthread_create
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:allocate_dtv
fun:_dl_allocate_tls
...
fun:pthread_create*
...
fun:server_run
...
}
{
stabber_server_run
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:pthread_create*
...
obj:*/libstabber*
...
}
{
glib_time_zone_cache
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_time_zone_new*
...
}
{
glib_time_zone_local
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_strdup
...
fun:g_time_zone_new_identifier
fun:g_time_zone_new_local
...
}
{
glib_date_time_format
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_date_time_format
...
}
{
glib_date_time_format_locale
Memcheck:Leak
match-leak-kinds: reachable
fun:*alloc
...
obj:*/libglib*
...
fun:g_date_time_format
...
}
# ============================================
# Original suppressions
# ============================================
{
_dl_init
Memcheck:Leak
@@ -2703,3 +2780,284 @@
fun:calloc
fun:_dl_allocate_tls
}
# pthread TLS allocation in stabber server threads
{
pthread_create_tls_stabber
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
...
fun:allocate_dtv
fun:_dl_allocate_tls
fun:allocate_stack
fun:pthread_create*
fun:server_run
}
# expect/tcl library allocations
{
tcl_alloc_expect
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:TclpAlloc
fun:Tcl_Alloc
...
fun:exp_expectl
}
{
exp_printify
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
...
fun:exp_printify
...
fun:exp_expectl
}
# Additional suppressions for functional tests
# libexpect still reachable allocations
{
exp_spawnv_malloc
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:exp_spawnv
fun:exp_spawnl
}
{
exp_spawnl_realloc
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
...
fun:exp_spawnv
fun:exp_spawnl
}
# libtcl memory pool allocations (expected)
{
tcl_alloc_pool
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:TclpAlloc
fun:Tcl_Alloc
}
{
tcl_alloc_pool_calloc
Memcheck:Leak
match-leak-kinds: reachable
fun:calloc
...
fun:TclpAlloc
fun:Tcl_Alloc
}
# pthread thread-local storage (normal for multi-threaded programs)
{
pthread_tls_allocate_dtv
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
fun:calloc
fun:allocate_dtv
fun:_dl_allocate_tls
fun:allocate_stack
fun:pthread_create*
}
# glib static initializations
{
glib_hash_table_init
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
fun:g_malloc
fun:g_hash_table_new_full
...
fun:call_init
fun:_dl_init
}
{
glib_array_init
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
fun:g_realloc
...
fun:call_init
fun:_dl_init
}
# fdopen/fopen allocations (FILE* buffers - normal)
{
fdopen_file_buffer
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:fdopen*
}
{
fopen_file_buffer
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:fopen*
}
# stabber log_init (server log file)
{
stabber_log_init
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:log_init
fun:server_run
}
# glib time zone (static allocation)
{
g_time_zone_new_local
Memcheck:Leak
match-leak-kinds: reachable
...
fun:g_time_zone_new_local
}
{
g_time_zone_array
Memcheck:Leak
match-leak-kinds: reachable
fun:realloc
fun:g_realloc
...
fun:g_array_sized_new
fun:g_time_zone_new_identifier
}
# stabber log g_date_time_format invalid read - benign race in logging
{
stabber_g_date_time_format_invalid_read
Memcheck:Addr1
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read2
Memcheck:Addr2
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read4
Memcheck:Addr4
...
fun:g_date_time_format
fun:log_println
...
}
{
stabber_g_date_time_format_invalid_read8
Memcheck:Addr8
...
fun:g_date_time_format
fun:log_println
...
}
# More generic suppression for g_date_time_format race condition
{
g_date_time_format_cond
Memcheck:Cond
...
fun:g_date_time_format
...
}
{
g_date_time_format_value1
Memcheck:Value1
...
fun:g_date_time_format
...
}
{
g_date_time_format_value2
Memcheck:Value2
...
fun:g_date_time_format
...
}
{
g_date_time_format_value4
Memcheck:Value4
...
fun:g_date_time_format
...
}
{
g_date_time_format_value8
Memcheck:Value8
...
fun:g_date_time_format
...
}
# Suppress all Addr errors in log_println
{
log_println_addr1
Memcheck:Addr1
...
fun:log_println
...
}
{
log_println_addr2
Memcheck:Addr2
...
fun:log_println
...
}
{
log_println_addr4
Memcheck:Addr4
...
fun:log_println
...
}
{
log_println_addr8
Memcheck:Addr8
...
fun:log_println
...
}

View File

@@ -139,8 +139,9 @@ static const struct cmd_t command_defs[] = {
"Show version and license information.")
},
// Max args: account + server <s> + port <p> + tls <t> + auth <a> = 9
{ CMD_PREAMBLE("/connect",
parse_args, 0, 7, NULL)
parse_args, 0, 9, NULL)
CMD_MAINFUNC(cmd_connect)
CMD_TAGS(
CMD_TAG_CONNECTION)
@@ -1902,9 +1903,9 @@ static const struct cmd_t command_defs[] = {
CMD_ARGS(
{ "where", "Show the current log file location." },
{ "rotate on|off", "Rotate log, default on. Does not take effect if you specified a filename yourself when starting Profanity." },
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MB)." },
{ "maxsize <bytes>", "With rotate enabled, specifies the max log size, defaults to 10485760 (10MiB)." },
{ "shared on|off", "Share logs between all instances, default: on. When off, the process id will be included in the log filename. Does not take effect if you specified a filename yourself when starting Profanity." },
{"level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO. Only works with default log file, not with user provided log file during startup via -f." })
{ "level INFO|DEBUG|WARN|ERROR", "Set the log level. Default is INFO." })
},
{ CMD_PREAMBLE("/carbons",

View File

@@ -5356,6 +5356,8 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
cons_bad_cmd_usage(command);
return TRUE;
}
if (!set_all)
break;
}
if (!set_all && n == ARRAY_SIZE(time_prefs)) {
cons_bad_cmd_usage(command);
@@ -6271,10 +6273,11 @@ cmd_log(ProfWin* window, const char* const command, gchar** args)
if (strcmp(subcmd, "level") == 0) {
log_level_t prof_log_level;
if (log_level_from_string(value, &prof_log_level) == 0) {
auto_char char* log_file = strdup(get_log_file_location());
log_close();
log_init(prof_log_level, NULL);
log_init(prof_log_level, log_file);
cons_show("Log level changed to: %s.", value);
cons_show("Log level changed to: %s (log file: %s).", value, log_file ? log_file : "[default]");
return TRUE;
}
}

View File

@@ -186,7 +186,7 @@ log_error(const char* const msg, ...)
}
void
log_init(log_level_t filter, char* log_file)
log_init(log_level_t filter, const char* const log_file)
{
level_filter = filter;

View File

@@ -50,7 +50,7 @@ typedef enum {
PROF_LEVEL_ERROR
} log_level_t;
void log_init(log_level_t filter, char* log_file);
void log_init(log_level_t filter, const char* const log_file);
log_level_t log_get_filter(void);
void log_close(void);
const gchar* get_log_file_location(void);

View File

@@ -54,6 +54,7 @@
#include "plugins/settings.h"
#include "plugins/disco.h"
#include "ui/ui.h"
#include "ui/win_types.h"
#include "ui/window_list.h"
#include "xmpp/roster_list.h"
@@ -205,6 +206,13 @@ api_get_current_recipient(void)
}
}
gchar*
api_get_current_window(void)
{
ProfWin* current = wins_get_current();
return win_get_title(current);
}
char*
api_get_current_muc(void)
{

View File

@@ -46,6 +46,7 @@ void api_notify(const char* message, const char* category, int timeout_ms);
void api_send_line(char* line);
char* api_get_current_recipient(void);
gchar* api_get_current_window(void);
char* api_get_current_muc(void);
gboolean api_current_win_is_console(void);
char* api_get_current_nick(void);

View File

@@ -165,6 +165,12 @@ c_api_get_current_recipient(void)
return api_get_current_recipient();
}
static char*
c_api_get_current_window(void)
{
return api_get_current_window();
}
static char*
c_api_get_current_muc(void)
{
@@ -477,6 +483,7 @@ c_api_init(void)
prof_notify = c_api_notify;
prof_send_line = c_api_send_line;
prof_get_current_recipient = c_api_get_current_recipient;
prof_get_current_window = c_api_get_current_window;
prof_get_current_muc = c_api_get_current_muc;
prof_current_win_is_console = c_api_current_win_is_console;
prof_get_current_nick = c_api_get_current_nick;

View File

@@ -62,6 +62,7 @@ void (*prof_notify)(const char* message, int timeout_ms, const char* category) =
void (*prof_send_line)(char* line) = NULL;
char* (*prof_get_current_recipient)(void) = NULL;
char* (*prof_get_current_window)(void) = NULL;
char* (*prof_get_current_muc)(void) = NULL;
int (*prof_current_win_is_console)(void) = NULL;
char* (*prof_get_current_nick)(void) = NULL;

View File

@@ -71,6 +71,7 @@ void (*prof_notify)(const char* message, int timeout_ms, const char* category);
void (*prof_send_line)(char* line);
char* (*prof_get_current_recipient)(void);
char* (*prof_get_current_window)(void);
char* (*prof_get_current_muc)(void);
int (*prof_current_win_is_console)(void);
char* (*prof_get_current_nick)(void);

View File

@@ -415,6 +415,15 @@ python_api_get_current_recipient(PyObject* self, PyObject* args)
}
}
static PyObject*
python_api_get_current_window(PyObject* self, PyObject* args)
{
allow_python_threads();
auto_gchar gchar* recipient = api_get_current_window();
disable_python_threads();
return Py_BuildValue("s", recipient);
}
static PyObject*
python_api_get_current_muc(PyObject* self, PyObject* args)
{
@@ -1530,6 +1539,7 @@ static PyMethodDef apiMethods[] = {
{ "send_line", python_api_send_line, METH_VARARGS, "Send a line of input." },
{ "notify", python_api_notify, METH_VARARGS, "Send desktop notification." },
{ "get_current_recipient", python_api_get_current_recipient, METH_VARARGS, "Return the jid of the recipient of the current window." },
{ "get_current_window", python_api_get_current_window, METH_VARARGS, "Return title of the current window." },
{ "get_current_muc", python_api_get_current_muc, METH_VARARGS, "Return the jid of the room of the current window." },
{ "get_current_nick", python_api_get_current_nick, METH_VARARGS, "Return nickname in current room." },
{ "get_name_from_roster", python_api_get_name_from_roster, METH_VARARGS, "Return nickname in roster of barejid." },

View File

@@ -1,40 +1,36 @@
/*
* buffer.c
* vim: expandtab:ts=4:sts=4:sw=4
*
* Message buffer implementation for CProof.
*
* This module provides an in-memory buffer for managing active chat entries in the
* console-based XMPP client. Separate from persistent SQLite storage, it holds
* messages and metadata solely for UI rendering, ensuring responsive scrolling and
* display without exceeding ncurses pad limits (10k lines). By tracking rendered
* line counts per entry, it proactively trims content to prevent overflow, enabling
* seamless integration with ncurses for position-aware rendering.
*
* Key features and operations:
* - Append/prepend entries (messages) with automatic line-based cleanup.
* - Track cumulative lines for overflow prevention and efficient buffer sizing.
* - Remove entries by ID or index; mark delivery receipts as received.
* - Retrieve entries by index or ID for rendering and history queries.
* - Store metadata including timestamps, senders, themes, and ncurses y-positions.
*
* CProof. Fork of Profanity (since 2025).
*
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
* Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>
* Copyright (C) 2025 CProof Developers
*
* This file is part of Profanity.
*
* Profanity is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Profanity is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give permission to
* link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two.
*
* You must obey the GNU General Public License in all respects for all of the
* code used other than OpenSSL. If you modify file(s) with this exception, you
* may extend this exception to your version of the file(s), but you are not
* obligated to do so. If you do not wish to do so, delete this exception
* statement from your version. If you delete this exception statement from all
* source files in the program, then also delete it here.
* Licensed under the GNU General Public License, version 3,
* with the OpenSSL exception. See LICENSE for details.
*
* vim: expandtab:ts=4:sts=4:sw=4
*/
#include "config.h"
#include "ui/window_list.h"
#include <stdlib.h>
#include <string.h>
@@ -54,15 +50,8 @@
#include "ui/window.h"
#include "ui/buffer.h"
#define MAX_BUFFER_SIZE 200
#define STRDUP_OR_NULL(str) ((str) ? strdup(str) : NULL)
struct prof_buff_t
{
GSList* entries;
int lines;
};
static void _free_entry(ProfBuffEntry* entry);
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);
static void _buffer_add(ProfBuff buffer, 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, gboolean append);
@@ -108,23 +97,24 @@ _buffer_add(ProfBuff buffer, const char* show_char, int pad_indent, GDateTime* t
buffer->lines += e->_lines;
while (g_slist_length(buffer->entries) >= MAX_BUFFER_SIZE) {
// With 10% margin to ensure overflow prevention
while (buffer->lines >= PAD_SIZE - (PAD_SIZE / 10) && g_slist_length(buffer->entries) > 1) {
// Delete message from the opposite size to free buffer
GSList* buffer_entry_to_delete = append ? buffer->entries : g_slist_last(buffer->entries);
ProfBuffEntry* entry_to_delete = (ProfBuffEntry*)buffer_entry_to_delete->data;
// log_debug("(Messages left in buffer: %d) DELETING: %s", g_slist_length(buffer->entries), entry_to_delete->id);
buffer->lines -= entry_to_delete->_lines;
_free_entry(entry_to_delete);
buffer->entries = g_slist_delete_link(buffer->entries, buffer_entry_to_delete);
}
if (from_jid && y_end_pos == y_start_pos) {
log_warning("Ncurses Overflow! From: %s, position: %d, ID: %s, append: %s, used message buffer size: %d, message buffer size: %d, rendered lines buffer size: %d",
from_jid, y_start_pos, id, append ? "TRUE" : "FALSE", g_slist_length(buffer->entries), MAX_BUFFER_SIZE, PAD_SIZE);
// At this point we have a message that caused overflow of the render buffer.
// Ideally, we want to clean other messages to rerender everything properly. To do so,
// first we need to determine the size of the message that we are trying to display,
// then we need to print the message.
// However, _buffer_add is too late in the code to do this, therefore it has to be done in the win_print_old_history.
// win_redraw will redraw with entries above removed,
// it will also recalculate actual size of the message that caused overflow
int old_lines = buffer->lines;
win_redraw(wins_get_current());
log_debug("Ncurses Overflow! From: %s, position: %d, ID: %s, append: %s, used message buffer size: %d, rendered lines: (old: %d/ actual: %d/ max: %d)",
from_jid, y_start_pos, id, append ? "TRUE" : "FALSE", g_slist_length(buffer->entries), old_lines, buffer->lines, PAD_SIZE);
}
buffer->entries = append ? g_slist_append(buffer->entries, e) : g_slist_prepend(buffer->entries, e);

View File

@@ -68,6 +68,12 @@ typedef struct prof_buff_entry_t
char* id;
} ProfBuffEntry;
struct prof_buff_t
{
GSList* entries;
int lines;
};
typedef struct prof_buff_t* ProfBuff;
ProfBuff buffer_create();

View File

@@ -268,7 +268,11 @@ _show_scrolled(ProfWin* current)
wattroff(win, bracket_attrs);
wattron(win, scrolled_attrs);
wprintw(win, "SCROLLED");
if (current->layout->unread_msg == 0) {
wprintw(win, "SCROLLED");
} else {
wprintw(win, "SCROLLED, NEW MESSAGES");
}
wattroff(win, scrolled_attrs);
wattron(win, bracket_attrs);

View File

@@ -120,6 +120,7 @@ typedef struct prof_layout_t
ProfBuff buffer;
int y_pos;
int paged;
int unread_msg;
} ProfLayout;
typedef struct prof_layout_simple_t

View File

@@ -103,6 +103,7 @@ _win_create_simple_layout(void)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
return &layout->base;
@@ -120,6 +121,7 @@ _win_create_split_layout(void)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
layout->subwin = NULL;
layout->sub_y_pos = 0;
@@ -197,6 +199,7 @@ win_create_muc(const char* const roomjid)
layout->base.buffer = buffer_create();
layout->base.y_pos = 0;
layout->base.paged = 0;
layout->base.unread_msg = 0;
scrollok(layout->base.win, TRUE);
new_win->window.layout = (ProfLayout*)layout;
@@ -698,9 +701,11 @@ void
win_page_down(ProfWin* window, int scroll_size)
{
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_space = getmaxy(stdscr) - 4;
int page_start_initial = *page_start;
if (scroll_size == 0)
scroll_size = page_space;
win_scroll_state_t* scroll_state = &window->scroll_state;
@@ -709,7 +714,11 @@ win_page_down(ProfWin* window, int scroll_size)
*page_start += scroll_size;
// Scrolled down after reaching the bottom of the page
if ((*page_start > total_rows - page_space || (*page_start == page_space && *page_start >= total_rows)) && window->type == WIN_CHAT) {
gboolean past_bottom = *page_start > total_rows_with_unread - page_space;
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);
if (bf_size > 0 && *scroll_state != WIN_SCROLL_REACHED_BOTTOM) {
// How many lines are left until end of the screen
@@ -743,13 +752,16 @@ win_page_down(ProfWin* window, int scroll_size)
window->layout->paged = 1;
// update only if position has changed
if (page_start_initial != *page_start) {
if ((page_start_initial != *page_start) || window->layout->unread_msg) {
win_update_virtual(window);
}
// switch off page if last line and space line visible
if (total_rows - *page_start == page_space) {
/* Switch off page if no messages left to read.
* TODO: update buffer end handling to check messages just after last entry.
*/
if (*scroll_state == WIN_SCROLL_REACHED_BOTTOM) {
window->layout->paged = 0;
window->layout->unread_msg = 0;
}
}
@@ -810,6 +822,7 @@ win_clear(ProfWin* window)
int* page_start = &(window->layout->y_pos);
*page_start = y;
window->layout->paged = 1;
window->layout->unread_msg = 0;
win_update_virtual(window);
}
@@ -914,6 +927,7 @@ void
win_move_to_end(ProfWin* window)
{
window->layout->paged = 0;
window->layout->unread_msg = 0;
int rows = getmaxy(stdscr);
int y = getcury(window->layout->win);
@@ -1696,6 +1710,13 @@ win_newline(ProfWin* window)
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, ...)
{
/* 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) {
timestamp = g_date_time_new_now_local();
} else {
@@ -2003,6 +2024,10 @@ win_redraw(ProfWin* window)
_win_print_internal(window, e->show_char, e->pad_indent, e->time, e->flags, e->theme_item, e->display_from, e->message, e->receipt);
}
e->y_end_pos = getcury(window->layout->win);
// Recalculate lines (might be incorrect in case of NCurses overflow)
window->layout->buffer->lines -= e->_lines;
e->_lines = e->y_end_pos - e->y_start_pos;
window->layout->buffer->lines += e->_lines;
}
}

View File

@@ -312,6 +312,7 @@ iq_reg2_cb(xmpp_conn_t* xmpp_conn, xmpp_stanza_t* stanza, void* userdata)
goto quit;
quit:
log_debug("[CONNDBG] iq_reg2_cb: disconnecting after registration completion");
xmpp_disconnect(xmpp_conn);
return 0;
@@ -550,6 +551,7 @@ connection_disconnect(void)
// don't disconnect already disconnected connection,
// or we get infinite loop otherwise
if (conn.conn_last_event == XMPP_CONN_CONNECT) {
log_debug("[CONNDBG] connection_disconnect: user-initiated disconnect (status=%d)", (int)conn.conn_status);
conn.conn_status = JABBER_DISCONNECTING;
xmpp_disconnect(conn.xmpp_conn);
@@ -557,6 +559,7 @@ connection_disconnect(void)
session_process_events();
}
} else {
log_debug("[CONNDBG] connection_disconnect: already disconnected (last_event=%d)", (int)conn.conn_last_event);
conn.conn_status = JABBER_DISCONNECTED;
}
@@ -979,6 +982,7 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
connection_get_jid();
conn.domain = strdup(conn.jid->domainpart);
log_debug("conn.domain=%s", conn.jid->domainpart);
connection_clear_data();
conn.features_by_jid = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)g_hash_table_destroy);
@@ -1018,6 +1022,9 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
// disconnected
case XMPP_CONN_DISCONNECT:
log_debug("Connection handler: XMPP_CONN_DISCONNECT");
log_debug("[CONNDBG] disconnect: previous_status=%d error=%d has_stream_error=%s",
(int)conn.conn_status, error,
(stream_error && stream_error->stanza) ? "yes" : "no");
// lost connection for unknown reason
if (conn.conn_status == JABBER_CONNECTED || conn.conn_status == JABBER_DISCONNECTING) {
@@ -1163,3 +1170,32 @@ connection_get_profanity_identifier(void)
{
return prof_identifier;
}
void
connection_debug_print_features()
{
log_debug("=== Connection Features ===");
GHashTableIter iter;
gpointer jid_ptr, features_ptr;
g_hash_table_iter_init(&iter, conn.features_by_jid);
while (g_hash_table_iter_next(&iter, &jid_ptr, &features_ptr)) {
const char* jid = (const char*)jid_ptr;
GHashTable* features = (GHashTable*)features_ptr;
if (!features || g_hash_table_size(features) == 0) {
log_debug("%s:\t(no features)", jid);
continue;
}
GList* feature_keys = g_hash_table_get_keys(features);
for (GList* l = feature_keys; l != NULL; l = l->next) {
const char* feature = (const char*)l->data;
log_debug("%s:\t%s", jid, feature);
}
g_list_free(feature_keys);
}
log_debug("=== End of Features ===");
}

View File

@@ -1417,6 +1417,8 @@ _manual_pong_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
return 0;
}
static gboolean autoping_error_shown = false;
static int
_autoping_timed_send(xmpp_conn_t* const conn, void* const userdata)
{
@@ -1425,10 +1427,13 @@ _autoping_timed_send(xmpp_conn_t* const conn, void* const userdata)
}
if (connection_supports(XMPP_FEATURE_PING) == FALSE) {
log_warning("Server doesn't advertise %s feature, disabling autoping.", XMPP_FEATURE_PING);
prefs_set_autoping(0);
cons_show_error("Server ping not supported (%s), autoping disabled.", XMPP_FEATURE_PING);
return 0;
// TODO: do we need to check it on each autoping call?
log_warning("Server doesn't advertise %s feature.", XMPP_FEATURE_PING);
if (!autoping_error_shown) {
connection_debug_print_features();
cons_show_error("Server ping not supported (%s). Check log for details.", XMPP_FEATURE_PING);
}
autoping_error_shown = 1;
}
if (autoping_wait) {
@@ -1499,7 +1504,7 @@ _auto_pong_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
if (g_strcmp0(errtype, STANZA_TYPE_CANCEL) == 0) {
log_warning("Server ping (id=%s) error type 'cancel', disabling autoping.", id);
prefs_set_autoping(0);
cons_show_error("Server ping not supported, autoping disabled.");
cons_show_error("Autoping is not supported by the server (stanza cancelled). The feature has been disabled automatically.");
xmpp_timed_handler_delete(connection_get_conn(), _autoping_timed_send);
}
@@ -2515,6 +2520,7 @@ _disco_items_result_handler(xmpp_stanza_t* const stanza)
GSList* items = NULL;
if ((g_strcmp0(id, "discoitemsreq") != 0) && (g_strcmp0(id, "discoitemsreq_onconnect") != 0)) {
log_warning("_disco_items_result_handler: Received unexpected disco id: %s", id);
return;
}

View File

@@ -218,6 +218,7 @@ session_connect_with_details(const char* const jid, const char* const passwd, co
void
session_autoping_fail(void)
{
log_debug("[CONNDBG] session_autoping_fail: autoping timeout, triggering lost connection");
session_lost_connection();
}
@@ -264,11 +265,14 @@ session_process_events(void)
if ((reconnect_sec != 0) && reconnect_timer) {
int elapsed_sec = g_timer_elapsed(reconnect_timer, NULL);
if (elapsed_sec > reconnect_sec) {
log_debug("[CONNDBG] session_process_events: auto-reconnect triggered after %d seconds (threshold=%d)",
elapsed_sec, reconnect_sec);
session_reconnect_now();
}
}
break;
case JABBER_RECONNECT:
log_debug("[CONNDBG] session_process_events: JABBER_RECONNECT state, calling session_reconnect_now");
session_reconnect_now();
break;
default:
@@ -327,12 +331,12 @@ session_login_success(gboolean secured)
// logged in with account
if (saved_account.name) {
log_debug("Connection handler: logged in with account name: %s", saved_account.name);
log_debug("[CONNDBG] Connection handler: logged in with account name: %s", saved_account.name);
sv_ev_login_account_success(saved_account.name, secured);
// logged in without account, use details to create new account
} else {
log_debug("Connection handler: logged in with jid: %s", saved_details.name);
log_debug("[CONNDBG] Connection handler: logged in with jid: %s", saved_details.name);
accounts_add(saved_details.name, saved_details.altdomain, saved_details.port, saved_details.tls_policy, saved_details.auth_policy);
accounts_set_jid(saved_details.name, saved_details.jid);
@@ -371,11 +375,11 @@ void
session_login_failed(void)
{
if (reconnect_timer == NULL) {
log_debug("Connection handler: No reconnect timer");
log_debug("[CONNDBG] Connection handler: No reconnect timer");
sv_ev_failed_login();
_session_free_internals();
} else {
log_debug("Connection handler: Restarting reconnect timer");
log_debug("[CONNDBG] Connection handler: Restarting reconnect timer");
if (prefs_get_reconnect() != 0) {
g_timer_start(reconnect_timer);
}
@@ -389,12 +393,16 @@ session_login_failed(void)
void
session_lost_connection(void)
{
log_debug("[CONNDBG] session_lost_connection: connection lost, reconnect_interval=%d",
prefs_get_reconnect());
/* this callback also clears all cached data */
sv_ev_lost_connection();
if (prefs_get_reconnect() != 0) {
assert(reconnect_timer == NULL);
reconnect_timer = g_timer_new();
log_debug("[CONNDBG] session_lost_connection: reconnect timer started");
} else {
log_debug("[CONNDBG] session_lost_connection: auto-reconnect disabled, cleaning up");
_session_free_internals();
}
}

View File

@@ -208,6 +208,8 @@ const char* connection_jid_for_feature(const char* const feature);
const char* connection_get_profanity_identifier(void);
void connection_debug_print_features();
char* message_send_chat(const char* const barejid, const char* const msg, const char* const oob_url, gboolean request_receipt, const char* const replace_id);
char* message_send_chat_otr(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id);
char* message_send_chat_pgp(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id);

View File

@@ -1,6 +1,41 @@
/*
* functionaltests.c
*
* Functional tests for CProof XMPP client.
* Uses cmocka framework with stabber mock XMPP server.
*
* Each test is wrapped with PROF_FUNC_TEST macro which sets up
* init_prof_test (starts stabber server and profanity client)
* and close_prof_test (cleanup) as setup/teardown functions.
*
* NOTE: We restart client and server for each test to ensure complete
* isolation. This prevents state leakage between tests (roster entries,
* MUC rooms, presence subscriptions, etc.). While slower, it eliminates
* flaky tests caused by leftover state. The overhead is acceptable since
* functional tests run less frequently than unit tests.
*
* Tests are organized into groups for better maintainability and parallel execution:
* Group 1: Connect, Ping, Rooms, Software (17 tests)
* Group 2: Message, Receipts, Roster, Chat Session (17 tests)
* Group 3: Presence (16 tests)
* Group 4: MUC, Carbons (19 tests)
*
* Parallel execution:
* ./functionaltests - run all tests sequentially
* ./functionaltests 1 - run group 1 only
* ./functionaltests 2 - run group 2 only
* ./functionaltests 3 - run group 3 only
* ./functionaltests 4 - run group 4 only
*
* For parallel execution, run multiple groups simultaneously:
* ./functionaltests 1 & ./functionaltests 2 & ./functionaltests 3 & ./functionaltests 4 & wait
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "prof_cmocka.h"
#include <sys/stat.h>
@@ -20,30 +55,89 @@
#include "test_muc.h"
#include "test_disconnect.h"
/* 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)
int main(int argc, char* argv[]) {
int
main(int argc, char* argv[])
{
int group = 0; /* 0 = all groups */
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;
}
}
const struct CMUnitTest all_tests[] = {
/* GROUP 1: Connect, Ping, Rooms, Software (17 tests)
* Basic XMPP session establishment and server queries */
/* Connection tests - verify login, roster, bookmarks */
const struct CMUnitTest group1_tests[] = {
PROF_FUNC_TEST(connect_jid_requests_roster),
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
PROF_FUNC_TEST(connect_jid_requests_bookmarks),
PROF_FUNC_TEST(connect_bad_password),
PROF_FUNC_TEST(connect_shows_presence_updates),
/* Ping tests - XEP-0199 XMPP Ping */
PROF_FUNC_TEST(ping_server),
PROF_FUNC_TEST(ping_server_not_supported),
PROF_FUNC_TEST(ping_responds_to_server_request),
PROF_FUNC_TEST(ping_jid),
PROF_FUNC_TEST(ping_jid_not_supported),
/* Room discovery - XEP-0045 */
PROF_FUNC_TEST(rooms_query),
PROF_FUNC_TEST(presence_away),
PROF_FUNC_TEST(presence_away_with_message),
/* 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),
};
/* GROUP 2: Message, Receipts, Roster, Chat Session (17 tests)
* Core messaging and contact management */
const struct CMUnitTest group2_tests[] = {
/* Basic message send/receive */
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
PROF_FUNC_TEST(message_receive_chatwin),
/* Message receipts - XEP-0184 */
PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
PROF_FUNC_TEST(send_receipt_request),
PROF_FUNC_TEST(send_receipt_on_request),
/* Roster management - add/remove/rename contacts */
PROF_FUNC_TEST(sends_new_item),
PROF_FUNC_TEST(sends_new_item_nick),
PROF_FUNC_TEST(sends_remove_item),
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
/* 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),
};
/* GROUP 3: Presence (16 tests)
* 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),
@@ -56,57 +150,67 @@ int main(int argc, char* argv[]) {
PROF_FUNC_TEST(presence_received),
PROF_FUNC_TEST(presence_missing_resource_defaults),
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
PROF_FUNC_TEST(message_receive_chatwin),
/* Disconnect - clean session termination */
PROF_FUNC_TEST(disconnect_ends_session),
};
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),
/* GROUP 4: MUC, Carbons (19 tests)
* 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_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
/* MUC room information display */
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
PROF_FUNC_TEST(shows_occupant_join),
/* MUC messaging */
PROF_FUNC_TEST(shows_message),
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
/* MUC console notification settings */
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_no_message_in_console_when_window_not_focussed),
/* Message Carbons - XEP-0280 (message sync across devices) */
PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled),
PROF_FUNC_TEST(send_disable_carbons),
PROF_FUNC_TEST(receive_carbon),
PROF_FUNC_TEST(receive_self_carbon),
PROF_FUNC_TEST(receive_private_carbon),
PROF_FUNC_TEST(send_receipt_request),
PROF_FUNC_TEST(send_receipt_on_request),
PROF_FUNC_TEST(does_not_send_receipt_request_to_barejid),
PROF_FUNC_TEST(sends_new_item),
PROF_FUNC_TEST(sends_new_item_nick),
PROF_FUNC_TEST(sends_remove_item),
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
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),
PROF_FUNC_TEST(sends_room_join),
PROF_FUNC_TEST(sends_room_join_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
PROF_FUNC_TEST(shows_history_message),
PROF_FUNC_TEST(shows_occupant_join),
PROF_FUNC_TEST(shows_message),
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
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_no_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(disconnect_ends_session),
};
return cmocka_run_group_tests(all_tests, NULL, NULL);
int result = 0;
switch (group) {
case 1:
result = cmocka_run_group_tests_name("Group 1: Connect/Ping/Rooms/Software", group1_tests, NULL, NULL);
break;
case 2:
result = cmocka_run_group_tests_name("Group 2: Message/Receipts/Roster/Session", group2_tests, NULL, NULL);
break;
case 3:
result = cmocka_run_group_tests_name("Group 3: Presence", group3_tests, NULL, NULL);
break;
case 4:
result = cmocka_run_group_tests_name("Group 4: MUC/Carbons", group4_tests, NULL, NULL);
break;
default:
/* Run all groups sequentially */
result |= cmocka_run_group_tests_name("Group 1: Connect/Ping/Rooms/Software", group1_tests, NULL, NULL);
result |= cmocka_run_group_tests_name("Group 2: Message/Receipts/Roster/Session", group2_tests, NULL, NULL);
result |= cmocka_run_group_tests_name("Group 3: Presence", group3_tests, NULL, NULL);
result |= cmocka_run_group_tests_name("Group 4: MUC/Carbons", group4_tests, NULL, NULL);
break;
}
return result;
}

View File

@@ -8,9 +8,12 @@
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pty.h>
#include <fcntl.h>
#include <sys/select.h>
#include <regex.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -18,6 +21,27 @@ char *config_orig;
char *data_orig;
int fd = 0;
int stub_port = 5230;
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.
* 64KB is sufficient for typical test output while keeping memory usage
* reasonable. When full, older half is discarded (ring buffer behavior).
*/
#define OUTPUT_BUF_SIZE 65536
static char output_buffer[OUTPUT_BUF_SIZE];
static size_t output_len = 0;
/* Timeout for expect operations in seconds */
static int expect_timeout = 30;
gboolean
_create_dir(const char *name)
@@ -60,7 +84,7 @@ _mkdir_recursive(const char *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");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -73,7 +97,7 @@ _create_config_dir(void)
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");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -86,7 +110,7 @@ _create_data_dir(void)
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");
if (!_mkdir_recursive(chatlogs_dir->str)) {
@@ -99,7 +123,7 @@ _create_chatlogs_dir(void)
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");
if (!_mkdir_recursive(logs_dir->str)) {
@@ -112,42 +136,124 @@ _create_logs_dir(void)
void
_cleanup_dirs(void)
{
int res = system("rm -rf ./tests/functionaltests/files");
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf ./tests/functionaltests/files/%d", stub_port);
int res = system(cmd);
if (res == -1) {
assert_true(FALSE);
}
}
/*
* Read available data from fd into output_buffer with timeout.
* Returns number of bytes read, 0 on timeout, -1 on error.
*/
static int
_read_output(int timeout_ms)
{
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret <= 0) {
return ret;
}
size_t space = OUTPUT_BUF_SIZE - output_len - 1;
if (space <= 0) {
/* Buffer full, shift content */
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE/2, OUTPUT_BUF_SIZE/2);
output_len = OUTPUT_BUF_SIZE/2;
space = OUTPUT_BUF_SIZE - output_len - 1;
}
ssize_t n = read(fd, output_buffer + output_len, space);
if (n > 0) {
output_len += n;
output_buffer[output_len] = '\0';
}
return n;
}
/*
* Custom implementation of exp_spawnl using forkpty.
* This avoids the segfault bug in libexpect on Arch Linux.
*/
void
prof_start(void)
{
// helper script sets terminal columns, avoids assertions failing
// based on the test runner terminal size
fd = exp_spawnl("sh",
"sh",
"-c",
"./tests/functionaltests/start_profanity.sh",
NULL);
FILE *fp = fdopen(fd, "r+");
assert_true(fp != NULL);
setbuf(fp, (char *)0);
struct winsize ws;
ws.ws_row = 24;
ws.ws_col = 300; /* Match COLUMNS=300 from start_profanity.sh */
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
/* Reset output buffer */
output_len = 0;
output_buffer[0] = '\0';
child_pid = forkpty(&fd, NULL, NULL, &ws);
if (child_pid < 0) {
fd = -1;
return;
}
if (child_pid == 0) {
/* Child process */
setenv("COLUMNS", "300", 1);
setenv("TERM", "xterm", 1);
execl("./profanity", "./profanity", "-l", "DEBUG", NULL);
/* If exec fails */
fprintf(stderr, "execl failed: %s\n", strerror(errno));
_exit(127);
}
/* Parent process */
/* Set non-blocking mode for reading */
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int
init_prof_test(void **state)
{
if (stbbr_start(STBBR_LOGDEBUG ,5230, 0) != 0) {
assert_true(FALSE);
gboolean started = FALSE;
for (int p = 5230; p < 5250; ++p) {
int ret = stbbr_start(STBBR_LOGDEBUG, p, 0);
if (ret == 0) {
stub_port = p;
started = TRUE;
break;
}
}
if (!started) {
assert_true(FALSE); // could not start stabber on any port in range
return -1;
}
// Generate unique XDG paths based on stub_port for parallel execution
snprintf(xdg_config_home, sizeof(xdg_config_home),
"./tests/functionaltests/files/%d/xdg_config_home", stub_port);
snprintf(xdg_data_home, sizeof(xdg_data_home),
"./tests/functionaltests/files/%d/xdg_data_home", stub_port);
// Give stabber server thread time to start listening
usleep(100000); // 100ms
config_orig = getenv("XDG_CONFIG_HOME");
data_orig = getenv("XDG_DATA_HOME");
setenv("XDG_CONFIG_HOME", XDG_CONFIG_HOME, 1);
setenv("XDG_DATA_HOME", XDG_DATA_HOME, 1);
setenv("XDG_CONFIG_HOME", xdg_config_home, 1);
setenv("XDG_DATA_HOME", xdg_data_home, 1);
_cleanup_dirs();
@@ -157,50 +263,69 @@ init_prof_test(void **state)
_create_logs_dir();
prof_start();
assert_true(prof_output_exact("Profanity"));
int prof_started = prof_output_regex("CProof\\. Type /help for help information\\.");
assert_true(prof_started);
// set UI options to make expect assertions faster and more reliable
prof_input("/inpblock timeout 5");
assert_true(prof_output_exact("Input blocking set to 5 milliseconds"));
assert_true(prof_output_regex("Input blocking set to 5 milliseconds"));
prof_input("/inpblock dynamic off");
assert_true(prof_output_exact("Dynamic input blocking disabled"));
assert_true(prof_output_regex("Dynamic input blocking disabled"));
prof_input("/notify chat off");
assert_true(prof_output_exact("Chat notifications disabled"));
assert_true(prof_output_regex("Chat notifications disabled"));
prof_input("/notify room off");
assert_true(prof_output_exact("Room notifications disabled"));
assert_true(prof_output_regex("Room notifications disabled"));
prof_input("/wrap off");
assert_true(prof_output_exact("Word wrap disabled"));
assert_true(prof_output_regex("Word wrap disabled"));
prof_input("/roster hide");
assert_true(prof_output_exact("Roster disabled"));
assert_true(prof_output_regex("Roster disabled"));
prof_input("/occupants default hide");
assert_true(prof_output_exact("Occupant list disabled"));
assert_true(prof_output_regex("Occupant list disabled"));
prof_input("/time console off");
prof_input("/time console off");
assert_true(prof_output_exact("Console time display disabled."));
assert_true(prof_output_regex("Console time display disabled\\."));
prof_input("/time chat off");
assert_true(prof_output_exact("Chat time display disabled."));
assert_true(prof_output_regex("Chat time display disabled\\."));
prof_input("/time muc off");
assert_true(prof_output_exact("MUC time display disabled."));
assert_true(prof_output_regex("MUC time display disabled\\."));
prof_input("/time config off");
assert_true(prof_output_exact("config time display disabled."));
assert_true(prof_output_regex("Config time display disabled\\."));
prof_input("/time private off");
assert_true(prof_output_exact("Private chat time display disabled."));
assert_true(prof_output_regex("Private chat time display disabled\\."));
prof_input("/time xml off");
assert_true(prof_output_exact("XML Console time display disabled."));
assert_true(prof_output_regex("XML Console time display disabled\\."));
return 0;
}
int
close_prof_test(void **state)
{
prof_input("/quit");
waitpid(exp_pid, NULL, 0);
if (fd > 0 && child_pid > 0) {
prof_input("/quit");
// Give profanity time to process quit command
sleep(1);
waitpid(child_pid, NULL, 0);
close(fd);
fd = 0;
child_pid = 0;
}
_cleanup_dirs();
setenv("XDG_CONFIG_HOME", config_orig, 1);
setenv("XDG_DATA_HOME", data_orig, 1);
if (config_orig) {
setenv("XDG_CONFIG_HOME", config_orig, 1);
}
if (data_orig) {
setenv("XDG_DATA_HOME", data_orig, 1);
}
stbbr_stop();
/*
* TODO: Replace with proper synchronization.
* stabber doesn't provide wait_stopped() API yet, so we use delay
* to ensure the port is released before the next test starts.
* See: https://git.jabber.space/devs/stabber/issues/3
*/
usleep(100000); // 100ms
return 0;
}
@@ -209,20 +334,75 @@ prof_input(const char *input)
{
GString *inp_str = g_string_new(input);
g_string_append(inp_str, "\r");
write(fd, inp_str->str, inp_str->len);
ssize_t _wn = write(fd, inp_str->str, inp_str->len);
(void)_wn;
g_string_free(inp_str, TRUE);
/* Small delay to let profanity process input */
usleep(10000);
}
/*
* Wait for exact text to appear in output.
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_exact(const char *text)
{
return (1 == exp_expectl(fd, exp_exact, text, 1, exp_end));
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(100) > 0) {
/* Keep reading while data available */
}
/* Check if text is in buffer */
if (strstr(output_buffer, text) != NULL) {
return 1;
}
usleep(50000); /* 50ms */
}
return 0;
}
/*
* Wait for regex pattern to match in output.
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_regex(const char *text)
prof_output_regex(const char *pattern)
{
return (1 == exp_expectl(fd, exp_regexp, text, 1, exp_end));
regex_t regex;
int ret;
ret = regcomp(&regex, pattern, REG_EXTENDED | REG_NOSUB);
if (ret != 0) {
return 0;
}
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(100) > 0) {
/* Keep reading while data available */
}
/* Check if pattern matches */
ret = regexec(&regex, output_buffer, 0, NULL, 0);
if (ret == 0) {
regfree(&regex);
return 1;
}
usleep(50000); /* 50ms */
}
regfree(&regex);
return 0;
}
void
@@ -241,33 +421,40 @@ prof_connect_with_roster(const char *roster)
stbbr_for_query("jabber:iq:roster", roster_str->str);
g_string_free(roster_str, TRUE);
stbbr_for_id("prof_presence_1",
"<presence id='prof_presence_1' lang='en' to='stabber@localhost/profanity' from='stabber@localhost/profanity'>"
"<priority>0</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io/' ver='f8mrtdyAmhnj8Ca+630bThSL718='/>"
"</presence>"
);
stbbr_auth_passwd("password");
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls disable auth legacy", stub_port);
prof_input(connect_cmd);
assert_true(prof_output_regex("password:"));
prof_input("password");
// Allow time for profanity to connect
exp_timeout = 30;
assert_true(prof_output_regex("stabber@localhost/profanity logged in successfully, .+online.+ \\(priority 0\\)\\."));
exp_timeout = 10;
stbbr_wait_for("prof_presence_*");
expect_timeout = 60;
assert_true(prof_output_regex("Connecting as stabber@localhost"));
assert_true(prof_output_regex("logged in successfully"));
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
expect_timeout = 60;
// Wait for presence stanza to be sent (content-based, not ID-based)
// Match the actual attribute order from stanza_attach_caps
assert_true(stbbr_received(
"<presence id=\"*\">"
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
"</presence>"
));
}
void
prof_timeout(int timeout)
{
exp_timeout = timeout;
expect_timeout = timeout;
}
void
prof_timeout_reset(void)
{
exp_timeout = 10;
expect_timeout = 60;
}
void

View File

@@ -1,8 +1,15 @@
#ifndef __H_PROFTEST
#define __H_PROFTEST
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
/*
* XDG paths are now dynamic, generated per-test based on stub_port.
* This allows parallel test execution without file conflicts.
* Use xdg_config_home and xdg_data_home variables instead of macros.
*/
extern char xdg_config_home[256];
extern char xdg_data_home[256];
extern int stub_port;
int init_prof_test(void **state);
int close_prof_test(void **state);

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -1,10 +1,10 @@
#include <glib.h>
#include <unistd.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -144,7 +144,11 @@ resets_to_barejid_after_presence_received(void **state)
"<show>dnd</show>"
"</presence>"
);
assert_true(prof_output_exact("Buddy1 (laptop) is dnd"));
// Wait for presence to be processed before sending next message.
// The presence output may appear in console or chat window depending on focus,
// so we use a small delay to ensure the session is reset to barejid.
// Under Valgrind, processing is slower, so this delay is necessary.
g_usleep(500000); // 500ms
prof_input("/msg buddy1@localhost Outgoing 2");
assert_true(stbbr_received(

View File

@@ -2,9 +2,9 @@
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -47,7 +47,9 @@ connect_jid_requests_bookmarks(void **state)
void
connect_bad_password(void **state)
{
prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow");
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls allow", stub_port);
prof_input(connect_cmd);
prof_input("badpassword");
assert_true(prof_output_exact("Login failed."));

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -1,13 +1,21 @@
#include <glib.h>
#include <unistd.h>
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
/*
* NOTE: We use prof_output_regex() throughout this file even for seemingly
* exact strings because some output may include timestamps, color codes,
* or other dynamic content depending on configuration. Using regex provides
* more robust matching. For patterns without regex metacharacters, the
* performance difference is negligible.
*/
void
sends_room_join(void **state)
{
@@ -15,7 +23,7 @@ sends_room_join(void **state)
prof_input("/join testroom@conference.localhost");
assert_true(stbbr_last_received(
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/stabber'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
@@ -30,7 +38,7 @@ sends_room_join_with_nick(void **state)
prof_input("/join testroom@conference.localhost nick testnick");
assert_true(stbbr_last_received(
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/testnick'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
@@ -45,7 +53,7 @@ sends_room_join_with_password(void **state)
prof_input("/join testroom@conference.localhost password testpassword");
assert_true(stbbr_last_received(
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/stabber'>"
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
@@ -62,7 +70,7 @@ sends_room_join_with_nick_and_password(void **state)
prof_input("/join testroom@conference.localhost nick testnick password testpassword");
assert_true(stbbr_last_received(
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/testnick'>"
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
@@ -77,8 +85,8 @@ shows_role_and_affiliation_on_join(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -89,7 +97,7 @@ shows_role_and_affiliation_on_join(void **state)
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
}
void
@@ -97,8 +105,8 @@ shows_subject_on_join(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -108,7 +116,7 @@ shows_subject_on_join(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost'>"
@@ -125,8 +133,8 @@ shows_history_message(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -136,7 +144,7 @@ shows_history_message(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -146,7 +154,9 @@ shows_history_message(void **state)
"</message>"
);
assert_true(prof_output_regex("testoccupant: an old message"));
prof_timeout(10);
assert_true(prof_output_regex("testoccupant:.*an old message"));
prof_timeout_reset();
}
void
@@ -154,8 +164,12 @@ shows_occupant_join(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
// Enable MUC status 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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -165,7 +179,7 @@ shows_occupant_join(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<presence to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -174,8 +188,9 @@ shows_occupant_join(void **state)
"</x>"
"</presence>"
);
sleep(1);
assert_true(prof_output_exact("-> testoccupant has joined the room, role: participant, affiliation: none"));
assert_true(prof_output_regex("testoccupant has joined"));
}
void
@@ -183,8 +198,8 @@ shows_message(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -194,7 +209,7 @@ shows_message(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -210,8 +225,8 @@ shows_me_message_from_occupant(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -221,7 +236,7 @@ shows_me_message_from_occupant(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -229,7 +244,7 @@ shows_me_message_from_occupant(void **state)
"</message>"
);
assert_true(prof_output_exact("*testoccupant did something"));
assert_true(prof_output_regex("\\*testoccupant did something"));
}
void
@@ -237,8 +252,8 @@ shows_me_message_from_self(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -248,7 +263,7 @@ shows_me_message_from_self(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
@@ -256,7 +271,7 @@ shows_me_message_from_self(void **state)
"</message>"
);
assert_true(prof_output_exact("*stabber did something"));
assert_true(prof_output_regex("\\*stabber did something"));
}
void
@@ -264,8 +279,8 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
{
prof_connect();
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -275,10 +290,10 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("CProof. Type /help for help information."));
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -286,7 +301,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
"</message>"
);
assert_true(prof_output_exact("<< room message: testoccupant in testroom@conference.localhost (win 2)"));
assert_true(prof_output_regex("<< room message: testoccupant in testroom@conference\\.localhost \\(win 2\\)"));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/anotheroccupant'>"
@@ -294,7 +309,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
"</message>"
);
assert_true(prof_output_exact("<< room message: anotheroccupant in testroom@conference.localhost (win 2)"));
assert_true(prof_output_regex("<< room message: anotheroccupant in testroom@conference\\.localhost \\(win 2\\)"));
}
void
@@ -303,10 +318,10 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
prof_connect();
prof_input("/console muc first");
assert_true(prof_output_exact("Console MUC messages set: first"));
assert_true(prof_output_regex("Console MUC messages set: first"));
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -316,21 +331,22 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("CProof. Type /help for help information."));
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
"<body>a new message</body>"
"</message>"
);
sleep(1);
assert_true(prof_output_exact("<< room message: testroom@conference.localhost (win 2)"));
assert_true(prof_output_regex("room message.*testroom@conference\\.localhost"));
prof_input("/clear");
prof_input("/about");
assert_true(prof_output_exact("Type '/help' to show complete help."));
assert_true(prof_output_regex("Type '/help' to show complete help\\."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/anotheroccupant'>"
@@ -339,7 +355,7 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
);
prof_timeout(2);
assert_false(prof_output_exact("<< room message: testroom@conference.localhost (win 2)"));
assert_false(prof_output_regex("<< room message: testroom@conference\\.localhost \\(win 2\\)"));
prof_timeout_reset();
}
@@ -349,10 +365,10 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
prof_connect();
prof_input("/console muc none");
assert_true(prof_output_exact("Console MUC messages set: none"));
assert_true(prof_output_regex("Console MUC messages set: none"));
stbbr_for_id("prof_join_4",
"<presence id='prof_join_4' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
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='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
@@ -362,10 +378,10 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
);
prof_input("/join testroom@conference.localhost");
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
assert_true(prof_output_regex("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("CProof. Type /help for help information."));
assert_true(prof_output_regex("CProof\\. Type /help for help information\\."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -374,6 +390,6 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
);
prof_timeout(2);
assert_false(prof_output_exact("testroom@conference.localhost (win 2)"));
assert_false(prof_output_regex("testroom@conference\\.localhost \\(win 2\\)"));
prof_timeout_reset();
}

View File

@@ -2,17 +2,26 @@
#include "prof_cmocka.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
void
ping_server(void **state)
{
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
/*
* This test verifies that profanity correctly handles the /ping command
* when the server supports ping (urn:xmpp:ping feature).
*
* We register disco#info response with ping support, then verify
* profanity sends ping and receives "Pinged server" confirmation.
*/
// Register disco#info response that includes 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='Prosody'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -20,37 +29,33 @@ ping_server(void **state)
"</iq>"
);
stbbr_for_id("prof_ping_4",
"<iq id='prof_ping_4' type='result' to='stabber@localhost/profanity'/>"
);
stbbr_for_id("prof_ping_5",
"<iq id='prof_ping_5' type='result' to='stabber@localhost/profanity'/>"
// Register ping response
stbbr_for_query("urn:xmpp:ping",
"<iq from='localhost' to='stabber@localhost/profanity' type='result'/>"
);
prof_connect();
/*
* Wait for disco#info exchange to complete.
* TODO: Replace with proper wait mechanism (e.g., wait for capabilities
* to be cached). Currently we use sleep to ensure disco#info response
* is processed before sending /ping command.
*/
sleep(3);
prof_input("/ping");
assert_true(stbbr_received(
"<iq id='prof_ping_4' type='get'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_exact("Ping response from server"));
prof_input("/ping");
assert_true(stbbr_received(
"<iq id='prof_ping_5' type='get'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_exact("Ping response from server"));
prof_timeout(10);
assert_true(prof_output_regex("Pinged server"));
prof_timeout_reset();
}
void
ping_server_not_supported(void **state)
{
stbbr_for_id("prof_disco_info_onconnect_2",
"<iq id='prof_disco_info_onconnect_2' to='stabber@localhost/profanity' type='result' from='localhost'>"
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>"
@@ -60,7 +65,7 @@ ping_server_not_supported(void **state)
prof_connect();
prof_input("/ping");
assert_true(prof_output_exact("Server does not support ping requests."));
assert_true(prof_output_regex("Server does not support ping requests"));
}
void
@@ -69,20 +74,21 @@ ping_responds_to_server_request(void **state)
prof_connect();
stbbr_send(
"<iq id='pingtest1' type='get' to='stabber@localhost/profanity' from='localhost'>"
"<ping xmlns='urn:xmpp:ping'/>"
"<iq id=\"pingtest1\" type=\"get\" to=\"stabber@localhost/profanity\" from=\"localhost\">"
"<ping xmlns=\"urn:xmpp:ping\"/>"
"</iq>"
);
assert_true(stbbr_received(
"<iq id='pingtest1' type='result' from='stabber@localhost/profanity' to='localhost'/>"
"<iq id='pingtest1' type='result' from='stabber@localhost/profanity' to='localhost'></iq>"
));
}
void ping_jid(void **state)
{
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
// Caps/disco info response without relying on a fixed id
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='urn:xmpp:ping'/>"
@@ -109,29 +115,30 @@ void ping_jid(void **state)
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
assert_true(stbbr_received(
"<iq id='prof_caps_4' to='buddy1@localhost/mobile' type='get'>"
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='/>"
"</iq>"
));
stbbr_for_id("prof_ping_5",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' id='prof_ping_5' type='result'/>"
// Respond to ping result regardless of id
stbbr_for_query("urn:xmpp:ping",
"<iq from='buddy1@localhost/mobile' to='stabber@localhost' type='result'/>"
);
prof_input("/ping buddy1@localhost/mobile");
assert_true(stbbr_received(
"<iq id='prof_ping_5' type='get' to='buddy1@localhost/mobile'>"
"<iq id='*' type='get' to='buddy1@localhost/mobile'>"
"<ping xmlns='urn:xmpp:ping'/>"
"</iq>"
));
assert_true(prof_output_exact("Ping response from buddy1@localhost/mobile"));
assert_true(prof_output_regex("Ping response from buddy1@localhost/mobile"));
}
void ping_jid_not_supported(void **state)
{
stbbr_for_id("prof_caps_4",
"<iq id='prof_caps_4' to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result' from='buddy1@localhost/mobile'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='>"
"<identity category='client' type='console' name='Profanity0.6.0'/>"
"<feature var='http://jabber.org/protocol/disco#info'/>"
@@ -157,7 +164,7 @@ void ping_jid_not_supported(void **state)
assert_true(prof_output_exact("Buddy1 (mobile) is online, \"I'm here\""));
assert_true(stbbr_received(
"<iq id='prof_caps_4' to='buddy1@localhost/mobile' type='get'>"
"<iq id='*' to='buddy1@localhost/mobile' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#LpT2xs3nun7jC2sq4gg3WRDQFZ4='/>"
"</iq>"
));

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -13,15 +12,16 @@ presence_online(void **state)
{
prof_connect();
prof_input("/online");
prof_input("/status set online");
assert_true(prof_output_exact("Status set to online (priority 0), \"online\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_3'>"
"<presence id='*'>"
"<status>online</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to online (priority 0)"));
}
void
@@ -29,16 +29,16 @@ presence_online_with_message(void **state)
{
prof_connect();
prof_input("/online \"Hi there\"");
prof_input("/status set online \"Hi there\"");
assert_true(prof_output_exact("Status set to online (priority 0), \"Hi there\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<status>Hi there</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to online (priority 0), \"Hi there\"."));
}
void
@@ -46,16 +46,17 @@ presence_away(void **state)
{
prof_connect();
prof_input("/away");
prof_input("/status set away");
assert_true(prof_output_exact("Status set to away (priority 0), \"away\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>away</show>"
"<status>away</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to away (priority 0)"));
}
void
@@ -63,17 +64,17 @@ presence_away_with_message(void **state)
{
prof_connect();
prof_input("/away \"I'm not here for a bit\"");
prof_input("/status set away \"I'm not here for a bit\"");
assert_true(prof_output_exact("Status set to away (priority 0), \"I'm not here for a bit\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>away</show>"
"<status>I'm not here for a bit</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to away (priority 0), \"I'm not here for a bit\"."));
}
void
@@ -81,16 +82,17 @@ presence_xa(void **state)
{
prof_connect();
prof_input("/xa");
prof_input("/status set xa");
assert_true(prof_output_exact("Status set to xa (priority 0), \"xa\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>xa</show>"
"<status>xa</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to xa (priority 0)"));
}
void
@@ -98,17 +100,17 @@ presence_xa_with_message(void **state)
{
prof_connect();
prof_input("/xa \"Gone to the shops\"");
prof_input("/status set xa \"Gone to the shops\"");
assert_true(prof_output_exact("Status set to xa (priority 0), \"Gone to the shops\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>xa</show>"
"<status>Gone to the shops</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to xa (priority 0), \"Gone to the shops\"."));
}
void
@@ -116,16 +118,17 @@ presence_dnd(void **state)
{
prof_connect();
prof_input("/dnd");
prof_input("/status set dnd");
assert_true(prof_output_exact("Status set to dnd (priority 0), \"dnd\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>dnd</show>"
"<status>dnd</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to dnd (priority 0)"));
}
void
@@ -133,17 +136,17 @@ presence_dnd_with_message(void **state)
{
prof_connect();
prof_input("/dnd \"Working\"");
prof_input("/status set dnd \"Working\"");
assert_true(prof_output_exact("Status set to dnd (priority 0), \"Working\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>dnd</show>"
"<status>Working</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to dnd (priority 0), \"Working\"."));
}
void
@@ -151,16 +154,17 @@ presence_chat(void **state)
{
prof_connect();
prof_input("/chat");
prof_input("/status set chat");
assert_true(prof_output_exact("Status set to chat (priority 0), \"chat\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>chat</show>"
"<status>chat</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0)"));
}
void
@@ -168,17 +172,17 @@ presence_chat_with_message(void **state)
{
prof_connect();
prof_input("/chat \"Free to talk\"");
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
}
void
@@ -188,14 +192,14 @@ presence_set_priority(void **state)
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
}
void
@@ -204,24 +208,24 @@ presence_includes_priority(void **state)
prof_connect();
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
prof_input("/chat \"Free to talk\"");
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 25), \"Free to talk\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_5'>"
"<presence id='*'>"
"<priority>25</priority>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 25), \"Free to talk\"."));
}
void
@@ -229,26 +233,26 @@ presence_keeps_status(void **state)
{
prof_connect();
prof_input("/chat \"Free to talk\"");
prof_input("/status set chat \"Free to talk\"");
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
assert_true(stbbr_received(
"<presence id='prof_presence_4'>"
"<presence id='*'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Status set to chat (priority 0), \"Free to talk\"."));
prof_input("/priority 25");
assert_true(prof_output_exact("Priority set to 25."));
assert_true(stbbr_received(
"<presence id='prof_presence_5'>"
"<presence id='*'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"</presence>"
));
assert_true(prof_output_exact("Priority set to 25."));
}
void

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
@@ -31,8 +30,9 @@ send_receipt_request(void **state)
prof_connect();
stbbr_for_id("prof_caps_4",
"<iq from='buddy1@localhost/laptop' to='stabber@localhost' id='prof_caps_4' type='result'>"
// Register disco#info response for capabilities query (receipts support)
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq from='buddy1@localhost/laptop' to='stabber@localhost' id='*' type='result'>"
"<query xmlns='http://jabber.org/protocol/disco#info' node='http://profanity-im.github.io#hAkb1xZdJV9BQpgGNw8zG5Xsals='>"
"<identity category='client' name='Profanity 0.5.0' type='console'/>"
"<feature var='urn:xmpp:receipts'/>"

View File

@@ -4,15 +4,14 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"
void
rooms_query(void **state)
{
stbbr_for_id("prof_confreq_4",
"<iq id='prof_confreq_4' type='result' to='stabber@localhost/profanity' from='conference.localhost'>"
stbbr_for_query("http://jabber.org/protocol/disco#items",
"<iq id='*' type='result' to='stabber@localhost/profanity' from='conference.localhost'>"
"<query xmlns='http://jabber.org/protocol/disco#items'>"
"<item jid='chatroom@conference.localhost' name='A chat room'/>"
"<item jid='hangout@conference.localhost' name='Another chat room'/>"
@@ -27,8 +26,8 @@ rooms_query(void **state)
assert_true(prof_output_exact("chatroom@conference.localhost (A chat room)"));
assert_true(prof_output_exact("hangout@conference.localhost (Another chat room)"));
assert_true(stbbr_last_received(
"<iq id='prof_confreq_4' to='conference.localhost' type='get'>"
assert_true(stbbr_received(
"<iq id='*' to='conference.localhost' type='get'>"
"<query xmlns='http://jabber.org/protocol/disco#items'/>"
"</iq>"
));

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -4,7 +4,6 @@
#include <string.h>
#include <stabber.h>
#include <expect.h>
#include "proftest.h"

View File

@@ -26,7 +26,7 @@
#include "log.h"
void
log_init(log_level_t filter, char* log_file)
log_init(log_level_t filter, const char* const log_file)
{
}
log_level_t

View File

@@ -1,7 +1,4 @@
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "prof_cmocka.h"
#include <glib.h>
#include "event/client_events.h"
#include "command/cmd_funcs.h"

View File

@@ -156,6 +156,11 @@ connection_get_profanity_identifier(void)
return "profident";
}
void
connection_debug_print_features()
{
}
jabber_conn_status_t
connection_register(const char* const altdomain, int port, const char* const tls_policy,
const char* const username, const char* const password)