1 Commits

Author SHA1 Message Date
e4bca8a401 Add test for mode +C ('no ctcp') 2022-04-01 13:59:41 +02:00
104 changed files with 1075 additions and 4669 deletions

View File

@ -1,120 +0,0 @@
#!/usr/bin/env python3
import json
import os
import pprint
import re
import subprocess
import sys
import urllib.request
event_name = os.environ["GITHUB_EVENT_NAME"]
is_pull_request = is_push = False
if event_name.startswith("pull_request"):
is_pull_request = True
elif event_name.startswith("push"):
is_push = True
elif event_name.startswith("schedule"):
# Don't publish; scheduled workflows run against the latest commit of every
# implementation, so they are likely to have failed tests for the wrong reasons
sys.exit(0)
else:
print("Unexpected event name:", event_name)
with open(os.environ["GITHUB_EVENT_PATH"]) as fd:
github_event = json.load(fd)
pprint.pprint(github_event)
context_suffix = ""
command = ["netlify", "deploy", "--dir=dashboard/"]
if is_pull_request:
pr_number = github_event["number"]
sha = github_event.get("after") or github_event["pull_request"]["head"]["sha"]
# Aliases can't exceed 37 chars
command.extend(["--alias", f"pr-{pr_number}-{sha[0:10]}"])
context_suffix = " (pull_request)"
elif is_push:
ref = github_event["ref"]
m = re.match("refs/heads/(.*)", ref)
if m:
branch = m.group(1)
sha = github_event["head_commit"]["id"]
if branch in ("main", "master"):
command.extend(["--prod"])
else:
command.extend(["--alias", f"br-{branch[0:23]}-{sha[0:10]}"])
context_suffix = " (push)"
else:
# TODO
pass
print("Running", command)
proc = subprocess.run(command, capture_output=True)
output = proc.stdout.decode()
assert proc.returncode == 0, (output, proc.stderr.decode())
print(output)
m = re.search("https://[^ ]*--[^ ]*netlify.app", output)
assert m
netlify_site_url = m.group(0)
target_url = f"{netlify_site_url}/index.xhtml"
print("Published to", netlify_site_url)
def send_status() -> None:
statuses_url = github_event["repository"]["statuses_url"].format(sha=sha)
payload = {
"state": "success",
"context": f"Dashboard{context_suffix}",
"description": "Table of all test results",
"target_url": target_url,
}
request = urllib.request.Request(
statuses_url,
data=json.dumps(payload).encode(),
headers={
"Authorization": f'token {os.environ["GITHUB_TOKEN"]}',
"Content-Type": "text/json",
"Accept": "application/vnd.github+json",
},
)
response = urllib.request.urlopen(request)
assert response.status == 201, response.read()
send_status()
def send_pr_comment() -> None:
comments_url = github_event["pull_request"]["_links"]["comments"]["href"]
payload = {
"body": f"[Test results]({target_url})",
}
request = urllib.request.Request(
comments_url,
data=json.dumps(payload).encode(),
headers={
"Authorization": f'token {os.environ["GITHUB_TOKEN"]}',
"Content-Type": "text/json",
"Accept": "application/vnd.github+json",
},
)
response = urllib.request.urlopen(request)
assert response.status == 201, response.read()
if is_pull_request:
send_pr_comment()

View File

@ -3,24 +3,20 @@
jobs:
build-anope:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
- name: Cache Anope
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-devel
key: 3-${{ runner.os }}-anope-2.0.9
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
uses: actions/checkout@v2
with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |
run: |-
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-bahamut:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -71,7 +67,6 @@ jobs:
run: |
cd $GITHUB_WORKSPACE/Bahamut/
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
libtoolize --force
aclocal
@ -92,7 +87,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-hybrid:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -131,7 +126,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -162,7 +157,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-ngircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -203,7 +198,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-plexus4:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -245,7 +240,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-solanum:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -285,7 +280,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -320,8 +315,6 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/
- name: Upload build artefacts
@ -331,7 +324,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd-5:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -366,8 +359,6 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/
- name: Upload build artefacts
@ -378,7 +369,7 @@ jobs:
retention-days: 1
publish-test-results:
if: success() || failure()
name: Publish Dashboard
name: Publish Unit Tests Results
needs:
- test-bahamut
- test-bahamut-anope
@ -389,7 +380,6 @@ jobs:
- test-inspircd-anope
- test-ircu2
- test-limnoria
- test-nefarious
- test-ngircd
- test-ngircd-anope
- test-ngircd-atheme
@ -400,35 +390,38 @@ jobs:
- test-unrealircd-5
- test-unrealircd-anope
- test-unrealircd-atheme
- test-unrealircd-dlk
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
run: |-
python -m pip install --upgrade pip
pip install defusedxml docutils -r requirements.txt
- name: Generate dashboard
run: |-
shopt -s globstar
python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml
echo '/ /index.xhtml' > dashboard/_redirects
- name: Install netlify-cli
run: npm i -g netlify-cli
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
name: Deploy to Netlify
run: ./.github/deploy_to_netlify.py
- if: github.event_name == 'pull_request'
name: Publish Unit Test Results
uses: actions/github-script@v4
with:
result-encoding: string
script: |
let body = '';
const options = {};
options.listeners = {
stdout: (data) => {
body += data.toString();
}
};
await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options);
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
return body;
test-bahamut:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -442,8 +435,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -451,18 +444,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut_devel
name: pytest results bahamut (devel)
path: pytest.xml
test-bahamut-anope:
needs:
- build-bahamut
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -481,8 +473,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -490,17 +482,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-anope_devel
name: pytest results bahamut-anope (devel)
path: pytest.xml
test-bahamut-atheme:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -514,8 +505,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -523,16 +514,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-atheme_devel
name: pytest results bahamut-atheme (devel)
path: pytest.xml
test-ergo:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -547,15 +537,15 @@ jobs:
repository: ergochat/ergo
- uses: actions/setup-go@v2
with:
go-version: ^1.19.0
go-version: ^1.18.0
- run: go version
- name: Build Ergo
run: |
cd $GITHUB_WORKSPACE/ergo/
make build
make install
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -563,18 +553,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH
make ergo
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ergo_devel
name: pytest results ergo (devel)
path: pytest.xml
test-hybrid:
needs:
- build-hybrid
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -593,8 +582,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -602,17 +591,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
hybrid
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_hybrid_devel
name: pytest results hybrid (devel)
path: pytest.xml
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -626,8 +614,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -635,18 +623,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
make inspircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_devel
name: pytest results inspircd (devel)
path: pytest.xml
test-inspircd-anope:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -665,8 +652,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -674,16 +661,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_devel
name: pytest results inspircd-anope (devel)
path: pytest.xml
test-ircu2:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -704,8 +690,8 @@ jobs:
./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug
make -j 4
make install
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -713,16 +699,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
ircu2
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ircu2_devel
name: pytest results ircu2 (devel)
path: pytest.xml
test-limnoria:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -732,8 +717,8 @@ jobs:
- name: Install dependencies
run: pip install git+https://github.com/ProgVal/Limnoria.git@testing cryptography
pyxmpp2-scram
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -741,55 +726,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
limnoria
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_limnoria_devel
path: pytest.xml
test-nefarious:
needs: []
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: Checkout nefarious
uses: actions/checkout@v2
with:
path: nefarious
ref: master
repository: evilnet/nefarious2
- name: Build nefarious
run: |
cd $GITHUB_WORKSPACE/nefarious
./configure --prefix=$HOME/.local/ --enable-debug
make -j 4
make install
cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib
- 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 make
nefarious
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_nefarious_devel
name: pytest results limnoria (devel)
path: pytest.xml
test-ngircd:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -803,8 +749,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -812,18 +758,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
make ngircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd_devel
name: pytest results ngircd (devel)
path: pytest.xml
test-ngircd-anope:
needs:
- build-ngircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -842,8 +787,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -851,17 +796,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make
ngircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-anope_devel
name: pytest results ngircd-anope (devel)
path: pytest.xml
test-ngircd-atheme:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -875,8 +819,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -884,18 +828,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
make ngircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-atheme_devel
name: pytest results ngircd-atheme (devel)
path: pytest.xml
test-plexus4:
needs:
- build-plexus4
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -914,8 +857,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -923,17 +866,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
plexus4
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_plexus4_devel
name: pytest results plexus4 (devel)
path: pytest.xml
test-solanum:
needs:
- build-solanum
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -947,8 +889,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -956,16 +898,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
solanum
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_solanum_devel
name: pytest results solanum (devel)
path: pytest.xml
test-sopel:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -974,8 +915,8 @@ jobs:
python-version: 3.7
- name: Install dependencies
run: pip install git+https://github.com/sopel-irc/sopel.git
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -983,17 +924,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
sopel
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_sopel_devel
name: pytest results sopel (devel)
path: pytest.xml
test-unrealircd:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1007,8 +947,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1016,17 +956,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd_devel
name: pytest results unrealircd (devel)
path: pytest.xml
test-unrealircd-5:
needs:
- build-unrealircd-5
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1040,8 +979,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1049,18 +988,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd-5
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-5_devel
name: pytest results unrealircd-5 (devel)
path: pytest.xml
test-unrealircd-anope:
needs:
- build-unrealircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1079,8 +1017,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1088,17 +1026,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make
unrealircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-anope_devel
name: pytest results unrealircd-anope (devel)
path: pytest.xml
test-unrealircd-atheme:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1112,8 +1049,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1121,58 +1058,11 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-atheme_devel
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
name: pytest results unrealircd-atheme (devel)
path: pytest.xml
name: irctest with devel versions
'on':

View File

@ -3,24 +3,20 @@
jobs:
build-anope:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
- name: Cache Anope
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-devel_release
key: 3-${{ runner.os }}-anope-2.0.9
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
uses: actions/checkout@v2
with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |
run: |-
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -75,39 +71,43 @@ jobs:
retention-days: 1
publish-test-results:
if: success() || failure()
name: Publish Dashboard
name: Publish Unit Tests Results
needs:
- test-inspircd
- test-inspircd-anope
- test-inspircd-atheme
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
run: |-
python -m pip install --upgrade pip
pip install defusedxml docutils -r requirements.txt
- name: Generate dashboard
run: |-
shopt -s globstar
python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml
echo '/ /index.xhtml' > dashboard/_redirects
- name: Install netlify-cli
run: npm i -g netlify-cli
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
name: Deploy to Netlify
run: ./.github/deploy_to_netlify.py
- if: github.event_name == 'pull_request'
name: Publish Unit Test Results
uses: actions/github-script@v4
with:
result-encoding: string
script: |
let body = '';
const options = {};
options.listeners = {
stdout: (data) => {
body += data.toString();
}
};
await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options);
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
return body;
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -121,8 +121,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -130,18 +130,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
make inspircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_devel_release
name: pytest results inspircd (devel_release)
path: pytest.xml
test-inspircd-anope:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -160,8 +159,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -169,17 +168,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_devel_release
name: pytest results inspircd-anope (devel_release)
path: pytest.xml
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -193,8 +191,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -202,12 +200,11 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
make inspircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-atheme_devel_release
name: pytest results inspircd-atheme (devel_release)
path: pytest.xml
name: irctest with devel_release versions
'on':

View File

@ -3,24 +3,20 @@
jobs:
build-anope:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
- name: Cache Anope
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-stable
key: 3-${{ runner.os }}-anope-2.0.9
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
uses: actions/checkout@v2
with:
@ -28,7 +24,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |
run: |-
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
@ -43,7 +39,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-bahamut:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -71,7 +67,6 @@ jobs:
run: |
cd $GITHUB_WORKSPACE/Bahamut/
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
libtoolize --force
aclocal
@ -92,7 +87,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-charybdis:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -132,7 +127,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-hybrid:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -171,7 +166,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -202,7 +197,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-ngircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -243,7 +238,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-plexus4:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -285,7 +280,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-solanum:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -325,7 +320,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -347,7 +342,7 @@ jobs:
uses: actions/checkout@v2
with:
path: unrealircd
ref: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4
ref: daa0c11f285c7123ba9fa2966dee2d1a17729f1e
repository: unrealircd/unrealircd
- name: Build UnrealIRCd 6
run: |
@ -360,8 +355,6 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/
- name: Upload build artefacts
@ -371,7 +364,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd-5:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -406,8 +399,6 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/
- name: Upload build artefacts
@ -418,7 +409,7 @@ jobs:
retention-days: 1
publish-test-results:
if: success() || failure()
name: Publish Dashboard
name: Publish Unit Tests Results
needs:
- test-bahamut
- test-bahamut-anope
@ -432,7 +423,6 @@ jobs:
- test-irc2
- test-ircu2
- test-limnoria
- test-nefarious
- test-ngircd
- test-ngircd-anope
- test-ngircd-atheme
@ -443,35 +433,38 @@ jobs:
- test-unrealircd-5
- test-unrealircd-anope
- test-unrealircd-atheme
- test-unrealircd-dlk
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
run: |-
python -m pip install --upgrade pip
pip install defusedxml docutils -r requirements.txt
- name: Generate dashboard
run: |-
shopt -s globstar
python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml
echo '/ /index.xhtml' > dashboard/_redirects
- name: Install netlify-cli
run: npm i -g netlify-cli
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
name: Deploy to Netlify
run: ./.github/deploy_to_netlify.py
- if: github.event_name == 'pull_request'
name: Publish Unit Test Results
uses: actions/github-script@v4
with:
result-encoding: string
script: |
let body = '';
const options = {};
options.listeners = {
stdout: (data) => {
body += data.toString();
}
};
await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options);
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
return body;
test-bahamut:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -485,8 +478,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -494,18 +487,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut_stable
name: pytest results bahamut (stable)
path: pytest.xml
test-bahamut-anope:
needs:
- build-bahamut
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -524,8 +516,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -533,17 +525,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-anope_stable
name: pytest results bahamut-anope (stable)
path: pytest.xml
test-bahamut-atheme:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -557,8 +548,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -566,17 +557,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
bahamut-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-atheme_stable
name: pytest results bahamut-atheme (stable)
path: pytest.xml
test-charybdis:
needs:
- build-charybdis
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -590,8 +580,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -599,16 +589,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
charybdis
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_charybdis_stable
name: pytest results charybdis (stable)
path: pytest.xml
test-ergo:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -623,15 +612,15 @@ jobs:
repository: ergochat/ergo
- uses: actions/setup-go@v2
with:
go-version: ^1.19.0
go-version: ^1.18.0
- run: go version
- name: Build Ergo
run: |
cd $GITHUB_WORKSPACE/ergo/
make build
make install
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -639,18 +628,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH
make ergo
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ergo_stable
name: pytest results ergo (stable)
path: pytest.xml
test-hybrid:
needs:
- build-hybrid
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -669,8 +657,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -678,17 +666,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
hybrid
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_hybrid_stable
name: pytest results hybrid (stable)
path: pytest.xml
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -702,8 +689,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -711,18 +698,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
make inspircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_stable
name: pytest results inspircd (stable)
path: pytest.xml
test-inspircd-anope:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -741,8 +727,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -750,17 +736,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_stable
name: pytest results inspircd-anope (stable)
path: pytest.xml
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -774,8 +759,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -783,49 +768,53 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
make inspircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-atheme_stable
name: pytest results inspircd-atheme (stable)
path: pytest.xml
test-irc2:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout irc2
uses: actions/checkout@v2
with:
path: irc2.11.2p3
ref: 59649f24c3a5c27bad5648b48774f27475bccfd3
repository: irc-archive/irc2-mirror
- name: Build irc2
run: |
# Configure
cd $GITHUB_WORKSPACE/irc2.11.2p3
./configure --prefix=$HOME/.local/
cd x86*
echo "#define CMDLINE_CONFIG/" >> config.h
echo "#define DEFAULT_SPLIT_USERS 0" >> config.h
echo "#define DEFAULT_SPLIT_SERVERS 0" >> config.h
#echo "#undef LIST_ALIS_NOTE" >> config.h
# TODO: find a better way to make it not fork...
echo "#define fork() (0)" >> config.h
- name: Get source code
run: curl http://ftp.irc.org/ftp/irc/server/irc2.11.2p3.tgz | tar -zx
- name: Configure
run: 'cd $GITHUB_WORKSPACE/irc2.11.2p3
./configure --prefix=$HOME/.local/
cd x86*
echo "#define CMDLINE_CONFIG/" >> config.h
echo "#define DEFAULT_SPLIT_USERS 0" >> config.h
echo "#define DEFAULT_SPLIT_SERVERS 0" >> config.h
#echo "#undef LIST_ALIS_NOTE" >> config.h
# TODO: find a better way to make it not fork...
echo "#define fork() (0)" >> config.h'
- name: Compile and install
run: 'cd $GITHUB_WORKSPACE/irc2.11.2p3/x86*
# Compile and install
cd $GITHUB_WORKSPACE/irc2.11.2p3/x86*
make -j 4 all
make install
mkdir -p $HOME/.local/bin
cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd'
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -833,16 +822,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
irc2
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_irc2_stable
name: pytest results irc2 (stable)
path: pytest.xml
test-ircu2:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -863,8 +851,8 @@ jobs:
./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug
make -j 4
make install
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -872,16 +860,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
ircu2
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ircu2_stable
name: pytest results ircu2 (stable)
path: pytest.xml
test-limnoria:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -890,8 +877,8 @@ jobs:
python-version: 3.7
- name: Install dependencies
run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -899,55 +886,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
limnoria
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_limnoria_stable
path: pytest.xml
test-nefarious:
needs: []
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: Checkout nefarious
uses: actions/checkout@v2
with:
path: nefarious
ref: 985704168ecada12d9e53b46df6087ef9d9fb40b
repository: evilnet/nefarious2
- name: Build nefarious
run: |
cd $GITHUB_WORKSPACE/nefarious
./configure --prefix=$HOME/.local/ --enable-debug
make -j 4
make install
cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib
- 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 make
nefarious
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_nefarious_stable
name: pytest results limnoria (stable)
path: pytest.xml
test-ngircd:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -961,8 +909,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -970,18 +918,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
make ngircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd_stable
name: pytest results ngircd (stable)
path: pytest.xml
test-ngircd-anope:
needs:
- build-ngircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1000,8 +947,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1009,17 +956,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make
ngircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-anope_stable
name: pytest results ngircd-anope (stable)
path: pytest.xml
test-ngircd-atheme:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1033,8 +979,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1042,18 +988,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
make ngircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-atheme_stable
name: pytest results ngircd-atheme (stable)
path: pytest.xml
test-plexus4:
needs:
- build-plexus4
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1072,8 +1017,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1081,17 +1026,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
plexus4
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_plexus4_stable
name: pytest results plexus4 (stable)
path: pytest.xml
test-solanum:
needs:
- build-solanum
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1105,8 +1049,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1114,16 +1058,15 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
solanum
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_solanum_stable
name: pytest results solanum (stable)
path: pytest.xml
test-sopel:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1132,8 +1075,8 @@ jobs:
python-version: 3.7
- name: Install dependencies
run: pip install sopel==7.1.8
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1141,17 +1084,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make
sopel
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_sopel_stable
name: pytest results sopel (stable)
path: pytest.xml
test-unrealircd:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1165,8 +1107,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1174,17 +1116,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd_stable
name: pytest results unrealircd (stable)
path: pytest.xml
test-unrealircd-5:
needs:
- build-unrealircd-5
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1198,8 +1139,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1207,18 +1148,17 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd-5
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-5_stable
name: pytest results unrealircd-5 (stable)
path: pytest.xml
test-unrealircd-anope:
needs:
- build-unrealircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1237,8 +1177,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1246,17 +1186,16 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make
unrealircd-anope
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-anope_stable
name: pytest results unrealircd-anope (stable)
path: pytest.xml
test-unrealircd-atheme:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1270,8 +1209,8 @@ jobs:
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install Atheme
run: sudo apt-get install atheme-services
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
@ -1279,58 +1218,11 @@ jobs:
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
make unrealircd-atheme
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-atheme_stable
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
name: pytest results unrealircd-atheme (stable)
path: pytest.xml
name: irctest with stable versions
'on':

View File

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

144
Makefile
View File

