Skip to content

Commit 6a49eaa

Browse files
authored
Merge pull request #825 from mitzkia/light_add_valgrind_execution_support
light: Add support to run light tests with valgrind
2 parents 1b2e278 + 94cdc77 commit 6a49eaa

File tree

5 files changed

+325
-4
lines changed

5 files changed

+325
-4
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: CI @ devshell
2+
3+
on:
4+
schedule:
5+
- cron: '00 00 * * *'
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
jobs:
12+
general:
13+
runs-on: ubuntu-latest
14+
container:
15+
image: ghcr.io/axoflow/axosyslog-dbld-devshell:latest
16+
options: --privileged --ulimit core=-1
17+
18+
strategy:
19+
matrix:
20+
build-tool: [autotools]
21+
cc: [gcc]
22+
fail-fast: false
23+
24+
steps:
25+
- name: Checkout AxoSyslog source
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Git safedir
29+
run: git config --global --add safe.directory "${GITHUB_WORKSPACE}"
30+
31+
- name: Setup environment
32+
run: |
33+
. .github/workflows/gh-tools.sh
34+
35+
# Setup corefiles
36+
ulimit -c unlimited
37+
COREFILES_DIR=/tmp/corefiles
38+
mkdir ${COREFILES_DIR}
39+
echo "${COREFILES_DIR}/core.%h.%e.%t" > /proc/sys/kernel/core_pattern
40+
41+
# Setup build time environment variables
42+
PYTHONUSERBASE="${HOME}/python_packages"
43+
CC="${{ matrix.cc }}"
44+
CXX=`[ $CC = gcc ] && echo g++ || echo clang++`
45+
SYSLOG_NG_INSTALL_DIR=${HOME}/install/syslog-ng
46+
CONFIGURE_FLAGS="
47+
--prefix=${SYSLOG_NG_INSTALL_DIR}
48+
--enable-debug
49+
--enable-all-modules
50+
--disable-java
51+
--disable-java-modules
52+
--enable-ebpf
53+
--with-python=3
54+
--enable-extra-warnings
55+
`[ $CC = clang ] && echo '--enable-force-gnu99' || true`
56+
"
57+
CMAKE_FLAGS="
58+
-DCMAKE_BUILD_TYPE=Debug
59+
-DCMAKE_C_FLAGS=-Werror
60+
-DCMAKE_INSTALL_PREFIX=${HOME}/install/syslog-ng
61+
-DPYTHON_VERSION=3
62+
"
63+
gh_export COREFILES_DIR PYTHONUSERBASE CC CXX SYSLOG_NG_INSTALL_DIR CONFIGURE_FLAGS CMAKE_FLAGS
64+
gh_path "${PYTHONUSERBASE}"
65+
66+
- name: autogen.sh
67+
if: matrix.build-tool == 'autotools'
68+
run: ./autogen.sh
69+
70+
- name: configure
71+
if: matrix.build-tool == 'autotools'
72+
run: |
73+
mkdir build
74+
cd build
75+
../configure ${CONFIGURE_FLAGS}
76+
77+
- name: make
78+
working-directory: ./build
79+
run: make V=1 -j $(nproc)
80+
81+
- name: make install
82+
working-directory: ./build
83+
run: make install
84+
85+
- name: Python virtualenv for syslog-ng runtime
86+
run: ${SYSLOG_NG_INSTALL_DIR}/bin/syslog-ng-update-virtualenv -y
87+
88+
- name: Light
89+
id: light
90+
working-directory: ./build
91+
run: |
92+
make light-valgrind-check
93+
python3 scripts/process_valgrind_output.py
94+
cp definitely_lost_blocks.txt reports/
95+
cp errors_in_context.txt reports/
96+
97+
- name: "Prepare artifact: light-reports"
98+
id: prepare-light-reports
99+
if: always() && steps.light.outcome == 'failure'
100+
run: |
101+
REPORTS_DIR=$GITHUB_WORKSPACE/tests/light/reports
102+
cp -r ${REPORTS_DIR} /tmp/light-reports
103+
find /tmp/light-reports -type p,s -print0 | xargs -0 rm -f
104+
tar -cz -f /tmp/light-reports.tar.gz /tmp/light-reports
105+
106+
- name: "Artifact: light-reports"
107+
uses: actions/upload-artifact@v4
108+
if: always() && steps.prepare-light-reports.outcome == 'success'
109+
with:
110+
name: light-reports-${{ matrix.build-tool }}-${{ matrix.cc }}
111+
path: /tmp/light-reports.tar.gz
112+
113+
- name: Dump corefile backtrace
114+
working-directory: ${{ env.COREFILES_DIR }}
115+
if: failure()
116+
run: |
117+
find -name "core.*syslog-ng*" -exec \
118+
gdb --ex="thread apply all bt full" --ex="quit" ${SYSLOG_NG_INSTALL_DIR}/sbin/syslog-ng --core {} \;
119+
120+
- name: "Artifact: corefiles"
121+
uses: actions/upload-artifact@v4
122+
if: failure()
123+
with:
124+
name: corefiles-${{ matrix.build-tool }}-${{ matrix.cc }}
125+
path: ${{ env.COREFILES_DIR }}

