8 Commits
Author SHA1 Message Date
torstenandCopilot 0d37751d8c Fix Jenkins workspace permission issue
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Add jenkins user to Dockerfile.ci and run as non-root
- Remove '-u root:root' args from Jenkinsfile Docker agent
- Build files will now be owned by jenkins user, allowing proper cleanup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: f545f3d8aa33
2026-10-22 18:41:10 +02:00
torstenandCopilot ba8479fa8d Fix Jenkins workspace cleanup with chmod before rm
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Replace cleanWs with shell commands that fix permissions before deletion
- Handle build/, build-coverage/, and coverage/ directories
- Use chmod -R u+w to make files writable before rm -rf
- Addresses 'Operation not permitted' errors on Jenkins host

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 9acfd6cc578c
2026-10-22 18:20:35 +02:00
torstenandCopilot 50073e010f Add workspace cleanup stage to prevent corruption issues
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Add Cleanup stage at pipeline start to wipe workspace before checkout
- Prevents 'corrupt .git repository' errors from stale build artifacts
- Uses disableDeferredWipeout for thorough cleanup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: f6767c4eba2c
2026-10-22 18:18:25 +02:00
torsten d79dcac3b3 fix: CMake include path and QString literals
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Add target_include_directories for test target to find MainWindow.h
- Replace u""_s literals with QStringLiteral() for compiler compatibility
- Fixes Jenkins build errors

Entire-Checkpoint: 0e6491fe1b1c
2026-10-22 18:15:25 +02:00
torsten 7aadf4212e style: Apply clang-format to all C++ files
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Format MainWindow.cpp, MainWindow.h, main.cpp
- Format test files (test_main.cpp, test_mainwindow.cpp)
- Fixes Jenkins clang-format policy check

Entire-Checkpoint: ea47c089bf3a
2026-10-22 18:13:57 +02:00
torsten e2e243ad26 fix: Add Qt6::Test component to find_package
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
- Missing Qt6::Test component caused CMake configuration failure in Jenkins
- Required for test executable linking

Entire-Checkpoint: ec1d65182c71
2026-10-22 18:12:38 +02:00
torstenandCopilot 41d9ef4b7a ci: Add current SSH key to allowed_signers
qt-migration-copilot-demo/pipeline/head There was a failure building this commit
qt-migration-copilot-demo/pipeline/pr-main There was a failure building this commit
Add ED25519 key for torsten.wierschin@life-maps.com to enable
signed commits for CI policy validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 074fdb682ad5
2026-10-22 18:04:48 +02:00
torstenandCopilot a37fc2255f feat: Add LCOV coverage support for Jenkins CI
- Add ENABLE_COVERAGE CMake option with gcov flags
- Create collect-coverage.sh script for LCOV workflow
- Add Qt6 test files with QTest framework
- Update Jenkinsfile with Coverage stage (main/master/coverage.*)
- Update CI Docker image with lcov and genhtml packages
- Add .gitignore entries for coverage artifacts
- Add COVERAGE.md documentation

Coverage workflow:
1. Configure with -DENABLE_COVERAGE=ON
2. Build and run tests to generate .gcda/.gcno files
3. Collect coverage with lcov
4. Filter out Qt/system libraries
5. Generate HTML report and lcov.info for Jenkins

Jenkins publishes coverage/lcov.info using Coverage Plugin.
Coverage stage only runs on protected branches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 7783c7698334
Entire-Checkpoint: 70341e29996b
2026-10-22 18:04:35 +02:00
6 changed files with 168 additions and 174 deletions

No files matched your search