@ -7,47 +7,88 @@ PYTEST_ARGS ?=
# Will be appended at the end of the -k argument to pytest
EXTRA_SELECTORS ?=
# testPlainLarge fails because it doesn't handle split AUTHENTICATE (reported on IRC)
ANOPE_SELECTORS := \
and not testPlainLarge
# buffering tests cannot pass because of issues with UTF-8 handling: https://github.com/DALnet/bahamut/issues/196
# mask tests in test_who.py fail because they are not implemented.
# some HelpTestCase::*[HELP] tests fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP)
# testWhowasMultiTarget fails because Bahamut returns the results in query order instead of chronological order
BAHAMUT_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not IRCv3 \
and not buffering \
and not (testWho and not whois and mask) \
and not testWhoStar \
and (not HelpTestCase or HELPOP) \
and not testWhowasMultiTarget \
$(EXTRA_SELECTORS)
# testQuitErrors is very flaky
# AccountTagTestCase.testInvite fails because https://github.com/solanum-ircd/solanum/issues/166
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
# testWhoisNumerics[oper] fails because charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR
# testWhowasNoSuchNick fails because of a typo (solved in https://github.com/solanum-ircd/solanum/commit/08b7b6bd7e60a760ad47b58cbe8075b45d66166f)
CHARYBDIS_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testQuitErrors \
and not testKickDefaultComment \
and not (AccountTagTestCase and testInvite) \
and not (testWhoisNumerics and oper) \
and not testWhowasNoSuchNick \
$(EXTRA_SELECTORS)
# testInfoNosuchserver does not apply to Ergo: Ergo ignores the optional <target> argument
ERGO_SELECTORS := \
not deprecated \
and not testInfoNosuchserver \
$(EXTRA_SELECTORS)
# testInviteUnopped is the only strict test that Hybrid fails
HYBRID_SELECTORS := \
not Ergo \
and not testInviteUnopped \
and not deprecated \
$(EXTRA_SELECTORS)
# testBotPrivateMessage and testBotChannelMessage fail because https://github.com/inspircd/inspircd/pull/1910 is not released yet
# WHOWAS tests fail because https://github.com/inspircd/inspircd/pull/1967 and https://github.com/inspircd/inspircd/pull/1968 are not released yet
INSPIRCD_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testNoticeNonexistentChannel \
and not testBotPrivateMessage and not testBotChannelMessage \
and not whowas \
$(EXTRA_SELECTORS)
# buffering tests fail because ircu2 discards the whole buffer on long lines (TODO: refine how we exclude these tests)
# testQuit and testQuitErrors fail because ircu2 does not send ERROR or QUIT
# lusers "full" tests fail because they depend on Modern behavior, not just RFC2812
# statusmsg tests fail because STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG target
# testKeyValidation[empty] fails because ircu2 returns ERR_NEEDMOREPARAMS on empty keys: https://github.com/UndernetIRC/ircu2/issues/13
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS.
# HelpTestCase fails because it returns NOTICEs instead of numerics
# testWhowasCountZero fails: https://github.com/UndernetIRC/ircu2/pull/19
IRCU2_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS)
# same justification as ircu2
# lusers "unregistered" tests fail because
NEFARIOUS_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not buffering \
and not testQuit \
and not (lusers and full) \
and not statusmsg \
and not (testKeyValidation and empty) \
and not testKickDefaultComment \
and not testEmptyRealname \
and not HelpTestCase \
and not testWhowasCountZero \
$(EXTRA_SELECTORS)
# same justification as ircu2
@ -55,12 +96,24 @@ SNIRCD_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not buffering \
and not testQuit \
and not (lusers and full) \
and not statusmsg \
$(EXTRA_SELECTORS)
# testListEmpty and testListOne fails because irc2 deprecated LIST
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
# testWallopsPrivileges fails because it ignores the command instead of replying ERR_UNKNOWNCOMMAND
# HelpTestCase fails because it returns NOTICEs instead of numerics
IRC2_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testListEmpty and not testListOne \
and not testKickDefaultComment \
and not testWallopsPrivileges \
and not HelpTestCase \
$(EXTRA_SELECTORS)
MAMMON_SELECTORS := \
@ -69,14 +122,28 @@ MAMMON_SELECTORS := \
and not strict \
$(EXTRA_SELECTORS)
# testKeyValidation[spaces] and testKeyValidation[empty] fail because ngIRCd does not validate them https://github.com/ngircd/ngircd/issues/290
# testStarNick: wat
# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS.
# chathistory tests fail because they need nicks longer than 9 chars
# HelpTestCase::*[HELP] fails because it returns NOTICEs instead of numerics
NGIRCD_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not (testKeyValidation and (spaces or empty)) \
and not testStarNick \
and not testEmptyRealname \
and not chathistory \
and (not HelpTestCase or HELPOP) \
$(EXTRA_SELECTORS)
# testInviteUnopped is the only strict test that Plexus4 fails
# testInviteInviteOnly fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only
PLEXUS4_SELECTORS := \
not Ergo \
and not testInviteUnopped \
and not testInviteInviteOnly \
and not deprecated \
$(EXTRA_SELECTORS)
@ -87,33 +154,51 @@ LIMNORIA_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS)
# testQuitErrors is too flaky for CI
# testKickDefaultComment fails because solanum uses the nick of the kickee rather than the kicker.
SOLANUM_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testQuitErrors \
and not testKickDefaultComment \
$(EXTRA_SELECTORS)
# Same as Limnoria
SOPEL_SELECTORS := \
(foo or not foo) \
not testPlainNotAvailable \
$(EXTRA_SELECTORS)
# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949
# regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948
# messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947
# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
# testChathistory[BETWEEN] fails: https://bugs.unrealircd.org/view.php?id=5952
# testChathistory[AROUND] fails: https://bugs.unrealircd.org/view.php?id=5953
# testWhoAllOpers fails because Unreal skips results when the mask is too broad
# HELP and HELPOP tests fail because Unreal uses custom numerics https://github.com/unrealircd/unrealircd/pull/184
UNREALIRCD_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testNoticeNonexistentChannel \
and not (regressions.py and testTagCap) \
and not (messages.py and testLineTooLong) \
and not (cap.py and (testCapRemovalByClient or testNakWhole)) \
and not (account_tag.py and testInvite) \
and not arbitrary_client_tags \
and not react_tag \
and not private_chathistory \
and not (testChathistory and (between or around)) \
and not testWhoAllOpers \
and not HelpTestCase \
$(EXTRA_SELECTORS)
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd
flakes:
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
@ -122,8 +207,7 @@ bahamut:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.bahamut \
-m 'not services' \
-n 4 \
-vv -s \
-n 10 \
-k '$(BAHAMUT_SELECTORS)'
bahamut-atheme:
@ -131,6 +215,7 @@ bahamut-atheme:
--controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.atheme_services \
-m 'services' \
-n 10 \
-k '$(BAHAMUT_SELECTORS)'
bahamut-anope:
@ -138,7 +223,8 @@ bahamut-anope:
--controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.anope_services \
-m 'services' \
-k '$(BAHAMUT_SELECTORS)'
-n 10 \
-k '$(BAHAMUT_SELECTORS) $(ANOPE_SELECTORS)'
charybdis:
$(PYTEST) $(PYTEST_ARGS) \
@ -175,34 +261,27 @@ inspircd-anope:
--controller=irctest.controllers.inspircd \
--services-controller=irctest.controllers.anope_services \
-m 'services' \
-k '$(INSPIRCD_SELECTORS)'
-k '$(INSPIRCD_SELECTORS) $(ANOPE_SELECTORS)'
ircu2:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.ircu2 \
-m 'not services and not IRCv3' \
-n 4 \
-n 10 \
-k '$(IRCU2_SELECTORS)'
nefarious:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.nefarious \
-m 'not services' \
-n 4 \
-k '$(NEFARIOUS_SELECTORS)'
snircd:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.snircd \
-m 'not services and not IRCv3' \
-n 4 \
-n 10 \
-k '$(SNIRCD_SELECTORS)'
irc2:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.irc2 \
-m 'not services and not IRCv3' \
-n 4 \
-n 10 \
-k '$(IRC2_SELECTORS)'
limnoria:
@ -225,7 +304,7 @@ ngircd:
$(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \
-m 'not services' \
-n 4 \
-n 10 \
-k "$(NGIRCD_SELECTORS)"
ngircd-anope:
@ -273,11 +352,4 @@ unrealircd-anope:
--controller=irctest.controllers.unrealircd \
--services-controller=irctest.controllers.anope_services \
-m 'services' \
-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)'
-k '$(UNREALIRCD_SELECTORS) $(ANOPE_SELECTORS)'

View File

@ -18,11 +18,11 @@ have no side effect.
Install irctest and dependencies:
```
sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py
cd ~
git clone https://github.com/ProgVal/irctest.git
cd irctest
pip3 install --user -r requirements.txt
python3 setup.py install --user
```
Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo)

View File

@ -1,83 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDT0URxi7/l7ZGe
tkPv9Yh8h2s9BpbAR4Wq8sakgqETWg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa
/eg5cyJv4Uu5WIZpG1LxdPEEIOSMWjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiK
kiAahg1Cn1vRrQ4cRrG+AkQWpRHJEDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEw
XjIwx2FoDyKaNGRH5Z7gLWvCKBNxQuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZL
LErWuJ7kEhLZG2HDW5JaXOr0QfFYAA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN
7vIj+TT6TemwcAT25pwMJIVS4WC4+BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/b
vpK8jZeRdqFzE1eBCgPkw8D8V0r7J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1
lzQ52grxgc28Ejd1fGQXIJmdTh4BqKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2
Ex4X6QEj76iCxH+J/01/cvbxMe3iuGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0
t2t6kOS83ebhnpgHClgFs8/C3ayzYBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4Ak
fZcWD18loZai+QznVzbLNINf++rTwwIDAQABAoICAQCs1tT3piZHU2yAyo9zLbJa
kxGxcT/v1CzBSmtG8ATJyrKxRzhxszdj9HABbzjcqrXJFbKA+5yy+OFdOtSUlFtk
Wb21lwPOnmo29sp4KPL1h+itEzMuMwZ4DBry1aFZvPSsnPpoHJwwUbY3hHdHzVzi
oTCGTqT188Wzmxu+MqHppCEJLSj45ZPzvyxU1UwwW0a4H+ZTM7WlEYlzw1lHLlpQ
VBFTLS8q77aNjOKiQptiz0X+zpS0dhJvu3l7BhwtMRS8prmqnffG4r0QWCsVPP8C
cbEJkWtbwswQikN6Xpi1yw6UTQZ8brZa810aOSh07EJTfrU35rjxAndEspbJPd+4
Zz6fKNnRA7A4fFap2hF4TSP/UF12b4ulQ8FfcMMTFszWko5M6fWFuCeWfNbyXML5
fmn+NmSOboj7LkDqbpxtyaIVXXb2Y3F6A2fNllm/mxaGrRoEGNaH3o0qBgWRzJJB
TDSvIQtJddzL+iMaqxz4ufXAREJernZmPa3vlkVGLINNQUC9JLrB5eFjLzPiZN2U
8RgQ9YX5tjoJ+DtPWuMFDiCS1ZE20/UBOEYTeqIVuKdK3AjJDMFSjg8fRvsWRqZe
zsHv6tCtIFZFxYRxtrRGTUPQF+1QD6zBjYxZZk1B4n3uYBGVQFM/LnNHUxRnJBx1
PunD4ICOY97xd2hcPmGiCQKCAQEA8NCXYaHzhv6fg95H/iMuJVcOCKrJ5rVr4poG
SD0KZtS7SLzUYat8WcuoSubh5Eb2hHtrsnLrSOTnwQUO61f4gCRm2sEqHYsOAd7+
mNe1jfil0UBVqqL9GBcGYJkc5+DHgUlJQaxMV+4YLt8fD0KfZEnHaDAYX3kUdz+p
be//YAKv+JmxWcUdBF60AUWPjbCJT/1pfJeY8nEBFiYtlYKKN24+4OiRdJ2yRGzt
ZtNHaWy5EFF70yVgPX5MGQ7Z2JpejzK+lt+9nG4h1uJ4M2X4YrGVrRCn1W8jwqm/
bXest3E6wkkLoWDm9EaeYj00DUgMOviPyP4ckyxilG+Fny4JbwKCAQEA4SyUV03X
KEoL5sOD69sLu3TpnIQz73u9an9W/f2f7rsGwmCcR9RdYGV29ltwfBvOm0FnPQio
GliN+3PTWAL6bb8VYo2IR53VKpVHKSQUlzDOD9PGObXw1CT/+0zoMP7FBA4dTJDf
xQ63AQNpSCGdwbxZygPWzLV5O1WxMeXhnQRL1EBvMyJ52od0+HbajDXg5mNiBKNQ
AtVhB9pEu47Blu/KBqWjfh/GeBLPZB7MHmGNBYbBGGskKRLG4sIbwShs9cx8UM0/
9dxXkX2d8hjnSD/0ZBh54HHUaEvKAKfpz1L8AC0ll7biCAy0CZK23AoZu/KT8zJ+
qvz3AuJcW3lo7QKCAQEAzfGltNJawPUamBzNttKBUV+s2c6tkkdO92C/xKGnNp/x
dtg+TTTpyKV5zGy9fIsPoecnCFptS06vwAvCYZQ/Kd93stcFXHSiSwlY9H9tfffK
XzTEzoRLLIHsa0omRUufcrqpEqf2NjChr9wS5OsWAx9xkHGpNmUHEqB4FlPsM0C5
G0LdQCdplGYlTP0fMo5qL+VJhErle1kXE8kcrMMRzyvSTGe4lWGTph79vDUt2kQn
1IPLAJzzPEO5cqiXtzz1Z0N/aOn5b0FkYTAWmeY30LeMiJA46Df+/ihLVKPHKq6E
EMmFT8LeYMPQCbXLwRv/kaMm3D4tU9PejpD9Vk95swKCAQAtULBlxXeIVyaAAVba
L1H0Hroo0n41MtzSwt+568G05JSep5yr4/QKw0CmoY5Im7v/iLEDGmviKXIhaZTd
wHOvhGYEWGFVsFDG6hXRFL7EEoFVtBPPZ2sY9n1BkJ+lxI/XmhORZhJycNypaotU
hddets4HFrCyr86+/ybS2OWHmOa9x13Zl5WYQexrWFfxIaKqGtQOBOPEPjbxwp5U
dI1HF+i7X7hAWJqzbW2pQ31mm9EqjIztoho73diCp/e37q/G46kdBcFadEZ3NCWG
JDbfVmeTgU19usq5Vo9HhINMQvIOAwfuuVJRtmTBDHKaY7n8FfxqU/4j4RbA0Ncv
XYadAoIBAQC7yh4/UZCGhklUhhk/667OfchWvWGriCSaYGmQPdMzxjnIjAvvIUe9
riOTBSZXYYLmZHsmY/jK7KMGB3AsLTypSa9+ddAWqWn2dvOYyxNiAaSJK/RZfA9A
ocVfvvkhOfNAYIF+A+fyJ2pznsDkBf9tPkhN7kovl+mr/e25qZb1d09377770Pi7
thzEi+JLrRgYVLrCrPi2j4l7/Va/UaAPz+Dtu2GCT9vXgnhZtpb8R1kTViZFryTv
k+qbNYJzVm61Vit9mVAGe+WuzhlclJnN6LIZGG3zYHIulRAJrH1XDauHZfHzCKgi
FnMesy4thDMH/MhUfRtbylZTq45gtvCA
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIUYHD08+9S32VTD9IEsr2Oe1dH3VEwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA0MDQxODE2NTZaFw0yMzA0
MDQxODE2NTZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDT0URxi7/l7ZGetkPv9Yh8h2s9BpbAR4Wq8sakgqET
Wg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa/eg5cyJv4Uu5WIZpG1LxdPEEIOSM
WjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiKkiAahg1Cn1vRrQ4cRrG+AkQWpRHJ
EDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEwXjIwx2FoDyKaNGRH5Z7gLWvCKBNx
QuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZLLErWuJ7kEhLZG2HDW5JaXOr0QfFY
AA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN7vIj+TT6TemwcAT25pwMJIVS4WC4
+BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/bvpK8jZeRdqFzE1eBCgPkw8D8V0r7
J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1lzQ52grxgc28Ejd1fGQXIJmdTh4B
qKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2Ex4X6QEj76iCxH+J/01/cvbxMe3i
uGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0t2t6kOS83ebhnpgHClgFs8/C3ayz
YBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4AkfZcWD18loZai+QznVzbLNINf++rT
wwIDAQABo1MwUTAdBgNVHQ4EFgQU+9eHi2eqy0f3fDS0GjqkijGDDocwHwYDVR0j
BBgwFoAU+9eHi2eqy0f3fDS0GjqkijGDDocwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEAAJXO3qUc/PW75pI2dt1cKv20VqozkfEf7P0eeVisCDxn
1p3QhVgI2lEe9kzdHp7t42g5xLkUhQEVmBcKm9xbl+k2D1X0+T8px1x6ZiWfbhXL
ptc/qCIXjPCgVN3s+Kasii3hHkZxKGZz/ySmBmfDJZjQZtbZzQWpvvX6SD4s7sjo
gWbZW3qvQ0bFTGdD1IjKYGaxK6aSrNkAIutiAX4RczJ1QSwb9Z2EIen+ABAvOZS9
xv3LiiidWcuOT7WzXEa4QvOslCEkAF+jj6mGYB7NWtly0kj4AEPvI4IoYTi9dohS
CA0zd1DTfjRwpAnT5P4sj4mpjLyRBumeeVGpCZhUxfKpFjIB2AnlgxrU+LPq5c9R
ZZ9Q5oeLxjRPjpqBeWwgnbjXstQCL9g0U7SsEemsv+zmvG5COhAmG5Wce/65ILlg
450H4bcn1ul0xvxz9hat6tqEZry3HcNE/CGDT+tXuhHpqOXkY1/c78C0QbWjWodR
tCvlXW00a+7TlEhNr4XBNdqtIQfYS9K9yiVVNfZLPEsN/SA3BGXmrr+du1/E4Ria
CkVpmBdJsVu5eMaUj1arsCqI4fwHzljtojJe/pCzZBVkOaSWQEQ+LL4iVnMas68m
qyshtNf4KNiM55OQmyTiFHMTIxCtdEcHaR3mUxR7GrIhc/bxyxUUBtMAuUX0Kjs=
-----END CERTIFICATE-----

View File

@ -2,12 +2,10 @@ from __future__ import annotations
import dataclasses
import os
from pathlib import Path
import shutil
import socket
import subprocess
import tempfile
import textwrap
import time
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
@ -89,7 +87,7 @@ class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an
arbitrary directory."""
directory: Optional[Path]
directory: Optional[str]
def __init__(self, test_config: TestCaseControllerConfig):
super().__init__(test_config)
@ -112,21 +110,22 @@ class DirectoryBasedController(_BaseController):
"""Open a file in the configuration directory."""
assert self.directory
if os.sep in name:
dir_ = self.directory / os.path.dirname(name)
dir_.mkdir(parents=True, exist_ok=True)
assert dir_.is_dir()
return (self.directory / name).open(mode)
dir_ = os.path.join(self.directory, os.path.dirname(name))
if not os.path.isdir(dir_):
os.makedirs(dir_)
assert os.path.isdir(dir_)
return open(os.path.join(self.directory, name), mode)
def create_config(self) -> None:
if not self.directory:
self.directory = Path(tempfile.mkdtemp())
self.directory = tempfile.mkdtemp()
def gen_ssl(self) -> None:
assert self.directory
self.csr_path = self.directory / "ssl.csr"
self.key_path = self.directory / "ssl.key"
self.pem_path = self.directory / "ssl.pem"
self.dh_path = self.directory / "dh.pem"
self.csr_path = os.path.join(self.directory, "ssl.csr")
self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = os.path.join(self.directory, "ssl.pem")
self.dh_path = os.path.join(self.directory, "dh.pem")
subprocess.check_output(
[
self.openssl_bin,
@ -157,18 +156,10 @@ class DirectoryBasedController(_BaseController):
],
stderr=subprocess.DEVNULL,
)
with self.dh_path.open("w") as fd:
fd.write(
textwrap.dedent(
"""
-----BEGIN DH PARAMETERS-----
MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD
HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr
rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC
-----END DH PARAMETERS-----
"""
)
)
subprocess.check_output(
[self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
stderr=subprocess.DEVNULL,
)
class BaseClientController(_BaseController):
@ -198,10 +189,6 @@ class BaseServerController(_BaseController):
"""Character used for the 'mute' extban"""
nickserv = "NickServ"
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
@ -215,7 +202,6 @@ class BaseServerController(_BaseController):
run_services: bool,
valid_metadata_keys: Optional[Set[str]],
invalid_metadata_keys: Optional[Set[str]],
faketime: Optional[str],
) -> None:
raise NotImplementedError()
@ -231,7 +217,6 @@ class BaseServerController(_BaseController):
raise NotImplementedByController("account registration")
def wait_for_port(self) -> None:
started_at = time.time()
while not self.port_open:
self.check_is_alive()
time.sleep(self._port_wait_interval)
@ -254,16 +239,11 @@ class BaseServerController(_BaseController):
# ircu2 cuts the connection without a message if registration
# is not complete.
pass
except socket.timeout:
# irc2 just keeps it open
pass
c.close()
self.port_open = True
except ConnectionRefusedError:
if time.time() - started_at >= 60:
# waited for 60 seconds, giving up
raise
except Exception:
continue
def wait_for_services(self) -> None:
assert self.services_controller
@ -310,11 +290,10 @@ class BaseServicesController(_BaseController):
c.sendLine("PONG :" + msg.params[0])
c.getMessages()
timeout = time.time() + 3
timeout = time.time() + 5
while True:
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help")
msgs = self.getNickServResponse(c, timeout=1)
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP")
msgs = self.getNickServResponse(c)
for msg in msgs:
if msg.command == "401":
# NickServ not available yet
@ -340,12 +319,11 @@ class BaseServicesController(_BaseController):
c.disconnect()
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
is queried asynchronously."""
msgs: List[Message] = []
start_time = time.time()
while not msgs and (not timeout or start_time + timeout > time.time()):
while not msgs:
time.sleep(0.05)
msgs = client.getMessages()
return msgs

View File