tests/light/Makefile.am

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ light-self-check pytest-self-check: light-venv
3131
light-check pytest-check: light-venv
3232
$(LIGHT_PYTEST_CMD) -o log_cli=$(PYTEST_VERBOSE) $(LIGHT_SRC_DIR)/functional_tests/$(PYTEST_SUBDIR) -n auto $(PYTEST_OPTS) --installdir=${prefix} --show-capture=no
3333

34+
light-valgrind-check pytest-valgrind-check: light-venv
35+
$(LIGHT_PYTEST_CMD) -o log_cli=$(PYTEST_VERBOSE) $(LIGHT_SRC_DIR)/functional_tests/$(PYTEST_SUBDIR) $(PYTEST_OPTS) --installdir=${prefix} --show-capture=no --run-under=valgrind
36+
3437
light-linters pytest-linters: light-venv
3538
find $(abs_top_srcdir)/tests/light/ -name "*.py" \
3639
-not -path "*reports*" \
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python
2+
#############################################################################
3+
# Copyright (c) 2025 Axoflow
4+
# Copyright (c) 2025 Andras Mitzki <andras.mitzki@axoflow.com>
5+
#
6+
# This program is free software: you can redistribute it and/or modify it
7+
# under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
#
19+
# As an additional exemption you are allowed to compile & link against the
20+
# OpenSSL libraries as published by the OpenSSL project. See the file
21+
# COPYING for details.
22+
#
23+
#############################################################################
24+
import os
25+
import re
26+
from typing import Iterable
27+
from typing import List
28+
29+
# precompiled patterns
30+
definitely_pat = re.compile(r'definitely lost in', re.IGNORECASE)
31+
generic_pat = re.compile(r'bytes in ')
32+
errors_in_context_pat = re.compile(r'errors in context', re.IGNORECASE)
33+
generic_errors_in_context_pat = re.compile(r'== $', re.IGNORECASE)
34+
_pid_re = re.compile(r'==\d+==\s*')
35+
_exclude_re = re.compile(r'blocks are definitely lost in loss record')
36+
37+
38+
def find_all_valgrind_logs(root_dir: str) -> List[str]:
39+
"""Return list of files under root_dir whose names end with '_valgrind_output'."""
40+
valgrind_logs = []
41+
for dirpath, _, filenames in os.walk(root_dir):
42+
for filename in filenames:
43+
if filename.endswith("_valgrind_output"):
44+
valgrind_logs.append(os.path.join(dirpath, filename))
45+
return valgrind_logs
46+
47+
48+
def file_contains_definitely_lost(file_path: str) -> bool:
49+
"""Quick scan to decide whether a file has 'definitely lost' (avoids loading large files unnecessarily)."""
50+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
51+
for line in f:
52+
if 'definitely lost' in line:
53+
return True
54+
return False
55+
56+
57+
def file_contains_errors_in_context(file_path: str) -> bool:
58+
"""Quick scan to decide whether a file has 'errors in context'."""
59+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
60+
for line in f:
61+
if 'errors in context' in line:
62+
return True
63+
return False
64+
65+
66+
def extract_definitely_lost_blocks_from_lines(lines: Iterable[str]) -> List[List[str]]:
67+
"""
68+
Parse lines and return a list of 'definitely lost' blocks.
69+
"""
70+
blocks: List[List[str]] = []
71+
current: List[str] | None = None
72+
73+
for line in lines:
74+
if definitely_pat.search(line):
75+
if current:
76+
blocks.append(current)
77+
current = [line.strip()]
78+
continue
79+
80+
if generic_pat.search(line):
81+
if current:
82+
blocks.append(current)
83+
current = None
84+
continue
85+
86+
if current is not None:
87+
current.append(line.strip())
88+
89+
if current:
90+
blocks.append(current)
91+
92+
return blocks
93+
94+
95+
def extract_errors_in_context_blocks_from_lines(lines: Iterable[str]) -> List[List[str]]:
96+
"""
97+
Parse lines and return a list of 'errors in context' blocks.
98+
"""
99+
blocks: List[List[str]] = []
100+
current: List[str] | None = None
101+
102+
for line in lines:
103+
if errors_in_context_pat.search(line):
104+
if current:
105+
blocks.append(current)
106+
current = [line.strip()]
107+
continue
108+
109+
if generic_errors_in_context_pat.search(line):
110+
if current:
111+
blocks.append(current)
112+
current = None
113+
continue
114+
115+
if current is not None:
116+
current.append(line.strip())
117+
118+
if current:
119+
blocks.append(current)
120+
121+
return blocks
122+
123+
124+
def extract_definitely_lost_blocks_from_file(file_path: str) -> List[List[str]]:
125+
"""Open file and extract blocks if it contains 'definitely lost'."""
126+
if not file_contains_definitely_lost(file_path):
127+
return []
128+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
129+
return extract_definitely_lost_blocks_from_lines(f)
130+
131+
132+
def extract_errors_in_context_blocks_from_file(file_path: str) -> List[List[str]]:
133+
"""Extract blocks containing 'errors in context' from a file."""
134+
if not file_contains_errors_in_context(file_path):
135+
return []
136+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
137+
return extract_errors_in_context_blocks_from_lines(f)
138+
139+
140+
def _normalize_line(line: str) -> str:
141+
"""Remove Valgrind PID markers like '==606373== ' and trim."""
142+
return _pid_re.sub('', line).strip()
143+
144+
145+
def dedupe_blocks(blocks: List[List[str]]) -> List[List[str]]:
146+
"""
147+
Deduplicate blocks based on their content, ignoring metadata lines.
148+
Returns a list of unique blocks.
149+
"""
150+
151+
content_map = {}
152+
for block in blocks:
153+
meta = _normalize_line(block[0])
154+
block_content = tuple(_normalize_line(line) for line in block[1:] if not line.endswith("=="))
155+
if block_content not in content_map:
156+
content_map.update({block_content: []})
157+
content_map[block_content].append(meta)
158+
159+
return content_map
160+
161+
162+
def write_file(file_path: str, blocks: List[List[str]]) -> None:
163+
"""Write blocks to a file."""
164+
with open(file_path, 'w', encoding='utf-8') as f:
165+
block_counter = 0
166+
for block_content, metas in blocks.items():
167+
block_counter += 1
168+
all_bytes = []
169+
for meta in metas:
170+
all_bytes.append(int(meta.split(' ')[0].strip().replace(',', '')))
171+
172+
f.write(f"\nBlock {block_counter}, number of occurrences in all tests: {len(metas)}, leaked bytes:\n {list(reversed(sorted(all_bytes)))}\n")
173+
for line in block_content:
174+
f.write(f"{line}\n")
175+
176+
177+
def main(root_dir: str) -> None:
178+
files = find_all_valgrind_logs(root_dir)
179+
assert files, f"No valgrind log files found in {root_dir}"
180+
all_definitely_lost_blocks: List[List[str]] = []
181+
all_errors_in_context_blocks: List[List[str]] = []
182+
for file in files:
183+
all_definitely_lost_blocks.extend(extract_definitely_lost_blocks_from_file(file))
184+
all_errors_in_context_blocks.extend(extract_errors_in_context_blocks_from_file(file))
185+
unique_definitely_lost_blocks = dedupe_blocks(all_definitely_lost_blocks)
186+
write_file("definitely_lost_blocks.txt", unique_definitely_lost_blocks)
187+
188+
unique_errors_in_context_blocks = dedupe_blocks(all_errors_in_context_blocks)
189+
write_file("errors_in_context_blocks.txt", unique_errors_in_context_blocks)
190+
191+
192+
if __name__ == "__main__":
193+
main("./reports/")

