4 Commits

Author SHA1 Message Date
686e0a1055 Initialize MySQL 2022-06-14 15:29:10 +02:00
d3e2a3eab5 ergo: Add $ERGO_HISTORY_BACKEND to opt-in to history mysql backend 2022-06-14 15:09:46 +02:00
8bd102a391 ergo: Create MySQL subprocess instead of using external DB
This starts each test with a clean database, so we can remove chan/nick
randomization from stateful tests (chathistory and roleplay).

It will also allow testing Ergo with a MySQL backend for the KV store
instead of buntdb.

Additionally, this makes it much easier to run these tests, than having
to manually configure such a database.
2022-06-14 15:02:06 +02:00
00f0515d36 Remove redundant configuration
This statement was a no-op, given the value defined in BASE_CONFIG
2022-06-14 15:01:28 +02:00
49 changed files with 634 additions and 2001 deletions

View File

@ -3,24 +3,20 @@
jobs: jobs:
build-anope: build-anope:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies - name: Cache Anope
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
key: 3-${{ runner.os }}-anope-devel key: 3-${{ runner.os }}-anope-2.0.9
path: '~/.cache path: '~/.cache
${ github.workspace }/anope ${{ github.workspace }}/anope
' '
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope - name: Checkout Anope
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9 ref: 2.0.9
repository: anope/anope repository: anope/anope
- name: Build Anope - name: Build Anope
run: | run: |-
cd $GITHUB_WORKSPACE/anope/ cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* . cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-bahamut: build-bahamut:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -71,7 +67,6 @@ jobs:
run: | run: |
cd $GITHUB_WORKSPACE/Bahamut/ cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force libtoolize --force
aclocal aclocal
@ -92,7 +87,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-hybrid: build-hybrid:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -131,7 +126,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-inspircd: build-inspircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -162,7 +157,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-ngircd: build-ngircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -203,7 +198,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-plexus4: build-plexus4:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -245,7 +240,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-solanum: build-solanum:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -285,7 +280,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-unrealircd: build-unrealircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -331,7 +326,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-unrealircd-5: build-unrealircd-5:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -400,8 +395,7 @@ jobs:
- test-unrealircd-5 - test-unrealircd-5
- test-unrealircd-anope - test-unrealircd-anope
- test-unrealircd-atheme - test-unrealircd-atheme
- test-unrealircd-dlk runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Download Artifacts - name: Download Artifacts
@ -428,7 +422,7 @@ jobs:
test-bahamut: test-bahamut:
needs: needs:
- build-bahamut - build-bahamut
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -462,7 +456,7 @@ jobs:
needs: needs:
- build-bahamut - build-bahamut
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -500,7 +494,7 @@ jobs:
test-bahamut-atheme: test-bahamut-atheme:
needs: needs:
- build-bahamut - build-bahamut
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -532,7 +526,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-ergo: test-ergo:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -547,7 +541,7 @@ jobs:
repository: ergochat/ergo repository: ergochat/ergo
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ^1.19.0 go-version: ^1.18.0
- run: go version - run: go version
- name: Build Ergo - name: Build Ergo
run: | run: |
@ -574,7 +568,7 @@ jobs:
needs: needs:
- build-hybrid - build-hybrid
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -612,7 +606,7 @@ jobs:
test-inspircd: test-inspircd:
needs: needs:
- build-inspircd - build-inspircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -646,7 +640,7 @@ jobs:
needs: needs:
- build-inspircd - build-inspircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -683,7 +677,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-ircu2: test-ircu2:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -722,7 +716,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-limnoria: test-limnoria:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -750,7 +744,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-nefarious: test-nefarious:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -789,7 +783,7 @@ jobs:
test-ngircd: test-ngircd:
needs: needs:
- build-ngircd - build-ngircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -823,7 +817,7 @@ jobs:
needs: needs:
- build-ngircd - build-ngircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -861,7 +855,7 @@ jobs:
test-ngircd-atheme: test-ngircd-atheme:
needs: needs:
- build-ngircd - build-ngircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -895,7 +889,7 @@ jobs:
needs: needs:
- build-plexus4 - build-plexus4
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -933,7 +927,7 @@ jobs:
test-solanum: test-solanum:
needs: needs:
- build-solanum - build-solanum
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -965,7 +959,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-sopel: test-sopel:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -993,7 +987,7 @@ jobs:
test-unrealircd: test-unrealircd:
needs: needs:
- build-unrealircd - build-unrealircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1026,7 +1020,7 @@ jobs:
test-unrealircd-5: test-unrealircd-5:
needs: needs:
- build-unrealircd-5 - build-unrealircd-5
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1060,7 +1054,7 @@ jobs:
needs: needs:
- build-unrealircd - build-unrealircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1098,7 +1092,7 @@ jobs:
test-unrealircd-atheme: test-unrealircd-atheme:
needs: needs:
- build-unrealircd - build-unrealircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1128,52 +1122,6 @@ jobs:
with: with:
name: pytest-results_unrealircd-atheme_devel name: pytest-results_unrealircd-atheme_devel
path: pytest.xml path: pytest.xml
test-unrealircd-dlk:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Checkout Dlk
uses: actions/checkout@v2
with:
path: Dlk-Services
ref: main
repository: DalekIRC/Dalek-Services
- name: Build Dlk
run: |
pip install pifpaf
wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{
github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace
}}/wordpress-latest.zip" make unrealircd-dlk
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-dlk_devel
path: pytest.xml
name: irctest with devel versions name: irctest with devel versions
'on': 'on':
schedule: schedule:

View File

@ -3,24 +3,20 @@
jobs: jobs:
build-anope: build-anope:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies - name: Cache Anope
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
key: 3-${{ runner.os }}-anope-devel_release key: 3-${{ runner.os }}-anope-2.0.9
path: '~/.cache path: '~/.cache
${ github.workspace }/anope ${{ github.workspace }}/anope
' '
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope - name: Checkout Anope
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9 ref: 2.0.9
repository: anope/anope repository: anope/anope
- name: Build Anope - name: Build Anope
run: | run: |-
cd $GITHUB_WORKSPACE/anope/ cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* . cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-inspircd: build-inspircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -80,7 +76,7 @@ jobs:
- test-inspircd - test-inspircd
- test-inspircd-anope - test-inspircd-anope
- test-inspircd-atheme - test-inspircd-atheme
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Download Artifacts - name: Download Artifacts
@ -107,7 +103,7 @@ jobs:
test-inspircd: test-inspircd:
needs: needs:
- build-inspircd - build-inspircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -141,7 +137,7 @@ jobs:
needs: needs:
- build-inspircd - build-inspircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -179,7 +175,7 @@ jobs:
test-inspircd-atheme: test-inspircd-atheme:
needs: needs:
- build-inspircd - build-inspircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7

View File

@ -3,24 +3,20 @@
jobs: jobs:
build-anope: build-anope:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies - name: Cache Anope
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
key: 3-${{ runner.os }}-anope-stable key: 3-${{ runner.os }}-anope-2.0.9
path: '~/.cache path: '~/.cache
${ github.workspace }/anope ${{ github.workspace }}/anope
' '
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope - name: Checkout Anope
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9 ref: 2.0.9
repository: anope/anope repository: anope/anope
- name: Build Anope - name: Build Anope
run: | run: |-
cd $GITHUB_WORKSPACE/anope/ cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* . cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-bahamut: build-bahamut:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -71,7 +67,6 @@ jobs:
run: | run: |
cd $GITHUB_WORKSPACE/Bahamut/ cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force libtoolize --force
aclocal aclocal
@ -92,7 +87,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-charybdis: build-charybdis:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -132,7 +127,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-hybrid: build-hybrid:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -171,7 +166,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-inspircd: build-inspircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -202,7 +197,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-ngircd: build-ngircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -243,7 +238,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-plexus4: build-plexus4:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -285,7 +280,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-solanum: build-solanum:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -325,7 +320,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-unrealircd: build-unrealircd:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -347,7 +342,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: unrealircd path: unrealircd
ref: da3c1c654481a33035b9c703957e1c25d0158259 ref: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4
repository: unrealircd/unrealircd repository: unrealircd/unrealircd
- name: Build UnrealIRCd 6 - name: Build UnrealIRCd 6
run: | run: |
@ -371,7 +366,7 @@ jobs:
path: ~/artefacts-*.tar.gz path: ~/artefacts-*.tar.gz
retention-days: 1 retention-days: 1
build-unrealircd-5: build-unrealircd-5:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Create directories - name: Create directories
run: cd ~/; mkdir -p .local/ go/ run: cd ~/; mkdir -p .local/ go/
@ -443,8 +438,7 @@ jobs:
- test-unrealircd-5 - test-unrealircd-5
- test-unrealircd-anope - test-unrealircd-anope
- test-unrealircd-atheme - test-unrealircd-atheme
- test-unrealircd-dlk runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Download Artifacts - name: Download Artifacts
@ -471,7 +465,7 @@ jobs:
test-bahamut: test-bahamut:
needs: needs:
- build-bahamut - build-bahamut
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -505,7 +499,7 @@ jobs:
needs: needs:
- build-bahamut - build-bahamut
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -543,7 +537,7 @@ jobs:
test-bahamut-atheme: test-bahamut-atheme:
needs: needs:
- build-bahamut - build-bahamut
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -576,7 +570,7 @@ jobs:
test-charybdis: test-charybdis:
needs: needs:
- build-charybdis - build-charybdis
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -608,7 +602,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-ergo: test-ergo:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -623,7 +617,7 @@ jobs:
repository: ergochat/ergo repository: ergochat/ergo
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ^1.19.0 go-version: ^1.18.0
- run: go version - run: go version
- name: Build Ergo - name: Build Ergo
run: | run: |
@ -650,7 +644,7 @@ jobs:
needs: needs:
- build-hybrid - build-hybrid
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -688,7 +682,7 @@ jobs:
test-inspircd: test-inspircd:
needs: needs:
- build-inspircd - build-inspircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -722,7 +716,7 @@ jobs:
needs: needs:
- build-inspircd - build-inspircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -760,7 +754,7 @@ jobs:
test-inspircd-atheme: test-inspircd-atheme:
needs: needs:
- build-inspircd - build-inspircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -792,7 +786,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-irc2: test-irc2:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -842,7 +836,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-ircu2: test-ircu2:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -881,7 +875,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-limnoria: test-limnoria:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -908,7 +902,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-nefarious: test-nefarious:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -947,7 +941,7 @@ jobs:
test-ngircd: test-ngircd:
needs: needs:
- build-ngircd - build-ngircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -981,7 +975,7 @@ jobs:
needs: needs:
- build-ngircd - build-ngircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1019,7 +1013,7 @@ jobs:
test-ngircd-atheme: test-ngircd-atheme:
needs: needs:
- build-ngircd - build-ngircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1053,7 +1047,7 @@ jobs:
needs: needs:
- build-plexus4 - build-plexus4
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1091,7 +1085,7 @@ jobs:
test-solanum: test-solanum:
needs: needs:
- build-solanum - build-solanum
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1123,7 +1117,7 @@ jobs:
path: pytest.xml path: pytest.xml
test-sopel: test-sopel:
needs: [] needs: []
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1151,7 +1145,7 @@ jobs:
test-unrealircd: test-unrealircd:
needs: needs:
- build-unrealircd - build-unrealircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1184,7 +1178,7 @@ jobs:
test-unrealircd-5: test-unrealircd-5:
needs: needs:
- build-unrealircd-5 - build-unrealircd-5
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1218,7 +1212,7 @@ jobs:
needs: needs:
- build-unrealircd - build-unrealircd
- build-anope - build-anope
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1256,7 +1250,7 @@ jobs:
test-unrealircd-atheme: test-unrealircd-atheme:
needs: needs:
- build-unrealircd - build-unrealircd
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@ -1286,52 +1280,6 @@ jobs:
with: with:
name: pytest-results_unrealircd-atheme_stable name: pytest-results_unrealircd-atheme_stable
path: pytest.xml path: pytest.xml
test-unrealircd-dlk:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Checkout Dlk
uses: actions/checkout@v2
with:
path: Dlk-Services
ref: effd18652fc1c847d1959089d9cca9ff9837a8c0
repository: DalekIRC/Dalek-Services
- name: Build Dlk
run: |
pip install pifpaf
wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{
github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace
}}/wordpress-latest.zip" make unrealircd-dlk
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-dlk_stable
path: pytest.xml
name: irctest with stable versions name: irctest with stable versions
'on': 'on':
pull_request: null pull_request: null

View File

@ -2,23 +2,22 @@ exclude: ^irctest/scram
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.11.5 rev: 5.5.2
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/PyCQA/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 5.0.4 rev: 3.8.3
hooks: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1 rev: v0.812
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-PyYAML, types-docutils]

View File