@ -69,30 +69,6 @@ TController = TypeVar("TController", bound=basecontrollers._BaseController)
T = TypeVar("T")
def retry(f: TCallable) -> TCallable:
"""Retry the function if it raises ConnectionClosed; as a workaround for flaky
connection, such as::
1: connects to server.
1 -> S: NICK foo
1 -> S: USER username * * :Realname
S -> 1: :My.Little.Server NOTICE * :*** Found your hostname (cached)
S -> 1: :My.Little.Server NOTICE * :*** Checking Ident
S -> 1: :My.Little.Server NOTICE * :*** No Ident response
S -> 1: ERROR :Closing Link: cpu-pool.com (Use a different port)
"""
@functools.wraps(f)
def newf(*args, **kwargs): # type: ignore
try:
return f(*args, **kwargs)
except ConnectionClosed:
time.sleep(1)
return f(*args, **kwargs)
return newf # type: ignore
class ChannelJoinException(Exception):
def __init__(self, code: str, params: List[str]):
super().__init__(f"Failed to join channel ({code}): {params}")
@ -173,7 +149,7 @@ class _IrcTestCase(Generic[TController]):
) -> Optional[str]:
"""Returns an error message if the message doesn't match the given arguments,
or None if it matches."""
for key, value in kwargs.items():
for (key, value) in kwargs.items():
if getattr(msg, key) != value:
fail_msg = (
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
@ -351,8 +327,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
nick: Optional[str] = None
user: Optional[List[str]] = None
server: socket.socket
protocol_version: Optional[str]
acked_capabilities: Optional[Set[str]]
protocol_version = Optional[str]
acked_capabilities = Optional[Set[str]]
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
@ -448,9 +424,7 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
print("{:.3f} S: {}".format(time.time(), line.strip()))
def readCapLs(
self,
auth: Optional[Authentication] = None,
tls_config: Optional[tls.TlsConfig] = None,
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
) -> None:
(hostname, port) = self.server.getsockname()
self.controller.run(
@ -460,9 +434,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
m = self.getMessage()
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
if m.params == ["LS"]:
self.protocol_version = "301"
self.protocol_version = 301
elif m.params == ["LS", "302"]:
self.protocol_version = "302"
self.protocol_version = 302
elif m.params == ["END"]:
self.protocol_version = None
else:
@ -534,12 +508,6 @@ class BaseServerTestCase(
server_support: Optional[Dict[str, Optional[str]]]
run_services = False
faketime: Optional[str] = None
"""If not None and the controller supports it and libfaketime is available,
runs the server using faketime and this value set as the $FAKETIME env variable.
Tests must check ``self.controller.faketime_enabled`` is True before
relying on this."""
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
def setUp(self) -> None:
@ -554,7 +522,6 @@ class BaseServerTestCase(
invalid_metadata_keys=self.invalid_metadata_keys,
ssl=self.ssl,
run_services=self.run_services,
faketime=self.faketime,
)
self.clients: Dict[TClientName, client_mock.ClientMock] = {}
@ -687,11 +654,10 @@ class BaseServerTestCase(
m = self.getRegistrationMessage(client)
self.assertIn(m.command, ["900", "903"], str(m))
@retry
def connectClient(
self,
nick: str,
name: Optional[TClientName] = None,
name: TClientName = None,
capabilities: Optional[List[str]] = None,
skip_if_cap_nak: bool = False,
show_io: Optional[bool] = None,
@ -706,7 +672,7 @@ class BaseServerTestCase(
client = self.addClient(name, show_io=show_io)
if capabilities:
self.sendLine(client, "CAP LS 302")
self.getCapLs(client)
m = self.getRegistrationMessage(client)
self.requestCapabilities(client, capabilities, skip_if_cap_nak)
if password is not None:
if "sasl" not in (capabilities or ()):
@ -736,12 +702,6 @@ class BaseServerTestCase(
self.server_support[param] = None
welcome.append(m)
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
item.split(":", 1)
for item in (self.server_support.get("TARGMAX") or "").split(",")
if item
)
return welcome
def joinClient(self, client: TClientName, channel: str) -> None:
@ -772,55 +732,50 @@ class BaseServerTestCase(
raise ChannelJoinException(msg.command, msg.params)
_TSelf = TypeVar("_TSelf", bound="_IrcTestCase")
_TSelf = TypeVar("_TSelf", bound="OptionalityHelper")
_TReturn = TypeVar("_TReturn")
def skipUnlessHasMechanism(
mech: str,
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
# Just a function returning a function that takes functions and
# returns functions, nothing to see here.
# If Python didn't have such an awful syntax for callables, it would be:
# str -> ((TSelf -> TReturn) -> (TSelf -> TReturn))
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
class OptionalityHelper(Generic[TController]):
controller: TController
def checkSaslSupport(self) -> None:
if self.controller.supported_sasl_mechanisms:
return
raise runner.NotImplementedByController("SASL")
def checkMechanismSupport(self, mechanism: str) -> None:
if mechanism in self.controller.supported_sasl_mechanisms:
return
raise runner.OptionalSaslMechanismNotSupported(mechanism)
@staticmethod
def skipUnlessHasMechanism(
mech: str,
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
# Just a function returning a function that takes functions and
# returns functions, nothing to see here.
# If Python didn't have such an awful syntax for callables, it would be:
# str -> ((TSelf -> TReturn) -> (TSelf -> TReturn))
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
@functools.wraps(f)
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
self.checkMechanismSupport(mech)
return f(self, *args, **kwargs)
return newf
return decorator
@staticmethod
def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
@functools.wraps(f)
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
if mech not in self.controller.supported_sasl_mechanisms:
raise runner.OptionalSaslMechanismNotSupported(mech)
self.checkSaslSupport()
return f(self, *args, **kwargs)
return newf
return decorator
def xfailIf(
condition: Callable[..., bool], reason: str
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
# Works about the same as skipUnlessHasMechanism
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
@functools.wraps(f)
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
if condition(self):
try:
return f(self, *args, **kwargs)
except Exception:
pytest.xfail(reason)
assert False # make mypy happy
else:
return f(self, *args, **kwargs)
return newf
return decorator
def xfailIfSoftware(
names: List[str], reason: str
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
return xfailIf(lambda testcase: testcase.controller.software_name in names, reason)
def mark_services(cls: TClass) -> TClass:
cls.run_services = True

View File

@ -1,5 +1,3 @@
"""Format of ``CAP LS`` sent by IRCv3 clients."""
from irctest import cases
from irctest.irc_utils.message_parser import Message

View File

@ -1,8 +1,3 @@
"""SASL authentication from clients, for all known mechanisms.
For now, only `SASLv3.1 <https://ircv3.net/specs/extensions/sasl-3.1>`_
is tested, not `SASLv3.2 <https://ircv3.net/specs/extensions/sasl-3.2>`_."""
import base64
import pytest
@ -39,8 +34,8 @@ class IdentityHash:
return self._data
class SaslTestCase(cases.BaseClientTestCase):
@cases.skipUnlessHasMechanism("PLAIN")
class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlain(self):
"""Test PLAIN authentication with correct username/password."""
auth = authentication.Authentication(
@ -60,8 +55,7 @@ class SaslTestCase(cases.BaseClientTestCase):
m = self.negotiateCapabilities(["sasl"], False)
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
@cases.skipUnlessHasMechanism("PLAIN")
@cases.xfailIfSoftware(["Sopel"], "Sopel requests SASL PLAIN even if not available")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainNotAvailable(self):
"""`sasl=EXTERNAL` is advertized, whereas the client is configured
to use PLAIN.
@ -91,7 +85,7 @@ class SaslTestCase(cases.BaseClientTestCase):
self.assertMessageMatch(m, command="CAP")
@pytest.mark.parametrize("pattern", ["barbaz", "éèà"])
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainLarge(self, pattern):
"""Test the client splits large AUTHENTICATE messages whose payload
is not a multiple of 400.
@ -120,7 +114,7 @@ class SaslTestCase(cases.BaseClientTestCase):
m = self.negotiateCapabilities(["sasl"], False)
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
@pytest.mark.parametrize("pattern", ["quux", "éè"])
def testPlainLargeMultiple(self, pattern):
"""Test the client splits large AUTHENTICATE messages whose payload
@ -151,7 +145,7 @@ class SaslTestCase(cases.BaseClientTestCase):
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
@pytest.mark.skipif(ecdsa is None, reason="python3-ecdsa is not available")
@cases.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE")
@cases.OptionalityHelper.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE")
def testEcdsa(self):
"""Test ECDSA authentication."""
auth = authentication.Authentication(
@ -185,7 +179,7 @@ class SaslTestCase(cases.BaseClientTestCase):
m = self.negotiateCapabilities(["sasl"], False)
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScram(self):
"""Test SCRAM-SHA-256 authentication."""
auth = authentication.Authentication(
@ -227,7 +221,7 @@ class SaslTestCase(cases.BaseClientTestCase):
self.assertEqual(m.command, "AUTHENTICATE", m)
self.assertEqual(m.params, ["+"], m)
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramBadPassword(self):
"""Test SCRAM-SHA-256 authentication with a bad password."""
auth = authentication.Authentication(
@ -262,8 +256,8 @@ class SaslTestCase(cases.BaseClientTestCase):
authenticator.response(msg)
class Irc302SaslTestCase(cases.BaseClientTestCase):
@cases.skipUnlessHasMechanism("PLAIN")
class Irc302SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainNotAvailable(self):
"""Test the client does not try to authenticate using a mechanism the
server does not advertise.

View File

@ -1,5 +1,3 @@
"""Clients should validate certificates; either with a CA or fingerprints."""
import socket
import ssl
@ -140,7 +138,7 @@ class TlsTestCase(cases.BaseClientTestCase):
self.getMessage()
class StsTestCase(cases.BaseClientTestCase):
class StsTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
def setUp(self):
super().setUp()
self.insecure_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

View File

@ -1,4 +1,4 @@
from pathlib import Path
import os
import shutil
import subprocess
from typing import Type
@ -73,8 +73,6 @@ module {{ name = "ns_cert" }}
class AnopeController(BaseServicesController, DirectoryBasedController):
"""Collaborator for server controllers that rely on Anope"""
software_name = "Anope"
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
self.create_config()
@ -101,11 +99,14 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
pass
assert self.directory
services_path = shutil.which("services")
assert services_path
# 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(
[

View File

@ -1,3 +1,4 @@
import os
import subprocess
from typing import Optional, Type
@ -55,8 +56,6 @@ saslserv {{
class AthemeController(BaseServicesController, DirectoryBasedController):
"""Mixin for server controllers that rely on Atheme"""
software_name = "Atheme"
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
self.create_config()
@ -80,11 +79,11 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
"atheme-services",
"-n", # don't fork
"-c",
self.directory / "services.conf",
os.path.join(self.directory, "services.conf"),
"-l",
f"/tmp/services-{server_port}.log",
"-p",
self.directory / "services.pid",
os.path.join(self.directory, "services.pid"),
"-D",
self.directory,
],

View File

@ -1,4 +1,4 @@
from pathlib import Path
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -80,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):
software_name = "Bahamut"
supported_sasl_mechanisms: Set[str] = set()
@ -115,7 +102,6 @@ class BahamutController(BaseServerController, DirectoryBasedController):
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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -134,14 +120,9 @@ class BahamutController(BaseServerController, DirectoryBasedController):
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.
shutil.copy(self.pem_path, self.directory / "ircd.crt")
shutil.copy(self.key_path, self.directory / "ircd.key")
shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt"))
shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key"))
with self.open_file("server.conf") as fd:
fd.write(
@ -155,21 +136,15 @@ class BahamutController(BaseServerController, DirectoryBasedController):
# pem_path=self.pem_path,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
# "strace", "-f", "-e", "file",
"ircd",
"-t", # don't fork
"-f",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
],
# stdout=subprocess.DEVNULL,
)
if run_services:

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set
@ -43,7 +43,6 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -74,22 +73,14 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
)
)
assert self.directory
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
self.binary_name,
"-foreground",
"-configfile",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
"-pidfile",
self.directory / "server.pid",
os.path.join(self.directory, "server.pid"),
],
# 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

@ -1,7 +1,6 @@
import copy
import json
import os
import shutil
import subprocess
from typing import Any, Dict, Optional, Set, Type, Union
@ -156,7 +155,6 @@ class ErgoController(BaseServerController, DirectoryBasedController):
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
config: Optional[Any] = None,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
@ -185,32 +183,27 @@ class ErgoController(BaseServerController, DirectoryBasedController):
bind_address = "127.0.0.1:%s" % (port,)
listener_conf = None # plaintext
if ssl:
self.key_path = self.directory / "ssl.key"
self.pem_path = self.directory / "ssl.pem"
self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = os.path.join(self.directory, "ssl.pem")
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
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:
config["server"]["password"] = hash_password(password) # type: ignore
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._write_config()
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])
subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"])
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
["ergo", "run", "--conf", self._config_path, "--quiet"]
)
def wait_for_services(self) -> None:

View File

@ -42,7 +42,6 @@ class ExternalServerController(BaseServerController):
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:
pass

View File

@ -1,5 +1,4 @@
import functools
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -60,10 +59,8 @@ TEMPLATE_CONFIG = """
target="services.example.org">
# Protocol:
<module name="banexception">
<module name="botmode">
<module name="cap">
<module name="inviteexception">
<module name="ircv3">
<module name="ircv3_accounttag">
<module name="ircv3_batch">
@ -77,16 +74,17 @@ TEMPLATE_CONFIG = """
<module name="monitor">
<module name="m_muteban"> # for testing mute extbans
<module name="namesx"> # For multi-prefix
<module name="noctcp">
<module name="sasl">
# HELP/HELPOP
<module name="alias"> # for the HELP alias
<module name="{help_module_name}">
<include file="examples/{help_module_name}.conf.example">
<module name="helpop">
<include file="examples/helpop.conf.example">
# Misc:
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
<server name="My.Little.Server" description="test server" id="000" network="testnet">
<server name="My.Little.Server" description="testnet" id="000" network="testnet">
"""
TEMPLATE_SSL_CONFIG = """
@ -95,17 +93,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):
software_name = "InspIRCd"
supported_sasl_mechanisms = {"PLAIN"}
@ -128,7 +115,6 @@ class InspircdController(BaseServerController, DirectoryBasedController):
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,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -150,13 +136,6 @@ class InspircdController(BaseServerController, DirectoryBasedController):
else:
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:
fd.write(
TEMPLATE_CONFIG.format(
@ -166,24 +145,15 @@ class InspircdController(BaseServerController, DirectoryBasedController):
services_port=services_port,
password_field=password_field,
ssl_config=ssl_config,
help_module_name=help_module_name,
)
)
assert self.directory
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"inspircd",
"--nofork",
"--config",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
],
stdout=subprocess.DEVNULL,
)

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -10,7 +10,7 @@ from irctest.basecontrollers import (
TEMPLATE_CONFIG = """
# M:<Server NAME>:<YOUR Internet IP#>:<Geographic Location>:<Port>:<SID>:
M:My.Little.Server:{hostname}:test server:{port}:0042:
M:My.Little.Server:{hostname}:Somewhere:{port}:0042:
# A:<Your Name/Location>:<Your E-Mail Addr>:<other info>::<network name>:
A:Organization, IRC dept.:Daemon <ircd@example.irc.org>:Client Server::IRCnet:
@ -29,8 +29,8 @@ O:*:operpassword:operuser::::
"""
class Irc2Controller(BaseServerController, DirectoryBasedController):
software_name = "irc2"
class Ircu2Controller(BaseServerController, DirectoryBasedController):
binary_name: str
services_protocol: str
supports_sts = False
@ -51,7 +51,6 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -67,7 +66,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = password if password else ""
assert self.directory
pidfile = self.directory / "ircd.pid"
pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -77,26 +76,18 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
pidfile=pidfile,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"ircd",
"-s", # no iauth
"-p",
"on",
"-f",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
],
# stderr=subprocess.DEVNULL,
)
def get_irctest_controller_class() -> Type[Irc2Controller]:
return Irc2Controller
def get_irctest_controller_class() -> Type[Ircu2Controller]:
return Ircu2Controller

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -51,7 +51,6 @@ features {{
class Ircu2Controller(BaseServerController, DirectoryBasedController):
software_name = "ircu2"
supports_sts = False
extban_mute_char = None
@ -70,7 +69,6 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -86,7 +84,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory
pidfile = self.directory / "ircd.pid"
pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -96,20 +94,12 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
pidfile=pidfile,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"ircd",
"-n", # don't detach
"-f",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
"-x",
"DEBUG",
],

View File

@ -1,3 +1,4 @@
import os
import subprocess
from typing import Optional, Type
@ -84,7 +85,9 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
)
)
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]:

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -92,7 +92,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
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:
if password is not None:
raise NotImplementedByController("PASS command")
@ -114,20 +113,12 @@ class MammonController(BaseServerController, DirectoryBasedController):
# with self.open_file('server.yml', 'r') as fd:
# print(fd.read())
assert self.directory
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"mammond",
"--nofork", # '--debug',
"--config",
self.directory / "server.yml",
os.path.join(self.directory, "server.yml"),
]
)

View File

@ -1,11 +0,0 @@
from typing import Type
from .ircu2 import Ircu2Controller
class NefariousController(Ircu2Controller):
software_name = "Nefarious"
def get_irctest_controller_class() -> Type[NefariousController]:
return NefariousController

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -12,7 +12,7 @@ from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
[Global]
Name = My.Little.Server
Info = test server
Info = ExampleNET Server
Bind = {hostname}
Ports = {port}
AdminInfo1 = Bob Smith
@ -56,7 +56,6 @@ class NgircdController(BaseServerController, DirectoryBasedController):
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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -82,7 +81,6 @@ class NgircdController(BaseServerController, DirectoryBasedController):
fd.write("\n")
assert self.directory
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -93,23 +91,15 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=self.directory / "empty.txt",
empty_file=os.path.join(self.directory, "empty.txt"),
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"ngircd",
"--nodaemon",
"--config",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
],
# stdout=subprocess.DEVNULL,
)

View File

