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..2d5e246 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +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) + +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..13093df --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,17 @@ +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + python3 \ + clang-format \ + cmake \ + ninja-build \ + git \ + g++ \ + make \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/Jenkinsfile b/Jenkinsfile index 7679191..2011f8e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,22 +1,62 @@ pipeline { - agent any + agent { + docker { + image 'lifemaps/ci-cpp:latest' + reuseNode true + args '-u root:root' + } + } + + options { + 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 { + script { + if (getContext(hudson.FilePath)) { + cleanWs deleteDirs: true, disableDeferredWipeout: true + } } } } diff --git a/ci/check_pr_policies.py b/ci/check_pr_policies.py new file mode 100644 index 0000000..fd87dcc --- /dev/null +++ b/ci/check_pr_policies.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(".").resolve() +TEST_DIRS = [ROOT / "tests", ROOT / "test"] +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 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 = 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()] + +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 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: + test_files = list(iter_test_files()) + if not test_files: + return False + + 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/ oder test/ für {rel} gefunden.") + sys.exit(1) + + print("Policy-Checks erfolgreich.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..0187866 --- /dev/null +++ b/main.cpp @@ -0,0 +1,7 @@ +#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 new file mode 100644 index 0000000..e56ac33 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,6 @@ +#include + +TEST(MainTest, Smoke) +{ + EXPECT_TRUE(true); +} \ No newline at end of file