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