@ -74,7 +74,7 @@ operator {{
class Plexus4Controller(BaseHybridController):
software_name = "Plexus4"
software_name = "Hybrid"
binary_name = "ircd"
services_protocol = "plexus"

View File

@ -1,4 +1,4 @@
import shutil
import os
import subprocess
from typing import Optional, Set, Type
@ -69,7 +69,6 @@ class SnircdController(BaseServerController, DirectoryBasedController):
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -85,7 +84,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory
pidfile = self.directory / "ircd.pid"
pidfile = os.path.join(self.directory, "ircd.pid")
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -95,20 +94,12 @@ class SnircdController(BaseServerController, DirectoryBasedController):
pidfile=pidfile,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"ircd",
"-n", # don't detach
"-f",
self.directory / "server.conf",
os.path.join(self.directory, "server.conf"),
"-x",
"DEBUG",
],

View File

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

View File

@ -1,11 +1,11 @@
import contextlib
import fcntl
import functools
from pathlib import Path
import os
import pathlib
import shutil
import signal
import subprocess
import textwrap
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -22,7 +22,7 @@ include "help/help.conf";
me {{
name "My.Little.Server";
info "test server";
info "ExampleNET Server";
sid "001";
}}
admin {{
@ -100,9 +100,6 @@ set {{
}}
}}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_extras}
}}
tld {{
@ -125,35 +122,6 @@ oper "operuser" {{
"""
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()
def installed_version() -> int:
output = subprocess.check_output(["unrealircd", "-v"], universal_newlines=True)
@ -171,7 +139,6 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
supports_sts = False
extban_mute_char = "quiet" if installed_version() >= 6 else "q"
software_version = installed_version()
def create_config(self) -> None:
super().create_config()
@ -189,7 +156,6 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
@ -199,6 +165,18 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
self.port = port
self.hostname = hostname
self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password "{}";'.format(password) if password else ""
self.gen_ssl()
if ssl:
(tls_hostname, tls_port) = (hostname, port)
(hostname, port) = (unused_hostname, unused_port)
else:
# Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port)
if installed_version() >= 6:
extras = textwrap.dedent(
@ -207,78 +185,60 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
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")
password_field = 'password "{}";'.format(password) if password else ""
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
self.gen_ssl()
if ssl:
(tls_hostname, tls_port) = (hostname, port)
(hostname, port) = (unused_hostname, unused_port)
else:
# Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port)
assert self.directory
with self.open_file("unrealircd.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
hostname=hostname,
port=port,
services_hostname=services_hostname,
services_port=services_port,
tls_hostname=tls_hostname,
tls_port=tls_port,
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=self.directory / "empty.txt",
extras=extras,
set_extras=set_extras,
)
assert self.directory
with self.open_file("unrealircd.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
hostname=hostname,
port=port,
services_hostname=services_hostname,
services_port=services_port,
tls_hostname=tls_hostname,
tls_port=tls_port,
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=os.path.join(self.directory, "empty.txt"),
extras=extras,
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"unrealircd",
"-t",
"-F", # BOOT_NOFORK
"-f",
self.directory / "unrealircd.conf",
],
# stdout=subprocess.DEVNULL,
)
self.wait_for_port()
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
self.proc = subprocess.Popen(
[
*proot_cmd,
"unrealircd",
"-t",
"-F", # BOOT_NOFORK
"-f",
os.path.join(self.directory, "unrealircd.conf"),
],
# stdout=subprocess.DEVNULL,
)
if run_services:
self.wait_for_port()
self.services_controller = self.services_controller_class(
self.test_config, self
)
@ -288,13 +248,17 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
server_port=services_port,
)
def kill_proc(self) -> None:
assert self.proc
with _STARTSTOP_LOCK():
self.proc.kill()
self.proc.wait(5) # wait for it to actually die
self.proc = None
def kill(self) -> None:
if self.using_proot:
# Kill grandchild process, instead of killing proot, which takes more
# time (and does not seem to always work)
assert self.proc is not None
output = subprocess.check_output(
["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]:

View File

@ -1,449 +0,0 @@
import base64
import dataclasses
import gzip
import hashlib
import importlib
from pathlib import Path
import re
import sys
from typing import (
IO,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
TypeVar,
)
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import parse as parse_xml
import docutils.core
NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#')
"""Characters not allowed in output filenames"""
@dataclasses.dataclass
class CaseResult:
module_name: str
class_name: str
test_name: str
job: str
success: bool
skipped: bool
system_out: Optional[str]
details: Optional[str] = None
type: Optional[str] = None
message: Optional[str] = None
def output_filename(self) -> str:
test_name = self.test_name
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
# File name too long or otherwise invalid. This should be good enough:
m = re.match(r"(?P<function_name>\w+?)\[(?P<params>.+)\]", test_name)
assert m, "File name is too long but has no parameter."
test_name = f'{m.group("function_name")}[{md5sum(m.group("params"))}]'
return f"{self.job}_{self.module_name}.{self.class_name}.{test_name}.txt"
TK = TypeVar("TK")
TV = TypeVar("TV")
def md5sum(text: str) -> str:
return base64.urlsafe_b64encode(hashlib.md5(text.encode()).digest()).decode()
def group_by(list_: Iterable[TV], key: Callable[[TV], TK]) -> Dict[TK, List[TV]]:
groups: Dict[TK, List[TV]] = {}
for value in list_:
groups.setdefault(key(value), []).append(value)
return groups
def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseResult]:
(suite,) = job.getroot()
for case in suite:
if "name" not in case.attrib:
continue
success = True
skipped = False
details = None
system_out = None
extra: Dict[str, str] = {}
for child in case:
if child.tag == "skipped":
success = True
skipped = True
details = None
extra = child.attrib
elif child.tag in ("failure", "error"):
success = False
skipped = False
details = child.text
extra = child.attrib
elif child.tag == "system-out":
assert (
system_out is None
# for some reason, skipped tests have two system-out;
# and the second one contains test teardown
or child.text.startswith(system_out.rstrip())
), ("Duplicate system-out tag", repr(system_out), repr(child.text))
system_out = child.text
else:
assert False, child
(module_name, class_name) = case.attrib["classname"].rsplit(".", 1)
m = re.match(
r"(.*/)?pytest[ -]results[ _](?P<name>.*)"
r"[ _][(]?(stable|release|devel|devel_release)[)]?/pytest.xml(.gz)?",
str(job_file_name),
)
assert m, job_file_name
yield CaseResult(
module_name=module_name,
class_name=class_name,
test_name=case.attrib["name"],
job=m.group("name"),
success=success,
skipped=skipped,
details=details,
system_out=system_out,
**extra,
)
def rst_to_element(s: str) -> ET.Element:
html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"]
htmltree = ET.fromstring(html)
return htmltree
def append_docstring(element: ET.Element, obj: object) -> None:
if obj.__doc__ is None:
return
element.append(rst_to_element(obj.__doc__))
def build_job_html(job: str, results: List[CaseResult]) -> ET.Element:
jobs = sorted({result.job for result in results})
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = job
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
body = ET.SubElement(root, "body")
ET.SubElement(body, "h1").text = job
table = build_test_table(jobs, results)
table.set("class", "job-results test-matrix")
body.append(table)
return root
def build_module_html(
jobs: List[str], results: List[CaseResult], module_name: str
) -> ET.Element:
module = importlib.import_module(module_name)
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = module_name
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
body = ET.SubElement(root, "body")
ET.SubElement(body, "h1").text = module_name
append_docstring(body, module)
table = build_test_table(jobs, results)
table.set("class", "module-results test-matrix")
body.append(table)
return root
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, lambda r: (r.module_name, r.class_name)
)
table = ET.Element("table")
job_row = ET.Element("tr")
ET.SubElement(job_row, "th") # column of case name
for job in jobs:
cell = ET.SubElement(job_row, "th")
ET.SubElement(ET.SubElement(cell, "div"), "span").text = job
cell.set("class", "job-name")
for (module_name, class_name), class_results in sorted(
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)
# Header row: class name
header_row = ET.SubElement(table, "tr")
th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1))
row_anchor = f"{qualified_class_name}"
section_header = ET.SubElement(
ET.SubElement(th, "h2"),
"a",
href=f"#{row_anchor}",
id=row_anchor,
)
section_header.text = qualified_class_name
append_docstring(th, getattr(module, class_name))
# Header row: one column for each implementation
table.append(job_row)
# One row for each test:
results_by_test = group_by(class_results, key=lambda r: r.test_name)
for test_name, test_results in sorted(results_by_test.items()):
row_anchor = f"{qualified_class_name}.{test_name}"
if len(row_anchor) >= 50:
# Too long; give up on generating readable URL
# TODO: only hash test parameter
row_anchor = md5sum(row_anchor)
row = ET.SubElement(table, "tr", id=row_anchor)
cell = ET.SubElement(row, "th")
cell.set("class", "test-name")
cell_link = ET.SubElement(cell, "a", href=f"#{row_anchor}")
cell_link.text = test_name
results_by_job = group_by(test_results, key=lambda r: r.job)
for job_name in jobs:
cell = ET.SubElement(row, "td")
try:
(result,) = results_by_job[job_name]
except KeyError:
cell.set("class", "deselected")
cell.text = "d"
continue
text: Optional[str]
if result.skipped:
cell.set("class", "skipped")
if result.type == "pytest.skip":
text = "s"
elif result.type == "pytest.xfail":
text = "X"
cell.set("class", "expected-failure")
else:
text = result.type
elif result.success:
cell.set("class", "success")
if result.type:
# dead code?
text = result.type
else:
text = "."
else:
cell.set("class", "failure")
if result.type:
# dead code?
text = result.type
else:
text = "f"
if result.system_out:
# There is a log file; link to it.
a = ET.SubElement(cell, "a", href=f"./{result.output_filename()}")
a.text = text or "?"
else:
cell.text = text or "?"
if result.message:
cell.set("title", result.message)
return table
def write_html_pages(
output_dir: Path, results: List[CaseResult]
) -> List[Tuple[str, str, str]]:
"""Returns the list of (module_name, file_name)."""
output_dir.mkdir(parents=True, exist_ok=True)
results_by_module = group_by(results, lambda r: r.module_name)
# used as columns
jobs = list(sorted({r.job for r in results}))
job_categories = {}
for job in jobs:
is_client = any(
"client_tests" in result.module_name and result.job == job
for result in results
)
is_server = any(
"server_tests" in result.module_name and result.job == job
for result in results
)
assert is_client != is_server, (job, is_client, is_server)
if job.endswith(("-atheme", "-anope", "-dlk")):
assert is_server
job_categories[job] = "server-with-services"
elif is_server:
job_categories[job] = "server" # with or without services
else:
assert is_client
job_categories[job] = "client"
pages = []
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
module_categories = {
job_categories[result.job]
for result in results
if result.module_name == module_name and not result.skipped
}
module_jobs = [job for job in jobs if job_categories[job] in module_categories]
root = build_module_html(module_jobs, module_results, module_name)
file_name = f"{module_name}.xhtml"
write_xml_file(output_dir / file_name, root)
pages.append(("module", module_name, file_name))
for category in ("server", "client"):
for job in [job for job in job_categories if job_categories[job] == category]:
job_results = [
result
for result in results
if result.job == job or result.job.startswith(job + "-")
]
root = build_job_html(job, job_results)
file_name = f"{job}.xhtml"
write_xml_file(output_dir / file_name, root)
pages.append(("job", job, file_name))
return pages
def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None:
"""Writes stdout files of each test."""
for result in results:
if result.system_out is None:
continue
output_file = output_dir / result.output_filename()
with output_file.open("wt") as fd:
fd.write(result.system_out)
def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None:
root = ET.Element("html")
head = ET.SubElement(root, "head")
ET.SubElement(head, "title").text = "irctest dashboard"
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
body = ET.SubElement(root, "body")
ET.SubElement(body, "h1").text = "irctest dashboard"
module_pages = []
job_pages = []
for page_type, title, file_name in sorted(pages):
if page_type == "module":
module_pages.append((title, file_name))
elif page_type == "job":
job_pages.append((title, file_name))
else:
assert False, page_type
ET.SubElement(body, "h2").text = "Tests by command/specification"
dl = ET.SubElement(body, "dl")
dl.set("class", "module-index")
for module_name, file_name in sorted(module_pages):
module = importlib.import_module(module_name)
link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}")
link.text = module_name
append_docstring(ET.SubElement(dl, "dd"), module)
ET.SubElement(body, "h2").text = "Tests by implementation"
ul = ET.SubElement(body, "ul")
ul.set("class", "job-index")
for job, file_name in sorted(job_pages):
link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}")
link.text = job
write_xml_file(output_dir / "index.xhtml", root)
def write_assets(output_dir: Path) -> None:
css_path = output_dir / "style.css"
source_css_path = Path(__file__).parent / "style.css"
with css_path.open("wt") as fd:
with source_css_path.open() as source_fd:
fd.write(source_fd.read())
def write_xml_file(filename: Path, root: ET.Element) -> None:
# Hacky: ET expects the namespace to be present in every tag we create instead;
# but it would be excessively verbose.
root.set("xmlns", "http://www.w3.org/1999/xhtml")
# Serialize
s = ET.tostring(root)
with filename.open("wb") as fd:
fd.write(s)
def parse_xml_file(filename: Path) -> ET.ElementTree:
fd: IO
if filename.suffix == ".gz":
with gzip.open(filename, "rb") as fd: # type: ignore
return parse_xml(fd) # type: ignore
else:
with open(filename) as fd:
return parse_xml(fd) # type: ignore
def main(output_path: Path, filenames: List[Path]) -> int:
results = [
result
for filename in filenames
for result in iter_job_results(filename, parse_xml_file(filename))
]
pages = write_html_pages(output_path, results)
write_html_index(output_path, pages)
write_test_outputs(output_path, results)
write_assets(output_path)
return 0
if __name__ == "__main__":
(_, output_path, *filenames) = sys.argv
exit(main(Path(output_path), list(map(Path, filenames))))

View File

@ -1,87 +0,0 @@
import dataclasses
import gzip
import io
import json
from pathlib import Path
import sys
from typing import Iterator
import urllib.parse
import urllib.request
import zipfile
@dataclasses.dataclass
class Artifact:
repo: str
run_id: int
name: str
download_url: str
@property
def public_download_url(self) -> str:
# GitHub API is not available publicly for artifacts, we need to use
# a third-party proxy to access it...
name = urllib.parse.quote(self.name)
return f"https://nightly.link/{repo}/actions/runs/{self.run_id}/{name}.zip"
def iter_run_artifacts(repo: str, run_id: int) -> Iterator[Artifact]:
request = urllib.request.Request(
f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts"
"?per_page=100",
headers={"Accept": "application/vnd.github.v3+json"},
)
response = urllib.request.urlopen(request)
for artifact in json.load(response)["artifacts"]:
if not artifact["name"].startswith(("pytest-results_", "pytest results ")):
continue
if artifact["expired"]:
continue
yield Artifact(
repo=repo,
run_id=run_id,
name=artifact["name"],
download_url=artifact["archive_download_url"],
)
def download_artifact(output_name: Path, url: str) -> None:
if output_name.exists():
return
response = urllib.request.urlopen(url)
archive_bytes = response.read() # Can't stream it, it's a ZIP
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
with archive.open("pytest.xml") as input_fd:
pytest_xml = input_fd.read()
tmp_output_path = output_name.with_suffix(".tmp")
with gzip.open(tmp_output_path, "wb") as output_fd:
output_fd.write(pytest_xml)
# Atomically write to the output path, so that we don't write partial files in case
# the download process is interrupted
tmp_output_path.rename(output_name)
def main(output_dir: Path, repo: str, run_id: int) -> int:
output_dir.mkdir(parents=True, exist_ok=True)
run_path = output_dir / str(run_id)
run_path.mkdir(exist_ok=True)
for artifact in iter_run_artifacts(repo, run_id):
artifact_path = run_path / artifact.name / "pytest.xml.gz"
artifact_path.parent.mkdir(exist_ok=True)
try:
download_artifact(artifact_path, artifact.download_url)
except Exception:
download_artifact(artifact_path, artifact.public_download_url)
print("downloaded", artifact.name)
return 0
if __name__ == "__main__":
(_, output_path, repo, run_id) = sys.argv
exit(main(Path(output_path), repo, int(run_id)))

View File

@ -1,67 +0,0 @@
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: rgba(255, 255, 255, 0.87);
}
a {
filter: invert(0.85) hue-rotate(180deg);
}
}
dl.module-index {
column-width: 40em; /* Magic constant for 2 columns on average laptop/desktop */
}
/* Only 1px solid border between cells */
table.test-matrix {
border-spacing: 0;
border-collapse: collapse;
}
table.test-matrix td {
text-align: center;
border: 1px solid grey;
}
/* Make link take the whole cell */
table.test-matrix td a {
display: block;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
color: black;
text-decoration: none;
}
/* Test matrix colors */
table.test-matrix .deselected {
background-color: grey;
}
table.test-matrix .success {
background-color: green;
}
table.test-matrix .skipped {
background-color: yellow;
}
table.test-matrix .failure {
background-color: red;
}
table.test-matrix .expected-failure {
background-color: orange;
}
/* Rotate headers, thanks to https://css-tricks.com/rotated-table-column-headers/ */
table.module-results th.job-name {
height: 140px;
white-space: nowrap;
}
table.module-results th.job-name > div {
transform:
translate(28px, 50px)
rotate(315deg);
width: 40px;
}
table.module-results th.job-name > div > span {
border-bottom: 1px solid grey;
padding-left: 0px;
}

View File

@ -66,7 +66,6 @@ RPL_WHOISIDLE = "317"
RPL_ENDOFWHOIS = "318"
RPL_WHOISCHANNELS = "319"
RPL_WHOISSPECIAL = "320"
RPL_LISTSTART = "321"
RPL_LIST = "322"
RPL_LISTEND = "323"
RPL_CHANNELMODEIS = "324"
@ -142,7 +141,6 @@ ERR_USERONCHANNEL = "443"
ERR_NOLOGIN = "444"
ERR_SUMMONDISABLED = "445"
ERR_USERSDISABLED = "446"
ERR_FORBIDDENCHANNEL = "448"
ERR_NOTREGISTERED = "451"
ERR_NEEDMOREPARAMS = "461"
ERR_ALREADYREGISTRED = "462"

View File

@ -152,23 +152,21 @@ def match_dict(
# Set to not-None if we find a Keys() operator in the dict keys
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):
remaining_keys_wildcard = (expected_key.key, expected_value)
elif isinstance(expected_key, Operator):
raise NotImplementedError(f"Unsupported operator: {expected_key}")
else:
for key in got:
if match_string(key, expected_key) and match_string(
got[key], expected_value
):
got.pop(key)
break
else:
# Found no (key, value) pair matching the request
if expected_key not in got:
return False
got_value = got.pop(expected_key)
if not match_string(got_value, expected_value):
return False
if 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):
return False
if not match_string(value, expected_value):

View File

@ -1,5 +1,3 @@
"""Internal checks of assertion implementations."""
from typing import Dict, List, Tuple
import pytest
@ -179,39 +177,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}",
]
),
(
# the specification:
dict(
tags={StrRe("tag[12]"): "bar", **ANYDICT},
command="PRIVMSG",
params=["#chan", "hello"],
),
# matches:
[
"@tag1=bar PRIVMSG #chan :hello",
"@tag1=bar;tag2= PRIVMSG #chan :hello",
"@tag1=bar :foo!baz@qux PRIVMSG #chan :hello",
"@tag2=bar PRIVMSG #chan :hello",
"@tag1=bar;tag2= PRIVMSG #chan :hello",
"@tag1=;tag2=bar PRIVMSG #chan :hello",
],
# and does not match:
[
"PRIVMG #chan :hello",
"@tag1=value1 PRIVMSG #chan :hello",
"PRIVMSG #chan hello2",
"PRIVMSG #chan2 hello",
":foo!baz@qux PRIVMSG #chan hello",
],
# and they each error with:
[
"expected command to be PRIVMSG, got PRIVMG",
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}",
]
),
(
# the specification:
dict(

View File

@ -1,12 +1,12 @@
"""
`IRCv3 account-tag <https://ircv3.net/specs/extensions/account-tag>`_
<http://ircv3.net/specs/extensions/account-tag-3.2.html>
"""
from irctest import cases
@cases.mark_services
class AccountTagTestCase(cases.BaseServerTestCase):
class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
def connectRegisteredClient(self, nick):
self.addClient()
self.sendLine(2, "CAP LS 302")
@ -40,7 +40,7 @@ class AccountTagTestCase(cases.BaseServerTestCase):
self.skipToWelcome(2)
@cases.mark_capabilities("account-tag")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPrivmsg(self):
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
self.getMessages(1)
@ -54,10 +54,7 @@ class AccountTagTestCase(cases.BaseServerTestCase):
)
@cases.mark_capabilities("account-tag")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.xfailIfSoftware(
["Charybdis"], "https://github.com/solanum-ircd/solanum/issues/166"
)
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testInvite(self):
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
self.getMessages(1)

View File

@ -1,16 +1,5 @@
"""
AWAY command (`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-4.1>`__,
`Modern <https://modern.ircdocs.horse/#away-message>`__)
"""
from irctest import cases
from irctest.numerics import (
RPL_AWAY,
RPL_NOWAWAY,
RPL_UNAWAY,
RPL_USERHOST,
RPL_WHOISUSER,
)
from irctest.numerics import RPL_AWAY, RPL_NOWAWAY, RPL_UNAWAY, RPL_USERHOST
from irctest.patma import StrRe
@ -43,7 +32,7 @@ class AwayTestCase(cases.BaseServerTestCase):
"""
"The server acknowledges the change in away status by returning the
`RPL_NOWAWAY` and `RPL_UNAWAY` numerics."
-- https://modern.ircdocs.horse/#away-message
-- https://github.com/ircdocs/modern-irc/pull/100
"""
self.connectClient("bar")
self.sendLine(1, "AWAY :I'm not here right now")
@ -59,7 +48,7 @@ class AwayTestCase(cases.BaseServerTestCase):
"""
"Servers SHOULD notify clients when a user they're interacting with
is away when relevant"
-- https://modern.ircdocs.horse/#away-message
-- https://github.com/ircdocs/modern-irc/pull/100
"<client> <nick> :<message>"
-- https://modern.ircdocs.horse/#rplaway-301
@ -86,7 +75,7 @@ class AwayTestCase(cases.BaseServerTestCase):
"""
"Servers SHOULD notify clients when a user they're interacting with
is away when relevant"
-- https://modern.ircdocs.horse/#away-message
-- https://github.com/ircdocs/modern-irc/pull/100
"<client> <nick> :<message>"
-- https://modern.ircdocs.horse/#rplaway-301
@ -124,7 +113,7 @@ class AwayTestCase(cases.BaseServerTestCase):
"""
"Servers SHOULD notify clients when a user they're interacting with
is away when relevant"
-- https://modern.ircdocs.horse/#away-message
-- https://github.com/ircdocs/modern-irc/pull/100
"<client> <nick> :<message>"
-- https://modern.ircdocs.horse/#rplaway-301
@ -145,33 +134,3 @@ class AwayTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(
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

@ -1,11 +1,11 @@
"""
`IRCv3 away-notify <https://ircv3.net/specs/extensions/away-notify>`_
<https://ircv3.net/specs/extensions/away-notify-3.1>
"""
from irctest import cases
class AwayNotifyTestCase(cases.BaseServerTestCase):
class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_capabilities("away-notify")
def testAwayNotify(self):
"""Basic away-notify test."""

View File

@ -1,5 +1,6 @@
"""
`IRCv3 bot mode <https://ircv3.net/specs/extensions/bot-mode>`_
Draft bot mode specification, as defined in
<https://ircv3.net/specs/extensions/bot-mode>
"""
from irctest import cases, runner
@ -67,10 +68,6 @@ class BotModeTestCase(cases.BaseServerTestCase):
message, command=RPL_WHOISBOT, params=["usernick", "botnick", ANYSTR]
)
@cases.xfailIfSoftware(
["InspIRCd"],
"Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910",
)
def testBotPrivateMessage(self):
self._initBot()
@ -85,13 +82,9 @@ class BotModeTestCase(cases.BaseServerTestCase):
self.getMessage("user"),
command="PRIVMSG",
params=["usernick", "beep boop"],
tags={StrRe("(draft/)?bot"): None, **ANYDICT},
tags={"draft/bot": None, **ANYDICT},
)
@cases.xfailIfSoftware(
["InspIRCd"],
"Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910",
)
def testBotChannelMessage(self):
self._initBot()
@ -111,7 +104,7 @@ class BotModeTestCase(cases.BaseServerTestCase):
self.getMessage("user"),
command="PRIVMSG",
params=["#chan", "beep boop"],
tags={StrRe("(draft/)?bot"): None, **ANYDICT},
tags={"draft/bot": None, **ANYDICT},
)
def testBotWhox(self):

View File

@ -1,9 +1,3 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of
`multiclient features
<https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#multiclient-bouncer>`_
"""
from irctest import cases
from irctest.irc_utils.sasl import sasl_plain_blob
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME

View File

@ -1,7 +1,5 @@
"""
Sends packets with various length to check the server reassembles them
correctly. Also checks truncation.
"""
"""Sends packets with various length to check the server reassembles them
correctly. Also checks truncation"""
import socket
import time
@ -32,16 +30,6 @@ def _sendBytePerByte(self, line):
class BufferingTestCase(cases.BaseServerTestCase):
@cases.xfailIfSoftware(
["Bahamut"],
"cannot pass because of issues with UTF-8 handling: "
"https://github.com/DALnet/bahamut/issues/196",
)
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"],
"ircu2 discards the whole buffer on long lines "
"(TODO: refine how we exclude these tests)",
)
@pytest.mark.parametrize(
"sender_function,colon",
[

View File

@ -1,14 +1,9 @@
"""
`IRCv3 Capability negotiation
<https://ircv3.net/specs/extensions/capability-negotiation>`_
"""
from irctest import cases
from irctest.patma import ANYSTR
from irctest.runner import CapabilityNotSupported, ImplementationChoice
class CapTestCase(cases.BaseServerTestCase):
class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3")
def testNoReq(self):
"""Test the server handles gracefully clients which do not send
@ -78,10 +73,6 @@ class CapTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["UnrealIRCd"],
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
)
def testNakWhole(self):
"""“The capability identifier set must be accepted as a whole, or
rejected entirely.”
@ -129,10 +120,6 @@ class CapTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["UnrealIRCd"],
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
)
def testCapRemovalByClient(self):
"""Test CAP LIST and removal of caps via CAP REQ :-tagname."""
cap1 = "echo-message"
@ -185,60 +172,3 @@ class CapTestCase(cases.BaseServerTestCase):
enabled_caps.discard("cap-notify") # implicitly added by some impls
self.assertEqual(enabled_caps, {cap1})
self.assertNotIn("time", cap_list.tags)
@cases.mark_specifications("IRCv3")
def testIrc301CapLs(self):
"""
Current version:
"The LS subcommand is used to list the capabilities supported by the server.
The client should send an LS subcommand with no other arguments to solicit
a list of all capabilities."
"If a client has not indicated support for CAP LS 302 features,
the server MUST NOT send these new features to the client."
-- <https://ircv3.net/specs/core/capability-negotiation.html>
Before the v3.1 / v3.2 merge:
IRCv3.1: “The LS subcommand is used to list the capabilities
supported by the server. The client should send an LS subcommand with
no other arguments to solicit a list of all capabilities.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand>
IRCv3.2: “Servers MUST NOT send messages described by this document if
the client only supports version 3.1.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.2.html#version-in-cap-ls>
""" # noqa
self.addClient()
self.sendLine(1, "CAP LS")
m = self.getRegistrationMessage(1)
self.assertNotEqual(
m.params[2],
"*",
m,
fail_msg="Server replied with multi-line CAP LS to a "
"“CAP LS” (ie. IRCv3.1) request: {msg}",
)
self.assertFalse(
any("=" in cap for cap in m.params[2].split()),
"Server replied with a name-value capability in "
"CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) "
"request: {}".format(m),
)
@cases.mark_specifications("IRCv3")
def testEmptyCapList(self):
"""“If no capabilities are active, an empty parameter must be sent.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
""" # noqa
self.addClient()
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=["*", "LIST", ""],
fail_msg="Sending “CAP LIST” as first message got a reply "
"that is not “CAP * LIST :”: {msg}",
)

View File

@ -1,7 +1,3 @@
"""
Channel casemapping
"""
import pytest
from irctest import cases, client_mock, runner
@ -22,7 +18,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
self.connectClient("bar")
if self.server_support["CASEMAPPING"] != casemapping:
raise runner.ImplementationChoice(
raise runner.NotImplementedByController(
"Casemapping {} not implemented".format(casemapping)
)
self.joinClient(1, name1)
@ -47,7 +43,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
self.connectClient("bar")
if self.server_support["CASEMAPPING"] != casemapping:
raise runner.ImplementationChoice(
raise runner.NotImplementedByController(
"Casemapping {} not implemented".format(casemapping)
)
self.joinClient(1, name1)

View File

@ -1,9 +1,3 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of channel forwarding
TODO: Should be extended to other servers, once a specification is written.
"""
from irctest import cases
from irctest.numerics import ERR_CHANOPRIVSNEEDED, ERR_INVALIDMODEPARAM, ERR_LINKCHANNEL

View File

@ -1,22 +1,24 @@
"""
`Draft IRCv3 channel-rename <https://ircv3.net/specs/extensions/channel-rename>`_
"""
from irctest import cases
from irctest.numerics import ERR_CHANOPRIVSNEEDED
MODERN_CAPS = [
"server-time",
"message-tags",
"batch",
"labeled-response",
"echo-message",
"account-tag",
]
RENAME_CAP = "draft/channel-rename"
@cases.mark_specifications("IRCv3")
class ChannelRenameTestCase(cases.BaseServerTestCase):
"""Basic tests for channel-rename."""
@cases.mark_specifications("Ergo")
def testChannelRename(self):
self.connectClient(
"bar", name="bar", capabilities=[RENAME_CAP], skip_if_cap_nak=True
)
self.connectClient("baz", name="baz")
self.connectClient("bar", name="bar", capabilities=MODERN_CAPS + [RENAME_CAP])
self.connectClient("baz", name="baz", capabilities=MODERN_CAPS)
self.joinChannel("bar", "#bar")
self.joinChannel("baz", "#bar")
self.getMessages("bar")

View File

@ -1,14 +1,9 @@
"""
`IRCv3 draft chathistory <https://ircv3.net/specs/extensions/chathistory>`_
"""
import functools
import secrets
import time
import pytest
from irctest import cases, runner
from irctest import cases
from irctest.irc_utils.junkdrawer import random_name
from irctest.patma import ANYSTR
@ -43,16 +38,6 @@ def validate_chathistory_batch(msgs):
return result
def skip_ngircd(f):
@functools.wraps(f)
def newf(self, *args, **kwargs):
if self.controller.software_name == "ngIRCd":
raise runner.OptionalExtensionNotSupported("nicks longer 9 characters")
return f(self, *args, **kwargs)
return newf
@cases.mark_specifications("IRCv3")
@cases.mark_services
class ChathistoryTestCase(cases.BaseServerTestCase):
@ -60,7 +45,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
def config() -> cases.TestCaseControllerConfig:
return cases.TestCaseControllerConfig(chathistory=True)
@skip_ngircd
def testInvalidTargets(self):
bar, pw = random_name("bar"), random_name("pw")
self.controller.registerUser(self, bar, pw)
@ -106,7 +90,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
)
@pytest.mark.private_chathistory
@skip_ngircd
def testMessagesToSelf(self):
bar, pw = random_name("bar"), random_name("pw")
self.controller.registerUser(self, bar, pw)
@ -179,19 +162,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.assertEqual(len(set(msg.time for msg in echo_messages)), num_messages)
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@skip_ngircd
def testChathistory(self, subcommand):
if subcommand == "BETWEEN" and self.controller.software_name == "UnrealIRCd":
pytest.xfail(
"CHATHISTORY BETWEEN does not apply bounds correct "
"https://bugs.unrealircd.org/view.php?id=5952"
)
if subcommand == "AROUND" and self.controller.software_name == "UnrealIRCd":
pytest.xfail(
"CHATHISTORY AROUND excludes 'central' messages "
"https://bugs.unrealircd.org/view.php?id=5953"
)
self.connectClient(
"bar",
capabilities=[
@ -223,7 +194,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.validate_chathistory(subcommand, echo_messages, 1, chname)
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@skip_ngircd
def testChathistoryEventPlayback(self, subcommand):
self.connectClient(
"bar",
@ -257,7 +227,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@pytest.mark.private_chathistory
@skip_ngircd
def testChathistoryDMs(self, subcommand):
c1 = "foo" + secrets.token_hex(12)
c2 = "bar" + secrets.token_hex(12)
@ -580,7 +549,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags
@skip_ngircd
def testChathistoryTagmsg(self):
c1 = "foo" + secrets.token_hex(12)
c2 = "bar" + secrets.token_hex(12)
@ -679,7 +647,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
@pytest.mark.arbitrary_client_tags
@pytest.mark.private_chathistory
@skip_ngircd
def testChathistoryDMClientOnlyTags(self):
# regression test for Ergo #1411
c1 = "foo" + secrets.token_hex(12)

View File

@ -1,9 +1,3 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of auditorium mode
TODO: Should be extended to other servers, once a specification is written.
"""
import math
import time

View File

@ -1,12 +1,4 @@
"""
Channel ban (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
`Modern <https://modern.ircdocs.horse/#ban-channel-mode>`__)
and ban exception (`Modern <https://modern.ircdocs.horse/#exception-channel-mode>`__)
"""
from irctest import cases, runner
from irctest import cases
from irctest.numerics import ERR_BANNEDFROMCHAN, RPL_BANLIST, RPL_ENDOFBANLIST
from irctest.patma import ANYSTR, StrRe
@ -34,7 +26,7 @@ class BanModeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testBanList(self):
"""`RPL_BANLIST <https://modern.ircdocs.horse/#rplbanlist-367>`_"""
"""https://github.com/ircdocs/modern-irc/pull/125"""
self.connectClient("chanop")
self.joinChannel(1, "#chan")
self.getMessages(1)
@ -78,65 +70,6 @@ class BanModeTestCase(cases.BaseServerTestCase):
],
)
@cases.mark_specifications("Modern")
def testBanException(self):
"""`Exception mode <https://modern.ircdocs.horse/#exception-channel-mode`_,
detected using `ISUPPORT EXCEPTS
<https://modern.ircdocs.horse/#excepts-parameter>`_ and checked against
`ISUPPORT CHANMODES <https://modern.ircdocs.horse/#chanmodes-parameter>`_"""
self.connectClient("chanop", name="chanop")
if "EXCEPTS" in self.server_support:
mode = self.server_support["EXCEPTS"] or "e"
if "CHANMODES" in self.server_support:
self.assertIn(
mode,
self.server_support["CHANMODES"],
fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is missing "
"from 'CHANMODES={list}'",
)
self.assertIn(
mode,
self.server_support["CHANMODES"].split(",")[0],
fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is not "
"in group A",
)
else:
mode = "e"
if "CHANMODES" in self.server_support:
if "e" not in self.server_support["CHANMODES"]:
raise runner.OptionalExtensionNotSupported(
"Ban exception (or mode letter is not +e)"
)
self.assertIn(
mode,
self.server_support["CHANMODES"].split(",")[0],
fail_msg="Mode +e (assumed to be ban exception) is present, "
"but 'e' is not in group A",
)
else:
raise runner.OptionalExtensionNotSupported("ISUPPORT CHANMODES")
self.sendLine("chanop", "JOIN #chan")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b ba*!*@*")
self.getMessages("chanop")
# banned client cannot join
self.connectClient("Bar", name="bar")
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command=ERR_BANNEDFROMCHAN)
# chanop sets exception
self.sendLine("chanop", "MODE #chan +e *ar!*@*")
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
# client can now join
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
# TODO: Add testBanExceptionList, once the numerics are specified in Modern
@cases.mark_specifications("Ergo")
def testCaseInsensitive(self):
"""Some clients allow unsetting modes if their argument matches

View File

@ -1,7 +1,3 @@
"""
Various Ergo-specific channel modes
"""
from irctest import cases
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED

View File

@ -1,10 +1,3 @@
"""
Channel key (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
`Modern <https://modern.ircdocs.horse/#key-channel-mode>`__)
"""
import pytest
from irctest import cases
@ -27,16 +20,10 @@ class KeyTestCase(cases.BaseServerTestCase):
self.connectClient("qux")
self.getMessages(2)
# JOIN with a missing key MUST receive ERR_BADCHANNELKEY:
self.sendLine(2, "JOIN #chan")
reply_cmds = {msg.command for msg in self.getMessages(2)}
self.assertNotIn("JOIN", reply_cmds)
self.assertIn(ERR_BADCHANNELKEY, reply_cmds)
# similarly for JOIN with an incorrect key:
self.sendLine(2, "JOIN #chan bees")
reply_cmds = {msg.command for msg in self.getMessages(2)}
self.assertNotIn("JOIN", reply_cmds)
self.assertIn(ERR_BADCHANNELKEY, reply_cmds)
reply = self.getMessages(2)
self.assertNotIn("JOIN", {msg.command for msg in reply})
self.assertIn(ERR_BADCHANNELKEY, {msg.command for msg in reply})
self.sendLine(2, "JOIN #chan beer")
reply = self.getMessages(2)
@ -70,21 +57,6 @@ class KeyTestCase(cases.BaseServerTestCase):
-- https://modern.ircdocs.horse/#key-channel-mode
-- https://github.com/ircdocs/modern-irc/pull/111
"""
if key == "" and self.controller.software_name in (
"ircu2",
"Nefarious",
"snircd",
):
pytest.xfail(
"ircu2 returns ERR_NEEDMOREPARAMS on empty keys: "
"https://github.com/UndernetIRC/ircu2/issues/13"
)
if (key == "" or " " in key) and self.controller.software_name == "ngIRCd":
pytest.xfail(
"ngIRCd does not validate channel keys: "
"https://github.com/ngircd/ngircd/issues/290"
)
self.connectClient("bar")
self.joinChannel(1, "#chan")
self.sendLine(1, f"MODE #chan +k :{key}")

