From 6d986b8967b302a9e1fbf3c0fa3db3b1234cb471 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:09:13 +0200 Subject: [PATCH 1/6] Adds testcase for PR policy checks --- .clang-format | 28 +++++++++++++ CMakeLists.txt | 7 ++++ Dockerfile.ci | 16 +++++++ Jenkinsfile | 42 +++++++++++++++++-- ci/check_pr_policies.py | 92 +++++++++++++++++++++++++++++++++++++++++ test/test_main.cpp | 5 +++ 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 Dockerfile.ci create mode 100644 ci/check_pr_policies.py create mode 100644 test/test_main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..587fba2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,28 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 120 +Language: Cpp +Standard: c++20 +PointerAlignment: Left +ReferenceAlignment: Left +SortIncludes: true +IncludeBlocks: Regroup +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + BeforeCatch: true + BeforeElse: true +AllowShortIfStatementsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +NamespaceIndentation: None +IndentCaseLabels: true +AlignTrailingComments: true +SeparateDefinitionBlocks: Always +SpaceBeforeParens: ControlStatements +Cpp11BracedListStyle: true \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..98e2b71 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +enable_testing() + +add_executable(test_main tests/test_main.cpp) +target_link_libraries(test_main GTest::gtest_main) + +include(GoogleTest) +gtest_discover_tests(test_main) \ No newline at end of file diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 0000000..0083297 --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,16 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + python3 \ + clang-format \ + cmake \ + ninja-build \ + git \ + build-essential \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 7679191..1cb9edd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,23 +1,59 @@ pipeline { - agent any + agent { + dockerfile { + filename 'Dockerfile.ci' + reuseNode true + } + } + + options { + skipDefaultCheckout() + timestamps() + disableConcurrentBuilds() + } + + environment { + BUILD_DIR = "build" + CHANGE_TARGET = "${env.CHANGE_TARGET ?: 'origin/main'}" + } stages { stage('Checkout') { steps { checkout scm + sh 'git fetch --all --prune' + sh 'git submodule update --init --recursive || true' + } + } + + stage('Policy Checks') { + steps { + sh 'python3 ci/check_pr_policies.py' + } + } + + stage('Configure') { + steps { + sh 'cmake -S . -B ${BUILD_DIR} -G Ninja -DCMAKE_BUILD_TYPE=Debug' } } stage('Build') { steps { - sh 'echo Build works' + sh 'cmake --build ${BUILD_DIR} --parallel' } } stage('Test') { steps { - sh 'echo Tests work' + sh 'ctest --test-dir ${BUILD_DIR} --output-on-failure' } } } + + post { + always { + cleanWs deleteDirs: true, disableDeferredWipeout: true + } + } } \ No newline at end of file diff --git a/ci/check_pr_policies.py b/ci/check_pr_policies.py new file mode 100644 index 0000000..fb0cf7d --- /dev/null +++ b/ci/check_pr_policies.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(".").resolve() +TEST_DIR = ROOT / "tests" +CPP_EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"} + +TEST_PATTERNS = [ + "test_{name}.cpp", + "test_{name}.h", + "{name}_test.cpp", + "{name}_test.h", + "{name}test.cpp", + "{name}test.h", + "{name}gtest.cpp", + "{name}gtest.h", +] + +def run(cmd: str) -> str: + return subprocess.check_output(cmd, shell=True, text=True).strip() + +def changed_files(): + target = os.environ.get("CHANGE_TARGET", "origin/main") + out = run(f"git diff --name-only {target}...HEAD") + return [line.strip() for line in out.splitlines() if line.strip()] + +def is_cpp_file(path: Path) -> bool: + return path.suffix.lower() in CPP_EXTENSIONS + +def require_clang_format(): + if not (ROOT / ".clang-format").exists(): + print("ERROR: .clang-format fehlt im Repository-Root.") + sys.exit(1) + +def check_format(path: Path): + subprocess.check_call(f"clang-format --dry-run --Werror '{path}'", shell=True) + +def candidate_names(path: Path): + stem = path.stem.lower() + names = {stem} + for suffix in ["_p", "_impl", "_private", "_priv", "_ui"]: + if stem.endswith(suffix): + names.add(stem[:-len(suffix)]) + return sorted(n for n in names if n) + +def has_related_test(path: Path) -> bool: + if not TEST_DIR.exists(): + return False + + test_files = [p for p in TEST_DIR.rglob("*") if p.is_file()] + test_names = [p.name.lower() for p in test_files] + + for name in candidate_names(path): + for pattern in TEST_PATTERNS: + if pattern.format(name=name).lower() in test_names: + return True + for test_name in test_names: + if name in test_name and ("test" in test_name or "gtest" in test_name): + return True + return False + +def main(): + require_clang_format() + + files = changed_files() + if not files: + print("Keine geänderten Dateien erkannt.") + return + + for rel in files: + path = ROOT / rel + if not path.exists(): + continue + if is_cpp_file(path): + print(f"Prüfe: {rel}") + try: + check_format(path) + except subprocess.CalledProcessError: + print(f"ERROR: Formatfehler in {rel}") + sys.exit(1) + + if not has_related_test(path): + print(f"ERROR: Kein passender Test unter tests/ für {rel} gefunden.") + sys.exit(1) + + print("Policy-Checks erfolgreich.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 0000000..f8318b3 --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,5 @@ +#include + +TEST(MainTest, Smoke) { + EXPECT_TRUE(true); +} \ No newline at end of file -- 2.54.0 From 87b2580e9a14ce2c7f75bc72fcd9ad5029740434 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:20:42 +0200 Subject: [PATCH 2/6] Fix dockerfile agent checkout ordering --- Jenkinsfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1cb9edd..cdef410 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,7 +7,6 @@ pipeline { } options { - skipDefaultCheckout() timestamps() disableConcurrentBuilds() } @@ -53,7 +52,11 @@ pipeline { post { always { - cleanWs deleteDirs: true, disableDeferredWipeout: true + script { + if (getContext(hudson.FilePath)) { + cleanWs deleteDirs: true, disableDeferredWipeout: true + } + } } } } \ No newline at end of file -- 2.54.0 From 5910e6747d92cba139302d3ed3e5dc4799170c14 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:40:03 +0200 Subject: [PATCH 3/6] Slims jenkins setup b/c resource shortage --- Dockerfile.ci | 7 ++++--- Jenkinsfile | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index 0083297..13093df 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,4 +1,4 @@ -FROM ubuntu:24.04 +FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive @@ -9,8 +9,9 @@ RUN apt-get update && \ cmake \ ninja-build \ git \ - build-essential \ + g++ \ + make \ ca-certificates && \ rm -rf /var/lib/apt/lists/* -WORKDIR /workspace \ No newline at end of file +WORKDIR /workspace diff --git a/Jenkinsfile b/Jenkinsfile index cdef410..2011f8e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,8 +1,9 @@ pipeline { agent { - dockerfile { - filename 'Dockerfile.ci' + docker { + image 'lifemaps/ci-cpp:latest' reuseNode true + args '-u root:root' } } -- 2.54.0 From 58c3cd055958d7d659dc4b2555490ff1cabc3a63 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:44:36 +0200 Subject: [PATCH 4/6] Fix PR policy target ref resolution --- ci/check_pr_policies.py | 39 ++++++++++++++++++++++++++++++++++- {test => tests}/test_main.cpp | 0 2 files changed, 38 insertions(+), 1 deletion(-) rename {test => tests}/test_main.cpp (100%) diff --git a/ci/check_pr_policies.py b/ci/check_pr_policies.py index fb0cf7d..26e385b 100644 --- a/ci/check_pr_policies.py +++ b/ci/check_pr_policies.py @@ -22,8 +22,45 @@ TEST_PATTERNS = [ def run(cmd: str) -> str: return subprocess.check_output(cmd, shell=True, text=True).strip() +def git_ref_exists(ref: str) -> bool: + try: + subprocess.check_call( + f"git rev-parse --verify --quiet '{ref}'", + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return True + except subprocess.CalledProcessError: + return False + +def resolve_target_ref() -> str: + raw = os.environ.get("CHANGE_TARGET", "").strip() + + candidates = [] + if raw: + candidates.append(raw) + if not raw.startswith("origin/"): + candidates.append(f"origin/{raw}") + + candidates.extend([ + "origin/main", + "main", + ]) + + seen = set() + for ref in candidates: + if ref and ref not in seen: + seen.add(ref) + if git_ref_exists(ref): + return ref + + print("ERROR: Kein gültiger Ziel-Branch-Ref gefunden. Geprüft:", ", ".join(candidates)) + sys.exit(1) + def changed_files(): - target = os.environ.get("CHANGE_TARGET", "origin/main") + target = resolve_target_ref() + print(f"Vergleiche gegen Ziel-Ref: {target}") out = run(f"git diff --name-only {target}...HEAD") return [line.strip() for line in out.splitlines() if line.strip()] diff --git a/test/test_main.cpp b/tests/test_main.cpp similarity index 100% rename from test/test_main.cpp rename to tests/test_main.cpp -- 2.54.0 From 684ad1cd3aea9a6eca445e32a6401a5138f80397 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:57:01 +0200 Subject: [PATCH 5/6] Add minimal C++ example and pass PR policy checks --- CMakeLists.txt | 17 +++++++++++++++++ ci/check_pr_policies.py | 18 ++++++++++-------- main.cpp | 6 ++++++ tests/test_main.cpp | 4 +--- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 98e2b71..2d5e246 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,22 @@ +cmake_minimum_required(VERSION 3.20) +project(sandbox LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(sandbox main.cpp) + enable_testing() +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + add_executable(test_main tests/test_main.cpp) target_link_libraries(test_main GTest::gtest_main) diff --git a/ci/check_pr_policies.py b/ci/check_pr_policies.py index 26e385b..fd87dcc 100644 --- a/ci/check_pr_policies.py +++ b/ci/check_pr_policies.py @@ -5,7 +5,7 @@ import sys from pathlib import Path ROOT = Path(".").resolve() -TEST_DIR = ROOT / "tests" +TEST_DIRS = [ROOT / "tests", ROOT / "test"] CPP_EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"} TEST_PATTERNS = [ @@ -43,10 +43,7 @@ def resolve_target_ref() -> str: if not raw.startswith("origin/"): candidates.append(f"origin/{raw}") - candidates.extend([ - "origin/main", - "main", - ]) + candidates.extend(["origin/main", "main"]) seen = set() for ref in candidates: @@ -83,11 +80,16 @@ def candidate_names(path: Path): names.add(stem[:-len(suffix)]) return sorted(n for n in names if n) +def iter_test_files(): + for test_dir in TEST_DIRS: + if test_dir.exists(): + yield from (p for p in test_dir.rglob("*") if p.is_file()) + def has_related_test(path: Path) -> bool: - if not TEST_DIR.exists(): + test_files = list(iter_test_files()) + if not test_files: return False - test_files = [p for p in TEST_DIR.rglob("*") if p.is_file()] test_names = [p.name.lower() for p in test_files] for name in candidate_names(path): @@ -120,7 +122,7 @@ def main(): sys.exit(1) if not has_related_test(path): - print(f"ERROR: Kein passender Test unter tests/ für {rel} gefunden.") + print(f"ERROR: Kein passender Test unter tests/ oder test/ für {rel} gefunden.") sys.exit(1) print("Policy-Checks erfolgreich.") diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..4c7d3d7 --- /dev/null +++ b/main.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "Hello from sandbox\n"; + return 0; +} \ No newline at end of file diff --git a/tests/test_main.cpp b/tests/test_main.cpp index f8318b3..eb056cf 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -1,5 +1,3 @@ #include -TEST(MainTest, Smoke) { - EXPECT_TRUE(true); -} \ No newline at end of file +TEST(MainTest, Smoke) { EXPECT_TRUE(true); } \ No newline at end of file -- 2.54.0 From 78de7eae917d6e748f10605e694126f8fc843e64 Mon Sep 17 00:00:00 2001 From: Torsten Wierschin Date: Tue, 4 Aug 2026 19:59:08 +0200 Subject: [PATCH 6/6] Reformats sources accordingly --- main.cpp | 7 ++++--- tests/test_main.cpp | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/main.cpp b/main.cpp index 4c7d3d7..0187866 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,7 @@ #include -int main() { - std::cout << "Hello from sandbox\n"; - return 0; +int main() +{ + std::cout << "Hello from sandbox\n"; + return 0; } \ No newline at end of file diff --git a/tests/test_main.cpp b/tests/test_main.cpp index eb056cf..e56ac33 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -1,3 +1,6 @@ #include -TEST(MainTest, Smoke) { EXPECT_TRUE(true); } \ No newline at end of file +TEST(MainTest, Smoke) +{ + EXPECT_TRUE(true); +} \ No newline at end of file -- 2.54.0