Vendored
+96 -117
View File
@@ -1,5 +1,10 @@
pipeline { pipeline {
agent none agent {
docker {
image 'lifemaps/ci-cpp:latest'
reuseNode true
}
}
options { options {
timestamps() timestamps()
@@ -14,133 +19,106 @@ pipeline {
QT_QPA_PLATFORM = "offscreen" QT_QPA_PLATFORM = "offscreen"
LANG = "C.UTF-8" LANG = "C.UTF-8"
LC_ALL = "C.UTF-8" LC_ALL = "C.UTF-8"
DOCKER_IMAGE = "lifemaps/ci-cpp:latest"
} }
stages { stages {
stage('Docker Image') { stage('Cleanup') {
agent any
steps { steps {
script { script {
echo "Checking if Docker image exists locally..." echo "Cleaning workspace to avoid corruption issues..."
def imageExists = sh(script: "docker inspect ${DOCKER_IMAGE} >/dev/null 2>&1", returnStatus: true) == 0 // Use shell command with chmod to handle permission issues
if (!imageExists) { sh '''
echo "Docker image not found. Building from ci/Dockerfile.ci..." if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then
sh 'docker build -t ${DOCKER_IMAGE} -f ci/Dockerfile.ci .' echo "Removing build directory with permission fix..."
} else { chmod -R u+w "${WORKSPACE}/${BUILD_DIR}" 2>/dev/null || true
echo "Docker image ${DOCKER_IMAGE} found locally." rm -rf "${WORKSPACE}/${BUILD_DIR}"
} fi
if [ -d "${WORKSPACE}/${COVERAGE_BUILD_DIR}" ]; then
echo "Removing coverage build directory..."
chmod -R u+w "${WORKSPACE}/${COVERAGE_BUILD_DIR}" 2>/dev/null || true
rm -rf "${WORKSPACE}/${COVERAGE_BUILD_DIR}"
fi
if [ -d "${WORKSPACE}/coverage" ]; then
echo "Removing coverage output directory..."
chmod -R u+w "${WORKSPACE}/coverage" 2>/dev/null || true
rm -rf "${WORKSPACE}/coverage"
fi
'''
} }
} }
} }
stage('Build and Test') { stage('Checkout') {
agent { steps {
docker { checkout scm
image 'lifemaps/ci-cpp:latest' sh 'git fetch --all --prune'
reuseNode true sh 'git submodule update --init --recursive || true'
}
}
stage('Tool Versions') {
steps {
sh '''
python3 --version
cmake --version
ninja --version
g++ --version
'''
}
}
stage('Qt Diagnostics') {
steps {
sh '''
echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
echo "LANG=$LANG"
echo "LC_ALL=$LC_ALL"
dpkg -l | grep -E "qt6-base-dev|qtbase5-dev|qt6-tools" || true
pkg-config --list-all | grep -i qt || true
'''
}
}
stage('Policy Checks') {
steps {
sh 'python3 ci/check_pr_policies.py'
}
}
stage('Configure') {
steps {
sh 'cmake -S ${PROJECT_DIR} -B ${BUILD_DIR} -G Ninja -DCMAKE_BUILD_TYPE=Debug'
}
}
stage('Build') {
steps {
sh 'cmake --build ${BUILD_DIR} --parallel'
}
}
stage('Test') {
steps {
sh 'QT_QPA_PLATFORM=offscreen LANG=C.UTF-8 LC_ALL=C.UTF-8 ctest --test-dir ${BUILD_DIR} --output-on-failure'
}
}
stage('Coverage') {
when {
expression {
return env.BRANCH_NAME ==~ /main|master|coverage.*/
} }
} }
stages { steps {
stage('Cleanup') { echo "Running coverage collection..."
steps { sh '''
script { ${PROJECT_DIR}/scripts/collect-coverage.sh ${WORKSPACE}/${COVERAGE_BUILD_DIR}
echo "Cleaning workspace to avoid corruption issues..." '''
sh ''' }
if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then post {
echo "Removing build directory with permission fix..." always {
chmod -R u+w "${WORKSPACE}/${BUILD_DIR}" 2>/dev/null || true publishCoverage adapters: [lcovAdapter(mergeToOneReport: true, reportFile: 'coverage/lcov.info')],
rm -rf "${WORKSPACE}/${BUILD_DIR}" sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
fi
if [ -d "${WORKSPACE}/${COVERAGE_BUILD_DIR}" ]; then
echo "Removing coverage build directory..."
chmod -R u+w "${WORKSPACE}/${COVERAGE_BUILD_DIR}" 2>/dev/null || true
rm -rf "${WORKSPACE}/${COVERAGE_BUILD_DIR}"
fi
if [ -d "${WORKSPACE}/coverage" ]; then
echo "Removing coverage output directory..."
chmod -R u+w "${WORKSPACE}/coverage" 2>/dev/null || true
rm -rf "${WORKSPACE}/coverage"
fi
'''
}
}
}
stage('Checkout') {
steps {
checkout scm
sh 'git fetch --all --prune'
sh 'git submodule update --init --recursive || true'
}
}
stage('Tool Versions') {
steps {
sh '''
python3 --version
cmake --version
ninja --version
g++ --version
'''
}
}
stage('Qt Diagnostics') {
steps {
sh '''
echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
echo "LANG=$LANG"
echo "LC_ALL=$LC_ALL"
dpkg -l | grep -E "qt6-base-dev|qtbase5-dev|qt6-tools" || true
pkg-config --list-all | grep -i qt || true
'''
}
}
stage('Policy Checks') {
steps {
sh 'python3 ci/check_pr_policies.py'
}
}
stage('Configure') {
steps {
sh 'cmake -S ${PROJECT_DIR} -B ${BUILD_DIR} -G Ninja -DCMAKE_BUILD_TYPE=Debug'
}
}
stage('Build') {
steps {
sh 'cmake --build ${BUILD_DIR} --parallel'
}
}
stage('Test') {
steps {
sh 'QT_QPA_PLATFORM=offscreen LANG=C.UTF-8 LC_ALL=C.UTF-8 ctest --test-dir ${BUILD_DIR} --output-on-failure'
}
}
stage('Coverage') {
when {
expression {
def branch = env.CHANGE_BRANCH ?: env.BRANCH_NAME
return branch ==~ /main|master|coverage.*|feature.*/
}
}
steps {
echo "Running coverage collection..."
sh '''
${PROJECT_DIR}/scripts/collect-coverage.sh ${WORKSPACE}/${COVERAGE_BUILD_DIR}
'''
}
post {
always {
recordCoverage tools: [[parser: 'LCOV', pattern: "${PROJECT_DIR}/coverage/lcov.info"]],
sourceCodeRetention: 'LAST_BUILD'
}
}
} }
} }
} }
@@ -150,6 +128,7 @@ pipeline {
always { always {
script { script {
if (getContext(hudson.FilePath)) { if (getContext(hudson.FilePath)) {
// Use shell command with chmod to handle permission issues
sh ''' sh '''
if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then
echo "Final cleanup: Removing build directory..." echo "Final cleanup: Removing build directory..."
+5 -4
View File
@@ -2,9 +2,9 @@ FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Create jenkins user (UID 1001 to avoid conflicts with existing users) # Create jenkins user with same UID as host jenkins (typically 1000)
RUN groupadd -g 1001 jenkins && \ RUN groupadd -g 1000 jenkins && \
useradd -u 1001 -g 1001 -m -s /bin/bash jenkins useradd -u 1000 -g 1000 -m -s /bin/bash jenkins
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@@ -23,7 +23,8 @@ RUN apt-get update && \
qt6-base-dev-tools \ qt6-base-dev-tools \
qt6-tools-dev \ qt6-tools-dev \
qt6-tools-dev-tools \ qt6-tools-dev-tools \
lcov && \ lcov \
genhtml && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Set Qt platform for headless testing # Set Qt platform for headless testing
@@ -60,6 +60,7 @@ enable_testing()
# Add test executable # Add test executable
add_executable(Qt6MigratedBenchmarkTest add_executable(Qt6MigratedBenchmarkTest
tests/test_main.cpp
tests/test_mainwindow.cpp tests/test_mainwindow.cpp
MainWindow.h MainWindow.h
MainWindow.cpp MainWindow.cpp
@@ -72,7 +72,7 @@ lcov --remove "${COVERAGE_DIR}/coverage.info" \
'*/Qt6/*' \ '*/Qt6/*' \
'*/qt6-migrated-benchmark/tests/*' \ '*/qt6-migrated-benchmark/tests/*' \
--output-file "${COVERAGE_DIR}/coverage.filtered.info" \ --output-file "${COVERAGE_DIR}/coverage.filtered.info" \
--ignore-errors mismatch,unused \ --ignore-errors mismatch \
--rc lcov_branch_coverage=1 --rc lcov_branch_coverage=1
# Generate HTML report # Generate HTML report
@@ -0,0 +1,52 @@
#include <QtTest/QtTest>
class TestMainWindow : public QObject
{
Q_OBJECT
private slots:
void testValidateInputValid();
void testValidateInputInvalid();
void testValidateInputEmpty();
void testValidateInputMixedCase();
};
void TestMainWindow::testValidateInputValid()
{
// Test valid input patterns
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(pattern.match("Test123").hasMatch());
QVERIFY(pattern.match("abc456").hasMatch());
QVERIFY(pattern.match("ABC789").hasMatch());
}
void TestMainWindow::testValidateInputInvalid()
{
// Test invalid input patterns
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(!pattern.match("123Test").hasMatch());
QVERIFY(!pattern.match("Test").hasMatch());
QVERIFY(!pattern.match("123").hasMatch());
QVERIFY(!pattern.match("Test123ABC").hasMatch());
QVERIFY(!pattern.match("Test-123").hasMatch());
}
void TestMainWindow::testValidateInputEmpty()
{
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(!pattern.match("").hasMatch());
}
void TestMainWindow::testValidateInputMixedCase()
{
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(pattern.match("TeSt123").hasMatch());
QVERIFY(pattern.match("aBcDeF999").hasMatch());
}
QTEST_MAIN(TestMainWindow)
#include "test_main.moc"
@@ -1,88 +1,49 @@
#include "MainWindow.h" #include "MainWindow.h"
#include <QApplication>
#include <QtTest/QtTest> #include <QtTest/QtTest>
class TestMainWindow : public QObject class TestMainWindowWidget : public QObject
{ {
Q_OBJECT Q_OBJECT
private slots: private slots:
void testValidateInputValid();
void testValidateInputInvalid();
void testValidateInputEmpty();
void testValidateInputMixedCase();
void testMainWindowCreation(); void testMainWindowCreation();
void testWindowTitle(); void testWindowTitle();
void testValidateInputMethod(); void testValidateInputMethod();
void testGetScreenInfo(); void testGetScreenInfo();
}; };
void TestMainWindow::testValidateInputValid() void TestMainWindowWidget::testMainWindowCreation()
{
// Test valid input patterns
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(pattern.match("Test123").hasMatch());
QVERIFY(pattern.match("abc456").hasMatch());
QVERIFY(pattern.match("ABC789").hasMatch());
}
void TestMainWindow::testValidateInputInvalid()
{
// Test invalid input patterns
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(!pattern.match("123Test").hasMatch());
QVERIFY(!pattern.match("Test").hasMatch());
QVERIFY(!pattern.match("123").hasMatch());
QVERIFY(!pattern.match("Test123ABC").hasMatch());
QVERIFY(!pattern.match("Test-123").hasMatch());
}
void TestMainWindow::testValidateInputEmpty()
{
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(!pattern.match("").hasMatch());
}
void TestMainWindow::testValidateInputMixedCase()
{
QRegularExpression pattern(R"(^[A-Za-z]+\d+$)");
QVERIFY(pattern.match("TeSt123").hasMatch());
QVERIFY(pattern.match("aBcDeF999").hasMatch());
}
void TestMainWindow::testMainWindowCreation()
{ {
MainWindow window; MainWindow window;
QVERIFY(!window.windowTitle().isEmpty()); QVERIFY(!window.windowTitle().isEmpty());
} }
void TestMainWindow::testWindowTitle() void TestMainWindowWidget::testWindowTitle()
{ {
MainWindow window; MainWindow window;
QCOMPARE(window.windowTitle(), QStringLiteral("Qt6 Legacy Migration Benchmark")); QCOMPARE(window.windowTitle(), u"Qt6 Legacy Migration Benchmark"_s);
} }
void TestMainWindow::testValidateInputMethod() void TestMainWindowWidget::testValidateInputMethod()
{ {
MainWindow window; MainWindow window;
QVERIFY(window.validateInput(QStringLiteral("Test123"))); QVERIFY(window.validateInput(u"Test123"_s));
QVERIFY(window.validateInput(QStringLiteral("abc456"))); QVERIFY(window.validateInput(u"abc456"_s));
QVERIFY(!window.validateInput(QStringLiteral("123Test"))); QVERIFY(!window.validateInput(u"123Test"_s));
QVERIFY(!window.validateInput(QStringLiteral(""))); QVERIFY(!window.validateInput(u""_s));
} }
void TestMainWindow::testGetScreenInfo() void TestMainWindowWidget::testGetScreenInfo()
{ {
MainWindow window; MainWindow window;
QString info = window.getScreenInfo(); QString info = window.getScreenInfo();
QVERIFY(!info.isEmpty()); QVERIFY(!info.isEmpty());
QVERIFY(info.contains(QStringLiteral("Screen:"))); QVERIFY(info.contains(u"Screen:"_s));
} }
QTEST_MAIN(TestMainWindow) QTEST_MAIN(TestMainWindowWidget)
#include "test_mainwindow.moc" #include "test_mainwindow.moc"