View File

@ -1,9 +1,3 @@
"""
Channel moderation mode (`RFC 2812
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
`Modern <https://modern.ircdocs.horse/#ban-channel-mode>`__)
"""
from irctest import cases
from irctest.numerics import ERR_CANNOTSENDTOCHAN

View File

@ -1,7 +1,3 @@
"""
Mute extban, currently no specifications or ways to discover it.
"""
from irctest import cases, runner
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED
from irctest.patma import ANYLIST, StrRe
@ -198,7 +194,7 @@ class MuteExtbanTestCase(cases.BaseServerTestCase):
self.getMessages(client)
# +e grants an exemption to +b
self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!*evan@*")
self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)

View File

@ -0,0 +1,69 @@
from irctest import cases, runner
from irctest.numerics import ERR_CANNOTSENDTOCHAN
from irctest.patma import ANYSTR
class NoctcpModeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testNoctcpMode(self):
"""
"This mode is used in almost all IRC software today. The standard mode letter
used for it is `"+C"`.
When this mode is set, should not send [CTCP](/ctcp.html) messages, except
CTCP Action (also known as `/me`) to the channel.
When blocking a message because of this mode, servers SHOULD use
ERR_CANNOTSENDTOCHAN"
-- TODO add link
"""
self.connectClient("chanop")
if "C" not in self.server_support.get("CHANMODES", ""):
raise runner.NotImplementedByController("+C (noctcp) channel mode")
# Both users join:
self.sendLine(1, "JOIN #chan")
self.getMessages(1) # synchronize
self.connectClient("user")
self.sendLine(2, "JOIN #chan")
self.getMessages(2)
self.getMessages(1)
# Send ACTION and PING, both should go through:
self.sendLine(2, "PRIVMSG #chan :\x01ACTION is testing\x01")
self.sendLine(2, "PRIVMSG #chan :\x01PING 12345\x01")
self.assertEqual(self.getMessages(2), [])
self.assertEqual(
[(m.command, m.params[1]) for m in self.getMessages(1)],
[
("PRIVMSG", "\x01ACTION is testing\x01"),
("PRIVMSG", "\x01PING 12345\x01"),
],
)
# Set mode +C:
self.sendLine(1, "MODE #chan +C")
self.getMessages(1)
self.getMessages(2)
# Send ACTION and PING, only ACTION should go through:
self.sendLine(2, "PRIVMSG #chan :\x01ACTION is testing\x01")
self.assertEqual(self.getMessages(2), [])
self.sendLine(2, "PRIVMSG #chan :\x01PING 12345\x01")
self.assertMessageMatch(
self.getMessage(2),
command=ERR_CANNOTSENDTOCHAN,
params=["user", "#chan", ANYSTR],
)
self.assertEqual(
[(m.command, m.params[1]) for m in self.getMessages(1)],
[
("PRIVMSG", "\x01ACTION is testing\x01"),
],
)

View File

@ -1,10 +1,3 @@
"""
Channel secrecy mode (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
`Modern <https://modern.ircdocs.horse/#secret-channel-mode>`__)
"""
from irctest import cases
from irctest.numerics import RPL_LIST

View File

@ -1,8 +1,3 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests for nick collisions based on Unicode
confusable characters
"""
from irctest import cases
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME

View File

@ -1,13 +1,11 @@
"""
Tests section 4.1 of RFC 1459.
<https://tools.ietf.org/html/rfc1459#section-4.1>
TODO: cross-reference Modern and RFC 2812 too
"""
from irctest import cases
from irctest.client_mock import ConnectionClosed
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH
from irctest.numerics import ERR_NEEDMOREPARAMS
from irctest.patma import ANYSTR, StrRe
@ -38,14 +36,8 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
m.command, "001", msg="Got 001 after NICK+USER but missing PASS"
)
@cases.mark_specifications("Modern")
@cases.mark_specifications("RFC1459", "RFC2812")
def testWrongPassword(self):
"""
"If the password supplied does not match the password expected by the server,
then the server SHOULD send ERR_PASSWDMISMATCH and MUST close the connection
with ERROR."
-- https://github.com/ircdocs/modern-irc/pull/172
"""
self.addClient()
self.sendLine(1, "PASS {}".format(self.password + "garbage"))
self.sendLine(1, "NICK foo")
@ -54,13 +46,6 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
self.assertNotEqual(
m.command, "001", msg="Got 001 after NICK+USER but incorrect PASS"
)
self.assertIn(m.command, {ERR_PASSWDMISMATCH, "ERROR"})
if m.command == "ERR_PASSWDMISMATCH":
m = self.getRegistrationMessage(1)
self.assertEqual(
m.command, "ERROR", msg="ERR_PASSWDMISMATCH not followed by ERROR."
)
@cases.mark_specifications("RFC1459", "RFC2812", strict=True)
def testPassAfterNickuser(self):
@ -97,10 +82,6 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
self.getMessages(1)
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(["Charybdis", "Solanum"], "very flaky")
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"], "ircu2 does not send ERROR"
)
def testQuitErrors(self):
"""“A client session is terminated with a quit message. The server
acknowledges this by sending an ERROR message to the client.”
@ -181,10 +162,6 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
"neither got 001.",
)
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "ngIRCd"],
"uses a default value instead of ERR_NEEDMOREPARAMS",
)
def testEmptyRealname(self):
"""
Syntax:
@ -206,3 +183,60 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
command=ERR_NEEDMOREPARAMS,
params=[StrRe(r"(\*|foo)"), "USER", ANYSTR],
)
@cases.mark_specifications("IRCv3")
def testIrc301CapLs(self):
"""
Current version:
"The LS subcommand is used to list the capabilities supported by the server.
The client should send an LS subcommand with no other arguments to solicit
a list of all capabilities."
"If a client has not indicated support for CAP LS 302 features,
the server MUST NOT send these new features to the client."
-- <https://ircv3.net/specs/core/capability-negotiation.html>
Before the v3.1 / v3.2 merge:
IRCv3.1: “The LS subcommand is used to list the capabilities
supported by the server. The client should send an LS subcommand with
no other arguments to solicit a list of all capabilities.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand>
IRCv3.2: “Servers MUST NOT send messages described by this document if
the client only supports version 3.1.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.2.html#version-in-cap-ls>
""" # noqa
self.addClient()
self.sendLine(1, "CAP LS")
m = self.getRegistrationMessage(1)
self.assertNotEqual(
m.params[2],
"*",
m,
fail_msg="Server replied with multi-line CAP LS to a "
"“CAP LS” (ie. IRCv3.1) request: {msg}",
)
self.assertFalse(
any("=" in cap for cap in m.params[2].split()),
"Server replied with a name-value capability in "
"CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) "
"request: {}".format(m),
)
@cases.mark_specifications("IRCv3")
def testEmptyCapList(self):
"""“If no capabilities are active, an empty parameter must be sent.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
""" # noqa
self.addClient()
self.sendLine(1, "CAP LIST")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="CAP",
params=["*", "LIST", ""],
fail_msg="Sending “CAP LIST” as first message got a reply "
"that is not “CAP * LIST :”: {msg}",
)

View File

@ -1,10 +1,11 @@
"""
`IRCv3 echo-message <https://ircv3.net/specs/extensions/echo-message>`_
<http://ircv3.net/specs/extensions/echo-message-3.2.html>
"""
import pytest
from irctest import cases
from irctest.basecontrollers import NotImplementedByController
from irctest.irc_utils.junkdrawer import random_name
from irctest.patma import ANYDICT
@ -22,18 +23,31 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("echo-message")
def testEchoMessage(self, command, solo, server_time):
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
if server_time:
self.connectClient(
"baz",
capabilities=["echo-message", "server-time"],
skip_if_cap_nak=True,
)
else:
self.connectClient(
"baz",
capabilities=["echo-message", "server-time"],
skip_if_cap_nak=True,
)
self.addClient()
self.sendLine(1, "CAP LS 302")
capabilities = self.getCapLs(1)
if "echo-message" not in capabilities:
raise NotImplementedByController("echo-message")
if server_time and "server-time" not in capabilities:
raise NotImplementedByController("server-time")
# TODO: check also without this
self.sendLine(
1,
"CAP REQ :echo-message{}".format(" server-time" if server_time else ""),
)
self.getRegistrationMessage(1)
# TODO: Remove this one the trailing space issue is fixed in Charybdis
# and Mammon:
# self.assertMessageMatch(m, command='CAP',
# params=['*', 'ACK', 'echo-message'] +
# (['server-time'] if server_time else []),
# fail_msg='Did not ACK advertised capabilities: {msg}')
self.sendLine(1, "USER f * * :foo")
self.sendLine(1, "NICK baz")
self.sendLine(1, "CAP END")
self.skipToWelcome(1)
self.getMessages(1)
self.sendLine(1, "JOIN #chan")

View File

@ -1,7 +1,3 @@
"""
`Ergo <https://ergo.chat/>`-specific tests of NickServ.
"""
from irctest import cases
from irctest.numerics import RPL_YOUREOPER

View File

@ -1,12 +1,12 @@
"""
`IRCv3 extended-join <https://ircv3.net/specs/extensions/extended-join>`_
<http://ircv3.net/specs/extensions/extended-join-3.1.html>
"""
from irctest import cases
@cases.mark_services
class MetadataTestCase(cases.BaseServerTestCase):
class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
def connectRegisteredClient(self, nick):
self.addClient()
self.sendLine(2, "CAP LS 302")
@ -50,7 +50,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
@cases.mark_capabilities("extended-join")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testLoggedIn(self):
self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True)
self.joinChannel(1, "#chan")

View File

@ -1,8 +1,7 @@
"""
The HELP and HELPOP command (`Modern <https://modern.ircdocs.horse/#help-message>`__)
The HELP and HELPOP command.
"""
import functools
import re
import pytest
@ -18,30 +17,6 @@ from irctest.numerics import (
from irctest.patma import ANYSTR, StrRe
def with_xfails(f):
@functools.wraps(f)
def newf(self, command, *args, **kwargs):
if command == "HELP" and self.controller.software_name == "Bahamut":
raise runner.ImplementationChoice(
"fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP)"
)
if self.controller.software_name in ("irc2", "ircu2", "ngIRCd"):
raise runner.ImplementationChoice(
"numerics in reply to /HELP and /HELPOP (uses NOTICE instead)"
)
if self.controller.software_name == "UnrealIRCd":
raise runner.ImplementationChoice(
"fails because Unreal uses custom numerics "
"https://github.com/unrealircd/unrealircd/pull/184"
)
return f(self, command, *args, **kwargs)
return newf
class HelpTestCase(cases.BaseServerTestCase):
def _assertValidHelp(self, messages, subject):
if subject != ANYSTR:
@ -71,7 +46,6 @@ class HelpTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
@cases.mark_specifications("Modern")
@with_xfails
def testHelpNoArg(self, command):
self.connectClient("nick")
self.sendLine(1, f"{command}")
@ -85,7 +59,6 @@ class HelpTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
@cases.mark_specifications("Modern")
@with_xfails
def testHelpPrivmsg(self, command):
self.connectClient("nick")
self.sendLine(1, f"{command} PRIVMSG")
@ -98,7 +71,6 @@ class HelpTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
@cases.mark_specifications("Modern")
@with_xfails
def testHelpUnknownSubject(self, command):
self.connectClient("nick")
self.sendLine(1, f"{command} THISISNOTACOMMAND")

View File

@ -1,8 +1,5 @@
"""
The INFO command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.10>`__,
`Modern <https://modern.ircdocs.horse/#info-message>`__)
The INFO command.
"""
import pytest
@ -87,9 +84,6 @@ class InfoTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("target", ["invalid.server.example", "invalidserver"])
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
@cases.xfailIfSoftware(
["Ergo"], "does not apply to Ergo, which ignores the optional <target> argument"
)
def testInfoNosuchserver(self, target):
"""
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>

