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 {
agent none
agent {
docker {
image 'lifemaps/ci-cpp:latest'
reuseNode true
}
}
options {
timestamps()
@@ -14,133 +19,106 @@ pipeline {
QT_QPA_PLATFORM = "offscreen"
LANG = "C.UTF-8"
LC_ALL = "C.UTF-8"
DOCKER_IMAGE = "lifemaps/ci-cpp:latest"
}
stages {
stage('Docker Image') {
agent any
stage('Cleanup') {
steps {
script {
echo "Checking if Docker image exists locally..."
def imageExists = sh(script: "docker inspect ${DOCKER_IMAGE} >/dev/null 2>&1", returnStatus: true) == 0
if (!imageExists) {
echo "Docker image not found. Building from ci/Dockerfile.ci..."
sh 'docker build -t ${DOCKER_IMAGE} -f ci/Dockerfile.ci .'
} else {
echo "Docker image ${DOCKER_IMAGE} found locally."
}
echo "Cleaning workspace to avoid corruption issues..."
// Use shell command with chmod to handle permission issues
sh '''
if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then
echo "Removing build directory with permission fix..."
chmod -R u+w "${WORKSPACE}/${BUILD_DIR}" 2>/dev/null || true
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') {
agent {
docker {
image 'lifemaps/ci-cpp:latest'
reuseNode true
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 {
return env.BRANCH_NAME ==~ /main|master|coverage.*/
}
}
stages {
stage('Cleanup') {
steps {
script {
echo "Cleaning workspace to avoid corruption issues..."
sh '''
if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then
echo "Removing build directory with permission fix..."
chmod -R u+w "${WORKSPACE}/${BUILD_DIR}" 2>/dev/null || true
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('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'
}
}
steps {
echo "Running coverage collection..."
sh '''
${PROJECT_DIR}/scripts/collect-coverage.sh ${WORKSPACE}/${COVERAGE_BUILD_DIR}
'''
}
post {
always {
publishCoverage adapters: [lcovAdapter(mergeToOneReport: true, reportFile: 'coverage/lcov.info')],
sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
}
}
}
@@ -150,6 +128,7 @@ pipeline {
always {
script {
if (getContext(hudson.FilePath)) {
// Use shell command with chmod to handle permission issues
sh '''
if [ -d "${WORKSPACE}/${BUILD_DIR}" ]; then
echo "Final cleanup: Removing build directory..."
+5 -4
View File
@@ -2,9 +2,9 @@ FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive
# Create jenkins user (UID 1001 to avoid conflicts with existing users)
RUN groupadd -g 1001 jenkins && \
useradd -u 1001 -g 1001 -m -s /bin/bash jenkins
# Create jenkins user with same UID as host jenkins (typically 1000)
RUN groupadd -g 1000 jenkins && \
useradd -u 1000 -g 1000 -m -s /bin/bash jenkins
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@@ -23,7 +23,8 @@ RUN apt-get update && \
qt6-base-dev-tools \
qt6-tools-dev \
qt6-tools-dev-tools \
lcov && \
lcov \
genhtml && \
rm -rf /var/lib/apt/lists/*
# Set Qt platform for headless testing
@@ -60,6 +60,7 @@ enable_testing()
# Add test executable
add_executable(Qt6MigratedBenchmarkTest
tests/test_main.cpp
tests/test_mainwindow.cpp
MainWindow.h
MainWindow.cpp
@@ -72,7 +72,7 @@ lcov --remove "${COVERAGE_DIR}/coverage.info" \
'*/Qt6/*' \
'*/qt6-migrated-benchmark/tests/*' \
--output-file "${COVERAGE_DIR}/coverage.filtered.info" \
--ignore-errors mismatch,unused \
--ignore-errors mismatch \
--rc lcov_branch_coverage=1
# 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 <QApplication>
#include <QtTest/QtTest>
class TestMainWindow : public QObject
class TestMainWindowWidget : public QObject
{
Q_OBJECT
private slots:
void testValidateInputValid();
void testValidateInputInvalid();
void testValidateInputEmpty();
void testValidateInputMixedCase();
void testMainWindowCreation();
void testWindowTitle();
void testValidateInputMethod();
void testGetScreenInfo();
};
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());
}
void TestMainWindow::testMainWindowCreation()
void TestMainWindowWidget::testMainWindowCreation()
{
MainWindow window;
QVERIFY(!window.windowTitle().isEmpty());
}
void TestMainWindow::testWindowTitle()
void TestMainWindowWidget::testWindowTitle()
{
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;
QVERIFY(window.validateInput(QStringLiteral("Test123")));
QVERIFY(window.validateInput(QStringLiteral("abc456")));
QVERIFY(!window.validateInput(QStringLiteral("123Test")));
QVERIFY(!window.validateInput(QStringLiteral("")));
QVERIFY(window.validateInput(u"Test123"_s));
QVERIFY(window.validateInput(u"abc456"_s));
QVERIFY(!window.validateInput(u"123Test"_s));
QVERIFY(!window.validateInput(u""_s));
}
void TestMainWindow::testGetScreenInfo()
void TestMainWindowWidget::testGetScreenInfo()
{
MainWindow window;
QString info = window.getScreenInfo();
QVERIFY(!info.isEmpty());
QVERIFY(info.contains(QStringLiteral("Screen:")));
QVERIFY(info.contains(u"Screen:"_s));
}
QTEST_MAIN(TestMainWindow)
QTEST_MAIN(TestMainWindowWidget)
#include "test_mainwindow.moc"