@ -122,8 +122,7 @@ bahamut:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.bahamut \ --controller=irctest.controllers.bahamut \
-m 'not services' \ -m 'not services' \
-n 4 \ -n 10 \
-vv -s \
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
bahamut-atheme: bahamut-atheme:
@ -131,6 +130,7 @@ bahamut-atheme:
--controller=irctest.controllers.bahamut \ --controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m 'services' \ -m 'services' \
-n 10 \
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
bahamut-anope: bahamut-anope:
@ -138,6 +138,7 @@ bahamut-anope:
--controller=irctest.controllers.bahamut \ --controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services' \
-n 10 \
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
charybdis: charybdis:
@ -181,28 +182,28 @@ ircu2:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.ircu2 \ --controller=irctest.controllers.ircu2 \
-m 'not services and not IRCv3' \ -m 'not services and not IRCv3' \
-n 4 \ -n 10 \
-k '$(IRCU2_SELECTORS)' -k '$(IRCU2_SELECTORS)'
nefarious: nefarious:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.nefarious \ --controller=irctest.controllers.nefarious \
-m 'not services' \ -m 'not services' \
-n 4 \ -n 10 \
-k '$(NEFARIOUS_SELECTORS)' -k '$(NEFARIOUS_SELECTORS)'
snircd: snircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.snircd \ --controller=irctest.controllers.snircd \
-m 'not services and not IRCv3' \ -m 'not services and not IRCv3' \
-n 4 \ -n 10 \
-k '$(SNIRCD_SELECTORS)' -k '$(SNIRCD_SELECTORS)'
irc2: irc2:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.irc2 \ --controller=irctest.controllers.irc2 \
-m 'not services and not IRCv3' \ -m 'not services and not IRCv3' \
-n 4 \ -n 10 \
-k '$(IRC2_SELECTORS)' -k '$(IRC2_SELECTORS)'
limnoria: limnoria:
@ -225,7 +226,7 @@ ngircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \ --controller irctest.controllers.ngircd \
-m 'not services' \ -m 'not services' \
-n 4 \ -n 10 \
-k "$(NGIRCD_SELECTORS)" -k "$(NGIRCD_SELECTORS)"
ngircd-anope: ngircd-anope:
@ -274,10 +275,3 @@ unrealircd-anope:
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'
unrealircd-dlk:
pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \
--services-controller=irctest.controllers.dlk_services \
-m 'services' \
-k '$(UNREALIRCD_SELECTORS)'

View File

@ -23,6 +23,7 @@ cd ~
git clone https://github.com/ProgVal/irctest.git git clone https://github.com/ProgVal/irctest.git
cd irctest cd irctest
pip3 install --user -r requirements.txt pip3 install --user -r requirements.txt
python3 setup.py install --user
``` ```
Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo) Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo)

View File