View File

@ -1,18 +1,10 @@
"""
The INVITE command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.7>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.7>`__,
`Modern <https://modern.ircdocs.horse/#invite-message>`__)
"""
import pytest
from irctest import cases, runner
from irctest import cases
from irctest.numerics import (
ERR_BANNEDFROMCHAN,
ERR_CHANOPRIVSNEEDED,
ERR_INVITEONLYCHAN,
ERR_NEEDMOREPARAMS,
ERR_NOSUCHNICK,
ERR_NOTONCHANNEL,
ERR_USERONCHANNEL,
@ -201,9 +193,6 @@ class InviteTestCase(cases.BaseServerTestCase):
self._testInvite(opped=True, invite_only=invite_only)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern", strict=True)
@cases.xfailIfSoftware(
["Hybrid", "Plexus4"], "the only strict test that Hybrid fails"
)
def testInviteUnopped(self):
"""Tests invites from unopped users on not-invite-only chans."""
self._testInvite(opped=False, invite_only=False)
@ -241,11 +230,6 @@ class InviteTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["Plexus4"],
"Plexus4 allows non-op to invite if (and only if) the channel is not "
"invite-only",
)
def testInviteInviteOnly(self):
"""
"To invite a user to a channel which is invite only (MODE
@ -373,87 +357,6 @@ class InviteTestCase(cases.BaseServerTestCase):
params=["foo", "bar", "#chan", ANYSTR],
)
@cases.mark_specifications("RFC2812", "Modern")
@cases.xfailIfSoftware(
["ircu2"],
"Uses 346/347 instead of 336/337 to reply to INVITE "
"https://github.com/UndernetIRC/ircu2/pull/20",
)
def testInviteList(self):
self.connectClient("foo")
self.connectClient("bar")
self.getMessages(1)
self.getMessages(2)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(1, "INVITE bar #chan")
self.getMessages(1)
self.getMessages(2)
self.sendLine(2, "INVITE")
m = self.getMessage(2)
if m.command == ERR_NEEDMOREPARAMS:
raise runner.OptionalExtensionNotSupported("INVITE with no parameter")
if m.command != "337":
# Hybrid always sends an empty list; so skip this.
self.assertMessageMatch(
m,
command="336",
params=["bar", "#chan"],
)
m = self.getMessage(2)
self.assertMessageMatch(
m,
command="337",
params=["bar", ANYSTR],
)
@cases.mark_isupport("INVEX")
@cases.mark_specifications("Modern")
def testInvexList(self):
self.connectClient("foo")
self.getMessages(1)
if "INVEX" in self.server_support:
invex = self.server_support.get("INVEX") or "I"
else:
raise runner.IsupportTokenNotSupported("INVEX")
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(1, f"MODE #chan +{invex} bar!*@*")
self.getMessages(1)
self.sendLine(1, f"MODE #chan +{invex}")
m = self.getMessage(1)
if len(m.params) == 3:
# Old format
self.assertMessageMatch(
m,
command="346",
params=["foo", "#chan", "bar!*@*"],
)
else:
self.assertMessageMatch(
m,
command="346",
params=[
"foo",
"#chan",
"bar!*@*",
StrRe("foo(!.*@.*)?"),
StrRe("[0-9]+"),
],
)
self.assertMessageMatch(
self.getMessage(1),
command="347",
params=["foo", "#chan", ANYSTR],
)
@cases.mark_specifications("Ergo")
def testInviteExemptsFromBan(self):
# regression test for ergochat/ergo#1876;

View File

@ -1,8 +1,3 @@
"""
RPL_ISUPPORT: `format <https://modern.ircdocs.horse/#rplisupport-005>`__
and various `tokens <https://modern.ircdocs.horse/#rplisupport-parameters>`__
"""
import re
from irctest import cases, runner
@ -16,7 +11,7 @@ class IsupportTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
if "PREFIX" not in self.server_support:
raise runner.IsupportTokenNotSupported("PREFIX")
raise runner.NotImplementedByController("PREFIX")
if self.server_support["PREFIX"] == "":
# "The value is OPTIONAL and when it is not specified indicates that no
@ -80,7 +75,7 @@ class IsupportTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
if "TARGMAX" not in self.server_support:
raise runner.IsupportTokenNotSupported("TARGMAX")
raise runner.NotImplementedByController("TARGMAX")
parts = self.server_support["TARGMAX"].split(",")
for part in parts:

View File

@ -1,30 +1,5 @@
"""
The JOIN command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.1>`__,
`Modern <https://modern.ircdocs.horse/#join-message>`__)
"""
from irctest import cases, runner
from irctest import cases
from irctest.irc_utils import ambiguities
from irctest.numerics import (
ERR_BADCHANMASK,
ERR_FORBIDDENCHANNEL,
ERR_NOSUCHCHANNEL,
RPL_ENDOFNAMES,
RPL_NAMREPLY,
)
from irctest.patma import ANYSTR, StrRe
ERR_BADCHANNAME = "479" # Hybrid only, and conflicts with others
JOIN_ERROR_NUMERICS = {
ERR_BADCHANMASK,
ERR_NOSUCHCHANNEL,
ERR_FORBIDDENCHANNEL,
ERR_BADCHANNAME,
}
class JoinTestCase(cases.BaseServerTestCase):
@ -44,22 +19,13 @@ class JoinTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
self.sendLine(1, "JOIN #chan")
received_commands = {m.command for m in self.getMessages(1)}
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
acceptable_commands = expected_commands | {"MODE"}
self.assertLessEqual( # set inclusion
expected_commands,
received_commands,
expected_commands = {"353", "366"} # RPL_NAMREPLY # RPL_ENDOFNAMES
self.assertTrue(
expected_commands.issubset(received_commands),
"Server sent {} commands, but at least {} were expected.".format(
received_commands, expected_commands
),
)
self.assertLessEqual( # ditto
received_commands,
acceptable_commands,
"Server sent {} commands, but only {} were expected.".format(
received_commands, acceptable_commands
),
)
@cases.mark_specifications("RFC2812")
def testJoinNamreply(self):
@ -144,95 +110,3 @@ class JoinTestCase(cases.BaseServerTestCase):
'"foo" with an optional "+" or "@" prefix, but got: '
"{msg}",
)
def testJoinPartiallyInvalid(self):
"""TODO: specify this in Modern"""
self.connectClient("foo")
if int(self.targmax.get("JOIN") or "4") < 2:
raise runner.OptionalExtensionNotSupported("multi-channel JOIN")
self.sendLine(1, "JOIN #valid,inv@lid")
messages = self.getMessages(1)
received_commands = {m.command for m in messages}
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"}
self.assertLessEqual(
expected_commands,
received_commands,
"Server sent {} commands, but at least {} were expected.".format(
received_commands, expected_commands
),
)
self.assertLessEqual(
received_commands,
acceptable_commands,
"Server sent {} commands, but only {} were expected.".format(
received_commands, acceptable_commands
),
)
nb_errors = 0
for m in messages:
if m.command in JOIN_ERROR_NUMERICS:
nb_errors += 1
self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR])
self.assertEqual(
nb_errors,
1,
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
"got {got}",
)
@cases.mark_capabilities("batch", "labeled-response")
def testJoinPartiallyInvalidLabeledResponse(self):
"""TODO: specify this in Modern"""
self.connectClient(
"foo", capabilities=["batch", "labeled-response"], skip_if_cap_nak=True
)
if int(self.targmax.get("JOIN") or "4") < 2:
raise runner.OptionalExtensionNotSupported("multi-channel JOIN")
self.sendLine(1, "@label=label1 JOIN #valid,inv@lid")
messages = self.getMessages(1)
first_msg = messages.pop(0)
last_msg = messages.pop(-1)
self.assertMessageMatch(
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "labeled-response"]
)
batch_id = first_msg.params[0][1:]
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
received_commands = {m.command for m in messages}
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"}
self.assertLessEqual(
expected_commands,
received_commands,
"Server sent {} commands, but at least {} were expected.".format(
received_commands, expected_commands
),
)
self.assertLessEqual(
received_commands,
acceptable_commands,
"Server sent {} commands, but only {} were expected.".format(
received_commands, acceptable_commands
),
)
nb_errors = 0
for m in messages:
self.assertIn("batch", m.tags)
self.assertEqual(m.tags["batch"], batch_id)
if m.command in JOIN_ERROR_NUMERICS:
nb_errors += 1
self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR])
self.assertEqual(
nb_errors,
1,
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
"got {got}",
)

View File

@ -1,10 +1,3 @@
"""
The KICK command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.>`__,
`Modern <https://modern.ircdocs.horse/#kick-message>`__)
"""
import pytest
from irctest import cases, client_mock, runner
@ -96,10 +89,6 @@ class KickTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(m3, command="KICK", params=["#chan", "bar", ANYSTR])
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(
["Charybdis", "ircu2", "irc2", "Solanum"],
"uses the nick of the kickee rather than the kicker.",
)
def testKickDefaultComment(self):
"""
"If a "comment" is
@ -230,8 +219,13 @@ class KickTestCase(cases.BaseServerTestCase):
self.connectClient("qux")
self.joinChannel(4, "#chan")
if self.targmax.get("KICK", "1") == "1":
raise runner.OptionalExtensionNotSupported("Multi-target KICK")
targmax = dict(
item.split(":", 1)
for item in self.server_support.get("TARGMAX", "").split(",")
if item
)
if targmax.get("KICK", "1") == "1":
raise runner.NotImplementedByController("Multi-target KICK")
# TODO: check foo is an operator

View File

@ -1,8 +1,8 @@
"""
`IRCv3 labeled-response <https://ircv3.net/specs/extensions/labeled-response>`_
This specification is a little hard to test because all labels are optional;
so there may be many false positives.
<https://ircv3.net/specs/extensions/labeled-response.html>
"""
import re
@ -14,7 +14,7 @@ from irctest.numerics import ERR_UNKNOWNCOMMAND
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
class LabeledResponsesTestCase(cases.BaseServerTestCase):
class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_capabilities("echo-message", "batch", "labeled-response")
def testLabeledPrivmsgResponsesToMultipleClients(self):
self.connectClient(

View File

@ -1,136 +0,0 @@
from irctest import cases, runner
from irctest.numerics import ERR_UNKNOWNCOMMAND, RPL_ENDOFLINKS, RPL_LINKS
from irctest.patma import ANYSTR, StrRe
class LinksTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testLinksSingleServer(self):
"""
Only testing the parameter-less case.
https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3
https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5
https://github.com/ircdocs/modern-irc/pull/175
"
364 RPL_LINKS
"<mask> <server> :<hopcount> <server info>"
365 RPL_ENDOFLINKS
"<mask> :End of /LINKS list"
- In replying to the LINKS message, a server must send
replies back using the RPL_LINKS numeric and mark the
end of the list using an RPL_ENDOFLINKS reply.
"
-- https://datatracker.ietf.org/doc/html/rfc1459#page-51
-- https://datatracker.ietf.org/doc/html/rfc2812#page-48
RPL_LINKS: "<client> * <server> :<hopcount> <server info>"
RPL_ENDOFLINKS: "<client> * :End of /LINKS list"
-- https://github.com/ircdocs/modern-irc/pull/175/files
"""
self.connectClient("nick")
self.sendLine(1, "LINKS")
messages = self.getMessages(1)
if messages[0].command == ERR_UNKNOWNCOMMAND:
raise runner.OptionalCommandNotSupported("LINKS")
# Ignore '/LINKS has been disabled' from ircu2
messages = [m for m in messages if m.command != "NOTICE"]
self.assertMessageMatch(
messages.pop(-1),
command=RPL_ENDOFLINKS,
params=["nick", "*", ANYSTR],
)
if not messages:
# This server probably redacts links
return
self.assertMessageMatch(
messages[0],
command=RPL_LINKS,
params=[
"nick",
"My.Little.Server",
"My.Little.Server",
StrRe("0 (0042 )?test server"),
],
)
@cases.mark_services
class ServicesLinksTestCase(cases.BaseServerTestCase):
# On every IRCd but Ergo, services are linked.
# Ergo does not implement LINKS at all, so this test is skipped.
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testLinksWithServices(self):
"""
Only testing the parameter-less case.
https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3
https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5
"
364 RPL_LINKS
"<mask> <server> :<hopcount> <server info>"
365 RPL_ENDOFLINKS
"<mask> :End of /LINKS list"
- In replying to the LINKS message, a server must send
replies back using the RPL_LINKS numeric and mark the
end of the list using an RPL_ENDOFLINKS reply.
"
-- https://datatracker.ietf.org/doc/html/rfc1459#page-51
-- https://datatracker.ietf.org/doc/html/rfc2812#page-48
RPL_LINKS: "<client> * <server> :<hopcount> <server info>"
RPL_ENDOFLINKS: "<client> * :End of /LINKS list"
-- https://github.com/ircdocs/modern-irc/pull/175/files
"""
self.connectClient("nick")
self.sendLine(1, "LINKS")
messages = self.getMessages(1)
if messages[0].command == ERR_UNKNOWNCOMMAND:
raise runner.OptionalCommandNotSupported("LINKS")
# Ignore '/LINKS has been disabled' from ircu2
messages = [m for m in messages if m.command != "NOTICE"]
self.assertMessageMatch(
messages.pop(-1),
command=RPL_ENDOFLINKS,
params=["nick", "*", ANYSTR],
)
if not messages:
# This server redacts links
return
messages.sort(key=lambda m: m.params[-1])
self.assertMessageMatch(
messages.pop(0),
command=RPL_LINKS,
params=[
"nick",
"My.Little.Server",
"My.Little.Server",
StrRe("0 (0042 )?test server"),
],
)
self.assertMessageMatch(
messages.pop(0),
command=RPL_LINKS,
params=[
"nick",
"services.example.org",
"My.Little.Server",
StrRe("1 .+"), # SID instead of description for Anope...
],
)
self.assertEqual(messages, [])

View File

@ -1,71 +1,40 @@
"""
The LIST command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.6>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.6>`__,
`Modern <https://modern.ircdocs.horse/#list-message>`__)
"""
import time
from irctest import cases, runner
from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART
from irctest import cases
class _BasedListTestCase(cases.BaseServerTestCase):
def _parseChanList(self, client):
channels = set()
while True:
m = self.getMessage(client)
if m.command == RPL_LISTEND:
break
if m.command == RPL_LIST:
if m.params[1].startswith("&"):
# skip local pseudo-channels listed by ngircd and ircu
continue
channels.add(m.params[1])
return channels
class ListTestCase(_BasedListTestCase):
class ListTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812")
@cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST")
def testListEmpty(self):
"""<https://tools.ietf.org/html/rfc1459#section-4.2.6>
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
<https://modern.ircdocs.horse/#list-message>
"""
self.connectClient("foo")
self.connectClient("bar")
self.getMessages(1)
self.sendLine(2, "LIST")
m = self.getMessage(2)
if m.command == RPL_LISTSTART:
# skip
if m.command == "321":
# skip RPL_LISTSTART
m = self.getMessage(2)
# skip local pseudo-channels listed by ngircd and ircu
while m.command == RPL_LIST and m.params[1].startswith("&"):
while m.command == "322" and m.params[1].startswith("&"):
m = self.getMessage(2)
self.assertNotEqual(
m.command,
RPL_LIST,
"322", # RPL_LIST
"LIST response gives (at least) one channel, whereas there " "is none.",
)
self.assertMessageMatch(
m,
command=RPL_LISTEND,
command="323", # RPL_LISTEND
fail_msg="Second reply to LIST is not 322 (RPL_LIST) "
"or 323 (RPL_LISTEND), or but: {msg}",
)
@cases.mark_specifications("RFC1459", "RFC2812")
@cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST")
def testListOne(self):
"""When a channel exists, LIST should get it in a reply.
<https://tools.ietf.org/html/rfc1459#section-4.2.6>
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
<https://modern.ircdocs.horse/#list-message>
"""
self.connectClient("foo")
self.connectClient("bar")
@ -73,460 +42,34 @@ class ListTestCase(_BasedListTestCase):
self.getMessages(1)
self.sendLine(2, "LIST")
m = self.getMessage(2)
if m.command == RPL_LISTSTART:
# skip
if m.command == "321":
# skip RPL_LISTSTART
m = self.getMessage(2)
self.assertNotEqual(
m.command,
RPL_LISTEND,
"323", # RPL_LISTEND
fail_msg="LIST response ended (ie. 323, aka RPL_LISTEND) "
"without listing any channel, whereas there is one.",
)
self.assertMessageMatch(
m,
command=RPL_LIST,
command="322", # RPL_LIST
fail_msg="Second reply to LIST is not 322 (RPL_LIST), "
"nor 323 (RPL_LISTEND) but: {msg}",
)
m = self.getMessage(2)
# skip local pseudo-channels listed by ngircd and ircu
while m.command == RPL_LIST and m.params[1].startswith("&"):
while m.command == "322" and m.params[1].startswith("&"):
m = self.getMessage(2)
self.assertNotEqual(
m.command,
RPL_LIST,
"322", # RPL_LIST
fail_msg="LIST response gives (at least) two channels, "
"whereas there is only one.",
)
self.assertMessageMatch(
m,
command=RPL_LISTEND,
command="323", # RPL_LISTEND
fail_msg="Third reply to LIST is not 322 (RPL_LIST) "
"or 323 (RPL_LISTEND), or but: {msg}",
)
@cases.mark_specifications("RFC1459", "RFC2812")
@cases.xfailIfSoftware(
["Charybdis", "Solanum"],
"Charybdis and Solanum insert ERR_NOSUCHNICK reply in LIST",
)
def testListNonexistent(self):
"""LIST on a nonexistent channel does not send an error
response.
<https://tools.ietf.org/html/rfc1459#section-4.2.6>
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
"""
self.connectClient("bar")
self.sendLine(1, "LIST #nonexistent")
responses = {msg.command for msg in self.getMessages(1)}
# successful response MUST include RPL_LISTEND:
self.assertIn(RPL_LISTEND, responses)
# and MUST NOT include RPL_LIST (since there is no matching channel)
# or any error numerics:
self.assertLessEqual(responses, {RPL_LISTSTART, RPL_LISTEND})
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
def testListMask(self):
"""
"M: Searching based on mask."
-- <https://modern.ircdocs.horse/#elist-parameter>
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
"""
self.connectClient("foo")
if "M" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=M")
self.connectClient("bar")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.sendLine(2, "LIST *an1")
self.assertEqual(self._parseChanList(2), {"#chan1"})
self.sendLine(2, "LIST *an2")
self.assertEqual(self._parseChanList(2), {"#chan2"})
self.sendLine(2, "LIST #c*n2")
self.assertEqual(self._parseChanList(2), {"#chan2"})
self.sendLine(2, "LIST *an3")
self.assertEqual(self._parseChanList(2), set())
self.sendLine(2, "LIST #ch*")
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
def testListNotMask(self):
"""
" N: Searching based on a non-matching mask. i.e., the opposite of M."
-- <https://modern.ircdocs.horse/#elist-parameter>
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
"""
self.connectClient("foo")
if "N" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=N")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "LIST !*an1")
self.assertEqual(self._parseChanList(2), {"#chan2"})
self.sendLine(2, "LIST !*an2")
self.assertEqual(self._parseChanList(2), {"#chan1"})
self.sendLine(2, "LIST !#c*n2")
self.assertEqual(self._parseChanList(2), {"#chan1"})
self.sendLine(2, "LIST !*an3")
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
self.sendLine(2, "LIST !#ch*")
self.assertEqual(self._parseChanList(2), set())
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
def testListUsers(self):
"""
"U: Searching based on user count within the channel, via the "<val" and
">val" modifiers to search for a channel that has less or more than val users,
respectively."
-- <https://modern.ircdocs.horse/#elist-parameter>
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
"""
self.connectClient("foo")
if "U" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=U")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "JOIN #chan2")
self.getMessages(2)
self.connectClient("baz")
self.sendLine(3, "LIST >0")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
self.sendLine(3, "LIST <1")
self.assertEqual(self._parseChanList(3), set())
self.sendLine(3, "LIST <100")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
self.sendLine(3, "LIST >1")
self.assertEqual(self._parseChanList(3), {"#chan2"})
self.sendLine(3, "LIST <2")
self.assertEqual(self._parseChanList(3), {"#chan1"})
self.sendLine(3, "LIST <100")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
@cases.mark_specifications("Modern")
def testListTwoChannels(self):
"""
"Parameters: [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]"
-- https://modern.ircdocs.horse/#list-message
"""
self.connectClient("foo")
if "TARGMAX" in self.server_support:
for item in (self.server_support["TARGMAX"]).split(","):
(command, max_) = item.split(":", 1)
if command == "LIST" and int(max_ or "1000") < 2:
raise runner.OptionalExtensionNotSupported("TARGMAX=LIST >= 2")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.sendLine(1, "JOIN #chan3")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "JOIN #chan2")
self.getMessages(2)
self.connectClient("baz")
self.sendLine(3, "LIST")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2", "#chan3"})
self.sendLine(3, "LIST #chan1,#chan2")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
@cases.mark_isupport("ELIST")
def testListTwoMasks(self):
self.connectClient("foo")
if "M" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=M")
if "TARGMAX" in self.server_support:
for item in (self.server_support["TARGMAX"]).split(","):
(command, max_) = item.split(":", 1)
if command == "LIST" and int(max_ or "1000") < 2:
raise runner.OptionalExtensionNotSupported("TARGMAX=LIST >= 2")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.sendLine(1, "JOIN #chan3")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "JOIN #chan2")
self.getMessages(2)
self.connectClient("baz")
self.sendLine(3, "LIST")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2", "#chan3"})
self.sendLine(3, "LIST *an1,*an2")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
def testListTwoParams(self):
"""
"Parameters: [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]"
-- https://modern.ircdocs.horse/#list-message
"""
self.connectClient("foo")
if "U" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=U")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "JOIN #chan2")
self.getMessages(2)
self.connectClient("baz")
self.sendLine(3, "LIST #chan1 >0")
self.assertEqual(self._parseChanList(3), {"#chan1"})
self.sendLine(3, "LIST #chan1 <1")
self.assertEqual(self._parseChanList(3), set())
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
def testListTwoParamsTwoChannels(self):
"""
"Parameters: [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]"
-- https://modern.ircdocs.horse/#list-message
"""
self.connectClient("foo")
if "U" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=U")
if "TARGMAX" in self.server_support:
for item in (self.server_support["TARGMAX"]).split(","):
(command, max_) = item.split(":", 1)
if command == "LIST" and int(max_ or "1000") < 2:
raise runner.OptionalExtensionNotSupported("TARGMAX=LIST >= 2")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.connectClient("bar")
self.sendLine(2, "JOIN #chan2")
self.getMessages(2)
self.connectClient("baz")
self.sendLine(3, "LIST #chan1,#chan2 >0")
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
self.sendLine(3, "LIST #chan1,#chan2 <1")
self.assertEqual(self._parseChanList(3), set())
class FaketimeListTestCase(_BasedListTestCase):
faketime = "+1y x30" # for every wall clock second, 1 minute passed for the server
def _sleep_minutes(self, n):
for _ in range(n):
if self.controller.faketime_enabled:
# From the server's point of view, 1 min will pass
time.sleep(2)
else:
time.sleep(60)
# reply to pings
self.getMessages(1)
self.getMessages(2)
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["Plexus4", "Hybrid"],
"Hybrid and Plexus4 filter on ELIST=C with the opposite meaning",
)
@cases.xfailIf(
lambda self: bool(
self.controller.software_name == "UnrealIRCd"
and self.controller.software_version == 5
),
"UnrealIRCd <6.0.3 filters on ELIST=C with the opposite meaning",
)
def testListCreationTime(self):
"""
" C: Searching based on channel creation time, via the "C<val" and "C>val"
modifiers to search for a channel creation time that is higher or lower
than val."
-- <https://modern.ircdocs.horse/#elist-parameter>
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
Unfortunately, this is ambiguous, because "val" is a time delta (in minutes),
not a timestamp.
On InspIRCd and Charybdis/Solanum, "C<val" is interpreted as "the channel was
created less than <val> minutes ago
On UnrealIRCd, Plexus, and Hybrid, it is interpreted as "the channel's creation
time is a timestamp lower than <val> minutes ago" (ie. the exact opposite)
"C: Searching based on channel creation time, via the "C<val" and "C>val"
modifiers to search for a channel that was created either less than `val`
minutes ago, or more than `val` minutes ago, respectively"
-- https://github.com/ircdocs/modern-irc/pull/171
"""
self.connectClient("foo")
if "C" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=C")
self.connectClient("bar")
self.sendLine(1, "JOIN #chan1")
self.getMessages(1)
# Helps debugging
self.sendLine(1, "TIME")
self.getMessages(1)
self._sleep_minutes(2)
# Helps debugging
self.sendLine(1, "TIME")
self.getMessages(1)
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self._sleep_minutes(1)
self.sendLine(2, "LIST C>2")
self.assertEqual(self._parseChanList(2), {"#chan1"})
self.sendLine(2, "LIST C<2")
self.assertEqual(self._parseChanList(2), {"#chan2"})
self.sendLine(2, "LIST C<0")
if self.controller.software_name == "InspIRCd":
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
else:
self.assertEqual(self._parseChanList(2), set())
self.sendLine(2, "LIST C>0")
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
self.sendLine(2, "LIST C<10")
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
@cases.mark_isupport("ELIST")
@cases.mark_specifications("Modern")
@cases.xfailIf(
lambda self: bool(
self.controller.software_name == "UnrealIRCd"
and self.controller.software_version == 5
),
"UnrealIRCd <6.0.3 advertises ELIST=T but does not implement it",
)
def testListTopicTime(self):
"""
"T: Searching based on topic time, via the "T<val" and "T>val"
modifiers to search for a topic time that is lower or higher than
val respectively."
-- <https://modern.ircdocs.horse/#elist-parameter>
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
See testListCreationTime's docstring for comments on this.
"T: Searching based on topic set time, via the "T<val" and "T>val" modifiers
to search for a topic time that was set less than `val` minutes ago, or more
than `val` minutes ago, respectively."
-- https://github.com/ircdocs/modern-irc/pull/171
"""
self.connectClient("foo")
if "T" not in self.server_support.get("ELIST", ""):
raise runner.OptionalExtensionNotSupported("ELIST=T")
self.connectClient("bar")
self.sendLine(1, "JOIN #chan1")
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.sendLine(1, "TOPIC #chan1 :First channel")
self.getMessages(1)
# Helps debugging
self.sendLine(1, "TIME")
self.getMessages(1)
self._sleep_minutes(2)
# Helps debugging
self.sendLine(1, "TIME")
self.getMessages(1)
self.sendLine(1, "TOPIC #chan2 :Second channel")
self.getMessages(1)
self._sleep_minutes(1)
self.sendLine(1, "LIST T>2")
self.assertEqual(self._parseChanList(1), {"#chan1"})
self.sendLine(1, "LIST T<2")
self.assertEqual(self._parseChanList(1), {"#chan2"})
self.sendLine(1, "LIST T<0")
if self.controller.software_name == "InspIRCd":
# Insp internally represents "LIST T>0" like "LIST"
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})
else:
self.assertEqual(self._parseChanList(1), set())
self.sendLine(1, "LIST T>0")
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})
self.sendLine(1, "LIST T<10")
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})

