Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 111 additions & 2 deletions .github/workflows/run-opensips-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,115 @@ on:
push:
pull_request:

# Previously this delegated to the tests repo's reusable workflow:
# uses: OpenSIPS/sipssert-opensips-tests/.github/workflows/run-tests.yml@main
# That workflow checks out `${{ github.repository }}` as the tests dir, which
# resolves to THIS framework repo when called from here -- so it looked for a
# non-existent `tests/run.yml` and the matrix step died (red since Feb 2026).
# Instead, run the conformance suite inline: install the framework from THIS
# checkout (so a PR's framework code is exercised) and pull the scenarios from
# the tests repo explicitly.

env:
TESTS_REPO: OpenSIPS/sipssert-opensips-tests

jobs:
opensips:
uses: OpenSIPS/sipssert-opensips-tests/.github/workflows/run-tests.yml@main
setup-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout SIPssert (this repo)
uses: actions/checkout@v6
with:
path: sipssert

- name: Checkout Tests repo
uses: actions/checkout@v6
with:
repository: ${{ env.TESTS_REPO }}
path: tests

- name: Read and parse YAML file
id: set-matrix
run: |
MATRIX=$(python3 sipssert/.github/actions/Set_Matrix/read_matrix.py tests/run.yml)
echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT"
shell: bash

test:
needs: setup-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout SIPssert (this repo)
uses: actions/checkout@v6
with:
path: sipssert

- name: Install SIPssert requirements
run: |
sudo apt update && sudo apt install -y tcpdump
sudo chmod +s "$(which tcpdump)"
shell: bash

- name: Install SIPssert framework (from this checkout)
run: |
cd sipssert
python3 setup.py install --user clean
shell: bash

- name: Checkout Tests repo
uses: actions/checkout@v6
with:
repository: ${{ env.TESTS_REPO }}
path: tests

- name: Run Test
uses: ./sipssert/.github/actions/Run_Tests
with:
tests: ${{ matrix.tests }}

aggregate:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- name: Checkout Tests repo
uses: actions/checkout@v6
with:
repository: ${{ env.TESTS_REPO }}
path: tests

- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
path: junit-reports

- name: Aggregate JUnit Reports
run: |
pip install junitparser
python tests/.github/scripts/aggregate_junit_reports.py
env:
REPORTS_DIR: junit-reports
OUTPUT_FILE: junit-final-report.xml

- name: Upload JUnit XML
uses: actions/upload-artifact@v7
with:
name: junit-final-report
path: junit-final-report.xml

- name: Print tests summary
uses: mikepenz/action-junit-report@v6
id: summary
if: success() || failure()
with:
check_name: 'SIPssert tests report'
report_paths: junit-final-report.xml
annotate_only: true
fail_on_failure: true
detailed_summary: true
include_passed: true
107 changes: 107 additions & 0 deletions sipssert/tasks/rtpengine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python
##
## This file is part of the SIPssert Testing Framework project
## Copyright (C) 2026 OpenSIPS Solutions
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
##

"""RTPengine runner class"""

import shlex

from sipssert.task import Task


class RTPEngineTask(Task):

"""RTPengine class"""

default_daemon = True
default_stop_timeout = 3
default_image = "debian:trixie-slim"

def __init__(self, test_dir, config):

super().__init__(test_dir, config)
self.entrypoint = config.get("entrypoint", "sh")

self.ip = config.get("ip", "127.0.0.1")
# `ng_port` is the rtpengine control (ng) port. Note we deliberately do
# NOT alias the base `port`/`ports` keys here: those are reserved by the
# base Task for container port *publishing* and a string value there
# (e.g. "22222") would crash get_ports(); the ng port is reached over
# the scenario bridge and never published.
self.ng_port = config.get("ng_port", "22222")
self.interface = config.get("interface", self.ip)
self.table = config.get("table", "-1")
self.install = config.get("install", True)
self.packages = self.normalize_list(config.get("packages", [
"rtpengine-daemon",
"iproute2",
]))
self.options = config.get("options", [])

if "healthcheck" not in config:
self.healthcheck = {
"test": f"ss -lnu | grep -q :{self.ng_port}",
"interval": 2000000000,
"timeout": 2000000000,
"retries": 120,
}

def get_task_args(self):
return [
"-c",
" && ".join(self.get_setup_commands() + [self.get_run_command()]),
]

def get_setup_commands(self):
if not self.install:
return []

packages = " ".join(shlex.quote(str(pkg)) for pkg in self.packages)
return [
"echo 'exit 101' > /usr/sbin/policy-rc.d",
"chmod +x /usr/sbin/policy-rc.d",
"apt-get update",
"DEBIAN_FRONTEND=noninteractive apt-get install -y "
f"--no-install-recommends {packages}",
]

def get_run_command(self):
args = [
"exec",
"rtpengine",
"--table=" + str(self.table),
"--interface=" + str(self.interface),
"--listen-ng=" + str(self.ip) + ":" + str(self.ng_port),
"--foreground",
"--log-stderr",
]
args += self.normalize_options(self.options)
return " ".join(shlex.quote(str(arg)) for arg in args)

def normalize_options(self, options):
return self.normalize_list(options)

def normalize_list(self, options):
if options is None:
return []
if isinstance(options, str):
return shlex.split(options)
return [str(option) for option in options]


# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
63 changes: 63 additions & 0 deletions tests/test_opensips_mi_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python
"""Regression tests pinning that the opensips-mi task can run an arbitrary
python script with forwarded arguments (via the base task `args:` support).

This is what lets the rtp_relay ng_checker run as `type: opensips-mi` instead of
a bespoke generic task.
"""

import importlib
import unittest


def make(config=None):
cfg = {"name": "ng checker"}
if config:
cfg.update(config)
mod = importlib.import_module("sipssert.tasks.opensips-mi")
return mod.OpenSIPSMITask("/test", cfg)


class TestOpenSIPSMIScript(unittest.TestCase):

def test_script_relative_is_mounted_under_home(self):
t = make({"script": "scripts/ng_checker.py"})
self.assertEqual(t.get_task_args(), ["/home/scripts/ng_checker.py"])

def test_script_absolute_is_left_untouched(self):
t = make({"script": "/abs/checker.py"})
self.assertEqual(t.get_task_args(), ["/abs/checker.py"])

def test_script_args_are_forwarded_after_script(self):
t = make({
"script": "scripts/ng_checker.py",
"args": ["/caps/ng.pcap", "savpf", "--ng-port", "22222"],
})
self.assertEqual(
t.get_args(),
["/home/scripts/ng_checker.py",
"/caps/ng.pcap", "savpf", "--ng-port", "22222"],
)

def test_args_with_special_chars_pass_through_as_argv(self):
# args are argv elements (no shell), so special chars survive verbatim
t = make({"script": "scripts/c.py", "args": ["a b", "x\\y", "--re=$x"]})
self.assertEqual(
t.get_args(),
["/home/scripts/c.py", "a b", "x\\y", "--re=$x"],
)

def test_integer_args_are_stringified(self):
t = make({"script": "scripts/c.py", "args": [22222]})
self.assertEqual(t.get_args(), ["/home/scripts/c.py", "22222"])

def test_no_script_keeps_mi_cli_fallback(self):
t = make({"mi_ip": "192.168.52.1", "mi_port": 8888})
self.assertEqual(
t.get_task_args(),
["-t", "http", "-i", "192.168.52.1", "-p", "8888"],
)


if __name__ == "__main__":
unittest.main()
Loading