tests/light/src/axosyslog_light/fixtures.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,14 @@ def setup(request):
303303
with get_session_data() as session_data:
304304
base_number_of_open_fds = session_data["base_number_of_open_fds"]
305305
number_of_open_fds = len(psutil.Process().open_files())
306-
assert base_number_of_open_fds + 1 == number_of_open_fds, "Previous testcase has unclosed opened fds"
306+
if request.config.getoption("--run-under") != "valgrind":
307+
assert base_number_of_open_fds + 1 == number_of_open_fds, "Previous testcase has unclosed opened fds"
307308
for net_conn in psutil.Process().net_connections(kind="inet"):
308309
if net_conn.status == "CLOSE_WAIT":
309310
# This is a workaround for clickhouse-connect not closing connections properly
310311
continue
311-
assert False, "Previous testcase has unclosed opened sockets: {}".format(net_conn)
312+
if request.config.getoption("--run-under") != "valgrind":
313+
assert False, "Previous testcase has unclosed opened sockets: {}".format(net_conn)
312314
testcase_parameters = request.getfixturevalue("testcase_parameters")
313315

314316
copy_file(testcase_parameters.get_testcase_file(), Path.cwd())

tests/light/src/axosyslog_light/syslog_ng/syslog_ng.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@ def stop(self, unexpected_messages: typing.List[str] = None) -> None:
9898
if not self._console_log_reader.wait_for_stop_message():
9999
logger.warning("Stop message has not been found, this might be because of a very long console log")
100100
self._console_log_reader.check_for_unexpected_messages(unexpected_messages)
101-
if self._external_tool == "valgrind":
102-
self._console_log_reader.handle_valgrind_log(Path(f"syslog_ng_{self.instance_paths.get_instance_name()}_valgrind_output"))
103101
logger.info("syslog-ng process has been stopped with PID: {}\n".format(saved_pid))
104102
else:
105103
if self._process is not None:

0 commit comments

Comments
 (0)