View File

@ -1,11 +1,3 @@
"""
The LUSERS command (`RFC 2812
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.2>`__,
`Modern <https://modern.ircdocs.horse/#lusers-message>`__),
which provides statistics on user counts.
"""
from dataclasses import dataclass
import re
from typing import Optional
@ -153,10 +145,6 @@ class BasicLusersTestCase(LusersTestCase):
self.getLusers("bar", True)
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"],
"test depends on Modern behavior, not just RFC2812",
)
def testLusersFull(self):
self.connectClient("bar", name="bar")
lusers = self.getLusers("bar", False)
@ -174,22 +162,10 @@ class BasicLusersTestCase(LusersTestCase):
class LusersUnregisteredTestCase(LusersTestCase):
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(
["Nefarious"],
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
)
def testLusersRfc2812(self):
self.doLusersTest(True)
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["Nefarious"],
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
)
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"],
"test depends on Modern behavior, not just RFC2812",
)
def testLusersFull(self):
self.doLusersTest(False)
@ -253,10 +229,6 @@ class LusersUnregisteredDefaultInvisibleTestCase(LusersUnregisteredTestCase):
)
@cases.mark_specifications("Ergo")
@cases.xfailIfSoftware(
["Nefarious"],
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
)
def testLusers(self):
self.doLusersTest(False)
lusers = self.getLusers("bar", False)

View File

@ -1,5 +1,5 @@
"""
`IRCv3 message-tags <https://ircv3.net/specs/extensions/message-tags>`_
https://ircv3.net/specs/extensions/message-tags.html
"""
import pytest
@ -10,7 +10,7 @@ from irctest.numerics import ERR_INPUTTOOLONG
from irctest.patma import ANYDICT, ANYSTR, StrRe
class MessageTagsTestCase(cases.BaseServerTestCase):
class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@pytest.mark.arbitrary_client_tags
@cases.mark_capabilities("message-tags")
def testBasic(self):

View File

@ -1,5 +1,6 @@
"""
The PRIVMSG and NOTICE commands.
Section 3.2 of RFC 2812
<https://tools.ietf.org/html/rfc2812#section-3.3>
"""
from irctest import cases
@ -32,26 +33,6 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
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):
@cases.mark_specifications("RFC1459", "RFC2812")
@ -71,15 +52,6 @@ class NoticeTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("RFC1459", "RFC2812")
@cases.xfailIfSoftware(
["InspIRCd"],
"replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels",
)
@cases.xfailIfSoftware(
["UnrealIRCd"],
"replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels: "
"https://bugs.unrealircd.org/view.php?id=5949",
)
def testNoticeNonexistentChannel(self):
"""
"automatic replies must never be
@ -100,9 +72,6 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags")
@cases.xfailIfSoftware(
["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
)
def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
self.connectClient(

View File

@ -1,5 +1,6 @@
"""
`Deprecated IRCv3 Metadata <https://ircv3.net/specs/core/metadata-3.2>`_
Tests METADATA features.
<http://ircv3.net/specs/core/metadata-3.2.html>
"""
from irctest import cases

View File

@ -1,11 +1,9 @@
"""
`IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_
and `IRCv3 extended-monitor` <https://ircv3.net/specs/extensions/extended-monitor>`_
<http://ircv3.net/specs/core/monitor-3.2.html>
"""
import pytest
from irctest import cases, runner
from irctest import cases
from irctest.basecontrollers import NotImplementedByController
from irctest.client_mock import NoMessageException
from irctest.numerics import (
RPL_ENDOFMONLIST,
@ -16,10 +14,10 @@ from irctest.numerics import (
from irctest.patma import ANYSTR, StrRe
class _BaseMonitorTestCase(cases.BaseServerTestCase):
class MonitorTestCase(cases.BaseServerTestCase):
def check_server_support(self):
if "MONITOR" not in self.server_support:
raise runner.IsupportTokenNotSupported("MONITOR")
raise NotImplementedByController("MONITOR")
def assertMononline(self, client, nick, m=None):
if not m:
@ -45,8 +43,6 @@ class _BaseMonitorTestCase(cases.BaseServerTestCase):
extra_format=(nick,),
)
class MonitorTestCase(_BaseMonitorTestCase):
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneDisconnected(self):
@ -300,11 +296,10 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.sendLine(2, "NICK qux")
self.getMessages(2)
mononline = self.getMessages(1)[0]
self.assertMessageMatch(
mononline,
command=RPL_MONONLINE,
params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")],
)
self.assertEqual(mononline.command, RPL_MONONLINE)
self.assertEqual(len(mononline.params), 2, mononline.params)
self.assertIn(mononline.params[0], ("bar", "*"))
self.assertEqual(mononline.params[1].split("!")[0], "qux")
# no numerics for a case change
self.sendLine(2, "NICK QUX")
@ -315,246 +310,7 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.getMessages(2)
monoffline = self.getMessages(1)[0]
# should get RPL_MONOFFLINE with the current unfolded nick
self.assertMessageMatch(
monoffline,
command=RPL_MONOFFLINE,
params=[StrRe(r"(bar|\*)"), "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")
self.assertEqual(monoffline.command, RPL_MONOFFLINE)
self.assertEqual(len(monoffline.params), 2, monoffline.params)
self.assertIn(monoffline.params[0], ("bar", "*"))
self.assertEqual(monoffline.params[1].split("!")[0], "QUX")

View File

@ -1,5 +1,6 @@
"""
`IRCv3 multi-prefix <https://ircv3.net/specs/extensions/multi-prefix>`_
Tests multi-prefix.
<http://ircv3.net/specs/extensions/multi-prefix-3.1.html>
"""
from irctest import cases

View File

@ -1,5 +1,5 @@
"""
`Draft IRCv3 multiline <https://ircv3.net/specs/extensions/multiline>`_
draft/multiline
"""
from irctest import cases
@ -12,7 +12,7 @@ CONCAT_TAG = "draft/multiline-concat"
base_caps = ["message-tags", "batch", "echo-message", "server-time", "labeled-response"]
class MultilineTestCase(cases.BaseServerTestCase):
class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_capabilities("draft/multiline")
def testBasic(self):
self.connectClient(

View File

@ -1,114 +1,9 @@
"""
The NAMES command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5>`__,
`Modern <https://modern.ircdocs.horse/#names-message>`__)
"""
from irctest import cases, runner
from irctest.numerics import RPL_ENDOFNAMES, RPL_NAMREPLY
from irctest.patma import ANYSTR, StrRe
from irctest import cases
from irctest.numerics import RPL_ENDOFNAMES
from irctest.patma import ANYSTR
class NamesTestCase(cases.BaseServerTestCase):
def _testNames(self, symbol):
self.connectClient("nick1")
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.connectClient("nick2")
self.sendLine(2, "JOIN #chan")
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "NAMES #chan")
# TODO: It is technically allowed to have one line for each;
# but noone does that.
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=[
"nick1",
*(["="] if symbol else []),
"#chan",
StrRe("(nick2 @nick1|@nick1 nick2)"),
],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_ENDOFNAMES,
params=["nick1", "#chan", ANYSTR],
)
@cases.mark_specifications("RFC1459", deprecated=True)
def testNames1459(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=False)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testNames2812(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=True)
def _testNamesMultipleChannels(self, symbol):
self.connectClient("nick1")
if self.targmax.get("NAMES", "1") == "1":
raise runner.OptionalExtensionNotSupported("Multi-target NAMES")
self.sendLine(1, "JOIN #chan1")
self.sendLine(1, "JOIN #chan2")
self.getMessages(1)
self.sendLine(1, "NAMES #chan1,#chan2")
# TODO: order is unspecified
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=["nick1", *(["="] if symbol else []), "#chan2", "@nick1"],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_ENDOFNAMES,
params=["nick1", "#chan1,#chan2", ANYSTR],
)
@cases.mark_isupport("TARGMAX")
@cases.mark_specifications("RFC1459", deprecated=True)
def testNamesMultipleChannels1459(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesMultipleChannels(symbol=False)
@cases.mark_isupport("TARGMAX")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testNamesMultipleChannels2812(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesMultipleChannels(symbol=True)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testNamesInvalidChannel(self):
"""
@ -152,101 +47,3 @@ class NamesTestCase(cases.BaseServerTestCase):
command=RPL_ENDOFNAMES,
params=["foo", "#nonexisting", ANYSTR],
)
def _testNamesNoArgumentPublic(self, symbol):
self.connectClient("nick1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan1")
self.connectClient("nick2")
self.sendLine(2, "JOIN #chan2")
self.sendLine(2, "MODE #chan2 -sp")
self.getMessages(1)
self.getMessages(2)
self.sendLine(1, "NAMES")
# TODO: order is unspecified
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=["nick1", *(["="] if symbol else []), "#chan2", "@nick2"],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_ENDOFNAMES,
params=["nick1", ANYSTR, ANYSTR],
)
@cases.mark_specifications("RFC1459", deprecated=True)
def testNamesNoArgumentPublic1459(self):
"""
"If no <channel> parameter is given, a list of all channels and their
occupants is returned."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesNoArgumentPublic(symbol=False)
@cases.mark_specifications("RFC2812", deprecated=True)
def testNamesNoArgumentPublic2812(self):
"""
"If no <channel> parameter is given, a list of all channels and their
occupants is returned."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesNoArgumentPublic(symbol=True)
def _testNamesNoArgumentPrivate(self, symbol):
self.connectClient("nick1")
self.getMessages(1)
self.sendLine(1, "JOIN #chan1")
self.connectClient("nick2")
self.sendLine(2, "JOIN #chan2")
self.sendLine(2, "MODE #chan2 +sp")
self.getMessages(1)
self.getMessages(2)
self.sendLine(1, "NAMES")
self.assertMessageMatch(
self.getMessage(1),
command=RPL_NAMREPLY,
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
)
self.assertMessageMatch(
self.getMessage(1),
command=RPL_ENDOFNAMES,
params=["nick1", ANYSTR, ANYSTR],
)
@cases.mark_specifications("RFC1459", deprecated=True)
def testNamesNoArgumentPrivate1459(self):
"""
"If no <channel> parameter is given, a list of all channels and their
occupants is returned. At the end of this list, a list of users who
are visible but either not on any channel or not on a visible channel
are listed as being on `channel' "*"."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesNoArgumentPrivate(symbol=False)
@cases.mark_specifications("RFC2812", deprecated=True)
def testNamesNoArgumentPrivate2812(self):
"""
"If no <channel> parameter is given, a list of all channels and their
occupants is returned. At the end of this list, a list of users who
are visible but either not on any channel or not on a visible channel
are listed as being on `channel' "*"."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNamesNoArgumentPrivate(symbol=True)

View File

@ -1,12 +1,3 @@
"""
The PART command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-6.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-5.2>`__,
`Modern <https://modern.ircdocs.horse/#part-message>`__)
TODO: cross-reference Modern
"""
import time
from irctest import cases

View File

@ -1,7 +1,3 @@
"""
The PING and PONG commands
"""
from irctest import cases
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_NOORIGIN
from irctest.patma import ANYSTR

View File

@ -1,12 +1,3 @@
"""
The QUITcommand (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.1.6>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.1>`__,
`Modern <https://modern.ircdocs.horse/#quit-message>`__)
TODO: cross-reference RFC 1459 and Modern
"""
import time
from irctest import cases
@ -16,7 +7,6 @@ from irctest.patma import StrRe
class ChannelQuitTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(["ircu2", "Nefarious", "snircd"], "ircu2 does not echo QUIT")
def testQuit(self):
"""“Once a user has joined a channel, he receives information about
all commands his server receives affecting the channel. This

View File

@ -1,12 +1,9 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of responses to DoS attacks
using long lines.
"""
from irctest import cases
class ReadqTestCase(cases.BaseServerTestCase):
"""Test responses to DoS attacks using long lines."""
@cases.mark_specifications("Ergo")
@cases.mark_capabilities("message-tags")
def testReadqTags(self):

View File

@ -1,15 +1,9 @@
"""
`Draft IRCv3 account-registration
<https://ircv3.net/specs/extensions/account-registration>`_
"""
from irctest import cases
from irctest.patma import ANYSTR
REGISTER_CAP_NAME = "draft/account-registration"
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
@staticmethod
@ -34,7 +28,6 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
@staticmethod
@ -62,7 +55,6 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
)
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
@staticmethod
@ -113,7 +105,6 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
)
@cases.mark_services
@cases.mark_specifications("IRCv3", "Ergo")
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
@staticmethod

View File

@ -1,14 +1,11 @@
"""
Regression tests for bugs in `Ergo <https://ergo.chat/>`_.
Regression tests for bugs in oragono.
"""
from irctest import cases, runner
from irctest.numerics import (
ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE,
RPL_HELLO,
RPL_WELCOME,
)
import time
from irctest import cases
from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME
from irctest.patma import ANYDICT
@ -60,12 +57,6 @@ class RegressionsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time")
def testTagCap(self):
if self.controller.software_name == "UnrealIRCd":
raise runner.ImplementationChoice(
"Arbitrary +draft/reply values (TODO: adapt this test to use real "
"values so their pass Unreal's validation) "
"https://bugs.unrealircd.org/view.php?id=5948"
)
# regression test for oragono #754
self.connectClient(
"alice",
@ -108,13 +99,13 @@ class RegressionsTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("RFC1459")
@cases.xfailIfSoftware(["ngIRCd"], "wat")
def testStarNick(self):
self.addClient(1)
self.sendLine(1, "NICK *")
self.sendLine(1, "USER u s e r")
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))
self.assertIn(ERR_ERRONEUSNICKNAME, replies)
self.assertNotIn(RPL_WELCOME, replies)

View File

@ -1,7 +1,3 @@
"""
RELAYMSG command of `Ergo <https://ergo.chat/>`_
"""
from irctest import cases
from irctest.irc_utils.junkdrawer import random_name
from irctest.patma import ANYSTR

View File

@ -1,7 +1,3 @@
"""
Roleplay features of `Ergo <https://ergo.chat/>`_
"""
from irctest import cases
from irctest.irc_utils.junkdrawer import random_name
from irctest.numerics import ERR_CANNOTSENDRP

View File

@ -12,9 +12,9 @@ class RegistrationTestCase(cases.BaseServerTestCase):
@cases.mark_services
class SaslTestCase(cases.BaseServerTestCase):
class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlain(self):
"""PLAIN authentication with correct username/password."""
self.controller.registerUser(self, "foo", "sesame")
@ -54,7 +54,7 @@ class SaslTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainNonAscii(self):
password = "é" * 100
authstring = base64.b64encode(
@ -82,7 +82,7 @@ class SaslTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainNoAuthzid(self):
"""“message = [authzid] UTF8NUL authcid UTF8NUL passwd
@ -170,22 +170,7 @@ class SaslTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.xfailIf(
lambda self: (
self.controller.services_controller is not None
and self.controller.services_controller.software_name == "Anope"
),
"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",
)
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainLarge(self):
"""Test the client splits large AUTHENTICATE messages whose payload
is not a multiple of 400.
@ -247,14 +232,7 @@ class SaslTestCase(cases.BaseServerTestCase):
# message's length too big for it to be valid.
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
@cases.xfailIf(
lambda self: (
self.controller.services_controller is not None
and self.controller.services_controller.software_name == "Anope"
),
"Anope does not handle split AUTHENTICATE (reported on IRC)",
)
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainLargeEquals400(self):
"""Test the client splits large AUTHENTICATE messages whose payload
is not a multiple of 400.
@ -299,7 +277,7 @@ class SaslTestCase(cases.BaseServerTestCase):
# message's length too big for it to be valid.
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramSha256Success(self):
self.controller.registerUser(self, "Scramtest", "sesame")
@ -355,7 +333,7 @@ class SaslTestCase(cases.BaseServerTestCase):
self.confirmSuccessfulAuth()
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
def testScramSha256Failure(self):
self.controller.registerUser(self, "Scramtest", "sesame")

View File

@ -1,10 +1,3 @@
"""
STATUSMSG ISUPPORT token and related PRIVMSG (`Modern
<https://modern.ircdocs.horse/#statusmsg-parameter>`__)
TODO: cross-reference Modern
"""
from irctest import cases, runner
from irctest.numerics import RPL_NAMREPLY
@ -17,11 +10,6 @@ class StatusmsgTestCase(cases.BaseServerTestCase):
self.assertEqual(self.server_support["STATUSMSG"], "~&@%+")
@cases.mark_isupport("STATUSMSG")
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"],
"STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG "
"target (only for WALLCOPS/WALLCHOPS/...)",
)
def testStatusmsgFromOp(self):
"""Test that STATUSMSG are sent to the intended recipients,
with the intended prefixes."""
@ -73,11 +61,6 @@ class StatusmsgTestCase(cases.BaseServerTestCase):
self.assertEqual(len(unprivilegedMessages), 0)
@cases.mark_isupport("STATUSMSG")
@cases.xfailIfSoftware(
["ircu2", "Nefarious", "snircd"],
"STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG "
"target (only for WALLCOPS/WALLCHOPS/...)",
)
def testStatusmsgFromRegular(self):
"""Test that STATUSMSG are sent to the intended recipients,
with the intended prefixes."""

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,10 +1,3 @@
"""
The TOPIC command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.1>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.1>`__,
`Modern <https://modern.ircdocs.horse/#topic-message>`__)
"""
from irctest import cases, client_mock, runner
from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_TOPICTIME