@ -1,27 +1,13 @@
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import multiprocessing
import os import os
from pathlib import Path
import shutil import shutil
import socket import socket
import subprocess import subprocess
import tempfile import tempfile
import textwrap
import time import time
from typing import ( from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
IO,
Any,
Callable,
Dict,
List,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
import irctest import irctest
@ -70,43 +56,9 @@ class _BaseController:
supported_sasl_mechanisms: Set[str] supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen] proc: Optional[subprocess.Popen]
_used_ports: Set[Tuple[str, int]]
"""``(hostname, port))`` used by this controller."""
# the following need to be shared between processes in case we are running in
# parallel (with pytest-xdist)
# The dicts are used as a set of (hostname, port), because _manager.set() doesn't
# exist.
_manager = multiprocessing.Manager()
_port_lock = _manager.Lock()
"""Lock for access to ``_all_used_ports`` and ``_available_ports``."""
_all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` used by all controllers."""
_available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` available to any controller."""
def __init__(self, test_config: TestCaseControllerConfig): def __init__(self, test_config: TestCaseControllerConfig):
self.test_config = test_config self.test_config = test_config
self.proc = None self.proc = None
self._used_ports = set()
def get_hostname_and_port(self) -> Tuple[str, int]:
with self._port_lock:
try:
# try to get a known available port
((hostname, port), _) = self._available_ports.popitem()
except KeyError:
# if there aren't any, iterate while we get a fresh one.
while True:
(hostname, port) = find_hostname_and_port()
if (hostname, port) not in self._all_used_ports:
# double-checking in self._used_ports to prevent collisions
# between controllers starting at the same time.
break
# Make this port unavailable to other processes
self._all_used_ports[(hostname, port)] = None
return (hostname, port)
def check_is_alive(self) -> None: def check_is_alive(self) -> None:
assert self.proc assert self.proc
@ -130,17 +82,12 @@ class _BaseController:
if self.proc: if self.proc:
self.kill_proc() self.kill_proc()
# move this controller's ports from _all_used_ports to _available_ports
for hostname, port in self._used_ports:
del self._all_used_ports[(hostname, port)]
self._available_ports[(hostname, port)] = None
class DirectoryBasedController(_BaseController): class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an """Helper for controllers whose software configuration is based on an
arbitrary directory.""" arbitrary directory."""
directory: Optional[Path] directory: Optional[str]
def __init__(self, test_config: TestCaseControllerConfig): def __init__(self, test_config: TestCaseControllerConfig):
super().__init__(test_config) super().__init__(test_config)
@ -163,21 +110,22 @@ class DirectoryBasedController(_BaseController):
"""Open a file in the configuration directory.""" """Open a file in the configuration directory."""
assert self.directory assert self.directory
if os.sep in name: if os.sep in name:
dir_ = self.directory / os.path.dirname(name) dir_ = os.path.join(self.directory, os.path.dirname(name))
dir_.mkdir(parents=True, exist_ok=True) if not os.path.isdir(dir_):
assert dir_.is_dir() os.makedirs(dir_)
return (self.directory / name).open(mode) assert os.path.isdir(dir_)
return open(os.path.join(self.directory, name), mode)
def create_config(self) -> None: def create_config(self) -> None:
if not self.directory: if not self.directory:
self.directory = Path(tempfile.mkdtemp()) self.directory = tempfile.mkdtemp()
def gen_ssl(self) -> None: def gen_ssl(self) -> None:
assert self.directory assert self.directory
self.csr_path = self.directory / "ssl.csr" self.csr_path = os.path.join(self.directory, "ssl.csr")
self.key_path = self.directory / "ssl.key" self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = self.directory / "ssl.pem" self.pem_path = os.path.join(self.directory, "ssl.pem")
self.dh_path = self.directory / "dh.pem" self.dh_path = os.path.join(self.directory, "dh.pem")
subprocess.check_output( subprocess.check_output(
[ [
self.openssl_bin, self.openssl_bin,
@ -208,18 +156,10 @@ class DirectoryBasedController(_BaseController):
], ],
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
with self.dh_path.open("w") as fd: subprocess.check_output(
fd.write( [self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
textwrap.dedent( stderr=subprocess.DEVNULL,
""" )
-----BEGIN DH PARAMETERS-----
MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD
HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr
rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC
-----END DH PARAMETERS-----
"""
)
)
class BaseClientController(_BaseController): class BaseClientController(_BaseController):
@ -253,6 +193,9 @@ class BaseServerController(_BaseController):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.faketime_enabled = False self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
def run( def run(
self, self,
hostname: str, hostname: str,
@ -261,6 +204,8 @@ class BaseServerController(_BaseController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]],
invalid_metadata_keys: Optional[Set[str]],
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
raise NotImplementedError() raise NotImplementedError()
@ -277,7 +222,6 @@ class BaseServerController(_BaseController):
raise NotImplementedByController("account registration") raise NotImplementedByController("account registration")
def wait_for_port(self) -> None: def wait_for_port(self) -> None:
started_at = time.time()
while not self.port_open: while not self.port_open:
self.check_is_alive() self.check_is_alive()
time.sleep(self._port_wait_interval) time.sleep(self._port_wait_interval)
@ -300,16 +244,11 @@ class BaseServerController(_BaseController):
# ircu2 cuts the connection without a message if registration # ircu2 cuts the connection without a message if registration
# is not complete. # is not complete.
pass pass
except socket.timeout:
# irc2 just keeps it open
pass
c.close() c.close()
self.port_open = True self.port_open = True
except ConnectionRefusedError: except Exception:
if time.time() - started_at >= 60: continue
# waited for 60 seconds, giving up
raise
def wait_for_services(self) -> None: def wait_for_services(self) -> None:
assert self.services_controller assert self.services_controller
@ -356,11 +295,10 @@ class BaseServicesController(_BaseController):
c.sendLine("PONG :" + msg.params[0]) c.sendLine("PONG :" + msg.params[0])
c.getMessages() c.getMessages()
timeout = time.time() + 3 timeout = time.time() + 5
while True: while True:
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help") c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP")
msgs = self.getNickServResponse(c)
msgs = self.getNickServResponse(c, timeout=1)
for msg in msgs: for msg in msgs:
if msg.command == "401": if msg.command == "401":
# NickServ not available yet # NickServ not available yet
@ -386,12 +324,11 @@ class BaseServicesController(_BaseController):
c.disconnect() c.disconnect()
self.services_up = True self.services_up = True
def getNickServResponse(self, client: Any, timeout: int = 0) -> List[Message]: def getNickServResponse(self, client: Any) -> List[Message]:
"""Wrapper aroung getMessages() that waits longer, because NickServ """Wrapper aroung getMessages() that waits longer, because NickServ
is queried asynchronously.""" is queried asynchronously."""
msgs: List[Message] = [] msgs: List[Message] = []
start_time = time.time() while not msgs:
while not msgs and (not timeout or start_time + timeout > time.time()):
time.sleep(0.05) time.sleep(0.05)
msgs = client.getMessages() msgs = client.getMessages()
return msgs return msgs

View File

@ -173,7 +173,7 @@ class _IrcTestCase(Generic[TController]):
) -> Optional[str]: ) -> Optional[str]:
"""Returns an error message if the message doesn't match the given arguments, """Returns an error message if the message doesn't match the given arguments,
or None if it matches.""" or None if it matches."""
for key, value in kwargs.items(): for (key, value) in kwargs.items():
if getattr(msg, key) != value: if getattr(msg, key) != value:
fail_msg = ( fail_msg = (
fail_msg or "expected {param} to be {expects}, got {got}: {msg}" fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
@ -351,8 +351,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
nick: Optional[str] = None nick: Optional[str] = None
user: Optional[List[str]] = None user: Optional[List[str]] = None
server: socket.socket server: socket.socket
protocol_version: Optional[str] protocol_version = Optional[str]
acked_capabilities: Optional[Set[str]] acked_capabilities = Optional[Set[str]]
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise __new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
@ -448,9 +448,7 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
print("{:.3f} S: {}".format(time.time(), line.strip())) print("{:.3f} S: {}".format(time.time(), line.strip()))
def readCapLs( def readCapLs(
self, self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
auth: Optional[Authentication] = None,
tls_config: Optional[tls.TlsConfig] = None,
) -> None: ) -> None:
(hostname, port) = self.server.getsockname() (hostname, port) = self.server.getsockname()
self.controller.run( self.controller.run(
@ -460,9 +458,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
m = self.getMessage() m = self.getMessage()
self.assertEqual(m.command, "CAP", "First message is not CAP LS.") self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
if m.params == ["LS"]: if m.params == ["LS"]:
self.protocol_version = "301" self.protocol_version = 301
elif m.params == ["LS", "302"]: elif m.params == ["LS", "302"]:
self.protocol_version = "302" self.protocol_version = 302
elif m.params == ["END"]: elif m.params == ["END"]:
self.protocol_version = None self.protocol_version = None
else: else:
@ -529,6 +527,8 @@ class BaseServerTestCase(
password: Optional[str] = None password: Optional[str] = None
ssl = False ssl = False
valid_metadata_keys: Set[str] = set()
invalid_metadata_keys: Set[str] = set()
server_support: Optional[Dict[str, Optional[str]]] server_support: Optional[Dict[str, Optional[str]]]
run_services = False run_services = False
@ -548,6 +548,8 @@ class BaseServerTestCase(
self.hostname, self.hostname,
self.port, self.port,
password=self.password, password=self.password,
valid_metadata_keys=self.valid_metadata_keys,
invalid_metadata_keys=self.invalid_metadata_keys,
ssl=self.ssl, ssl=self.ssl,
run_services=self.run_services, run_services=self.run_services,
faketime=self.faketime, faketime=self.faketime,
@ -687,7 +689,7 @@ class BaseServerTestCase(
def connectClient( def connectClient(
self, self,
nick: str, nick: str,
name: Optional[TClientName] = None, name: TClientName = None,
capabilities: Optional[List[str]] = None, capabilities: Optional[List[str]] = None,
skip_if_cap_nak: bool = False, skip_if_cap_nak: bool = False,
show_io: Optional[bool] = None, show_io: Optional[bool] = None,
@ -732,8 +734,8 @@ class BaseServerTestCase(
self.server_support[param] = None self.server_support[param] = None
welcome.append(m) welcome.append(m)
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment] self.targmax: Dict[str, Optional[str]] = dict(
item.split(":", 1) item.split(":", 1) # type: ignore
for item in (self.server_support.get("TARGMAX") or "").split(",") for item in (self.server_support.get("TARGMAX") or "").split(",")
if item if item
) )

View File

@ -74,19 +74,7 @@ class ClientMock:
continue continue
if not synchronize: if not synchronize:
got_pong = True got_pong = True
try: for line in data.decode().split("\r\n"):
decoded_data = data.decode()
except UnicodeDecodeError:
print(
"{time:.3f}{ssl} S -> {client} - failed to decode: {data!r}".format(
time=time.time(),
ssl=" (ssl)" if self.ssl else "",
client=self.name,
data=data,
)
)
raise
for line in decoded_data.split("\r\n"):
if line: if line:
if self.show_io: if self.show_io:
print( print(

View File

@ -1,4 +1,4 @@
from pathlib import Path import os
import shutil import shutil
import subprocess import subprocess
from typing import Type from typing import Type
@ -101,11 +101,14 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
pass pass
assert self.directory assert self.directory
services_path = shutil.which("services")
assert services_path
# Config and code need to be in the same directory, *obviously* # Config and code need to be in the same directory, *obviously*
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib") os.symlink(
os.path.join(
os.path.dirname(shutil.which("services")), "..", "lib" # type: ignore
),
os.path.join(self.directory, "lib"),
)
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
[ [

View File

@ -1,3 +1,4 @@
import os
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Type
@ -80,11 +81,11 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
"atheme-services", "atheme-services",
"-n", # don't fork "-n", # don't fork
"-c", "-c",
self.directory / "services.conf", os.path.join(self.directory, "services.conf"),
"-l", "-l",
f"/tmp/services-{server_port}.log", f"/tmp/services-{server_port}.log",
"-p", "-p",
self.directory / "services.pid", os.path.join(self.directory, "services.pid"),
"-D", "-D",
self.directory, self.directory,
], ],

View File

@ -1,9 +1,14 @@
from pathlib import Path import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Set, Type from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """ TEMPLATE_CONFIG = """
global {{ global {{
@ -75,19 +80,6 @@ oper {{
""" """
def initialize_entropy(directory: Path) -> None:
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/include/dh.h#L35-L38
nb_rand_bytes = 512 // 8
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/src/dh.c#L186
entropy_file_size = nb_rand_bytes * 4
# Not actually random; but we don't care.
entropy = b"\x00" * entropy_file_size
with (directory / ".ircd.entropy").open("wb") as fd:
fd.write(entropy)
class BahamutController(BaseServerController, DirectoryBasedController): class BahamutController(BaseServerController, DirectoryBasedController):
software_name = "Bahamut" software_name = "Bahamut"
supported_sasl_mechanisms: Set[str] = set() supported_sasl_mechanisms: Set[str] = set()
@ -107,14 +99,21 @@ class BahamutController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(unused_hostname, unused_port) = self.get_hostname_and_port() (unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port() (services_hostname, services_port) = find_hostname_and_port()
password_field = "passwd {};".format(password) if password else "" password_field = "passwd {};".format(password) if password else ""
@ -122,14 +121,9 @@ class BahamutController(BaseServerController, DirectoryBasedController):
assert self.directory assert self.directory
# Bahamut reads some bytes from /dev/urandom on startup, which causes
# GitHub Actions to sometimes freeze and timeout.
# This initializes the entropy file so Bahamut does not need to do it itself.
initialize_entropy(self.directory)
# they are hardcoded... thankfully Bahamut reads them from the CWD. # they are hardcoded... thankfully Bahamut reads them from the CWD.
shutil.copy(self.pem_path, self.directory / "ircd.crt") shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt"))
shutil.copy(self.key_path, self.directory / "ircd.key") shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key"))
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
@ -156,7 +150,7 @@ class BahamutController(BaseServerController, DirectoryBasedController):
"ircd", "ircd",
"-t", # don't fork "-t", # don't fork
"-f", "-f",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
], ],
) )

View File

@ -1,8 +1,14 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional from typing import Optional, Set
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_SSL_CONFIG = """ TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}"; ssl_private_key = "{key_path}";
@ -36,13 +42,19 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(services_hostname, services_port) = self.get_hostname_and_port() (services_hostname, services_port) = find_hostname_and_port()
password_field = 'password = "{}";'.format(password) if password else "" password_field = 'password = "{}";'.format(password) if password else ""
if ssl: if ssl:
self.gen_ssl() self.gen_ssl()
@ -76,9 +88,9 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
self.binary_name, self.binary_name,
"-foreground", "-foreground",
"-configfile", "-configfile",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
"-pidfile", "-pidfile",
self.directory / "server.pid", os.path.join(self.directory, "server.pid"),
], ],
# stderr=subprocess.DEVNULL, # stderr=subprocess.DEVNULL,
) )

View File

@ -1,245 +0,0 @@
import os
from pathlib import Path
import secrets
import subprocess
from typing import Optional, Type
import irctest
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
import irctest.cases
import irctest.runner
TEMPLATE_DLK_CONFIG = """\
info {{
SID "00A";
network-name "testnetwork";
services-name "services.example.org";
admin-email "admin@example.org";
}}
link {{
hostname "{server_hostname}";
port "{server_port}";
password "password";
}}
log {{
debug "yes";
}}
sql {{
port "3306";
username "pifpaf";
password "pifpaf";
database "pifpaf";
sockfile "{mysql_socket}";
prefix "{dlk_prefix}";
}}
wordpress {{
prefix "{wp_prefix}";
}}
"""
TEMPLATE_DLK_WP_CONFIG = """
<?php
global $wpconfig;
$wpconfig = [
"dbprefix" => "{wp_prefix}",
"default_avatar" => "https://valware.uk/wp-content/plugins/ultimate-member/assets/img/default_avatar.jpg",
"forumschan" => "#DLK-Support",
];
"""
TEMPLATE_WP_CONFIG = """
define( 'DB_NAME', 'pifpaf' );
define( 'DB_USER', 'pifpaf' );
define( 'DB_PASSWORD', 'pifpaf' );
define( 'DB_HOST', 'localhost:{mysql_socket}' );
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );
define( 'AUTH_KEY', 'put your unique phrase here' );
define( 'SECURE_AUTH_KEY', 'put your unique phrase here' );
define( 'LOGGED_IN_KEY', 'put your unique phrase here' );
define( 'NONCE_KEY', 'put your unique phrase here' );
define( 'AUTH_SALT', 'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT', 'put your unique phrase here' );
define( 'NONCE_SALT', 'put your unique phrase here' );
$table_prefix = '{wp_prefix}';
define( 'WP_DEBUG', false );
if (!defined('ABSPATH')) {{
define( 'ABSPATH', '{wp_path}' );
}}
/* That's all, stop editing! Happy publishing. */
/** Absolute path to the WordPress directory. */
/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';
"""
class DlkController(BaseServicesController, DirectoryBasedController):
"""Mixin for server controllers that rely on DLK"""
software_name = "Dlk-Services"
def run_sql(self, sql: str) -> None:
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
subprocess.run(
["mysql", "-S", mysql_socket, "pifpaf"],
input=sql.encode(),
check=True,
)
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
self.create_config()
if protocol == "unreal4":
protocol = "unreal5"
assert protocol in ("unreal5",), protocol
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
assert self.directory
try:
self.wp_cli_path = Path(os.environ["IRCTEST_WP_CLI_PATH"])
if not self.wp_cli_path.is_file():
raise KeyError()
except KeyError:
raise RuntimeError(
"$IRCTEST_WP_CLI_PATH must be set to a WP-CLI executable (eg. "
"downloaded from <https://raw.githubusercontent.com/wp-cli/builds/"
"gh-pages/phar/wp-cli.phar>)"
) from None
try:
self.dlk_path = Path(os.environ["IRCTEST_DLK_PATH"])
if not self.dlk_path.is_dir():
raise KeyError()
except KeyError:
raise RuntimeError("$IRCTEST_DLK_PATH is not set") from None
self.dlk_path = self.dlk_path.resolve()
# Unpack a fresh Wordpress install in the temporary directory.
# In theory we could have a common Wordpress install and only wp-config.php
# in the temporary directory; but wp-cli assumes wp-config.php must be
# in a Wordpress directory, and fails in various places if it isn't.
# Rather than symlinking everything to make it work, let's just copy
# the whole code, it's not that big.
try:
wp_zip_path = Path(os.environ["IRCTEST_WP_ZIP_PATH"])
if not wp_zip_path.is_file():
raise KeyError()
except KeyError:
raise RuntimeError(
"$IRCTEST_WP_ZIP_PATH must be set to a Wordpress source zipball "
"(eg. downloaded from <https://wordpress.org/latest.zip>)"
) from None
subprocess.run(
["unzip", wp_zip_path, "-d", self.directory], stdout=subprocess.DEVNULL
)
self.wp_path = self.directory / "wordpress"
rand_hex = secrets.token_hex(6)
self.wp_prefix = f"wp{rand_hex}_"
self.dlk_prefix = f"dlk{rand_hex}_"
template_vars = dict(
protocol=protocol,
server_hostname=server_hostname,
server_port=server_port,
mysql_socket=mysql_socket,
wp_path=self.wp_path,
wp_prefix=self.wp_prefix,
dlk_prefix=self.dlk_prefix,
)
# Configure Wordpress
wp_config_path = self.directory / "wp-config.php"
with open(wp_config_path, "w") as fd:
fd.write(TEMPLATE_WP_CONFIG.format(**template_vars))
subprocess.run(
[
"php",
self.wp_cli_path,
"core",
"install",
"--url=http://localhost/",
"--title=irctest site",
"--admin_user=adminuser",
"--admin_email=adminuser@example.org",
f"--path={self.wp_path}",
],
check=True,
)
# Configure Dlk
dlk_log_dir = self.directory / "logs"
dlk_conf_dir = self.directory / "conf"
dlk_conf_path = dlk_conf_dir / "dalek.conf"
os.mkdir(dlk_conf_dir)
with open(dlk_conf_path, "w") as fd:
fd.write(TEMPLATE_DLK_CONFIG.format(**template_vars))
dlk_wp_config_path = dlk_conf_dir / "wordpress.conf"
with open(dlk_wp_config_path, "w") as fd:
fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars))
(dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf")
self.proc = subprocess.Popen(
[
"php",
"src/dalek",
],
cwd=self.dlk_path,
env={
**os.environ,
"DALEK_CONF_DIR": str(dlk_conf_dir),
"DALEK_LOG_DIR": str(dlk_log_dir),
},
)
def terminate(self) -> None:
super().terminate()
def kill(self) -> None:
super().kill()
def registerUser(
self,
case: irctest.cases.BaseServerTestCase,
username: str,
password: Optional[str] = None,
) -> None:
assert password
subprocess.run(
[
"php",
self.wp_cli_path,
"user",
"create",
username,
f"{username}@example.org",
f"--user_pass={password}",
f"--path={self.wp_path}",
],
check=True,
)
def get_irctest_controller_class() -> Type[DlkController]:
return DlkController

View File

@ -3,9 +3,13 @@ import json
import os import os
import shutil import shutil
import subprocess import subprocess
from typing import Any, Dict, Optional, Type, Union from typing import Any, Dict, List, Optional, Set, Type, Union
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.cases import BaseServerTestCase from irctest.cases import BaseServerTestCase
BASE_CONFIG = { BASE_CONFIG = {
@ -135,6 +139,7 @@ class ErgoController(BaseServerController, DirectoryBasedController):
supported_sasl_mechanisms = {"PLAIN", "SCRAM-SHA-256"} supported_sasl_mechanisms = {"PLAIN", "SCRAM-SHA-256"}
supports_sts = True supports_sts = True
extban_mute_char = "m" extban_mute_char = "m"
mysql_proc: Optional[subprocess.Popen] = None
def create_config(self) -> None: def create_config(self) -> None:
super().create_config() super().create_config()
@ -149,9 +154,17 @@ class ErgoController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
config: Optional[Any] = None, config: Optional[Any] = None,
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
self.create_config() self.create_config()
if config is None: if config is None:
config = copy.deepcopy(BASE_CONFIG) config = copy.deepcopy(BASE_CONFIG)
@ -161,7 +174,7 @@ class ErgoController(BaseServerController, DirectoryBasedController):
enable_chathistory = self.test_config.chathistory enable_chathistory = self.test_config.chathistory
enable_roleplay = self.test_config.ergo_roleplay enable_roleplay = self.test_config.ergo_roleplay
if enable_chathistory or enable_roleplay: if enable_chathistory or enable_roleplay:
config = self.addMysqlToConfig(config) self.addDatabaseToConfig(config)
if enable_roleplay: if enable_roleplay:
config["roleplay"] = {"enabled": True} config["roleplay"] = {"enabled": True}
@ -173,19 +186,21 @@ class ErgoController(BaseServerController, DirectoryBasedController):
bind_address = "127.0.0.1:%s" % (port,) bind_address = "127.0.0.1:%s" % (port,)
listener_conf = None # plaintext listener_conf = None # plaintext
if ssl: if ssl:
self.key_path = self.directory / "ssl.key" self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = self.directory / "ssl.pem" self.pem_path = os.path.join(self.directory, "ssl.pem")
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}} listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
config["server"]["listeners"][bind_address] = listener_conf # type: ignore config["server"]["listeners"][bind_address] = listener_conf # type: ignore
config["datastore"]["path"] = str(self.directory / "ircd.db") # type: ignore config["datastore"]["path"] = os.path.join( # type: ignore
self.directory, "ircd.db"
)
if password is not None: if password is not None:
config["server"]["password"] = hash_password(password) # type: ignore config["server"]["password"] = hash_password(password) # type: ignore
assert self.proc is None assert self.proc is None
self._config_path = self.directory / "server.yml" self._config_path = os.path.join(self.directory, "server.yml")
self._config = config self._config = config
self._write_config() self._write_config()
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"]) subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])
@ -201,6 +216,16 @@ class ErgoController(BaseServerController, DirectoryBasedController):
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"] [*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
) )
def terminate(self) -> None:
if self.mysql_proc is not None:
self.mysql_proc.terminate()
super().terminate()
def kill(self) -> None:
if self.mysql_proc is not None:
self.mysql_proc.kill()
super().kill()
def wait_for_services(self) -> None: def wait_for_services(self) -> None:
# Nothing to wait for, they start at the same time as Ergo. # Nothing to wait for, they start at the same time as Ergo.
pass pass
@ -252,32 +277,107 @@ class ErgoController(BaseServerController, DirectoryBasedController):
config.update(LOGGING_CONFIG) config.update(LOGGING_CONFIG)
return config return config
def addMysqlToConfig(self, config: Optional[Dict] = None) -> Dict: def addDatabaseToConfig(self, config: Dict) -> None:
mysql_password = os.getenv("MYSQL_PASSWORD") history_backend = os.environ.get("ERGO_HISTORY_BACKEND", "memory")
if config is None: if history_backend == "memory":
config = self.baseConfig() # nothing to do, this is the default
if not mysql_password: pass
return config elif history_backend == "mysql":
config["datastore"]["mysql"] = { socket_path = self.startMysql()
"enabled": True, self.createMysqlDatabase(socket_path, "ergo_history")
"host": "localhost", config["datastore"]["mysql"] = {
"user": "ergo", "enabled": True,
"password": mysql_password, "socket-path": socket_path,
"history-database": "ergo_history", "history-database": "ergo_history",
"timeout": "3s", "timeout": "3s",
} }
config["accounts"]["multiclient"] = { config["history"]["persistent"] = {
"enabled": True, "enabled": True,
"allowed-by-default": True, "unregistered-channels": True,
"always-on": "disabled", "registered-channels": "opt-out",
} "direct-messages": "opt-out",
config["history"]["persistent"] = { }
"enabled": True, else:
"unregistered-channels": True, raise ValueError(
"registered-channels": "opt-out", f"Invalid $ERGO_HISTORY_BACKEND value: {history_backend}. "
"direct-messages": "opt-out", f"It should be 'memory' (the default) or 'mysql'"
} )
return config
def startMysql(self) -> str:
"""Starts a new MySQL server listening on a UNIX socket, returns the socket
path"""
# Function based on pifpaf's MySQL driver:
# https://github.com/jd/pifpaf/blob/3.1.5/pifpaf/drivers/mysql.py
assert self.directory
mysql_dir = os.path.join(self.directory, "mysql")
socket_path = os.path.join(mysql_dir, "mysql.socket")
os.mkdir(mysql_dir)
print("Starting MySQL...")
try:
subprocess.check_call(
[
"mysqld",
"--no-defaults",
"--tmpdir=" + mysql_dir,
"--initialize-insecure",
"--datadir=" + mysql_dir,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
# Initialize the old way
subprocess.check_call(
[
"mysql_install_db",
"--no-defaults",
"--tmpdir=" + mysql_dir,
"--datadir=" + mysql_dir,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
self.mysql_proc = subprocess.Popen(
[
"mysqld",
"--no-defaults",
"--tmpdir=" + mysql_dir,
"--datadir=" + mysql_dir,
"--socket=" + socket_path,
"--skip-networking",
"--skip-grant-tables",
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
mysql_stdout = self.mysql_proc.stdout
assert mysql_stdout is not None # for mypy...
lines: List[bytes] = []
while self.mysql_proc.returncode is None:
line = mysql_stdout.readline()
lines.append(lines)
if b"mysqld: ready for connections." in line:
break
assert self.mysql_proc.returncode is None, (
"MySQL unexpected stopped: " + b"\n".join(lines).decode()
)
print("MySQL started")
return socket_path
def createMysqlDatabase(self, socket_path: str, database_name: str) -> None:
subprocess.check_call(
[
"mysql",
"--no-defaults",
"-S",
socket_path,
"-e",
f"CREATE DATABASE {database_name};",
]
)
def rehash(self, case: BaseServerTestCase, config: Dict) -> None: def rehash(self, case: BaseServerTestCase, config: Dict) -> None:
self._config = config self._config = config

View File

@ -1,5 +1,5 @@
import os import os
from typing import Optional, Tuple, Type from typing import Optional, Set, Tuple, Type
from irctest.basecontrollers import BaseServerController from irctest.basecontrollers import BaseServerController
@ -39,6 +39,9 @@ class ExternalServerController(BaseServerController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
pass pass

View File

@ -1,9 +1,14 @@
import functools import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """ TEMPLATE_CONFIG = """
# Clients: # Clients:
@ -76,8 +81,8 @@ TEMPLATE_CONFIG = """
# HELP/HELPOP # HELP/HELPOP
<module name="alias"> # for the HELP alias <module name="alias"> # for the HELP alias
<module name="{help_module_name}"> <module name="helpop">
<include file="examples/{help_module_name}.conf.example"> <include file="examples/helpop.conf.example">
# Misc: # Misc:
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log"> <log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
@ -90,17 +95,6 @@ TEMPLATE_SSL_CONFIG = """
""" """
@functools.lru_cache()
def installed_version() -> int:
output = subprocess.check_output(["inspircd", "--version"], universal_newlines=True)
if output.startswith("InspIRCd-3"):
return 3
if output.startswith("InspIRCd-4"):
return 4
else:
assert False, f"unexpected version: {output}"
class InspircdController(BaseServerController, DirectoryBasedController): class InspircdController(BaseServerController, DirectoryBasedController):
software_name = "InspIRCd" software_name = "InspIRCd"
supported_sasl_mechanisms = {"PLAIN"} supported_sasl_mechanisms = {"PLAIN"}
@ -120,13 +114,20 @@ class InspircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str] = None, faketime: Optional[str] = None,
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(services_hostname, services_port) = self.get_hostname_and_port() (services_hostname, services_port) = find_hostname_and_port()
password_field = 'password="{}"'.format(password) if password else "" password_field = 'password="{}"'.format(password) if password else ""
@ -138,13 +139,6 @@ class InspircdController(BaseServerController, DirectoryBasedController):
else: else:
ssl_config = "" ssl_config = ""
if installed_version() == 3:
help_module_name = "helpop"
elif installed_version() == 4:
help_module_name = "help"
else:
assert False, f"unexpected version: {installed_version()}"
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
TEMPLATE_CONFIG.format( TEMPLATE_CONFIG.format(
@ -154,7 +148,6 @@ class InspircdController(BaseServerController, DirectoryBasedController):
services_port=services_port, services_port=services_port,
password_field=password_field, password_field=password_field,
ssl_config=ssl_config, ssl_config=ssl_config,
help_module_name=help_module_name,
) )
) )
assert self.directory assert self.directory
@ -171,7 +164,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
"inspircd", "inspircd",
"--nofork", "--nofork",
"--config", "--config",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
], ],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
) )

View File

@ -1,6 +1,7 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -49,8 +50,14 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: if run_services:
@ -61,7 +68,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
self.create_config() self.create_config()
password_field = password if password else "" password_field = password if password else ""
assert self.directory assert self.directory
pidfile = self.directory / "ircd.pid" pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
TEMPLATE_CONFIG.format( TEMPLATE_CONFIG.format(
@ -86,7 +93,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
"-p", "-p",
"on", "on",
"-f", "-f",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
], ],
# stderr=subprocess.DEVNULL, # stderr=subprocess.DEVNULL,
) )

View File

@ -1,6 +1,7 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -68,8 +69,14 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: if run_services:
@ -80,7 +87,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
self.create_config() self.create_config()
password_field = 'password = "{}";'.format(password) if password else "" password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory assert self.directory
pidfile = self.directory / "ircd.pid" pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
TEMPLATE_CONFIG.format( TEMPLATE_CONFIG.format(
@ -103,7 +110,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
"ircd", "ircd",
"-n", # don't detach "-n", # don't detach
"-f", "-f",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
"-x", "-x",
"DEBUG", "DEBUG",
], ],

View File

@ -1,3 +1,4 @@
import os
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Type
@ -84,7 +85,9 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
) )
) )
assert self.directory assert self.directory
self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"]) self.proc = subprocess.Popen(
["supybot", os.path.join(self.directory, "bot.conf")]
)
def get_irctest_controller_class() -> Type[LimnoriaController]: def get_irctest_controller_class() -> Type[LimnoriaController]:

View File

@ -1,3 +1,4 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Set, Type from typing import Optional, Set, Type
@ -33,10 +34,10 @@ extensions:
- mammon.ext.ircv3.sasl - mammon.ext.ircv3.sasl
- mammon.ext.misc.nopost - mammon.ext.misc.nopost
metadata: metadata:
restricted_keys: [] restricted_keys:
{restricted_keys}
whitelist: whitelist:
- display-name {authorized_keys}
- avatar
monitor: monitor:
limit: 20 limit: 20
motd: motd:
@ -89,6 +90,9 @@ class MammonController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if password is not None: if password is not None:
@ -104,6 +108,8 @@ class MammonController(BaseServerController, DirectoryBasedController):
directory=self.directory, directory=self.directory,
hostname=hostname, hostname=hostname,
port=port, port=port,
authorized_keys=make_list(valid_metadata_keys or set()),
restricted_keys=make_list(restricted_metadata_keys or set()),
) )
) )
# with self.open_file('server.yml', 'r') as fd: # with self.open_file('server.yml', 'r') as fd:
@ -122,7 +128,7 @@ class MammonController(BaseServerController, DirectoryBasedController):
"mammond", "mammond",
"--nofork", # '--debug', "--nofork", # '--debug',
"--config", "--config",
self.directory / "server.yml", os.path.join(self.directory, "server.yml"),
] ]
) )

View File

@ -1,8 +1,14 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Set, Type from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """ TEMPLATE_CONFIG = """
[Global] [Global]
@ -48,13 +54,20 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(unused_hostname, unused_port) = self.get_hostname_and_port() (unused_hostname, unused_port) = find_hostname_and_port()
password_field = "Password = {}".format(password) if password else "" password_field = "Password = {}".format(password) if password else ""
@ -81,7 +94,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password_field=password_field, password_field=password_field,
key_path=self.key_path, key_path=self.key_path,
pem_path=self.pem_path, pem_path=self.pem_path,
empty_file=self.directory / "empty.txt", empty_file=os.path.join(self.directory, "empty.txt"),
) )
) )
@ -97,7 +110,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
"ngircd", "ngircd",
"--nodaemon", "--nodaemon",
"--config", "--config",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
], ],
# stdout=subprocess.DEVNULL, # stdout=subprocess.DEVNULL,
) )

View File

@ -1,6 +1,7 @@
import os
import shutil import shutil
import subprocess import subprocess
from typing import Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -67,8 +68,14 @@ class SnircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl: if ssl:
raise NotImplementedByController("TLS") raise NotImplementedByController("TLS")
if run_services: if run_services:
@ -79,7 +86,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
self.create_config() self.create_config()
password_field = 'password = "{}";'.format(password) if password else "" password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory assert self.directory
pidfile = self.directory / "ircd.pid" pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
TEMPLATE_CONFIG.format( TEMPLATE_CONFIG.format(
@ -102,7 +109,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
"ircd", "ircd",
"-n", # don't detach "-n", # don't detach
"-f", "-f",
self.directory / "server.conf", os.path.join(self.directory, "server.conf"),
"-x", "-x",
"DEBUG", "DEBUG",
], ],

View File

@ -1,4 +1,4 @@
from pathlib import Path import os
import subprocess import subprocess
import tempfile import tempfile
from typing import Optional, TextIO, Type, cast from typing import Optional, TextIO, Type, cast
@ -38,14 +38,14 @@ class SopelController(BaseClientController):
super().kill() super().kill()
if self.filename: if self.filename:
try: try:
(Path("~/.sopel/").expanduser() / self.filename).unlink() os.unlink(os.path.join(os.path.expanduser("~/.sopel/"), self.filename))
except OSError: # File does not exist except OSError: #  File does not exist
pass pass
def open_file(self, filename: str, mode: str = "a") -> TextIO: def open_file(self, filename: str, mode: str = "a") -> TextIO:
dir_path = Path("~/.sopel/").expanduser() dir_path = os.path.expanduser("~/.sopel/")
dir_path.mkdir(parents=True, exist_ok=True) os.makedirs(dir_path, exist_ok=True)
return cast(TextIO, (dir_path / filename).open(mode)) return cast(TextIO, open(os.path.join(dir_path, filename), mode))
def create_config(self) -> None: def create_config(self) -> None:
with self.open_file(self.filename): with self.open_file(self.filename):
@ -73,7 +73,7 @@ class SopelController(BaseClientController):
auth_method="auth_method = sasl" if auth else "", auth_method="auth_method = sasl" if auth else "",
) )
) )
self.proc = subprocess.Popen(["sopel", "-c", self.filename]) self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename])
def get_irctest_controller_class() -> Type[SopelController]: def get_irctest_controller_class() -> Type[SopelController]:

View File

@ -1,17 +1,21 @@
import contextlib
import fcntl
import functools import functools
from pathlib import Path import os
import pathlib
import shutil import shutil
import signal
import subprocess import subprocess
import textwrap import textwrap
from typing import Callable, ContextManager, Iterator, Optional, Type from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """ TEMPLATE_CONFIG = """
include "modules.default.conf"; include "modules.default.conf";
loadmodule "third/metadata2";
include "operclass.default.conf"; include "operclass.default.conf";
{extras} {extras}
include "help/help.conf"; include "help/help.conf";
@ -97,7 +101,7 @@ set {{
}} }}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_v6only} {set_extras}
}} }}
@ -120,53 +124,6 @@ oper "operuser" {{
}} }}
""" """
SET_V6ONLY = """
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
plaintext-policy {
server warn; // https://www.unrealircd.org/docs/FAQ#server-requires-tls
oper warn; // https://www.unrealircd.org/docs/FAQ#oper-requires-tls
}
anti-flood {
everyone {
connect-flood 255:10;
}
}
"""
def _filelock(path: Path) -> Callable[[], ContextManager]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@contextlib.contextmanager
def f() -> Iterator[None]:
with open(path, "a") as fd:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
return f
_UNREALIRCD_BIN = shutil.which("unrealircd")
if _UNREALIRCD_BIN:
_UNREALIRCD_PREFIX = Path(_UNREALIRCD_BIN).parent.parent
# Try to keep that lock file specific to this Unrealircd instance
_LOCK_PATH = _UNREALIRCD_PREFIX / "irctest-unrealircd-startstop.lock"
else:
# unrealircd not found; we are probably going to crash later anyway...
_LOCK_PATH = Path("/tmp/irctest-unrealircd-startstop.lock")
_STARTSTOP_LOCK = _filelock(_LOCK_PATH)
"""
Unreal cleans its tmp/ directory after each run, which prevents
multiple processes from starting/stopping at the same time.
"""
@functools.lru_cache() @functools.lru_cache()
def installed_version() -> int: def installed_version() -> int:
@ -200,33 +157,24 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
password: Optional[str], password: Optional[str],
ssl: bool, ssl: bool,
run_services: bool, run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str], faketime: Optional[str],
) -> None: ) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None assert self.proc is None
self.port = port self.port = port
self.hostname = hostname self.hostname = hostname
self.create_config() self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
if installed_version() >= 6: (services_hostname, services_port) = find_hostname_and_port()
extras = textwrap.dedent(
"""
include "snomasks.default.conf";
loadmodule "cloak_md5";
"""
)
set_v6only = SET_V6ONLY
else:
extras = ""
set_v6only = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
password_field = 'password "{}";'.format(password) if password else "" password_field = 'password "{}";'.format(password) if password else ""
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
self.gen_ssl() self.gen_ssl()
if ssl: if ssl:
(tls_hostname, tls_port) = (hostname, port) (tls_hostname, tls_port) = (hostname, port)
@ -235,6 +183,31 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
# Unreal refuses to start without TLS enabled # Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port) (tls_hostname, tls_port) = (unused_hostname, unused_port)
if installed_version() >= 6:
extras = textwrap.dedent(
"""
include "snomasks.default.conf";
loadmodule "cloak_md5";
"""
)
set_extras = textwrap.indent(
textwrap.dedent(
"""
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
"""
),
" ",
)
else:
extras = ""
set_extras = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
assert self.directory assert self.directory
with self.open_file("unrealircd.conf") as fd: with self.open_file("unrealircd.conf") as fd:
@ -249,33 +222,49 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
password_field=password_field, password_field=password_field,
key_path=self.key_path, key_path=self.key_path,
pem_path=self.pem_path, pem_path=self.pem_path,
empty_file=self.directory / "empty.txt", empty_file=os.path.join(self.directory, "empty.txt"),
set_v6only=set_v6only,
extras=extras, extras=extras,
set_extras=set_extras,
) )
) )
proot_cmd = []
self.using_proot = False
if shutil.which("proot"):
unrealircd_path = shutil.which("unrealircd")
if unrealircd_path:
unrealircd_prefix = pathlib.Path(unrealircd_path).parents[1]
tmpdir = os.path.join(self.directory, "tmp")
os.mkdir(tmpdir)
# Unreal cleans its tmp/ directory after each run, which prevents
# multiple processes from running at the same time.
# Using PRoot, we can isolate them, with a tmp/ directory for each
# process, so they don't interfere with each other, allowing use of
# the -n option (of pytest-xdist) to speed-up tests
proot_cmd = ["proot", "-b", f"{tmpdir}:{unrealircd_prefix}/tmp"]
self.using_proot = True
if faketime and shutil.which("faketime"): if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime] faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True self.faketime_enabled = True
else: else:
faketime_cmd = [] faketime_cmd = []
with _STARTSTOP_LOCK(): self.proc = subprocess.Popen(
self.proc = subprocess.Popen( [
[ *proot_cmd,
*faketime_cmd, *faketime_cmd,
"unrealircd", "unrealircd",
"-t", "-t",
"-F", # BOOT_NOFORK "-F", # BOOT_NOFORK
"-f", "-f",
self.directory / "unrealircd.conf", os.path.join(self.directory, "unrealircd.conf"),
], ],
# stdout=subprocess.DEVNULL, # stdout=subprocess.DEVNULL,
) )
self.wait_for_port()
if run_services: if run_services:
self.wait_for_port()
self.services_controller = self.services_controller_class( self.services_controller = self.services_controller_class(
self.test_config, self self.test_config, self
) )
@ -285,13 +274,17 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
server_port=services_port, server_port=services_port,
) )
def kill_proc(self) -> None: def kill(self) -> None:
assert self.proc if self.using_proot:
# Kill grandchild process, instead of killing proot, which takes more
with _STARTSTOP_LOCK(): # time (and does not seem to always work)
self.proc.kill() assert self.proc is not None
self.proc.wait(5) # wait for it to actually die output = subprocess.check_output(
self.proc = None ["ps", "-opid", "--no-headers", "--ppid", str(self.proc.pid)]
)
(grandchild_pid,) = [int(line) for line in output.decode().split()]
os.kill(grandchild_pid, signal.SIGKILL)
super().kill()
def get_irctest_controller_class() -> Type[UnrealircdController]: def get_irctest_controller_class() -> Type[UnrealircdController]:

View File

@ -39,7 +39,7 @@ class CaseResult:
type: Optional[str] = None type: Optional[str] = None
message: Optional[str] = None message: Optional[str] = None
def output_filename(self) -> str: def output_filename(self):
test_name = self.test_name test_name = self.test_name
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST: if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
# File name too long or otherwise invalid. This should be good enough: # File name too long or otherwise invalid. This should be good enough:
@ -75,7 +75,7 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
skipped = False skipped = False
details = None details = None
system_out = None system_out = None
extra: Dict[str, str] = {} extra = {}
for child in case: for child in case:
if child.tag == "skipped": if child.tag == "skipped":
success = True success = True
@ -173,7 +173,6 @@ def build_module_html(
def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
multiple_modules = len({r.module_name for r in results}) > 1
results_by_module_and_class = group_by( results_by_module_and_class = group_by(
results, lambda r: (r.module_name, r.class_name) results, lambda r: (r.module_name, r.class_name)
) )
@ -187,32 +186,22 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
ET.SubElement(ET.SubElement(cell, "div"), "span").text = job ET.SubElement(ET.SubElement(cell, "div"), "span").text = job
cell.set("class", "job-name") cell.set("class", "job-name")
for (module_name, class_name), class_results in sorted( for ((module_name, class_name), class_results) in sorted(
results_by_module_and_class.items() results_by_module_and_class.items()
): ):
if multiple_modules:
# if the page shows classes from various modules, use the fully-qualified
# name in order to disambiguate and be clearer (eg. show
# "irctest.server_tests.extended_join.MetadataTestCase" instead of just
# "MetadataTestCase" which looks like it's about IRCv3's METADATA spec.
qualified_class_name = f"{module_name}.{class_name}"
else:
# otherwise, it's not needed, so let's not display it
qualified_class_name = class_name
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
# Header row: class name # Header row: class name
header_row = ET.SubElement(table, "tr") header_row = ET.SubElement(table, "tr")
th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1))
row_anchor = f"{qualified_class_name}" row_anchor = f"{class_name}"
section_header = ET.SubElement( section_header = ET.SubElement(
ET.SubElement(th, "h2"), ET.SubElement(th, "h2"),
"a", "a",
href=f"#{row_anchor}", href=f"#{row_anchor}",
id=row_anchor, id=row_anchor,
) )
section_header.text = qualified_class_name section_header.text = class_name
append_docstring(th, getattr(module, class_name)) append_docstring(th, getattr(module, class_name))
# Header row: one column for each implementation # Header row: one column for each implementation
@ -220,8 +209,8 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
# One row for each test: # One row for each test:
results_by_test = group_by(class_results, key=lambda r: r.test_name) results_by_test = group_by(class_results, key=lambda r: r.test_name)
for test_name, test_results in sorted(results_by_test.items()): for (test_name, test_results) in sorted(results_by_test.items()):
row_anchor = f"{qualified_class_name}.{test_name}" row_anchor = f"{class_name}.{test_name}"
if len(row_anchor) >= 50: if len(row_anchor) >= 50:
# Too long; give up on generating readable URL # Too long; give up on generating readable URL
# TODO: only hash test parameter # TODO: only hash test parameter
@ -303,7 +292,7 @@ def write_html_pages(
for result in results for result in results
) )
assert is_client != is_server, (job, is_client, is_server) assert is_client != is_server, (job, is_client, is_server)
if job.endswith(("-atheme", "-anope", "-dlk")): if job.endswith(("-atheme", "-anope")):
assert is_server assert is_server
job_categories[job] = "server-with-services" job_categories[job] = "server-with-services"
elif is_server: elif is_server:
@ -314,7 +303,7 @@ def write_html_pages(
pages = [] pages = []
for module_name, module_results in sorted(results_by_module.items()): for (module_name, module_results) in sorted(results_by_module.items()):
# Filter out client jobs if this is a server test module, and vice versa # Filter out client jobs if this is a server test module, and vice versa
module_categories = { module_categories = {
job_categories[result.job] job_categories[result.job]
@ -366,7 +355,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
module_pages = [] module_pages = []
job_pages = [] job_pages = []
for page_type, title, file_name in sorted(pages): for (page_type, title, file_name) in sorted(pages):
if page_type == "module": if page_type == "module":
module_pages.append((title, file_name)) module_pages.append((title, file_name))
elif page_type == "job": elif page_type == "job":
@ -379,7 +368,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
dl = ET.SubElement(body, "dl") dl = ET.SubElement(body, "dl")
dl.set("class", "module-index") dl.set("class", "module-index")
for module_name, file_name in sorted(module_pages): for (module_name, file_name) in sorted(module_pages):
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}") link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}")
@ -391,7 +380,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
ul = ET.SubElement(body, "ul") ul = ET.SubElement(body, "ul")
ul.set("class", "job-index") ul.set("class", "job-index")
for job, file_name in sorted(job_pages): for (job, file_name) in sorted(job_pages):
link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}")
link.text = job link.text = job

View File

@ -18,7 +18,7 @@ class Artifact:
download_url: str download_url: str
@property @property
def public_download_url(self) -> str: def public_download_url(self):
# GitHub API is not available publicly for artifacts, we need to use # GitHub API is not available publicly for artifacts, we need to use
# a third-party proxy to access it... # a third-party proxy to access it...
name = urllib.parse.quote(self.name) name = urllib.parse.quote(self.name)

View File

@ -152,7 +152,7 @@ def match_dict(
# Set to not-None if we find a Keys() operator in the dict keys # Set to not-None if we find a Keys() operator in the dict keys
remaining_keys_wildcard = None remaining_keys_wildcard = None
for expected_key, expected_value in expected.items(): for (expected_key, expected_value) in expected.items():
if isinstance(expected_key, RemainingKeys): if isinstance(expected_key, RemainingKeys):
remaining_keys_wildcard = (expected_key.key, expected_value) remaining_keys_wildcard = (expected_key.key, expected_value)
else: else:
@ -168,7 +168,7 @@ def match_dict(
if remaining_keys_wildcard: if remaining_keys_wildcard:
(expected_key, expected_value) = remaining_keys_wildcard (expected_key, expected_value) = remaining_keys_wildcard
for key, value in got.items(): for (key, value) in got.items():
if not match_string(key, expected_key): if not match_string(key, expected_key):
return False return False
if not match_string(value, expected_value): if not match_string(value, expected_value):

View File

@ -9,7 +9,6 @@ from irctest.patma import ANYSTR
REGISTER_CAP_NAME = "draft/account-registration" REGISTER_CAP_NAME = "draft/account-registration"
@cases.mark_services
@cases.mark_specifications("IRCv3") @cases.mark_specifications("IRCv3")
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase): class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
@staticmethod @staticmethod
@ -34,7 +33,6 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR]) self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
@cases.mark_services
@cases.mark_specifications("IRCv3") @cases.mark_specifications("IRCv3")
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase): class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
@staticmethod @staticmethod
@ -62,7 +60,6 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
) )
@cases.mark_services
@cases.mark_specifications("IRCv3") @cases.mark_specifications("IRCv3")
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase): class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
@staticmethod @staticmethod
@ -113,7 +110,6 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
) )
@cases.mark_services
@cases.mark_specifications("IRCv3", "Ergo") @cases.mark_specifications("IRCv3", "Ergo")
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase): class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
@staticmethod @staticmethod

View File

@ -4,13 +4,7 @@ AWAY command (`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-4
""" """
from irctest import cases from irctest import cases
from irctest.numerics import ( from irctest.numerics import RPL_AWAY, RPL_NOWAWAY, RPL_UNAWAY, RPL_USERHOST
RPL_AWAY,
RPL_NOWAWAY,
RPL_UNAWAY,
RPL_USERHOST,
RPL_WHOISUSER,
)
from irctest.patma import StrRe from irctest.patma import StrRe
@ -145,33 +139,3 @@ class AwayTestCase(cases.BaseServerTestCase):
self.assertMessageMatch( self.assertMessageMatch(
self.getMessage(2), command=RPL_USERHOST, params=["qux", StrRe(r"bar=-.*")] self.getMessage(2), command=RPL_USERHOST, params=["qux", StrRe(r"bar=-.*")]
) )
@cases.mark_specifications("Modern")
def testAwayEmptyMessage(self):
"""
"If [AWAY] is sent with a nonempty parameter (the 'away message')
then the user is set to be away. If this command is sent with no
parameters, or with the empty string as the parameter, the user is no
longer away."
-- https://modern.ircdocs.horse/#away-message
"""
self.connectClient("bar", name="bar")
self.connectClient("qux", name="qux")
self.sendLine("bar", "AWAY :I'm not here right now")
replies = self.getMessages("bar")
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
self.sendLine("qux", "WHOIS bar")
replies = self.getMessages("qux")
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
self.assertIn(RPL_AWAY, [msg.command for msg in replies])
# empty final parameter to AWAY is treated the same as no parameter,
# i.e., the client is considered to be no longer away
self.sendLine("bar", "AWAY :")
replies = self.getMessages("bar")
self.assertIn(RPL_UNAWAY, [msg.command for msg in replies])
self.sendLine("qux", "WHOIS bar")
replies = self.getMessages("qux")
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
self.assertNotIn(RPL_AWAY, [msg.command for msg in replies])

View File

@ -18,8 +18,6 @@ EVENT_PLAYBACK_CAP = "draft/event-playback"
# Keep this in sync with validate_chathistory() # Keep this in sync with validate_chathistory()
SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"] SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"]
MYSQL_PASSWORD = ""
def validate_chathistory_batch(msgs): def validate_chathistory_batch(msgs):
batch_tag = None batch_tag = None

View File

@ -32,26 +32,6 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN # ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
self.assertIn(msg.command, ("401", "403", "404")) self.assertIn(msg.command, ("401", "403", "404"))
@cases.mark_specifications("RFC1459", "RFC2812")
def testPrivmsgToUser(self):
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
self.connectClient("foo")
self.connectClient("bar")
self.sendLine(1, "PRIVMSG bar :hey there!")
self.getMessages(1)
pms = [msg for msg in self.getMessages(2) if msg.command == "PRIVMSG"]
self.assertEqual(len(pms), 1)
self.assertMessageMatch(pms[0], command="PRIVMSG", params=["bar", "hey there!"])
@cases.mark_specifications("RFC1459", "RFC2812")
def testPrivmsgNonexistentUser(self):
"""https://tools.ietf.org/html/rfc2812#section-3.3.1"""
self.connectClient("foo")
self.sendLine(1, "PRIVMSG bar :hey there!")
msg = self.getMessage(1)
# ERR_NOSUCHNICK
self.assertIn(msg.command, ("401"))
class NoticeTestCase(cases.BaseServerTestCase): class NoticeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812") @cases.mark_specifications("RFC1459", "RFC2812")
@ -100,13 +80,8 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase): class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags") @cases.mark_capabilities("message-tags")
@cases.xfailIf( @cases.xfailIfSoftware(
lambda self: bool( ["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
self.controller.software_name == "UnrealIRCd"
and self.controller.software_version == 5
),
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
"https://bugs.unrealircd.org/view.php?id=5947",
) )
def testLineTooLong(self): def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)

View File

@ -6,8 +6,8 @@ from irctest import cases
class MetadataTestCase(cases.BaseServerTestCase): class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"display-name", "avatar"} valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"indisplay-name", "inavatar"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testInIsupport(self): def testInIsupport(self):
@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testGetOneUnsetValid(self): def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>""" """<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient("foo") self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name") self.sendLine(1, "METADATA * GET valid_key1")
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
""" """
self.connectClient("foo") self.connectClient("foo")
self.sendLine(1, "METADATA * GET display-name avatar") self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
m, m,
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"display-name", "valid_key1",
m, m,
fail_msg="Response to “METADATA * GET display-name avatar" fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to display-name first: {msg}", "did not respond to valid_key1 first: {msg}",
) )
m = self.getMessage(1) m = self.getMessage(1)
self.assertMessageMatch( self.assertMessageMatch(
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"avatar", "valid_key2",
m, m,
fail_msg="Response to “METADATA * GET display-name avatar" fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to avatar as second response: {msg}", "did not respond to valid_key2 as second response: {msg}",
) )
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.assertEqual( self.assertEqual(
m.params[1], m.params[1],
"display-name", "valid_key1",
m, m,
fail_msg="Second param of 761 after setting “{expects}” to " fail_msg="Second param of 761 after setting “{expects}” to "
"{}” is not “{expects}”: {msg}.", "{}” is not “{expects}”: {msg}.",
@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testSetGetValid(self): def testSetGetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>""" """<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue("*", "display-name", "myvalue") self.assertSetGetValue("*", "valid_key1", "myvalue")
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetZeroCharInValue(self): def testSetGetZeroCharInValue(self):
@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
""" """
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero") self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero")
@cases.mark_specifications("IRCv3", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetHeartInValue(self): def testSetGetHeartInValue(self):
@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue( self.assertSetGetValue(
"*", "*",
"display-name", "valid_key1",
"->{}<-".format(heart), "->{}<-".format(heart),
"zero->{}<-zero".format(heart.encode()), "zero->{}<-zero".format(heart.encode()),
) )
@ -223,7 +223,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
# Sending directly because it is not valid UTF-8 so Python would # Sending directly because it is not valid UTF-8 so Python would
# not like it # not like it
self.clients[1].conn.sendall( self.clients[1].conn.sendall(
b"METADATA * SET display-name " b":invalid UTF-8 ->\xc3<-\r\n" b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n"
) )
commands = {m.command for m in self.getMessages(1)} commands = {m.command for m in self.getMessages(1)}
self.assertNotIn( self.assertNotIn(
@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"UTF-8 was answered with 761 (RPL_KEYVALUE)", "UTF-8 was answered with 761 (RPL_KEYVALUE)",
) )
self.clients[1].conn.sendall( self.clients[1].conn.sendall(
b"METADATA * SET display-name " b":invalid UTF-8: \xc3\r\n" b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n"
) )
commands = {m.command for m in self.getMessages(1)} commands = {m.command for m in self.getMessages(1)}
self.assertNotIn( self.assertNotIn(

View File

@ -1,617 +0,0 @@
"""
`IRCv3 Metadata 2 <https://github.com/ircv3/ircv3-specifications/pull/501>`_
(not to be confused with the `deprecated IRCv3 Metadata
<https://ircv3.net/specs/core/metadata-3.2>`_)
"""
import itertools
import pytest
from irctest import cases, runner
from irctest.patma import ANYDICT, ANYLIST, ANYSTR, StrRe
CLIENT_NICKS = {
1: "foo",
2: "bar",
}
class MetadataTestCase(cases.BaseServerTestCase):
def getBatchMessages(self, client):
messages = self.getMessages(client)
first_msg = messages.pop(0)
last_msg = messages.pop(-1)
self.assertMessageMatch(
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "metadata"]
)
batch_id = first_msg.params[0][1:]
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
return (batch_id, messages)
def sub(self, client, keys):
self.sendLine(2, "METADATA * SUB " + " ".join(keys))
acknowledged_subs = []
for m in self.getMessages(2):
self.assertMessageMatch(
m,
command="770", # RPL_METADATASUBOK
params=["bar", *ANYLIST],
)
acknowledged_subs.extend(m.params[1:])
self.assertEqual(
sorted(acknowledged_subs),
sorted(keys),
fail_msg="Expected RPL_METADATASUBOK to ack {expects}, got {got}",
)
@cases.mark_specifications("IRCv3")
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
tags={"batch": batch_id, **ANYDICT},
command="766", # ERR_NOMATCHINGKEY
fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a "
"request to an unset valid METADATA key: {msg}",
)
@cases.mark_specifications("IRCv3")
def testGetTwoUnsetValid(self):
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE,
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name avatar")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[0],
params=["foo", "foo", "display-name", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to display-name first: {msg}",
)
self.assertMessageMatch(
messages[1],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with two 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[1],
params=["foo", "foo", "avatar", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to avatar as second response: {msg}",
)
@cases.mark_specifications("IRCv3")
def testListNoSet(self):
"""“This subcommand MUST list all currently-set metadata keys along
with their values. The response will be zero or more RPL_KEYVALUE
events, following by RPL_METADATAEND event.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * LIST")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 0, fail_msg="Expected empty batch")
@cases.mark_specifications("IRCv3")
def testListInvalidTarget(self):
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA foobar LIST")
m = self.getMessage(1)
self.assertMessageMatch(
m,
command="FAIL",
params=["METADATA", "INVALID_TARGET", "foobar", ANYSTR],
fail_msg="Response to “METADATA <invalid target> LIST” was "
"not 765 (ERR_TARGETINVALID) but: {msg}",
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(
"762",
commands,
fail_msg="Sent “METADATA <invalid target> LIST”, got FAIL INVALID_TARGET, "
"and then 762 (RPL_METADATAEND)",
)
def assertSetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} SET {} :{}".format(target, key, value))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertUnsetValue(self, client, target, key):
self.sendLine(client, "METADATA {} SET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertGetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertValueNotSet(self, client, target, key):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertSetGetValue(self, client, target, key, value):
self.assertSetValue(client, target, key, value)
self.assertGetValue(client, target, key, value)
def assertUnsetGetValue(self, client, target, key):
self.assertUnsetValue(client, target, key)
self.assertValueNotSet(client, target, key)
@pytest.mark.parametrize(
"set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"])
)
@cases.mark_specifications("IRCv3")
def testSetGetUser(self, set_target, get_target):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, set_target, "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetUserAgain(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
@cases.mark_specifications("IRCv3")
def testSetUnsetUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertUnsetGetValue(1, "*", "display-name")
@cases.mark_specifications("IRCv3")
def testGetOtherUser(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "foo", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetOtherUser(self):
"""Not required by the spec, but it makes little sense to allow anyone to
write a channel's metadata"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other.
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The Second"],
)
self.assertUnsetGetValue(1, "*", "display-name")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR],
)
@cases.mark_specifications("IRCv3")
def testSubUserSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set its display-name even though "
"they don't share a channel",
)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
@cases.mark_specifications("IRCv3")
def testSetGetChannel(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "#chan", "display-name", "Hash Channel")
@cases.mark_specifications("IRCv3")
def testSetUnsetChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertUnsetGetValue(1, "#chan", "display-name")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertValueNotSet(2, "#chan", "display-name")
@cases.mark_specifications("IRCv3")
def testSetGetChannelNotOp(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(2, "METADATA #chan SET display-name :Sharp Channel")
self.assertMessageMatch(
self.getMessage(2),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "#chan", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(1),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
self.assertSetGetValue(1, "#chan", "display-name", "Harsh Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Harsh Channel"],
)
@cases.mark_specifications("IRCv3")
def testSubChannelSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set #chan's display-name even "
"though they are not in it",
)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
@cases.mark_specifications("IRCv3")
def testSetGetValidBeforeConnect(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.addClient(1)
self.sendLine(1, "CAP LS 302")
caps = self.getCapLs(1)
if "before-connect" not in (caps["draft/metadata-2"] or "").split(","):
raise runner.OptionalExtensionNotSupported(
"draft/metadata-2=before-connect"
)
self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.sendLine(1, "NICK foo")
self.sendLine(1, "USER foo 0 * :foo")
self.sendLine(1, "CAP END")
self.skipToWelcome(1)
self.assertGetValue(1, "*", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetHeartInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
heart = b"\xf0\x9f\x92\x9c".decode()
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(
1,
"*",
"display-name",
"->{}<-".format(heart),
)
def _testSetInvalidValue(self, value):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# Sending directly because it is not valid UTF-8 so Python would
# not like it
self.clients[1].conn.sendall(
b"METADATA * SET display-name :invalid UTF-8 ->\xc3<-\r\n"
)
try:
commands = {m.command for m in self.getMessages(1)}
except UnicodeDecodeError:
assert False, "Server sent invalid UTF-8"
self.assertNotIn(
"761",
commands, # RPL_KEYVALUE
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.clients[1].conn.sendall(b"METADATA * SET display-name :" + value + b"\r\n")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "VALUE_INVALID", ANYSTR],
)
messages = self.getMessages(1)
self.assertNotIn(
"761", # RPL_KEYVALUE
{m.command for m in messages},
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.assertEqual(
messages,
[],
fail_msg="Unexpected response to METADATA SET with invalid value: {got}",
)
@cases.mark_specifications("IRCv3")
def testSetInvalidUtf8(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
self._testSetInvalidValue(b"invalid UTF-8: \xc3")
@cases.mark_specifications("IRCv3")
def testSetTooManyChars(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
self._testSetInvalidValue(b"abcd" * 120)
@cases.mark_specifications("IRCv3")
def testSetTooManyBytes(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
heart = b"\xf0\x9f\x92\x9c"
self._testSetInvalidValue(heart * 120)

View File

@ -1,10 +1,7 @@
""" """
`IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_ `IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_
and `IRCv3 extended-monitor` <https://ircv3.net/specs/extensions/extended-monitor>`_
""" """
import pytest
from irctest import cases, runner from irctest import cases, runner
from irctest.client_mock import NoMessageException from irctest.client_mock import NoMessageException
from irctest.numerics import ( from irctest.numerics import (
@ -16,7 +13,7 @@ from irctest.numerics import (
from irctest.patma import ANYSTR, StrRe from irctest.patma import ANYSTR, StrRe
class _BaseMonitorTestCase(cases.BaseServerTestCase): class MonitorTestCase(cases.BaseServerTestCase):
def check_server_support(self): def check_server_support(self):
if "MONITOR" not in self.server_support: if "MONITOR" not in self.server_support:
raise runner.IsupportTokenNotSupported("MONITOR") raise runner.IsupportTokenNotSupported("MONITOR")
@ -45,8 +42,6 @@ class _BaseMonitorTestCase(cases.BaseServerTestCase):
extra_format=(nick,), extra_format=(nick,),
) )
class MonitorTestCase(_BaseMonitorTestCase):
@cases.mark_specifications("IRCv3") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR") @cases.mark_isupport("MONITOR")
def testMonitorOneDisconnected(self): def testMonitorOneDisconnected(self):
@ -300,11 +295,10 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.sendLine(2, "NICK qux") self.sendLine(2, "NICK qux")
self.getMessages(2) self.getMessages(2)
mononline = self.getMessages(1)[0] mononline = self.getMessages(1)[0]
self.assertMessageMatch( self.assertEqual(mononline.command, RPL_MONONLINE)
mononline, self.assertEqual(len(mononline.params), 2, mononline.params)
command=RPL_MONONLINE, self.assertIn(mononline.params[0], ("bar", "*"))
params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")], self.assertEqual(mononline.params[1].split("!")[0], "qux")
)
# no numerics for a case change # no numerics for a case change
self.sendLine(2, "NICK QUX") self.sendLine(2, "NICK QUX")
@ -315,246 +309,7 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.getMessages(2) self.getMessages(2)
monoffline = self.getMessages(1)[0] monoffline = self.getMessages(1)[0]
# should get RPL_MONOFFLINE with the current unfolded nick # should get RPL_MONOFFLINE with the current unfolded nick
self.assertMessageMatch( self.assertEqual(monoffline.command, RPL_MONOFFLINE)
monoffline, self.assertEqual(len(monoffline.params), 2, monoffline.params)
command=RPL_MONOFFLINE, self.assertIn(monoffline.params[0], ("bar", "*"))
params=[StrRe(r"(bar|\*)"), "QUX"], self.assertEqual(monoffline.params[1].split("!")[0], "QUX")
)
class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase):
def _setupExtendedMonitor(self, monitor_before_connect, watcher_caps, watched_caps):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html"""
self.connectClient(
"foo",
capabilities=["draft/extended-monitor", *watcher_caps],
skip_if_cap_nak=True,
)
if monitor_before_connect:
self.sendLine(1, "MONITOR + bar")
self.getMessages(1)
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
self.getMessages(2)
else:
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
self.getMessages(2)
self.sendLine(1, "MONITOR + bar")
self.assertMononline(1, "bar")
self.assertEqual(self.getMessages(1), [])
class ExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "away-notify")
@pytest.mark.parametrize(
"monitor_before_connect,cap",
[
pytest.param(
monitor_before_connect,
cap,
id=("monitor_before_connect" if monitor_before_connect else "")
+ "-"
+ ("with-cap" if cap else ""),
)
for monitor_before_connect in [True, False]
for cap in [True, False]
],
)
def testExtendedMonitorAway(self, monitor_before_connect, cap):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
with https://ircv3.net/specs/extensions/away-notify
"""
if cap:
self._setupExtendedMonitor(
monitor_before_connect, ["away-notify"], ["away-notify"]
)
else:
self._setupExtendedMonitor(monitor_before_connect, ["away-notify"], [])
self.sendLine(2, "AWAY :afk")
self.getMessages(2)
self.assertMessageMatch(
self.getMessage(1), nick="bar", command="AWAY", params=["afk"]
)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
self.sendLine(2, "AWAY")
self.getMessages(2)
self.assertMessageMatch(
self.getMessage(1), nick="bar", command="AWAY", params=[]
)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "away-notify")
@pytest.mark.parametrize(
"monitor_before_connect,cap",
[
pytest.param(
monitor_before_connect,
cap,
id=("monitor_before_connect" if monitor_before_connect else "")
+ "-"
+ ("with-cap" if cap else ""),
)
for monitor_before_connect in [True, False]
for cap in [True, False]
],
)
def testExtendedMonitorAwayNoCap(self, monitor_before_connect, cap):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
does nothing when ``away-notify`` is not enabled by the watcher
"""
if cap:
self._setupExtendedMonitor(monitor_before_connect, [], ["away-notify"])
else:
self._setupExtendedMonitor(monitor_before_connect, [], [])
self.sendLine(2, "AWAY :afk")
self.getMessages(2)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
self.sendLine(2, "AWAY")
self.getMessages(2)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "setname")
@pytest.mark.parametrize("monitor_before_connect", [True, False])
def testExtendedMonitorSetName(self, monitor_before_connect):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
with https://ircv3.net/specs/extensions/setname
"""
self._setupExtendedMonitor(monitor_before_connect, ["setname"], ["setname"])
self.sendLine(2, "SETNAME :new name")
self.getMessages(2)
self.assertMessageMatch(
self.getMessage(1), nick="bar", command="SETNAME", params=["new name"]
)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "setname")
@pytest.mark.parametrize("monitor_before_connect", [True, False])
def testExtendedMonitorSetNameNoCap(self, monitor_before_connect):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
does nothing when ``setname`` is not enabled by the watcher
"""
self._setupExtendedMonitor(monitor_before_connect, [], ["setname"])
self.sendLine(2, "SETNAME :new name")
self.getMessages(2)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
@cases.mark_services
class AuthenticatedExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "account-notify")
@pytest.mark.parametrize(
"monitor_before_connect,cap",
[
pytest.param(
monitor_before_connect,
cap,
id=("monitor_before_connect" if monitor_before_connect else "")
+ "-"
+ ("with-cap" if cap else ""),
)
for monitor_before_connect in [True, False]
for cap in [True, False]
],
)
def testExtendedMonitorAccountNotify(self, monitor_before_connect, cap):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
does nothing when ``account-notify`` is not enabled by the watcher
"""
self.controller.registerUser(self, "jilles", "sesame")
if cap:
self._setupExtendedMonitor(
monitor_before_connect,
["account-notify"],
["account-notify", "sasl", "cap-notify"],
)
else:
self._setupExtendedMonitor(
monitor_before_connect, ["account-notify"], ["sasl", "cap-notify"]
)
self.sendLine(2, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(2)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
m = self.getRegistrationMessage(2)
self.assertMessageMatch(
m,
command="900",
fail_msg="Did not send 900 after correct SASL authentication.",
)
self.getMessages(2)
self.assertMessageMatch(
self.getMessage(1), nick="bar", command="ACCOUNT", params=["jilles"]
)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities("extended-monitor", "account-notify")
@pytest.mark.parametrize(
"monitor_before_connect,cap",
[
pytest.param(
monitor_before_connect,
cap,
id=("monitor_before_connect" if monitor_before_connect else "")
+ "-"
+ ("with-cap" if cap else ""),
)
for monitor_before_connect in [True, False]
for cap in [True, False]
],
)
def testExtendedMonitorAccountNotifyNoCap(self, monitor_before_connect, cap):
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
does nothing when ``account-notify`` is not enabled by the watcher
"""
self.controller.registerUser(self, "jilles", "sesame")
if cap:
self._setupExtendedMonitor(
monitor_before_connect, [], ["account-notify", "sasl", "cap-notify"]
)
else:
self._setupExtendedMonitor(
monitor_before_connect, [], ["sasl", "cap-notify"]
)
self.sendLine(2, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(2)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
m = self.getRegistrationMessage(2)
self.assertMessageMatch(
m,
command="900",
fail_msg="Did not send 900 after correct SASL authentication.",
)
self.getMessages(2)
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")

View File

@ -2,13 +2,10 @@
Regression tests for bugs in `Ergo <https://ergo.chat/>`_. Regression tests for bugs in `Ergo <https://ergo.chat/>`_.
""" """
import time
from irctest import cases, runner from irctest import cases, runner
from irctest.numerics import ( from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME
ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE,
RPL_HELLO,
RPL_WELCOME,
)
from irctest.patma import ANYDICT from irctest.patma import ANYDICT
@ -114,7 +111,8 @@ class RegressionsTestCase(cases.BaseServerTestCase):
self.sendLine(1, "NICK *") self.sendLine(1, "NICK *")
self.sendLine(1, "USER u s e r") self.sendLine(1, "USER u s e r")
replies = {"NOTICE"} replies = {"NOTICE"}
while replies <= {"NOTICE", RPL_HELLO}: time.sleep(2) # give time to slow servers, like irc2 to reply
while replies == {"NOTICE"}:
replies = set(msg.command for msg in self.getMessages(1, synchronize=False)) replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
self.assertIn(ERR_ERRONEUSNICKNAME, replies) self.assertIn(ERR_ERRONEUSNICKNAME, replies)
self.assertNotIn(RPL_WELCOME, replies) self.assertNotIn(RPL_WELCOME, replies)

View File

@ -178,14 +178,6 @@ class SaslTestCase(cases.BaseServerTestCase):
), ),
"Anope does not handle split AUTHENTICATE (reported on IRC)", "Anope does not handle split AUTHENTICATE (reported on IRC)",
) )
@cases.xfailIf(
lambda self: (
self.controller.services_controller is not None
and self.controller.services_controller.software_name == "Dlk-Services"
),
"Dlk does not handle split AUTHENTICATE "
"https://github.com/DalekIRC/Dalek-Services/issues/28",
)
def testPlainLarge(self): def testPlainLarge(self):
"""Test the client splits large AUTHENTICATE messages whose payload """Test the client splits large AUTHENTICATE messages whose payload
is not a multiple of 400. is not a multiple of 400.

View File

@ -1,48 +0,0 @@
import math
import time
from irctest import cases
from irctest.numerics import RPL_TIME
from irctest.patma import ANYSTR, StrRe
class TimeTestCase(cases.BaseServerTestCase):
def testTime(self):
self.connectClient("user")
time_before = math.floor(time.time())
self.sendLine(1, "TIME")
msg = self.getMessage(1)
time_after = math.ceil(time.time())
if len(msg.params) == 5:
# ircu2, snircd
self.assertMessageMatch(
msg,
command=RPL_TIME,
params=["user", "My.Little.Server", StrRe("[0-9]+"), "0", ANYSTR],
)
self.assertIn(
int(msg.params[2]),
range(time_before, time_after + 1),
"Timestamp not in expected range",
)
elif len(msg.params) == 4:
# bahamut
self.assertMessageMatch(
msg,
command=RPL_TIME,
params=["user", "My.Little.Server", StrRe("[0-9]+"), ANYSTR],
)
self.assertIn(
int(msg.params[2]),
range(time_before, time_after + 1),
"Timestamp not in expected range",
)
else:
# Common case
self.assertMessageMatch(
msg, command=RPL_TIME, params=["user", "My.Little.Server", ANYSTR]
)

View File

@ -1,21 +1,36 @@
""" """
`Ergo <https://ergo.chat/>`_-specific tests of non-Unicode filtering `Ergo <https://ergo.chat/>`_-specific tests of non-Unicode filtering
TODO: turn this into a test of `IRCv3 UTF8ONLY
<https://ircv3.net/specs/extensions/utf8-only>`_ <https://ircv3.net/specs/extensions/utf8-only>`_
""" """
from irctest import cases, runner from irctest import cases
from irctest.patma import ANYSTR from irctest.patma import ANYSTR
class Utf8TestCase(cases.BaseServerTestCase): class Utf8TestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Ergo") @cases.mark_specifications("Ergo")
def testNonUtf8Filtering(self): def testUtf8Validation(self):
self.connectClient( self.connectClient(
"bar", "bar",
capabilities=["batch", "echo-message", "labeled-response"], capabilities=["batch", "echo-message", "labeled-response"],
) )
self.joinChannel(1, "#qux") self.joinChannel(1, "#qux")
self.sendLine(1, "PRIVMSG #qux hi")
ms = self.getMessages(1)
self.assertMessageMatch(
[m for m in ms if m.command == "PRIVMSG"][0], params=["#qux", "hi"]
)
self.sendLine(1, b"PRIVMSG #qux hi\xaa")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
tags={},
)
self.sendLine(1, b"@label=xyz PRIVMSG #qux hi\xaa") self.sendLine(1, b"@label=xyz PRIVMSG #qux hi\xaa")
self.assertMessageMatch( self.assertMessageMatch(
self.getMessage(1), self.getMessage(1),
@ -23,26 +38,3 @@ class Utf8TestCase(cases.BaseServerTestCase):
params=["PRIVMSG", "INVALID_UTF8", ANYSTR], params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
tags={"label": "xyz"}, tags={"label": "xyz"},
) )
@cases.mark_isupport("UTF8ONLY")
def testUtf8Validation(self):
self.connectClient("foo")
self.connectClient("bar")
if "UTF8ONLY" not in self.server_support:
raise runner.IsupportTokenNotSupported("UTF8ONLY")
self.sendLine(1, "PRIVMSG bar hi")
self.getMessages(1) # synchronize
ms = self.getMessages(2)
self.assertMessageMatch(
[m for m in ms if m.command == "PRIVMSG"][0], params=["bar", "hi"]
)
self.sendLine(1, b"PRIVMSG bar hi\xaa")
m = self.getMessage(1)
assert m.command in ("FAIL", "WARN", "ERROR")
if m.command in ("FAIL", "WARN"):
self.assertMessageMatch(m, params=["PRIVMSG", "INVALID_UTF8", ANYSTR])

View File

@ -37,8 +37,8 @@ class BaseWhoTestCase:
self.sendLine(1, f"USER {self.username} 0 * :{self.realname}") self.sendLine(1, f"USER {self.username} 0 * :{self.realname}")
if auth: if auth:
self.sendLine(1, "CAP END") self.sendLine(1, "CAP END")
self.getRegistrationMessage(1)
self.skipToWelcome(1) self.skipToWelcome(1)
self.getMessages(1)
self.sendLine(1, "JOIN #chan") self.sendLine(1, "JOIN #chan")
self.getMessages(1) self.getMessages(1)
@ -503,34 +503,3 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
command=RPL_ENDOFWHO, command=RPL_ENDOFWHO,
params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], params=["otherNick", InsensitiveStr("coolNick"), ANYSTR],
) )
class WhoInvisibleTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoInvisible(self):
if self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self.connectClient("evan", name="evan")
self.sendLine("evan", "MODE evan +i")
self.getMessages("evan")
self.connectClient("shivaram", name="shivaram")
self.getMessages("shivaram")
self.sendLine("shivaram", "WHO eva*")
reply_cmds = {msg.command for msg in self.getMessages("shivaram")}
self.assertEqual(reply_cmds, {RPL_ENDOFWHO})
# invisibility should not be respected for plain nicknames, only for masks:
self.sendLine("shivaram", "WHO evan")
replies = self.getMessages("shivaram")
reply_cmds = {msg.command for msg in replies}
self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO})
# invisibility should not be respected if the users share a channel
self.joinChannel("evan", "#test")
self.joinChannel("shivaram", "#test")
self.sendLine("shivaram", "WHO eva*")
replies = self.getMessages("shivaram")
reply_cmds = {msg.command for msg in replies}
self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO})

View File

@ -71,10 +71,7 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
last_message, last_message,
command=RPL_ENDOFWHOIS, command=RPL_ENDOFWHOIS,
params=["nick1", "nick2", ANYSTR], params=["nick1", "nick2", ANYSTR],
fail_msg=( fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})",
f"Expected RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS}) as last message, "
f"got {{msg}}"
),
) )
unexpected_messages = [] unexpected_messages = []
@ -99,12 +96,6 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
], ],
) )
elif m.command == RPL_WHOISSPECIAL: elif m.command == RPL_WHOISSPECIAL:
services_controller = self.controller.services_controller
if (
services_controller is not None
and services_controller.software_name == "Dlk-Services"
):
continue
# Technically allowed, but it's a bad style to use this without # Technically allowed, but it's a bad style to use this without
# explicit configuration by the operators. # explicit configuration by the operators.
assert False, "RPL_WHOISSPECIAL in use with default configuration" assert False, "RPL_WHOISSPECIAL in use with default configuration"

View File

@ -98,7 +98,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...], "Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS" both followed with RPL_ENDOFWHOWAS"
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
self.connectClient("nick1") self.connectClient("nick1")
@ -210,7 +210,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"The history is searched backward, returning the most recent entry first." "The history is searched backward, returning the most recent entry first."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
@ -224,7 +224,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"If there are multiple entries, up to <count> replies will be returned" "If there are multiple entries, up to <count> replies will be returned"
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
@ -238,7 +238,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"If there are multiple entries, up to <count> replies will be returned" "If there are multiple entries, up to <count> replies will be returned"
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
@ -253,10 +253,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
is done." is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://github.com/ircdocs/modern-irc/pull/170
"If given, <count> SHOULD be a positive number. Otherwise, a full search
"is done.
-- https://modern.ircdocs.horse/#whowas-message
""" """
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1")
@ -274,10 +271,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
is done." is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://github.com/ircdocs/modern-irc/pull/170
"If given, <count> SHOULD be a positive number. Otherwise, a full search
"is done.
-- https://modern.ircdocs.horse/#whowas-message
""" """
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0")
@ -286,7 +280,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
""" """
"Wildcards are allowed in the <target> parameter." "Wildcards are allowed in the <target> parameter."
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
if self.controller.software_name == "Bahamut": if self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHOWAS mask") raise runner.OptionalExtensionNotSupported("WHOWAS mask")
@ -330,7 +324,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
""" """
"If the `<nick>` argument is missing, they SHOULD send a single reply, using "If the `<nick>` argument is missing, they SHOULD send a single reply, using
either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS" either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS"
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
# But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS # But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS
# instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns # instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns
@ -364,7 +358,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
""" """
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
and: and:
@ -377,7 +371,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...], "Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS" both followed with RPL_ENDOFWHOWAS"
-- https://modern.ircdocs.horse/#whowas-message -- https://github.com/ircdocs/modern-irc/pull/170
""" """
self.connectClient("nick1") self.connectClient("nick1")

View File

@ -27,19 +27,16 @@ class Specifications(enum.Enum):
@enum.unique @enum.unique
class Capabilities(enum.Enum): class Capabilities(enum.Enum):
ACCOUNT_NOTIFY = "account-notify"
ACCOUNT_TAG = "account-tag" ACCOUNT_TAG = "account-tag"
AWAY_NOTIFY = "away-notify" AWAY_NOTIFY = "away-notify"
BATCH = "batch" BATCH = "batch"
ECHO_MESSAGE = "echo-message" ECHO_MESSAGE = "echo-message"
EXTENDED_JOIN = "extended-join" EXTENDED_JOIN = "extended-join"
EXTENDED_MONITOR = "extended-monitor"
LABELED_RESPONSE = "labeled-response" LABELED_RESPONSE = "labeled-response"
MESSAGE_TAGS = "message-tags" MESSAGE_TAGS = "message-tags"
MULTILINE = "draft/multiline" MULTILINE = "draft/multiline"
MULTI_PREFIX = "multi-prefix" MULTI_PREFIX = "multi-prefix"
SERVER_TIME = "server-time" SERVER_TIME = "server-time"
SETNAME = "setname"
STS = "sts" STS = "sts"
@classmethod @classmethod
@ -59,7 +56,6 @@ class IsupportTokens(enum.Enum):
MONITOR = "MONITOR" MONITOR = "MONITOR"
STATUSMSG = "STATUSMSG" STATUSMSG = "STATUSMSG"
TARGMAX = "TARGMAX" TARGMAX = "TARGMAX"
UTF8ONLY = "UTF8ONLY"
WHOX = "WHOX" WHOX = "WHOX"
@classmethod @classmethod

View File

@ -116,7 +116,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None return None
return { return {
"runs-on": "ubuntu-20.04", "runs-on": "ubuntu-latest",
"steps": [ "steps": [
{ {
"name": "Create directories", "name": "Create directories",
@ -144,9 +144,13 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads = [] downloads = []
install_steps = [] install_steps = []
for software_id in test_config.get("software", []): for software_id in test_config.get("software", []):
software_config = config["software"][software_id] if software_id == "anope":
# TODO: don't hardcode anope here
software_config = {"separate_build_job": True}
else:
software_config = config["software"][software_id]
env += software_config.get("env", "") + " " env += test_config.get("env", {}).get(version_flavor.value, "") + " "
if "prefix" in software_config: if "prefix" in software_config:
env += ( env += (
f"PATH={software_config['prefix']}/sbin" f"PATH={software_config['prefix']}/sbin"
@ -191,7 +195,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = [] unpack = []
return { return {
"runs-on": "ubuntu-20.04", "runs-on": "ubuntu-latest",
"needs": needs, "needs": needs,
"steps": [ "steps": [
{"uses": "actions/checkout@v2"}, {"uses": "actions/checkout@v2"},
@ -241,6 +245,47 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
} }
def get_build_job_anope():
return {
"runs-on": "ubuntu-latest",
"steps": [
{"uses": "actions/checkout@v2"},
{
"name": "Create directories",
"run": "cd ~/; mkdir -p .local/ go/",
},
{
"name": "Cache Anope",
"uses": "actions/cache@v2",
"with": {
"path": "~/.cache\n${{ github.workspace }}/anope\n",
"key": "3-${{ runner.os }}-anope-2.0.9",
},
},
{
"name": "Checkout Anope",
"uses": "actions/checkout@v2",
"with": {
"repository": "anope/anope",
"ref": "2.0.9",
"path": "anope",
},
},
{
"name": "Build Anope",
"run": script(
"cd $GITHUB_WORKSPACE/anope/",
"cp $GITHUB_WORKSPACE/data/anope/* .",
"CFLAGS=-O0 ./Config -quick",
"make -C build -j 4",
"make -C build install",
),
},
*upload_steps("anope"),
],
}
def upload_steps(software_id): def upload_steps(software_id):
"""Make a tarball (to preserve permissions) and upload""" """Make a tarball (to preserve permissions) and upload"""
return [ return [
@ -263,6 +308,7 @@ def upload_steps(software_id):
def generate_workflow(config: dict, version_flavor: VersionFlavor): def generate_workflow(config: dict, version_flavor: VersionFlavor):
on: dict on: dict
if version_flavor == VersionFlavor.STABLE: if version_flavor == VersionFlavor.STABLE:
on = {"push": None, "pull_request": None} on = {"push": None, "pull_request": None}
@ -280,6 +326,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
} }
jobs = {} jobs = {}
jobs["build-anope"] = get_build_job_anope()
for software_id in config["software"]: for software_id in config["software"]:
software_config = config["software"][software_id] software_config = config["software"][software_id]
@ -306,7 +353,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
jobs["publish-test-results"] = { jobs["publish-test-results"] = {
"name": "Publish Dashboard", "name": "Publish Dashboard",
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)), "needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)),
"runs-on": "ubuntu-20.04", "runs-on": "ubuntu-latest",
# the build-and-test job might be skipped, we don't need to run # the build-and-test job might be skipped, we don't need to run
# this job then # this job then
"if": "success() || failure()", "if": "success() || failure()",

View File

@ -12,9 +12,6 @@ disallow_untyped_defs = False
[mypy-irctest.client_tests.*] [mypy-irctest.client_tests.*]
disallow_untyped_defs = False disallow_untyped_defs = False
[mypy-irctest.self_tests.*]
disallow_untyped_defs = False
[mypy-defusedxml.*] [mypy-defusedxml.*]
ignore_missing_imports = True ignore_missing_imports = True

View File

@ -1,15 +0,0 @@
Lower Bahamut's delay between processing incoming commands
diff --git a/src/s_bsd.c b/src/s_bsd.c
index fcc1d02..951fd8c 100644
--- a/src/s_bsd.c
+++ b/src/s_bsd.c
@@ -1458,7 +1458,7 @@ int do_client_queue(aClient *cptr)
int dolen = 0, done;
while (SBufLength(&cptr->recvQ) && !NoNewLine(cptr) &&
- ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 10) ||
+ ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 20) ||
IsNegoServer(cptr)))
{
/* If it's become registered as a server, just parse the whole block */

View File

@ -18,20 +18,16 @@ markers =
private_chathistory private_chathistory
# capabilities # capabilities
account-notify
account-tag account-tag
away-notify away-notify
batch batch
echo-message echo-message
extended-join extended-join
extended-monitor
labeled-response labeled-response
message-tags message-tags
metadata-2
draft/multiline draft/multiline
multi-prefix multi-prefix
server-time server-time
setname
sts sts
# isupport tokens # isupport tokens
@ -42,7 +38,6 @@ markers =
PREFIX PREFIX
STATUSMSG STATUSMSG
TARGMAX TARGMAX
UTF8ONLY
WHOX WHOX
python_classes = *TestCase Test* python_classes = *TestCase Test*

View File

@ -42,7 +42,7 @@ def partial_compaction(d):
# tests separate # tests separate
compacted_d = {} compacted_d = {}
successes = [] successes = []
for k, v in d.items(): for (k, v) in d.items():
if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0: if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0:
successes.append((k, v)) successes.append((k, v))
else: else:

View File

@ -105,7 +105,6 @@ software:
build_script: | build_script: |
cd $GITHUB_WORKSPACE/Bahamut/ cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force libtoolize --force
aclocal aclocal
@ -131,7 +130,7 @@ software:
pre_deps: pre_deps:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '^1.19.0' go-version: '^1.18.0'
- run: go version - run: go version
separate_build_job: false separate_build_job: false
build_script: | build_script: |
@ -268,8 +267,8 @@ software:
name: UnrealIRCd 6 name: UnrealIRCd 6
repository: unrealircd/unrealircd repository: unrealircd/unrealircd
refs: refs:
stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7 stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7 release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
devel: unreal60_dev devel: unreal60_dev
devel_release: null devel_release: null
path: unrealircd path: unrealircd
@ -301,47 +300,6 @@ software:
separate_build_job: true separate_build_job: true
build_script: *unrealircd_build_script build_script: *unrealircd_build_script
#############################
# Services:
anope:
name: Anope
repository: anope/anope
separate_build_job: true
path: anope
refs:
stable: "2.0.9"
release: "2.0.9"
devel: "2.0.9"
devel_release: "2.0.9"
build_script: |
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
make -C build -j 4
make -C build install
dlk:
name: Dlk
repository: DalekIRC/Dalek-Services
separate_build_job: false
path: Dlk-Services
refs:
stable: &dlk_stable "effd18652fc1c847d1959089d9cca9ff9837a8c0"
release: *dlk_stable
devel: "main"
devel_release: *dlk_stable
build_script: |
pip install pifpaf
wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip
env: >-
IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services"
IRCTEST_WP_CLI_PATH="${{ github.workspace }}/wp-cli.phar"
IRCTEST_WP_ZIP_PATH="${{ github.workspace }}/wordpress-latest.zip"
############################# #############################
# Clients: # Clients:
@ -444,9 +402,6 @@ tests:
unrealircd-anope: unrealircd-anope:
software: [unrealircd, anope] software: [unrealircd, anope]
unrealircd-dlk:
software: [unrealircd, dlk]
limnoria: limnoria:
software: [limnoria] software: [limnoria]