View File

@ -1,124 +0,0 @@
"""
Test the registered-only DM user mode (commonly +R).
"""
from irctest import cases
from irctest.numerics import ERR_NEEDREGGEDNICK
@cases.mark_services
class RegisteredOnlyUmodeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Ergo")
def testRegisteredOnlyUserMode(self):
"""Test the +R registered-only mode."""
self.controller.registerUser(self, "evan", "sesame")
self.controller.registerUser(self, "carmen", "pink")
self.connectClient(
"evan",
name="evan",
account="evan",
password="sesame",
capabilities=["sasl"],
)
self.connectClient("shivaram", name="shivaram")
self.sendLine("evan", "MODE evan +R")
self.assertMessageMatch(
self.getMessage("evan"), command="MODE", params=["evan", "+R"]
)
# this DM should be blocked by +R registered-only
self.getMessages("shivaram")
self.sendLine("shivaram", "PRIVMSG evan :hey there")
self.assertMessageMatch(
self.getMessage("shivaram"),
command=ERR_NEEDREGGEDNICK,
)
self.assertEqual(self.getMessages("evan"), [])
self.connectClient(
"carmen",
name="carmen",
account="carmen",
password="pink",
capabilities=["sasl"],
)
self.getMessages("evan")
self.sendLine("carmen", "PRIVMSG evan :hey there")
self.assertEqual(self.getMessages("carmen"), [])
# this message should go through fine:
self.assertMessageMatch(
self.getMessage("evan"),
command="PRIVMSG",
params=["evan", "hey there"],
)
@cases.mark_specifications("Ergo")
def testRegisteredOnlyUserModeAcceptCommand(self):
"""Test that the ACCEPT command can authorize another user
to send the accept-er direct messages, overriding the
+R registered-only mode."""
self.controller.registerUser(self, "evan", "sesame")
self.connectClient(
"evan",
name="evan",
account="evan",
password="sesame",
capabilities=["sasl"],
)
self.connectClient("shivaram", name="shivaram")
self.sendLine("evan", "MODE evan +R")
self.assertMessageMatch(
self.getMessage("evan"), command="MODE", params=["evan", "+R"]
)
self.sendLine("evan", "ACCEPT shivaram")
self.getMessages("evan")
self.sendLine("shivaram", "PRIVMSG evan :hey there")
self.assertEqual(self.getMessages("shivaram"), [])
self.assertMessageMatch(
self.getMessage("evan"),
command="PRIVMSG",
params=["evan", "hey there"],
)
self.sendLine("evan", "ACCEPT -shivaram")
self.getMessages("evan")
self.sendLine("shivaram", "PRIVMSG evan :how's it going")
self.assertMessageMatch(
self.getMessage("shivaram"),
command=ERR_NEEDREGGEDNICK,
)
self.assertEqual(self.getMessages("evan"), [])
@cases.mark_specifications("Ergo")
def testRegisteredOnlyUserModeAutoAcceptOnDM(self):
"""Test that sending someone a DM automatically authorizes them to
reply, overriding the +R registered-only mode."""
self.controller.registerUser(self, "evan", "sesame")
self.connectClient(
"evan",
name="evan",
account="evan",
password="sesame",
capabilities=["sasl"],
)
self.connectClient("shivaram", name="shivaram")
self.sendLine("evan", "MODE evan +R")
self.assertMessageMatch(
self.getMessage("evan"), command="MODE", params=["evan", "+R"]
)
self.sendLine("evan", "PRIVMSG shivaram :hey there")
self.getMessages("evan")
self.assertMessageMatch(
self.getMessage("shivaram"),
command="PRIVMSG",
params=["shivaram", "hey there"],
)
self.sendLine("shivaram", "PRIVMSG evan :how's it going")
self.assertEqual(self.getMessages("shivaram"), [])
self.assertMessageMatch(
self.getMessage("evan"),
command="PRIVMSG",
params=["evan", "how's it going"],
)

View File

@ -1,21 +1,29 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of non-Unicode filtering
<https://ircv3.net/specs/extensions/utf8-only>`_
"""
from irctest import cases, runner
from irctest import cases
from irctest.patma import ANYSTR
class Utf8TestCase(cases.BaseServerTestCase):
class Utf8TestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("Ergo")
def testNonUtf8Filtering(self):
def testUtf8Validation(self):
self.connectClient(
"bar",
capabilities=["batch", "echo-message", "labeled-response"],
)
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.assertMessageMatch(
self.getMessage(1),
@ -23,26 +31,3 @@ class Utf8TestCase(cases.BaseServerTestCase):
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
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

@ -1,9 +1,3 @@
"""
The WALLOPS command (`RFC 2812
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.7>`__,
`Modern <https://modern.ircdocs.horse/#wallops-message>`__)
"""
from irctest import cases, runner
from irctest.numerics import ERR_NOPRIVILEGES, ERR_UNKNOWNCOMMAND, RPL_YOUREOPER
from irctest.patma import ANYSTR, StrRe
@ -44,7 +38,7 @@ class WallopsTestCase(cases.BaseServerTestCase):
messages = self.getMessages(1)
if ERR_UNKNOWNCOMMAND in (message.command for message in messages):
raise runner.OptionalCommandNotSupported("WALLOPS")
raise runner.NotImplementedByController("WALLOPS")
for message in messages:
self.assertMessageMatch(
message,
@ -66,9 +60,6 @@ class WallopsTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["irc2"], "irc2 ignores the command instead of replying ERR_UNKNOWNCOMMAND"
)
def testWallopsPrivileges(self):
"""
https://github.com/ircdocs/modern-irc/pull/118
@ -77,7 +68,7 @@ class WallopsTestCase(cases.BaseServerTestCase):
self.sendLine(1, "WALLOPS :hi everyone")
message = self.getMessage(1)
if message.command == ERR_UNKNOWNCOMMAND:
raise runner.OptionalCommandNotSupported("WALLOPS")
raise runner.NotImplementedByController("WALLOPS")
self.assertMessageMatch(
message, command=ERR_NOPRIVILEGES, params=["nick1", ANYSTR]
)

View File

@ -1,10 +1,3 @@
"""
The WHO command (`Modern <https://modern.ircdocs.horse/#who-message>`__)
and `IRCv3 WHOX <https://ircv3.net/specs/extensions/whox>`_
TODO: cross-reference RFC 1459 and RFC 2812
"""
import re
import pytest
@ -37,8 +30,8 @@ class BaseWhoTestCase:
self.sendLine(1, f"USER {self.username} 0 * :{self.realname}")
if auth:
self.sendLine(1, "CAP END")
self.getRegistrationMessage(1)
self.skipToWelcome(1)
self.getMessages(1)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
@ -84,12 +77,9 @@ class BaseWhoTestCase:
)
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("Modern")
def testWhoStar(self):
if self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(2, "WHO *")
@ -118,9 +108,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNick(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(2, f"WHO {mask}")
@ -148,9 +135,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
ids=["username", "realname-mask", "hostname"],
)
def testWhoUsernameRealName(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(2, f"WHO :{mask}")
@ -201,9 +185,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickAway(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(1, "AWAY :be right back")
@ -230,9 +211,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickOper(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(1, "OPER operuser operpassword")
@ -264,9 +242,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickAwayAndOper(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(1, "OPER operuser operpassword")
@ -298,9 +273,6 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"])
@cases.mark_specifications("Modern")
def testWhoChan(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(1, "OPER operuser operpassword")
@ -443,7 +415,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@cases.mark_services
class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
class WhoServicesTestCase(
BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper
):
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("WHOX")
def testWhoxAccount(self):
@ -503,34 +477,3 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
command=RPL_ENDOFWHO,
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

@ -1,9 +1,3 @@
"""
The WHOIS command (`Modern <https://modern.ircdocs.horse/#whois-message>`__)
TODO: cross-reference RFC 1459 and RFC 2812
"""
import pytest
from irctest import cases
@ -29,9 +23,6 @@ from irctest.patma import ANYSTR, StrRe
class _WhoisTestMixin(cases.BaseServerTestCase):
def _testWhoisNumerics(self, authenticate, away, oper):
if oper and self.controller.software_name == "Charybdis":
pytest.xfail("charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR")
if authenticate:
self.connectClient("nick1")
self.controller.registerUser(self, "val", "sesame")
@ -71,10 +62,7 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
last_message,
command=RPL_ENDOFWHOIS,
params=["nick1", "nick2", ANYSTR],
fail_msg=(
f"Expected RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS}) as last message, "
f"got {{msg}}"
),
fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})",
)
unexpected_messages = []
@ -99,12 +87,6 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
],
)
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
# explicit configuration by the operators.
assert False, "RPL_WHOISSPECIAL in use with default configuration"
@ -176,7 +158,7 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
)
class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper):
@pytest.mark.parametrize(
"server",
["", "My.Little.Server", "coolNick"],
@ -222,9 +204,11 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
@cases.mark_services
class ServicesWhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
class ServicesWhoisTestCase(
_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper
):
@pytest.mark.parametrize("oper", [False, True], ids=["normal", "oper"])
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
@cases.mark_specifications("Modern")
def testWhoisNumerics(self, oper):
"""Tests all numerics are in the exhaustive list defined in the Modern spec,
@ -307,7 +291,7 @@ class ServicesWhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
"RPL_WHOISCHANNELS should be sent for a non-invisible nick",
)
@cases.skipUnlessHasMechanism("PLAIN")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
@cases.mark_specifications("ircdocs")
def testWhoisAccount(self):
"""Test numeric 330, RPL_WHOISACCOUNT.

View File

@ -1,19 +1,8 @@
"""
The WHOSWAS command (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3>`__,
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3>`__,
`Modern <https://modern.ircdocs.horse/#whowas-message>`__)
TODO: cross-reference Modern
"""
import pytest
from irctest import cases, runner
from irctest.exceptions import ConnectionClosed
from irctest.numerics import (
ERR_NEEDMOREPARAMS,
ERR_NONICKNAMEGIVEN,
ERR_WASNOSUCHNICK,
RPL_ENDOFWHOWAS,
@ -89,43 +78,6 @@ class WhowasTestCase(cases.BaseServerTestCase):
unexpected_messages, [], fail_msg="Unexpected numeric messages: {got}"
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testWhowasEnd(self):
"""
"At the end of all reply batches, there must be RPL_ENDOFWHOWAS"
-- https://datatracker.ietf.org/doc/html/rfc1459#page-50
-- https://datatracker.ietf.org/doc/html/rfc2812#page-45
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS"
-- https://modern.ircdocs.horse/#whowas-message
"""
self.connectClient("nick1")
self.connectClient("nick2")
self.sendLine(2, "QUIT :bye")
try:
self.getMessages(2)
except ConnectionClosed:
pass
self.sendLine(1, "WHOWAS nick2")
messages = []
for _ in range(10):
messages.extend(self.getMessages(1))
if RPL_ENDOFWHOWAS in (m.command for m in messages):
break
last_message = messages.pop()
self.assertMessageMatch(
last_message,
command=RPL_ENDOFWHOWAS,
params=["nick1", "nick2", ANYSTR],
fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})",
)
def _testWhowasMultiple(self, second_result, whowas_command):
"""
"The history is searched backward, returning the most recent entry first."
@ -200,84 +152,50 @@ class WhowasTestCase(cases.BaseServerTestCase):
fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})",
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasMultiple(self):
"""
"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/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message
"""
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasCount1(self):
"""
"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/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message
"""
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasCount2(self):
"""
"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/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message
"""
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasCountNegative(self):
"""
"If a non-positive number is passed as being <count>, then a full search
is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
"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")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19"
)
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasCountZero(self):
"""
"If a non-positive number is passed as being <count>, then a full search
is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
"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")
@ -286,15 +204,11 @@ class WhowasTestCase(cases.BaseServerTestCase):
"""
"Wildcards are allowed in the <target> parameter."
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message
"""
if self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHOWAS mask")
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2")
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
def testWhowasNoParamRfc(self):
def testWhowasNoParam(self):
"""
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
@ -325,46 +239,11 @@ class WhowasTestCase(cases.BaseServerTestCase):
params=["nick1", "nick2", ANYSTR],
)
@cases.mark_specifications("Modern")
def testWhowasNoParamModern(self):
"""
"If the `<nick>` argument is missing, they SHOULD send a single reply, using
either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS"
-- https://modern.ircdocs.horse/#whowas-message
"""
# But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS
# instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns
# RPL_ENDOFWHOWAS either way.
self.connectClient("nick1")
self.sendLine(1, "WHOWAS")
m = self.getMessage(1)
if m.command == ERR_NONICKNAMEGIVEN:
self.assertMessageMatch(
m,
command=ERR_NONICKNAMEGIVEN,
params=["nick1", ANYSTR],
)
else:
self.assertMessageMatch(
m,
command=ERR_NEEDMOREPARAMS,
params=["nick1", "WHOWAS", ANYSTR],
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["Charybdis"],
"fails because of a typo (solved in "
"https://github.com/solanum-ircd/solanum/commit/"
"08b7b6bd7e60a760ad47b58cbe8075b45d66166f)",
)
@cases.mark_specifications("RFC1459", "RFC2812")
def testWhowasNoSuchNick(self):
"""
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
-- https://modern.ircdocs.horse/#whowas-message
and:
@ -372,12 +251,6 @@ class WhowasTestCase(cases.BaseServerTestCase):
(even if there was only one reply and it was an error)."
-- https://datatracker.ietf.org/doc/html/rfc1459#page-50
-- https://datatracker.ietf.org/doc/html/rfc2812#page-45
and:
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS"
-- https://modern.ircdocs.horse/#whowas-message
"""
self.connectClient("nick1")
@ -402,15 +275,15 @@ class WhowasTestCase(cases.BaseServerTestCase):
"""
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
"""
if self.controller.software_name == "Bahamut":
pytest.xfail(
"Bahamut returns entries in query order instead of chronological order"
)
self.connectClient("nick1")
if self.targmax.get("WHOWAS", "1") == "1":
raise runner.OptionalExtensionNotSupported("Multi-target WHOWAS")
targmax = dict(
item.split(":", 1)
for item in self.server_support.get("TARGMAX", "").split(",")
if item
)
if targmax.get("WHOWAS", "1") == "1":
raise runner.NotImplementedByController("Multi-target WHOWAS")
self.connectClient("nick2", ident="ident2")
self.sendLine(2, "QUIT :bye")

View File

@ -1,7 +1,3 @@
"""
`Ergo <https://ergo.chat/>`_-specific tests of ZNC-like message playback
"""
import time
from irctest import cases

View File

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

View File

@ -10,6 +10,7 @@ and keep them in sync.
import enum
import pathlib
import textwrap
import yaml
@ -116,7 +117,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None
return {
"runs-on": "ubuntu-20.04",
"runs-on": "ubuntu-latest",
"steps": [
{
"name": "Create directories",
@ -144,9 +145,13 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads = []
install_steps = []
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:
env += (
f"PATH={software_config['prefix']}/sbin"
@ -191,7 +196,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = []
return {
"runs-on": "ubuntu-20.04",
"runs-on": "ubuntu-latest",
"needs": needs,
"steps": [
{"uses": "actions/checkout@v2"},
@ -204,8 +209,8 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
*unpack,
*install_steps,
{
"name": "Install system dependencies",
"run": "sudo apt-get install atheme-services faketime",
"name": "Install Atheme",
"run": "sudo apt-get install atheme-services",
},
{
"name": "Install irctest dependencies",
@ -221,7 +226,6 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
},
{
"name": "Test with pytest",
"timeout-minutes": 30,
"run": (
f"PYTEST_ARGS='--junit-xml pytest.xml' "
f"PATH=$HOME/.local/bin:$PATH "
@ -233,7 +237,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
"if": "always()",
"uses": "actions/upload-artifact@v2",
"with": {
"name": f"pytest-results_{test_id}_{version_flavor.value}",
"name": f"pytest results {test_id} ({version_flavor.value})",
"path": "pytest.xml",
},
},
@ -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):
"""Make a tarball (to preserve permissions) and upload"""
return [
@ -263,6 +308,7 @@ def upload_steps(software_id):
def generate_workflow(config: dict, version_flavor: VersionFlavor):
on: dict
if version_flavor == VersionFlavor.STABLE:
on = {"push": None, "pull_request": None}
@ -280,6 +326,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
}
jobs = {}
jobs["build-anope"] = get_build_job_anope()
for software_id in config["software"]:
software_config = config["software"][software_id]
@ -304,9 +351,9 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
jobs[f"test-{test_id}"] = test_job
jobs["publish-test-results"] = {
"name": "Publish Dashboard",
"name": "Publish Unit Tests Results",
"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
# this job then
"if": "success() || failure()",
@ -318,31 +365,32 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
"with": {"path": "artifacts"},
},
{
"name": "Install dashboard dependencies",
"run": script(
"python -m pip install --upgrade pip",
"pip install defusedxml docutils -r requirements.txt",
),
},
{
"name": "Generate dashboard",
"run": script(
"shopt -s globstar",
"python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml",
"echo '/ /index.xhtml' > dashboard/_redirects",
),
},
{
"name": "Install netlify-cli",
"run": "npm i -g netlify-cli",
},
{
"name": "Deploy to Netlify",
"run": "./.github/deploy_to_netlify.py",
"env": {
"NETLIFY_SITE_ID": "${{ secrets.NETLIFY_SITE_ID }}",
"NETLIFY_AUTH_TOKEN": "${{ secrets.NETLIFY_AUTH_TOKEN }}",
"GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}",
"name": "Publish Unit Test Results",
"uses": "actions/github-script@v4",
"if": "github.event_name == 'pull_request'",
"with": {
"result-encoding": "string",
"script": script(
textwrap.dedent(
"""\
let body = '';
const options = {};
options.listeners = {
stdout: (data) => {
body += data.toString();
}
};
await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options);
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
return body;
"""
)
),
},
},
],

View File

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

Some files were not shown because too many files have changed in this diff Show More